Skip to content

Commit

Permalink
[#497] [책 상세] 책 상세 페이지 api 연결 (#500)
Browse files Browse the repository at this point in the history
* feat: bookmark 기능 구현

- api, query 함수명에 명확히 bookmark를 포함하도록 수정

* feat: 책 코멘트 작성 기능 구현

* feat: 코멘트 수정 기능 구현

* refactor: 코멘트 수정 mutation 파라미터 구조 수정

* feat: 코멘트 삭제 기능 구현

* fix: 코드리뷰 반영
  • Loading branch information
gxxrxn committed Jun 17, 2024
1 parent e6f7878 commit 7842028
Show file tree
Hide file tree
Showing 18 changed files with 359 additions and 95 deletions.
11 changes: 6 additions & 5 deletions src/apis/book/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
APIBookCommentPagination,
APIBookDetail,
APIBookmarkedUserList,
APICreateBookCommentRequest,
APIPatchBookCommentRequest,
APISearchedBook,
APISearchedBookPagination,
Expand Down Expand Up @@ -37,17 +38,17 @@ const bookAPI = {
getBookInfo: (bookId: APIBook['bookId']) =>
publicApi.get<APIBookDetail>(`/service-api/books/${bookId}`),

getBookUserInfo: (bookId: APIBook['bookId']) =>
getBookmarkUserInfo: (bookId: APIBook['bookId']) =>
publicApi.get<APIBookmarkedUserList>(`/service-api/books/${bookId}/users`),

createBook: ({ book }: { book: APISearchedBook }) =>
publicApi.post<Pick<APIBook, 'bookId'>>('/service-api/books', book),

creaetComment: (
bookId: APIBook['bookId'],
{ comment }: { comment: APIPatchBookCommentRequest['comment'] }
comment: APICreateBookCommentRequest['comment']
) =>
publicApi.post<APIPatchBookCommentRequest['comment']>(
publicApi.post<APICreateBookCommentRequest['comment']>(
`/service-api/books/${bookId}/comments`,
{ comment }
),
Expand Down Expand Up @@ -80,14 +81,14 @@ const bookAPI = {
commentId: APIBookComment['commentId']
) => publicApi.delete(`/service-api/books/${bookId}/comments/${commentId}`),

setBookMarked: (bookId: APIBook['bookId']) =>
addBookmark: (bookId: APIBook['bookId']) =>
bookshelfAPI.getMySummaryBookshelf().then(({ data: { bookshelfId } }) =>
publicApi.post(`/service-api/bookshelves/${bookshelfId}/books`, {
bookId,
})
),

unsetBookMarked: (bookId: APIBook['bookId']) =>
removeBookmark: (bookId: APIBook['bookId']) =>
bookshelfAPI
.getMySummaryBookshelf()
.then(({ data: { bookshelfId } }) =>
Expand Down
79 changes: 78 additions & 1 deletion src/app/book/[bookId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
'use client';
import { useRef } from 'react';

import { APIBook } from '@/types/book';
import { useBookTitle } from '@/queries/book/useBookInfoQuery';
import { useHasBookComment } from '@/queries/book/useBookCommentsQuery';
import useCreateBookCommentMutation from '@/queries/book/useCreateBookCommentMutation';
import useToast from '@/v1/base/Toast/useToast';
import useDisclosure from '@/hooks/useDisclosure';
import {
checkAuthentication,
isAxiosErrorWithCustomCode,
} from '@/utils/helpers';
import { SERVICE_ERROR_MESSAGE } from '@/constants';

import Skeleton from '@/v1/base/Skeleton';
import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import TopNavigation from '@/v1/base/TopNavigation';
import BottomActionButton from '@/v1/base/BottomActionButton';
import LoginBottomActionButton from '@/v1/base/LoginBottomActionButton';
import CommentDrawer from '@/v1/comment/CommentDrawer';
import BackButton from '@/v1/base/BackButton';
import BookInfo, { BookInfoSkeleton } from '@/v1/book/detail/BookInfo';
import BookCommentList from '@/v1/comment/BookCommentList';
Expand All @@ -16,6 +28,8 @@ const BookDetailPage = ({
}: {
params: { bookId: APIBook['bookId'] };
}) => {
const isAuthenticated = checkAuthentication();

return (
<>
<BookTopNavigation bookId={bookId} />
Expand All @@ -27,8 +41,12 @@ const BookDetailPage = ({
<BookCommentList bookId={bookId} />
</div>
</div>
{isAuthenticated ? (
<AddBookCommentButton bookId={bookId} />
) : (
<LoginBottomActionButton />
)}
</SSRSafeSuspense>
<BottomActionButton>코멘트 작성하기</BottomActionButton>
</>
);
};
Expand Down Expand Up @@ -63,6 +81,65 @@ const Heading = ({ text }: { text: string }) => (
<p className="text-xl font-bold">{text}</p>
);

const AddBookCommentButton = ({ bookId }: { bookId: APIBook['bookId'] }) => {
const {
isOpen: isDrawerOpen,
onOpen: onDrawerOpen,
onClose: onDrawerClose,
} = useDisclosure();
const { show: showToast } = useToast();

const commentRef = useRef<HTMLTextAreaElement>(null);
const createComment = useCreateBookCommentMutation(bookId);

const { data: hasBookComment } = useHasBookComment(bookId);

const handleCommentCreate = () => {
const comment = commentRef.current?.value;

if (!comment) {
return;
}

createComment.mutate(comment, {
onSuccess: () => {
onDrawerClose();
showToast({ type: 'success', message: '코멘트를 등록했어요 🎉' });
},
onError: error => {
if (isAxiosErrorWithCustomCode(error)) {
const { code } = error.response.data;
const message = SERVICE_ERROR_MESSAGE[code];
showToast({ type: 'error', message });
return;
}

showToast({ type: 'error', message: '코멘트를 등록하지 못했어요 🥲' });
},
});
};

if (hasBookComment) {
return null;
}

return (
<>
<BottomActionButton onClick={onDrawerOpen}>
코멘트 작성하기
</BottomActionButton>
<CommentDrawer
isOpen={isDrawerOpen}
onClose={onDrawerClose}
title="책 코멘트 작성하기"
placeholder="작성해주신 코멘트가 다른 사람들에게 많은 도움이 될 거예요!"
onConfirm={handleCommentCreate}
ref={commentRef}
/>
</>
);
};

const BookTitleSkeleton = () => (
<Skeleton>
<Skeleton.Text fontSize="medium" width="20rem" />
Expand Down
11 changes: 1 addition & 10 deletions src/app/group/[groupId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
'use client';

import Link from 'next/link';

import { checkAuthentication } from '@/utils/helpers';
import { KAKAO_LOGIN_URL } from '@/constants/url';

import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import BookGroupInfo from '@/v1/bookGroup/detail/BookGroupInfo';
import BookGroupCommentList from '@/v1/comment/BookGroupCommentList';
import BookGroupNavigation from '@/v1/bookGroup/BookGroupNavigation';
import JoinBookGroupButton from '@/v1/bookGroup/detail/JoinBookGroupButton';
import BottomActionButton from '@/v1/base/BottomActionButton';
import LoginBottomActionButton from '@/v1/base/LoginBottomActionButton';

const DetailBookGroupPage = ({
params: { groupId },
Expand Down Expand Up @@ -65,9 +62,3 @@ const PageSkeleton = () => (
);

const Divider = () => <p className="w-app h-[0.5rem] bg-background"></p>;

const LoginBottomActionButton = () => (
<Link href={KAKAO_LOGIN_URL}>
<BottomActionButton>로그인 및 회원가입</BottomActionButton>
</Link>
);
1 change: 0 additions & 1 deletion src/components/ReactQueryProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ const ReactQueryProvider: NextPage<PropTypes> = ({ children }) => {
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: true,
retry: 0,
},
},
Expand Down
17 changes: 0 additions & 17 deletions src/queries/book/useBookCommentPatchMutation.ts

This file was deleted.

7 changes: 7 additions & 0 deletions src/queries/book/useBookCommentsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ export const useBookComments = (bookId: APIBook['bookId']) =>
useBookCommentsQuery(bookId, {
select: transformBookCommentsData,
});

export const useHasBookComment = (bookId: APIBook['bookId']) =>
useBookCommentsQuery(bookId, {
select: ({ bookComments }) =>
bookComments.filter(comment => comment.writtenByCurrentUser === true)
.length,
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import bookAPI from '@/apis/book';
import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
import bookKeys from './key';

const useBookUserInfoQuery = <TData = APIBookmarkedUserList>(
const useBookmarkUserQuery = <TData = APIBookmarkedUserList>(
bookId: number,
options?: UseQueryOptions<APIBookmarkedUserList, unknown, TData>
) =>
useQueryWithSuspense(
bookKeys.bookmark(bookId),
() => bookAPI.getBookUserInfo(bookId).then(({ data }) => data),
() => bookAPI.getBookmarkUserInfo(bookId).then(({ data }) => data),
options
);

export default useBookUserInfoQuery;
export default useBookmarkUserQuery;
19 changes: 19 additions & 0 deletions src/queries/book/useCreateBookCommentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { APIBook } from '@/types/book';
import bookAPI from '@/apis/book';
import bookKeys from './key';

const useCreateBookCommentMutation = (bookId: APIBook['bookId']) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (newComment: string) =>
bookAPI.creaetComment(bookId, newComment).then(({ data }) => data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.comments(bookId) });
},
});
};

export default useCreateBookCommentMutation;
19 changes: 19 additions & 0 deletions src/queries/book/useDeleteBookCommentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { APIBook, APIBookComment } from '@/types/book';
import bookAPI from '@/apis/book';
import bookKeys from './key';

const useDeleteBookCommentMutation = (bookId: APIBook['bookId']) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (commentId: APIBookComment['commentId']) =>
bookAPI.deletComment(bookId, commentId).then(({ data }) => data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.comments(bookId) });
},
});
};

export default useDeleteBookCommentMutation;
21 changes: 21 additions & 0 deletions src/queries/book/usePatchBookCommentMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import { APIBook, APIBookComment } from '@/types/book';
import bookAPI from '@/apis/book';
import bookKeys from './key';

const usePatchBookCommentMutation = (bookId: APIBook['bookId']) => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: (data: {
commentId: APIBookComment['commentId'];
comment: string;
}) => bookAPI.patchComment({ bookId, data }).then(({ data }) => data),
onSettled: () => {
queryClient.invalidateQueries({ queryKey: bookKeys.comments(bookId) });
},
});
};

export default usePatchBookCommentMutation;
57 changes: 57 additions & 0 deletions src/queries/book/useUpdateBookmarkMutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';

import type { APIBook, APIBookmarkedUserList } from '@/types/book';
import bookAPI from '@/apis/book';
import bookKeys from './key';

const updateBookmark = ({
bookId,
newValue,
}: {
bookId: APIBook['bookId'];
newValue: boolean;
}) => {
return newValue
? bookAPI.addBookmark(bookId).then(({ data }) => data)
: bookAPI.removeBookmark(bookId).then(({ data }) => data);
};

const useUpdateBookmarkMutation = (bookId: APIBook['bookId']) => {
const queryClient = useQueryClient();
const bookmarkQueryKey = bookKeys.bookmark(bookId);

return useMutation({
mutationFn: (newValue: boolean) => updateBookmark({ bookId, newValue }),
onMutate: async newValue => {
await queryClient.cancelQueries({ queryKey: bookmarkQueryKey });

const previousData =
queryClient.getQueryData<APIBookmarkedUserList>(bookmarkQueryKey);

if (previousData) {
// 낙관적 업데이트
queryClient.setQueryData<APIBookmarkedUserList>(bookmarkQueryKey, {
...previousData,
isInMyBookshelf: newValue,
});
}

return { previousData };
},
onError: (_err, _variables, context) => {
if (!context || !context.previousData) {
return;
}

queryClient.setQueryData<APIBookmarkedUserList>(
bookmarkQueryKey,
context.previousData
);
},
onSettled: () => {
queryClient.invalidateQueries({ queryKey: bookmarkQueryKey });
},
});
};

export default useUpdateBookmarkMutation;
5 changes: 5 additions & 0 deletions src/types/book.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ export interface APIBookComment {
writtenByCurrentUser: boolean;
}

export interface APICreateBookCommentRequest
extends Pick<APIBookComment, 'commentId'> {
comment: string;
}

export interface APIPatchBookCommentRequest
extends Pick<APIBookComment, 'commentId'> {
comment: string;
Expand Down
2 changes: 1 addition & 1 deletion src/ui/BookDetail/BookCommentList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ const BookCommentList = ({ bookId, isInMyBookshelf }: Props) => {
}

bookAPI
.creaetComment(bookId, { comment })
.creaetComment(bookId, comment)
.then(() => bookCommentsQueryInfo.refetch())
.catch(handleCommentError)
.finally(onCreateDrawerClose);
Expand Down
12 changes: 12 additions & 0 deletions src/v1/base/LoginBottomActionButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Link from 'next/link';

import { KAKAO_LOGIN_URL } from '@/constants/url';
import BottomActionButton from './BottomActionButton';

const LoginBottomActionButton = () => (
<Link href={KAKAO_LOGIN_URL}>
<BottomActionButton>로그인 및 회원가입</BottomActionButton>
</Link>
);

export default LoginBottomActionButton;
Loading

0 comments on commit 7842028

Please sign in to comment.