Skip to content

Commit

Permalink
Merge pull request #484 from depromeet/feat/follow-list
Browse files Browse the repository at this point in the history
[Feat] 팔로잉/팔로워 리스트 페이지
  • Loading branch information
sumi-0011 authored Feb 8, 2024
2 parents b849dfb + 3bf4785 commit c9c9e9d
Show file tree
Hide file tree
Showing 26 changed files with 558 additions and 158 deletions.
48 changes: 41 additions & 7 deletions src/apis/follow.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import getQueryKey from '@/apis/getQueryKey';
import apiInstance from '@/apis/instance.api';
import { type FollowMemberType } from '@/apis/schema/member';
import { type FollowerMemberWithStatusType, type FollowMemberType, type FollowStatus } from '@/apis/schema/member';
import { type MissionItemTypeWithRecordId } from '@/apis/schema/mission';
import { useQuery, type UseQueryOptions } from '@tanstack/react-query';
import useMutationHandleError from '@/hooks/query/useMutationHandleError';
import { type UseMutationOptions, useQuery, type UseQueryOptions } from '@tanstack/react-query';

type GetFollowMembersResponse = FollowMemberType[];

Expand All @@ -16,10 +17,15 @@ interface FollowsResponse {
followerCount: number;
followStatus: FollowStatus;
}
export enum FollowStatus {
FOLLOWING = 'FOLLOWING',
FOLLOWED_BY_ME = 'FOLLOWED_BY_ME',
NOT_FOLLOWING = 'NOT_FOLLOWING',

interface FollowListResponse {
targetNickname: string;
followingList: FollowerMemberWithStatusType[];
followerList: FollowerMemberWithStatusType[];
}

interface DeleteFollowResponse {
followStatus: FollowStatus;
}

export const FOLLOW_API = {
Expand All @@ -36,7 +42,7 @@ export const FOLLOW_API = {
const { data } = await apiInstance.post(`/follows`, { targetId });
return data;
},
deleteFollow: async (targetId: number) => {
deleteFollow: async (targetId: number): Promise<DeleteFollowResponse> => {
const { data } = await apiInstance.delete(`/follows`, { data: { targetId } });
return data;
},
Expand All @@ -48,6 +54,10 @@ export const FOLLOW_API = {
const { data } = await apiInstance.get<FollowsResponse>(`/follows/${followId}`);
return data;
},
getFollowList: async (targetId: number): Promise<FollowListResponse> => {
const { data } = await apiInstance.get<FollowListResponse>(`/follows/${targetId}/list`);
return data;
},
};

export const useFollowMembers = (options?: UseQueryOptions<GetFollowMembersResponse>) => {
Expand Down Expand Up @@ -81,3 +91,27 @@ export const useFollowsCountTargetId = (followId: number, option?: UseQueryOptio
...option,
});
};

export const useFetFollowList = (targetId: number, option?: UseQueryOptions<FollowListResponse>) => {
return useQuery<FollowListResponse>({
queryKey: getQueryKey('followList', { targetId }),
queryFn: () => FOLLOW_API.getFollowList(targetId),
...option,
});
};

export const useAddFollow = (options?: UseMutationOptions<unknown, unknown, number>) =>
useMutationHandleError(
{ mutationFn: FOLLOW_API.addFollow, ...options },
{
offset: 'default',
},
);

export const useDeleteFollow = (options?: UseMutationOptions<DeleteFollowResponse, unknown, number>) =>
useMutationHandleError(
{ mutationFn: FOLLOW_API.deleteFollow, ...options },
{
offset: 'default',
},
);
5 changes: 5 additions & 0 deletions src/apis/getQueryKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ type QueryList = {
me?: undefined;
id?: number;
};
// follow
followMembers: undefined;
followsCountMe: undefined;
followsCountTargetId: {
followId: number;
};
followList: {
targetId: number;
};

memberSocial: undefined;
searchNickname: {
nickname: string;
Expand Down
9 changes: 2 additions & 7 deletions src/apis/member.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import getQueryKey from '@/apis/getQueryKey';
import apiInstance from '@/apis/instance.api';
import { type FollowStatusType, type MemberType } from '@/apis/schema/member';
import { type FollowerMemberWithStatusType, type MemberType } from '@/apis/schema/member';
import { type UploadBaseRequest, type UploadUrlBaseResponse } from '@/apis/schema/upload';
import {
useMutation,
Expand Down Expand Up @@ -33,12 +33,7 @@ interface SocialLoginInfoResponse {
email: 'string';
}

type SearchNicknameResponse = {
memberId: number;
nickname: string;
profileImageUrl: string;
followStatus: FollowStatusType;
}[];
type SearchNicknameResponse = FollowerMemberWithStatusType[];

enum AUTH_PROVIDER {
KAKAO = 'KAKAO',
Expand Down
15 changes: 13 additions & 2 deletions src/apis/schema/member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,21 @@ export interface FollowMemberType {
profileImageUrl: string;
}

export type FollowStatusType = 'FOLLOWING' | 'FOLLOWED_BY_ME' | 'NOT_FOLLOWING';

export enum FileExtension {
JPEG = 'JPEG',
JPG = 'JPG',
PNG = 'PNG',
}

export interface FollowerMemberWithStatusType {
memberId: number;
nickname: string;
profileImageUrl: string;
followStatus: FollowStatus;
}

export enum FollowStatus {
FOLLOWING = 'FOLLOWING',
FOLLOWED_BY_ME = 'FOLLOWED_BY_ME',
NOT_FOLLOWING = 'NOT_FOLLOWING',
}
5 changes: 3 additions & 2 deletions src/app/mypage/MyProfile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ const tabs = [

export default function MyProfile() {
const { data } = useGetMembersMe();
const missionId = data?.memberId ?? 0;
const { data: symbolStackData } = useGetMissionStack(missionId.toString());
const memberId = data?.memberId ?? 0;
const { data: symbolStackData } = useGetMissionStack(memberId.toString());
const symbolStack = symbolStackData?.symbolStack ?? 0;
const { data: followCountData } = useFollowsCountMembers();

return (
<ProfileContent
memberId={memberId}
profileImageUrl={data?.profileImageUrl || null}
nickname={data?.nickname || ''}
symbolStack={symbolStack}
Expand Down
3 changes: 2 additions & 1 deletion src/app/profile/[id]/FollowButton.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FOLLOW_API, FollowStatus } from '@/apis/follow';
import { FOLLOW_API } from '@/apis/follow';
import getQueryKey from '@/apis/getQueryKey';
import { isSeverError } from '@/apis/instance.api';
import { FollowStatus } from '@/apis/schema/member';
import Button from '@/components/Button/Button';
import GradientTextButton from '@/components/Button/GradientTextButton';
import { useSnackBar } from '@/components/SnackBar/SnackBarProvider';
Expand Down
5 changes: 4 additions & 1 deletion src/app/profile/[id]/ProfileContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface ProfileContentProps {
followerCount: number;
followingCount: number;
rightElement?: React.ReactNode;
memberId: number;
}

function ProfileContent({
Expand All @@ -22,6 +23,7 @@ function ProfileContent({
followingCount,
rightElement,
children,
memberId,
}: PropsWithChildren<ProfileContentProps>) {
return (
<div className={containerCss}>
Expand All @@ -33,7 +35,8 @@ function ProfileContent({
<div>
<p className={userNameCss}>{nickname}</p>
<span className={followerTabCss}>
팔로잉 {followingCount} &nbsp; 팔로워 {followerCount}
<Link href={ROUTER.PROFILE.FOLLOW_LIST(memberId, 'following')}>팔로잉 {followingCount}</Link> &nbsp;
<Link href={ROUTER.PROFILE.FOLLOW_LIST(memberId, 'follower')}>팔로워 {followerCount}</Link>
</span>
</div>
{rightElement}
Expand Down
67 changes: 67 additions & 0 deletions src/app/profile/[id]/follows/FollowingList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import Link from 'next/link';
import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member';
import { useGetMeId, useViewList } from '@/app/profile/[id]/follows/index.hooks';
import {
FollowingMember,
type MemberItemProps,
MineMemberItem,
NotFollowingMember,
} from '@/components/ListItem/Follow/MemberItem';
import { stagger } from '@/components/Motion/Motion.constants';
import StaggerWrapper from '@/components/Motion/StaggerWrapper';
import { ROUTER } from '@/constants/router';
import { css } from '@/styled-system/css';

interface Props {
list: FollowerMemberWithStatusType[];
refetch: () => void;
}

function FollowingList(props: Props) {
const { list, onUpdateItem } = useViewList(props.list);

return (
<StaggerWrapper wrapperOverrideCss={containerCss} staggerVariants={stagger(0.1)}>
{list.map((item) => (
<Link key={item.memberId} href={ROUTER.PROFILE.DETAIL(item.memberId)} passHref>
<Item
{...item}
onUpdateList={(_item) => {
onUpdateItem(_item);
props.refetch();
}}
/>
</Link>
))}
</StaggerWrapper>
);
}

export default FollowingList;

interface ItemProps extends Omit<MemberItemProps, 'onButtonClick'> {
onUpdateList: (item: FollowerMemberWithStatusType) => void;
}

function Item({ onUpdateList, ...props }: ItemProps) {
const myId = useGetMeId();

if (props.memberId === myId) {
return <MineMemberItem {...props} />;
}

switch (props.followStatus) {
case FollowStatus.FOLLOWING:
return <FollowingMember {...props} onButtonClick={onUpdateList} />;
case FollowStatus.NOT_FOLLOWING:
case FollowStatus.FOLLOWED_BY_ME:
return <NotFollowingMember {...props} onButtonClick={onUpdateList} />;
default:
return null;
}
}

const containerCss = css({
padding: '16px',
width: '100%',
});
80 changes: 80 additions & 0 deletions src/app/profile/[id]/follows/MyFollowerList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import Link from 'next/link';
import { useAddFollow } from '@/apis/follow';
import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member';
import { useViewList } from '@/app/profile/[id]/follows/index.hooks';
import { ProfileListItem } from '@/components/ListItem';
import { stagger } from '@/components/Motion/Motion.constants';
import StaggerWrapper from '@/components/Motion/StaggerWrapper';
import { ROUTER } from '@/constants/router';
import { css } from '@/styled-system/css';

interface Props {
list: FollowerMemberWithStatusType[];
refetch: () => void;
}

function MyFollowerList(props: Props) {
const { list, onUpdateItem } = useViewList(props.list);

return (
<StaggerWrapper wrapperOverrideCss={containerCss} staggerVariants={stagger(0.1)}>
{list.map((item) => (
<Item
key={`${item.memberId}-${item.followStatus}`}
item={item}
onUpdateItem={(_item) => {
onUpdateItem(_item);
props.refetch();
}}
/>
))}
</StaggerWrapper>
);
}

export default MyFollowerList;

interface ItemProps {
item: FollowerMemberWithStatusType;
onUpdateItem: (member: FollowerMemberWithStatusType) => void;
}

function Item({ item, onUpdateItem }: ItemProps) {
const { mutate } = useAddFollow({
onSuccess: () => {
onUpdateItem({ ...item, followStatus: FollowStatus.FOLLOWING });
},
});

const isFollowing = item.followStatus === FollowStatus.FOLLOWING;

return (
<Link key={item.memberId} href={ROUTER.PROFILE.DETAIL(item.memberId)}>
<ProfileListItem
variant={isFollowing ? 'one-button' : 'two-button'}
subElement={
!isFollowing && (
<span className={followLabelCss} onClick={() => mutate(item.memberId)}>
팔로우
</span>
)
}
buttonElement={
// TODO : 삭제 버튼 추가 필요 (맞팔 관계 팔로우 삭제, 맞팔 x, 팔로워 관계 삭제)
// 일정 상 무리라고 판단 (2/6) 추후 수정
<div></div>
}
thumbnailUrl={item.profileImageUrl}
name={item.nickname}
/>
</Link>
);
}

const containerCss = css({
padding: '16px',
});

const followLabelCss = css({
padding: '8px 12px',
});
27 changes: 27 additions & 0 deletions src/app/profile/[id]/follows/index.hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
import { useGetMembersMe } from '@/apis/member';
import { type FollowerMemberWithStatusType } from '@/apis/schema/member';
import { sorFollowerList } from '@/app/profile/[id]/follows/index.utils';

export const useViewList = (list: FollowerMemberWithStatusType[]) => {
const myId = useGetMeId();
const [viewList, setViewList] = useState(list);

const onUpdateItem = (member: FollowerMemberWithStatusType) => {
setViewList((prev) => prev.map((item) => (item.memberId === member.memberId ? member : item)));
};

useEffect(() => {
const sortList = sorFollowerList(list, Number(myId));
setViewList(sortList);
}, []);

return { list: viewList, onUpdateItem };
};

// @description 현재 로그인한 사용자의 memberId를 가져옵니다.
export const useGetMeId = () => {
const { data } = useGetMembersMe();
const memberId = data?.memberId ?? 0;
return memberId;
};
15 changes: 15 additions & 0 deletions src/app/profile/[id]/follows/index.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { type FollowerMemberWithStatusType, FollowStatus } from '@/apis/schema/member';

export const sorFollowerList = (list: FollowerMemberWithStatusType[], myId: number): FollowerMemberWithStatusType[] => {
// 1순위) 내 계정, 내가 팔로잉 중인 계정, 내가 팔로우 중이지 않은 계정 순으로 리스트 나열
// 2순위) 가나다 순, ABC순으로 나열

const myAccount = list.filter((item) => item.memberId === myId);
const followingList = list.filter((item) => item.followStatus === FollowStatus.FOLLOWING);
const followedByMeList = list.filter((item) => item.followStatus === FollowStatus.FOLLOWED_BY_ME);
const notFollowingList = list.filter((item) => item.followStatus === FollowStatus.NOT_FOLLOWING);

const sortedList = [...myAccount, ...followingList, ...followedByMeList, ...notFollowingList];

return sortedList;
};
Loading

0 comments on commit c9c9e9d

Please sign in to comment.