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

✨ 피드 북마크 API 연동 #52

Merged
merged 15 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions src/app/mocks/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { likeHandlers } from './handler/like';
import { followHandler } from './handler/follow';
import { searchHandler } from './handler/search';
import { userHandler } from './handler/user';
import { bookmarkHandlers } from './handler/bookmark';

// 브라우저에서 실행하기 위한 mocking worker 초기화
export const worker = setupWorker(
...commentHandlers,
...feedHandlers,
...bookmarkHandlers,
...likeHandlers,
...followHandler,
...searchHandler,
Expand Down
20 changes: 10 additions & 10 deletions src/app/mocks/consts/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const feeds: Feeds = {
commentCount: comments[1].length,

isLiked: likes[1].isLiked,
isBookmark: false,
isBookmarked: true,

createdAt: '2024-04-16 12:00:00',
updatedAt: '2024-04-16 12:00:00',
Expand All @@ -76,7 +76,7 @@ export const feeds: Feeds = {
commentCount: comments[2].length,

isLiked: likes[2].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 12:10:00',
updatedAt: '2024-04-16 12:10:00',
Expand Down Expand Up @@ -104,7 +104,7 @@ export const feeds: Feeds = {
commentCount: comments[3].length,

isLiked: likes[3].isLiked,
isBookmark: false,
isBookmarked: true,

createdAt: '2024-04-16 12:20:00',
updatedAt: '2024-04-16 12:20:00',
Expand Down Expand Up @@ -140,7 +140,7 @@ export const feeds: Feeds = {
commentCount: comments[4].length,

isLiked: likes[4].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 12:30:00',
updatedAt: '2024-04-16 12:30:00',
Expand Down Expand Up @@ -168,7 +168,7 @@ export const feeds: Feeds = {
commentCount: comments[5].length,

isLiked: likes[5].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 12:40:00',
updatedAt: '2024-04-16 12:40:00',
Expand Down Expand Up @@ -196,7 +196,7 @@ export const feeds: Feeds = {
commentCount: comments[6].length,

isLiked: likes[6].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 12:50:00',
updatedAt: '2024-04-16 12:50:00',
Expand All @@ -220,7 +220,7 @@ export const feeds: Feeds = {
commentCount: comments[7].length,

isLiked: likes[7].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 13:00:00',
updatedAt: '2024-04-16 13:00:00',
Expand All @@ -240,7 +240,7 @@ export const feeds: Feeds = {
commentCount: comments[8].length,

isLiked: likes[8].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 13:10:00',
updatedAt: '2024-04-16 13:10:00',
Expand All @@ -264,7 +264,7 @@ export const feeds: Feeds = {
commentCount: comments[9].length,

isLiked: likes[9].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-04-16 13:20:00',
updatedAt: '2024-04-16 13:20:00',
Expand All @@ -285,7 +285,7 @@ for (let i = 10; i < 100; i++) {
commentCount: comments[i].length,

isLiked: likes[i].isLiked,
isBookmark: false,
isBookmarked: false,

createdAt: '2024-05-03 12:00:00',
updatedAt: '2024-05-03 12:00:00',
Expand Down
47 changes: 47 additions & 0 deletions src/app/mocks/handler/bookmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { http } from 'msw';

import {
createHttpErrorResponse,
createHttpSuccessResponse,
} from '../dir/response';
import { feeds } from '../consts/feed';

export const bookmarkHandlers = [
// 1️⃣ 피드 북마크
http.put('/feeds/:feed_id/bookmarks', ({ params }) => {
const { feed_id } = params;

if (isNaN(Number(feed_id))) {
return createHttpErrorResponse('4220');
}

const formattedFeedId = Number(feed_id);

if (!feeds[formattedFeedId]) {
return createHttpErrorResponse('4040');
}

feeds[formattedFeedId].isBookmarked = true;

return createHttpSuccessResponse({ isBookmarked: true });
}),

// 2️⃣ 피드 북마크 취소
http.delete('/feeds/:feed_id/bookmarks', ({ params }) => {
const { feed_id } = params;

if (isNaN(Number(feed_id))) {
return createHttpErrorResponse('4220');
}

const formattedFeedId = Number(feed_id);

if (!feeds[formattedFeedId]) {
return createHttpErrorResponse('4040');
}

feeds[formattedFeedId].isBookmarked = false;

return createHttpSuccessResponse({ isBookmarked: false });
}),
];
5 changes: 1 addition & 4 deletions src/app/mocks/handler/feed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ interface ReportForm {

export const feedHandlers = [
// 1️⃣ 피드 목록 조회
/**
* @todo pageCount를 쿼리 파라미터로 받도록 수정
*/
http.get('/feeds', ({ request }) => {
const url = new URL(request.url);
const page = url.searchParams.get('page') || 1;
Expand Down Expand Up @@ -77,7 +74,7 @@ export const feedHandlers = [
commentCount: 0,

isLiked: false,
isBookmark: false,
isBookmarked: false,

createdAt: getCurrentDate(),
updatedAt: getCurrentDate(),
Expand Down
1 change: 1 addition & 0 deletions src/features/feed-bookmark/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useBookmarks } from './useBookmarks';
55 changes: 55 additions & 0 deletions src/features/feed-bookmark/api/useBookmarks.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { requestBookmarkFeed, requestUnbookmarkFeed } from '@/shared/axios';
import { FeedsQueryData } from '@/shared/consts';
import { QUERY_KEYS } from '@/shared/react-query';
import { isErrorResponse } from '@/shared/utils';

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

export const useBookmarks = (feedId: number, isBookmarked: boolean) => {
const queryClient = useQueryClient();

const { mutate: handleBookmarkFeed, isPending } = useMutation({
mutationFn: () =>
isBookmarked
? requestUnbookmarkFeed(feedId)
: requestBookmarkFeed(feedId),
onMutate: async () => {
await queryClient.cancelQueries({
queryKey: [QUERY_KEYS.feeds],
});

// 이전 쿼리값의 스냅샷
const previousQueryData = queryClient.getQueryData<FeedsQueryData>([
QUERY_KEYS.feeds,
]);

if (!previousQueryData) return;

// 업데이트 될 쿼리값
const updatedQueryData = updateBookmarkStatusInFeeds(
previousQueryData,
feedId,
);

// setQueryData 함수를 사용해 newTodo로 Optimistic Update를 실시한다.
await queryClient.setQueryData([QUERY_KEYS.feeds], updatedQueryData);

return { previousQueryData };
},
onError: (_, __, context) => {
queryClient.setQueryData([QUERY_KEYS.feeds], context?.previousQueryData);
},
onSuccess: (response, _, context) => {
if (isErrorResponse(response)) {
queryClient.setQueryData([QUERY_KEYS.feeds], context.previousQueryData);
return;
}

queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.feed, feedId] });
},
});

return { handleBookmarkFeed, isPending };
};
1 change: 1 addition & 0 deletions src/features/feed-bookmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BookmarkButton } from './ui';
26 changes: 26 additions & 0 deletions src/features/feed-bookmark/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { FeedsQueryData } from '@/shared/consts';

export function updateBookmarkStatusInFeeds(
previousQueryData: FeedsQueryData,
feedId: number,
) {
const { pages: previousPages } = previousQueryData;

return {
...previousQueryData,
pages: previousPages.map((pageData) => {
const { data } = pageData;
const updateFeeds = data.feeds.map((feed) =>
feed.id === feedId
? {
...feed,
isBookmarked: !feed.isBookmarked,
}
: feed,
);
const updatedData = { ...data, feeds: updateFeeds };

return { ...pageData, data: updatedData };
}),
};
}
43 changes: 43 additions & 0 deletions src/features/feed-bookmark/test/useBookmarks.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { renderHook, act, waitFor } from '@testing-library/react';
import { expect, test, vi } from 'vitest';

import * as bookmarkModule from '@/shared/axios';
import { createQueryClientWrapper } from '@/shared/tests';

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

test('북마크 상태가 아닐 때, 북마크 버튼을 클릭하면 북마크 요청이 발생한다.', async () => {
// given
// requestBookmarkFeed 함수를 스파이한다.
const spy = vi.spyOn(bookmarkModule, 'requestBookmarkFeed');
const { result } = renderHook(() => useBookmarks(1, false), {
wrapper: createQueryClientWrapper(),
});

// requestBookmarkFeed가 호출되지 않았는지 확인
await waitFor(() => expect(spy).not.toHaveBeenCalled());

// when
// 좋아요 버튼 클릭
await act(async () => result.current.handleBookmarkFeed());

// then
// requestBookmarkFeed가 호출되었는지 확인
await waitFor(() => expect(spy).toHaveBeenCalled());
});

test('북마크 상태일 때, 북마크 버튼을 클릭하면 북마크 취소 요청이 발생한다.', async () => {
// given
const spy = vi.spyOn(bookmarkModule, 'requestUnbookmarkFeed');
const { result } = renderHook(() => useBookmarks(1, true), {
wrapper: createQueryClientWrapper(),
});

await waitFor(() => expect(spy).not.toHaveBeenCalled());

// when
await act(async () => result.current.handleBookmarkFeed());

// then
await waitFor(() => expect(spy).toHaveBeenCalled());
});
31 changes: 31 additions & 0 deletions src/features/feed-bookmark/ui/BookmarkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ICON_ACTIVE_COLOR } from '@/shared/consts';
import { Icon } from '@/shared/ui';

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

interface BookmarkButtonProps {
feedId: number;
isBookmarked: boolean;
}

export const BookmarkButton: React.FC<BookmarkButtonProps> = ({
feedId,
isBookmarked,
}) => {
const { handleBookmarkFeed, isPending } = useBookmarks(feedId, isBookmarked);

return (
<button
className='icon icon-btn'
onClick={() => handleBookmarkFeed()}
disabled={isPending}
>
<Icon
name='bookmark'
width='20'
height='20'
color={isBookmarked ? ICON_ACTIVE_COLOR : 'none'}
/>
</button>
);
};
1 change: 1 addition & 0 deletions src/features/feed-bookmark/ui/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { BookmarkButton } from './BookmarkButton';
2 changes: 1 addition & 1 deletion src/features/feed-main-like/api/useLikes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { requestLikeFeed, requestUnlikeFeed } from '@/shared/axios';
import { FeedsQueryData } from '@/shared/consts';
import { QUERY_KEYS } from '@/shared/react-query';
import { isErrorResponse } from '@/shared/utils';

import { FeedsQueryData } from '../consts';
import { updateLikeStatusInFeeds } from '../lib';

export const useLikes = (feedId: number, isLiked: boolean) => {
Expand Down
6 changes: 0 additions & 6 deletions src/features/feed-main-like/consts/index.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/features/feed-main-like/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FeedsQueryData } from '../consts';
import { FeedsQueryData } from '@/shared/consts';

export function updateLikeStatusInFeeds(
previousQueryData: FeedsQueryData,
Expand Down
23 changes: 23 additions & 0 deletions src/shared/axios/bookmark/bookmark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { axiosInstance } from '../config';

/**
* 피드 북마크 API
* @param feedId 피드 아이디
* @returns 피드 북마크 상태
*/
export async function requestBookmarkFeed(feedId: number) {
const { data } = await axiosInstance.put(`/feeds/${feedId}/bookmarks`);

return data;
}

/**
* 피드 북마크 취소 API
* @param feedId 피드 아이디
* @returns 피드 북마크 상태
*/
export async function requestUnbookmarkFeed(feedId: number) {
const { data } = await axiosInstance.delete(`/feeds/${feedId}/bookmarks`);

return data;
}
1 change: 1 addition & 0 deletions src/shared/axios/bookmark/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './bookmark';
1 change: 1 addition & 0 deletions src/shared/axios/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export { axiosInstance } from './config';
export * from './like';
export * from './bookmark';
Loading
Loading