Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

팔로워,팔로잉-유저-확인기능-추가 #83

Merged
merged 9 commits into from
Dec 13, 2023
Prev Previous commit
New : 팔로잉, 팔로우 유저 확인기능 추가
  • Loading branch information
jobkaeHenry committed Dec 13, 2023
commit 052ef6dc1ad055417a714e92a184c844084cc5dd
22 changes: 22 additions & 0 deletions client/src/app/(protectedRoute)/user/follow-list/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"use client";
import CustomAppbar from "@/components/layout/CustomAppbar";
import CustomContainer from "@/components/layout/CustomContainer";
import { useMyInfoQuery } from "@/queries/auth/useMyInfoQuery";
import { ReactNode } from "react";

type FollowListLayoutProps = {
children: ReactNode;
};

const FollowListLayout = ({ children }: FollowListLayoutProps) => {
const { data: myInfo } = useMyInfoQuery();

return (
<>
<CustomAppbar title={`${myInfo?.nickname??'닉네임'}의 목록`}/>
<CustomContainer>{children}</CustomContainer>
</>
);
};

export default FollowListLayout;
39 changes: 39 additions & 0 deletions client/src/app/(protectedRoute)/user/follow-list/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { Box } from "@mui/material";
import CustomToggleButtonGroup from "@/components/CustomToggleButtonGroup";
import { appbarHeight } from "@/const/uiSizes";
import { Suspense, useState } from "react";
import FollowingList from "@/components/user/followList/FollowingList";
import FollowingUserCardSkeleton from "@/components/user/followList/FollowingUserCardSkeleton";
import ComponentRepeater from "@/components/ComponentRepeater";
import FollowerList from "@/components/user/followList/FollowerList";

const FollowListPage = () => {
const selectableList = ["팔로잉", "팔로워"];
const [currentView, setCurrentView] = useState(selectableList[0]);

return (
<>
<CustomToggleButtonGroup
value={selectableList}
onChange={setCurrentView}
sx={{ position: "fixed", top: appbarHeight, left: 0, right: 0 }}
/>
{/* Fixed로 빠진 button 위치만큼의 place holder */}
<Box height={26} />
<Suspense
fallback={
<ComponentRepeater count={5}>
<FollowingUserCardSkeleton />
</ComponentRepeater>
}
>
{currentView === "팔로잉" && <FollowingList /> }
{currentView === "팔로워" && <FollowerList /> }
</Suspense>
</>
);
};

export default FollowListPage;
72 changes: 72 additions & 0 deletions client/src/components/CustomToggleButtonGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"use client";
import {
ToggleButton,
ToggleButtonGroup,
ToggleButtonGroupProps,
Typography,
} from "@mui/material";
import { useState } from "react";

interface CustomToggleButtonGroupType
extends Omit<ToggleButtonGroupProps, "onChange" | "value" | "children"> {
onChange: (val: string) => void;
value: string[];
}

const CustomToggleButtonGroup = ({
onChange,
value,
sx,
...toggleBtnGroupProps
}: CustomToggleButtonGroupType) => {
const [currentValue, setCurrentValue] = useState(value[0]);

return (
<ToggleButtonGroup
value={currentValue}
exclusive
fullWidth
onChange={(_e, val) => {
if (val !== null) {
setCurrentValue(val);
onChange(val);
}
}}
sx={{ backgroundColor: "background.paper",px:2, ...sx }}
{...toggleBtnGroupProps}
>
{value.map((val, i) => {
return (
<ToggleButton
key={i}
disableRipple
value={val}
fullWidth
sx={ToggleButtonStyle}
>
<Typography fontSize="caption1" fontWeight="bold">
{val}
</Typography>
</ToggleButton>
);
})}
</ToggleButtonGroup>
);
};

const ToggleButtonStyle = {
border: 0,
borderRadius: 0,
"&.Mui-selected": {
backgroundColor: "background.paper",
borderBottom: "1px solid",
":hover": {
backgroundColor: "background.paper",
},
},
":hover": {
backgroundColor: "background.paper",
},
};

export default CustomToggleButtonGroup;
61 changes: 61 additions & 0 deletions client/src/components/user/followList/FollowUserCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { Button, Stack, Typography } from "@mui/material";
import React from "react";
import UserAvatar from "@/components/user/info/UserAvatar";
import { useRouter } from "next/navigation";
import { USER_PAGE } from "@/const/clientPath";
import useUnFollowMutation from "@/queries/user/useUnFollowMutation";

type Props = {
imageUrl?: string;
nickName: string;
userId: string;
content: string;
userPk: number;
};

const FollowUserCard = ({
userPk,
imageUrl,
nickName,
userId,
content,
}: Props) => {
const router = useRouter();
const { mutate: unfollowHandler } = useUnFollowMutation();

return (
<Stack direction="row" gap={1} py={1}>
<UserAvatar
src={imageUrl}
alt={`${nickName}의 프로필`}
fallback={nickName}
onClick={() => router.push(USER_PAGE(userPk))}
sx={{ cursor: "pointer" }}
/>
<Stack gap={1} flexGrow={1}>
<Stack direction="row" justifyContent={"space-between"}>
<Stack
sx={{ cursor: "pointer" }}
onClick={() => router.push(USER_PAGE(userPk))}
>
<Typography fontSize="caption1" color="text.main">
{nickName}
</Typography>
<Typography fontSize="caption2" color="text.secondary">
@{userId}
</Typography>
</Stack>
<Button
variant="outlined"
onClick={() => unfollowHandler(String(userPk))}
>
언팔로우
</Button>
</Stack>
<Typography>{content}</Typography>
</Stack>
</Stack>
);
};

export default FollowUserCard;
43 changes: 43 additions & 0 deletions client/src/components/user/followList/FollowerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import FollowUserCard from "@/components/user/followList/FollowUserCard";
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import FollowingUserCardSkeleton from "@/components/user/followList/FollowingUserCardSkeleton";
import ComponentRepeater from "@/components/ComponentRepeater";
import useFollowerUserInfiniteQuery from "@/queries/user/useFollowerUserInfiniteQuery";

const FollowerList = () => {
const { data, isFetchingNextPage, hasNextPage, fetchNextPage } =
useFollowerUserInfiniteQuery();
const { ref, inView } = useInView();

useEffect(() => {
if (hasNextPage && inView) fetchNextPage();
}, [inView, hasNextPage]);

return (
<>
{data.pages.map((page) =>
page.content.map(({ nickname, id, introduction }) => (
<FollowUserCard
key={id}
nickName={nickname}
userId={id}
content={introduction}
/>
))
)}
{isFetchingNextPage ? (
<ComponentRepeater count={5}>
<FollowingUserCardSkeleton />
</ComponentRepeater>
) : (
// 인터섹션옵저버
hasNextPage && <div style={{ height: 60 }} ref={ref}></div>
)}
</>
);
};

export default FollowerList;
45 changes: 45 additions & 0 deletions client/src/components/user/followList/FollowingList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"use client";

import FollowUserCard from "@/components/user/followList/FollowUserCard";
import useFollowingUserInfiniteQuery from "@/queries/user/useFollowingUserInfiniteQuery";
import { useEffect } from "react";
import { useInView } from "react-intersection-observer";
import FollowingUserCardSkeleton from "@/components/user/followList/FollowingUserCardSkeleton";
import ComponentRepeater from "@/components/ComponentRepeater";

const FollowingList = () => {
const { data, isFetchingNextPage, hasNextPage, fetchNextPage } =
useFollowingUserInfiniteQuery();
const { ref, inView } = useInView();

useEffect(() => {
if (hasNextPage && inView) fetchNextPage();
}, [inView, hasNextPage]);

return (
<>
{data.pages.map((page) =>
page.content.map(({ nickname, id, introduction, profileImgUrls, userNo }) => (
<FollowUserCard
key={id}
nickName={nickname}
userId={id}
userPk={userNo}
imageUrl={profileImgUrls[0]?.attachUrl}
content={introduction}
/>
))
)}
{isFetchingNextPage ? (
<ComponentRepeater count={5}>
<FollowingUserCardSkeleton />
</ComponentRepeater>
) : (
// 인터섹션옵저버
hasNextPage && <div style={{ height: 60 }} ref={ref}></div>
)}
</>
);
};

export default FollowingList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Stack } from "@mui/material";
import { Skeleton } from "@mui/material";

const FollowingUserCardSkeleton = () => {
return (
<Stack direction="row" gap={1} py={1}>
<Skeleton width={40} height={40} variant="circular" />
<Stack gap={1} flexGrow={1}>
<Stack direction="row" justifyContent={"space-between"}>
<Stack>
<Skeleton width={50} />
<Skeleton width={60} />
</Stack>
<Skeleton height={40} width={80} variant="rectangular" />
</Stack>
<Skeleton width={"100%"} />
</Stack>
</Stack>
);
};

export default FollowingUserCardSkeleton;
3 changes: 3 additions & 0 deletions client/src/queries/user/useFollowMutation.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import { UserInfoInterface } from "@/types/user/userInfoInterface";
import { MyInfoQueryKeys } from "../auth/useMyInfoQuery";
import { MyInfoInterface } from "@/types/auth/myInfo";
import { useErrorHandler } from "@/utils/errorHandler";
import { followerUserQueryKey } from "./useFollowerUserInfiniteQuery";

const useFollowMutation = () => {
const queryClient = useQueryClient();
@@ -59,6 +60,8 @@ const useFollowMutation = () => {
queryClient.invalidateQueries({
queryKey: UserInfoQueryKey.byId(userInfo?.userNo),
});
// TODO 낙관적업데이트 구현
queryClient.invalidateQueries({ queryKey: followerUserQueryKey.all });
},
});
};
51 changes: 51 additions & 0 deletions client/src/queries/user/useFollowerUserInfiniteQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";
import { FOLLOWER_USER } from "@/const/serverPath";
import useAxiosPrivate from "@/hooks/useAxiosPrivate";
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
import Pagenated, { PagenationParams } from "@/types/Pagenated";
import FollowingUserInterface from "@/types/user/followingUserInterface";

const useFollowerUserInfiniteQuery = () => {
return useSuspenseInfiniteQuery({
queryKey: followerUserQueryKey.all,

queryFn: async ({ pageParam = 0 }) =>
await getFollowerUserFn({ page: pageParam }),

getNextPageParam: ({ currentPage, hasNextPage }) =>
hasNextPage ? currentPage + 1 : undefined,

getPreviousPageParam: ({ currentPage }) =>
currentPage > 0 ? currentPage - 1 : undefined,
initialPageParam: 0,
});
};

export const getFollowerUserFn = async ({
page = 0,
size = 10,
sort = "desc",
}: PagenationParams) => {
const axiosPrivate = useAxiosPrivate();
const { data } = await axiosPrivate.get<Pagenated<FollowingUserInterface>>(
FOLLOWER_USER,
{
params: {
page,
size,
sort,
},
}
);
return {
...data,
currentPage: page,
hasNextPage: data.totalElements / ((page + 1) * size) > 1,
};
};

export const followerUserQueryKey = {
all: ["follower"],
};

export default useFollowerUserInfiniteQuery;
Loading