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

✨ Follow 기능 구현 #95

Merged
merged 31 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
c24dad5
feat: useFollow unit test 작성
Legitgoons Jun 13, 2024
d15faf0
feat: useUnfollow unit test 작성
Legitgoons Jun 13, 2024
d2dda3c
feat: follow axios 함수 구현
Legitgoons Jun 15, 2024
7119275
feat: follow query key 추가
Legitgoons Jun 15, 2024
dd1c888
feat: updateRelationshipStatus 구현
Legitgoons Jun 15, 2024
b235c01
feat: useFollow 구현
Legitgoons Jun 15, 2024
9d6a28d
feat: useFollow의 isPrivate -> locked로 수정
Legitgoons Jun 26, 2024
04881aa
feat: useUnfollow 구현
Legitgoons Jun 26, 2024
eacd133
feat: updateRelationshipStatus isPrivate -> locked 수정
Legitgoons Jun 26, 2024
b01c820
feat: follow 요청이 relationshipStatus를 반환하도록 수정
Legitgoons Jun 26, 2024
56cae1d
feat: follow 관련 unit test 수정
Legitgoons Jun 26, 2024
7a52d9f
feat: fetchRelationshipStatus 구현
Legitgoons Jun 26, 2024
b346fd1
feat: useGetRelationshipStatus 구현
Legitgoons Jun 26, 2024
c514f3a
feat: ProfileFollowButton 구현
Legitgoons Jun 26, 2024
3f88518
feat: SkeletonProfileButton 구현
Legitgoons Jun 26, 2024
8177a6e
feat: SkeletonProfileUser 수정
Legitgoons Jun 26, 2024
b556f3c
feat: ProfileUser에 ProfileFollowButton 추가
Legitgoons Jun 26, 2024
71fcd17
feat: fetchUser 함수 분리
Legitgoons Jun 26, 2024
ee43ded
feat: IPhoneLayout defaultSize 확대
Legitgoons Jun 26, 2024
8ca244f
fix: useFollow, useUnfollow 수정
Legitgoons Jun 26, 2024
d87ab55
fix: updateRelationshipStatus 수정
Legitgoons Jun 26, 2024
7442ba2
feat: useFollow, useUnfollow mutationKey 추가
Legitgoons Jun 26, 2024
ae66f54
feat: handleMutationError에 mutationKeys 배열 생성, follow 추가
Legitgoons Jun 26, 2024
3f135fd
fix: relationshipStatus mock data 수정
Legitgoons Jun 29, 2024
86af6a0
feat: ProfileFollowButton switch문 수정
Legitgoons Jun 29, 2024
aceb30b
feat: followHandler에 followerCount/folloingCount 로직 추가
Legitgoons Jun 30, 2024
3e0c705
feat: useFollow, useUnfollow에서 user 가져오도록 수정
Legitgoons Jun 30, 2024
50c93ce
feat: mutitonKeys -> mutationCautionToastKeys 이름 변경
Legitgoons Jun 30, 2024
5810093
feat: useFollow와 useUnfollow hook 병합
Legitgoons Jul 2, 2024
c4de780
feat: hook 병합에 따라 ProfileFollowButton 수정
Legitgoons Jul 2, 2024
2cfe267
feat: RelationshipStatusCalculate 주석 작성
Legitgoons Jul 2, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/app/layout/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import './RootLayout.scss';
*/
export const RootLayout = () => {
return (
<IPhoneLayout>
<IPhoneLayout defaultSize={85}>
<div className='wrap'>
<Outlet />
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/mocks/consts/relationshipStatus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const relationshipStatus: Relationship = {
3: 'pending',
4: 'none',
5: 'following',
6: 'pending',
6: 'none',
7: 'following',
8: 'none',
9: 'none',
Expand Down
20 changes: 11 additions & 9 deletions src/app/mocks/handler/follow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,20 @@ export const followHandler = [
case 'none':
if (users[formattedUserId].locked) {
relationshipStatus[formattedUserId] = 'pending';
} else relationshipStatus[formattedUserId] = 'following';
break;
return createHttpSuccessResponse({ relationshipStatus: 'pending' });
} else {
users[formattedUserId].followerCount += 1;
users[1].followingCount += 1;
relationshipStatus[formattedUserId] = 'following';
return createHttpSuccessResponse({ relationshipStatus: 'following' });
}
case 'following':
return createHttpErrorResponse('4220');
case 'pending':
return createHttpErrorResponse('4220');
default:
return createHttpErrorResponse('4040');
}

return createHttpSuccessResponse({});
}),
// 2️⃣ 언팔로우 & 팔로우 요청 취소
http.delete('/users/:user_id/follow', ({ params }) => {
Expand All @@ -56,16 +59,15 @@ export const followHandler = [
return createHttpErrorResponse('4220');
case 'following':
relationshipStatus[formattedUserId] = 'none';
break;
users[formattedUserId].followerCount -= 1;
users[1].followingCount -= 1;
return createHttpSuccessResponse({ relationshipStatus: 'none' });
case 'pending':
relationshipStatus[formattedUserId] = 'none';
break;

return createHttpSuccessResponse({ relationshipStatus: 'none' });
default:
return createHttpErrorResponse('4040');
}

return createHttpSuccessResponse({});
}),
// 3️⃣ 팔로우 확인
http.get('/users/:user_id/follow', ({ params }) => {
Expand Down
2 changes: 2 additions & 0 deletions src/features/follow/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { useFollow } from './useFollow';
export { useUnfollow } from './useUnfollow';
69 changes: 69 additions & 0 deletions src/features/follow/api/useFollow.tsx
psychology50 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { requestFollow } from '@/shared/axios';
import { FetchRelationshipStatus } from '@/shared/consts';
import { QUERY_KEYS } from '@/shared/react-query';
import { isErrorResponse } from '@/shared/utils';

import { updateRelationshipStatus } from '../lib';

export const useFollow = (userId: number, locked: boolean) => {
const queryClient = useQueryClient();

const {
data,
mutate: handleFollow,
isPending: isPendingFollow,
} = useMutation({
mutationKey: [QUERY_KEYS.follow],
mutationFn: () => requestFollow(userId),
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: [QUERY_KEYS.follow, userId],
});

const previousQueryData =
queryClient.getQueryData<FetchRelationshipStatus>([
QUERY_KEYS.follow,
userId,
]);

if (!previousQueryData) return;

const updatedQueryData = updateRelationshipStatus(
previousQueryData,
locked,
);

await queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
updatedQueryData,
);

return { previousQueryData };
},
onError: (_, __, context) => {
queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
context?.previousQueryData,
);
},
onSuccess: (response, _, context) => {
if (isErrorResponse(response)) {
queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
context.previousQueryData,
);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.follow, userId] });
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.users, userId],
});
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.users, 1] });
},
});

return { data, handleFollow, isPendingFollow };
};
69 changes: 69 additions & 0 deletions src/features/follow/api/useUnfollow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { requestUnfollow } from '@/shared/axios';
import { FetchRelationshipStatus } from '@/shared/consts';
import { QUERY_KEYS } from '@/shared/react-query';
import { isErrorResponse } from '@/shared/utils';

import { updateRelationshipStatus } from '../lib';

export const useUnfollow = (userId: number, locked: boolean) => {
const queryClient = useQueryClient();

const {
data,
mutate: handleUnfollow,
isPending: isPendingUnfollow,
} = useMutation({
mutationKey: [QUERY_KEYS.follow],
mutationFn: () => requestUnfollow(userId),
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: [QUERY_KEYS.follow, userId],
});

const previousQueryData =
queryClient.getQueryData<FetchRelationshipStatus>([
QUERY_KEYS.follow,
userId,
]);

if (!previousQueryData) return;

const updatedQueryData = updateRelationshipStatus(
previousQueryData,
locked,
);

await queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
updatedQueryData,
);

return { previousQueryData };
},
onError: (_, __, context) => {
queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
context?.previousQueryData,
);
},
onSuccess: (response, _, context) => {
if (isErrorResponse(response)) {
queryClient.setQueryData(
[QUERY_KEYS.follow, userId],
context.previousQueryData,
);
}
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.follow, userId] });
queryClient.invalidateQueries({
queryKey: [QUERY_KEYS.users, userId],
});
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.users, 1] });
},
});

return { data, handleUnfollow, isPendingUnfollow };
};
1 change: 1 addition & 0 deletions src/features/follow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './api';
1 change: 1 addition & 0 deletions src/features/follow/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { updateRelationshipStatus } from './updateRelationshipStatus';
34 changes: 34 additions & 0 deletions src/features/follow/lib/updateRelationshipStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { RelationshipStatus, FetchRelationshipStatus } from '@/shared/consts';

const RelationshipStatusCalculate = (
psychology50 marked this conversation as resolved.
Show resolved Hide resolved
relationshipStatus: RelationshipStatus,
locked: boolean,
) => {
switch (relationshipStatus) {
case 'self':
return 'self';
case 'following':
return 'none';
case 'none':
return locked ? 'pending' : 'following';
case 'pending':
return 'none';
default:
return relationshipStatus;
}
};

export function updateRelationshipStatus(
previousRelationshipStatusData: FetchRelationshipStatus,
locked: boolean,
) {
return {
code: previousRelationshipStatusData.code,
data: {
relationshipStatus: RelationshipStatusCalculate(
previousRelationshipStatusData.data.relationshipStatus,
locked,
),
},
};
}
45 changes: 45 additions & 0 deletions src/features/follow/test/useFollow.unit.test.tsx
psychology50 marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { createQueryClientWrapper } from '@/shared/tests';

import { useFollow } from '../api';

describe('Follow 기능 테스트', () => {
describe('none 상태일 때', () => {
it('공개 계정이라면 following 상태로 변경된다.', async () => {
//given
const { result } = renderHook(() => useFollow(9, false), {
wrapper: createQueryClientWrapper(),
});

//when
act(() => result.current.handleFollow());

//then
await waitFor(() => {
const {
data: { relationshipStatus: initialStatus },
} = result.current.data;

expect(initialStatus).toBe('following');
});
});

it('비공개 계정이라면 pending 상태로 변경된다.', async () => {
const { result } = renderHook(() => useFollow(4, true), {
wrapper: createQueryClientWrapper(),
});

act(() => result.current.handleFollow());

await waitFor(() => {
const {
data: { relationshipStatus: initialStatus },
} = result.current.data;

expect(initialStatus).toBe('pending');
});
});
});
});
46 changes: 46 additions & 0 deletions src/features/follow/test/useUnfollow.unit.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { describe, expect, it } from 'vitest';

import { createQueryClientWrapper } from '@/shared/tests';

import { useUnfollow } from '../api';

describe('Unfollow 기능 테스트', () => {
it('following 상태일 때, none 상태로 변경된다.', async () => {
// given
const { result } = renderHook(() => useUnfollow(2, false), {
wrapper: createQueryClientWrapper(),
});

// when
act(() => result.current.handleUnfollow());

// then
await waitFor(() => {
const {
data: { relationshipStatus: initialStatus },
} = result.current.data;

expect(initialStatus).toBe('none');
});
});

it('pending 상태일 때, none 상태로 변경된다.', async () => {
// given
const { result } = renderHook(() => useUnfollow(3, true), {
wrapper: createQueryClientWrapper(),
});

//when
act(() => result.current.handleUnfollow());

//then
await waitFor(() => {
const {
data: { relationshipStatus: initialStatus },
} = result.current.data;

expect(initialStatus).toBe('none');
});
});
});
34 changes: 34 additions & 0 deletions src/shared/axios/follow/follow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { axiosInstance } from '../config/instance';

/**
* 팔로우 API
* @param userId 유저 아이디
* @returns 관계 상태
*/
export async function requestFollow(userId: number) {
const { data } = await axiosInstance.post(`/users/${userId}/follow`);

return data;
}

/**
* 언팔로우/팔로우 취소 API
* @param userId 유저 아이디
* @returns 관계 상태
*/
export async function requestUnfollow(userId: number) {
const { data } = await axiosInstance.delete(`/users/${userId}/follow`);

return data;
}

/**
* 팔로우 확인 API
* @param userId 유저 아이디
* @returns 관계 상태
*/
export async function fetchRelationshipStatus(userId: number) {
psychology50 marked this conversation as resolved.
Show resolved Hide resolved
const { data } = await axiosInstance.get(`/users/${userId}/follow`);

return data;
}
1 change: 1 addition & 0 deletions src/shared/axios/follow/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './follow';
2 changes: 2 additions & 0 deletions src/shared/axios/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export { axiosInstance } from './config';
export * from './like';
export * from './bookmark';
export * from './follow';
export * from './user';
1 change: 1 addition & 0 deletions src/shared/axios/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './user';
14 changes: 14 additions & 0 deletions src/shared/axios/user/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { FetchUser } from '@/shared/consts';

import { axiosInstance } from '../config/instance';

/**
* 유저 정보 API
* @param userId 유저 아이디
* @returns 유저 정보
*/
export async function fetchUser(userId: number): Promise<FetchUser> {
const { data } = await axiosInstance.get(`/users/${userId}`);

return data;
}
Loading
Loading