Skip to content

Commit

Permalink
✨ 피드 북마크 API 연동 (#52)
Browse files Browse the repository at this point in the history
* feat: 피드 모킹 데이터 북마크 여부 수정
* feat: 북마크 버튼 분리
* feat: 북마크 모킹 API 추가
* feat: isBookmark -> isBookmarked 네이밍 변경
* feat: FeedsQueryData 공용 폴더로 이동
* feat: 피드 북마크 기능
* feat: useBookmark public api 적용
* feat: bookmark -> boormkars 경로 수정
* feat: 피드 북마크 API 분리
* feat: 북마크 API 분리
* feat: 북마크 취소 API 연결
* feat: 피드 북마크 취소 API uri 수정
* feat: 주석 추가 및 함수명 수정
* test: 피드 북마크 테스트 코드
* style: 주석 수정

Closes #PW-298
  • Loading branch information
BangDori authored May 13, 2024
1 parent cb775c9 commit 2ca4aae
Show file tree
Hide file tree
Showing 19 changed files with 255 additions and 27 deletions.
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

0 comments on commit 2ca4aae

Please sign in to comment.