diff --git a/src/app/group/[groupId]/page.tsx b/src/app/group/[groupId]/page.tsx
index e3522157..cc0d40d5 100644
--- a/src/app/group/[groupId]/page.tsx
+++ b/src/app/group/[groupId]/page.tsx
@@ -1,14 +1,16 @@
'use client';
import Link from 'next/link';
+
+import { isAuthed } from '@/utils/helpers';
+import { KAKAO_LOGIN_URL } from '@/constants/url';
+
import SSRSafeSuspense from '@/components/SSRSafeSuspense';
import BookGroupInfo from '@/v1/bookGroup/detail/BookGroupInfo';
-import CommentList from '@/v1/bookGroup/detail/CommentList';
+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 { isAuthed } from '@/utils/helpers';
-import { KAKAO_LOGIN_URL } from '@/constants/url';
const DetailBookGroupPage = ({
params: { groupId },
@@ -30,7 +32,7 @@ const DetailBookGroupPage = ({
-
+
{isAuthed() ? (
diff --git a/src/queries/book/key.ts b/src/queries/book/key.ts
index 7a93c4c5..b6d4e61e 100644
--- a/src/queries/book/key.ts
+++ b/src/queries/book/key.ts
@@ -8,6 +8,8 @@ const bookKeys = {
bestSeller: () => [...bookKeys.all, 'bestSeller'],
bookmark: (bookId: APIBook['bookId']) =>
[...bookKeys.detail(bookId), 'bookmark'] as const,
+ comments: (bookId: APIBook['bookId']) =>
+ [...bookKeys.detail(bookId), 'comments'] as const,
};
export default bookKeys;
diff --git a/src/queries/book/useBookCommentsQuery.ts b/src/queries/book/useBookCommentsQuery.ts
index 82738f80..8d90b437 100644
--- a/src/queries/book/useBookCommentsQuery.ts
+++ b/src/queries/book/useBookCommentsQuery.ts
@@ -1,19 +1,49 @@
-import { UseQueryOptions, useQuery } from '@tanstack/react-query';
+import { UseQueryOptions } from '@tanstack/react-query';
+import type {
+ APIBook,
+ APIBookCommentPagination,
+ BookComment,
+} from '@/types/book';
import bookAPI from '@/apis/book';
-import type { APIBook } from '@/types/book';
+import useQueryWithSuspense from '@/hooks/useQueryWithSuspense';
+import bookKeys from './key';
-const useBookCommentsQuery = (
+const useBookCommentsQuery = (
bookId: APIBook['bookId'],
- options?: Pick<
- UseQueryOptions>['data']>,
- 'onSuccess' | 'onError'
+ options?: UseQueryOptions<
+ Awaited>['data'],
+ unknown,
+ TData
>
) =>
- useQuery(
- ['bookComments', bookId],
+ useQueryWithSuspense(
+ bookKeys.comments(bookId),
() => bookAPI.getComments(bookId).then(({ data }) => data),
options
);
export default useBookCommentsQuery;
+
+const transformBookCommentsData = ({
+ bookComments,
+}: APIBookCommentPagination) => {
+ return bookComments.map(
+ ({ contents, createdAt, commentId, userId, userProfileImage, nickname }) =>
+ ({
+ id: commentId,
+ writer: {
+ id: userId,
+ profileImageSrc: userProfileImage,
+ name: nickname,
+ },
+ createdAt,
+ content: contents,
+ } as BookComment)
+ );
+};
+
+export const useBookComments = (bookId: APIBook['bookId']) =>
+ useBookCommentsQuery(bookId, {
+ select: transformBookCommentsData,
+ });
diff --git a/src/stories/comment/CommentList.stories.tsx b/src/stories/comment/CommentList.stories.tsx
new file mode 100644
index 00000000..21e53162
--- /dev/null
+++ b/src/stories/comment/CommentList.stories.tsx
@@ -0,0 +1,58 @@
+import { Meta, StoryObj } from '@storybook/react';
+import CommentList from '@/v1/comment/CommentList';
+
+const meta: Meta = {
+ title: 'comment/CommentList',
+ component: CommentList,
+ tags: ['autodocs'],
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+const comments = [
+ {
+ id: 1,
+ writer: {
+ id: 2,
+ profileImageSrc: 'https://bit.ly/kent-c-dodds',
+ name: 'Kent C. Dodds',
+ },
+ createdAt: '2023.02.05',
+ content: '추천해요!',
+ },
+ {
+ id: 2,
+ writer: {
+ id: 3,
+ profileImageSrc: 'https://i.pravatar.cc/300',
+ name: '김계란',
+ },
+ createdAt: '2023.02.07',
+ content: '읽고 또 읽어도 새로워요. 🫠',
+ },
+];
+
+export const Default: Story = {
+ args: {
+ comments,
+ isEditableComment: ({ writer }) => writer.id === 3,
+ },
+};
+
+export const Hidden: Story = {
+ args: {
+ comments,
+ isHidden: true,
+ hiddenText: '멤버만 볼 수 있어요 🥲',
+ },
+};
+
+export const Empty: Story = {
+ args: {
+ comments: [],
+ emptyText: `아직 코멘트가 없어요.
+ 첫 코멘트의 주인공이 되어보세요!`,
+ },
+};
diff --git a/src/types/book.ts b/src/types/book.ts
index 4d52378c..aa89b209 100644
--- a/src/types/book.ts
+++ b/src/types/book.ts
@@ -1,6 +1,6 @@
import { BookSearchPagination, Pagination } from './common';
import { APIJobGroup } from './job';
-import { APIUser } from './user';
+import { APIUser, Writer } from './user';
export interface APIBook {
bookId: number;
@@ -82,6 +82,12 @@ export interface APIBookCommentPagination extends Pagination {
bookComments: APIBookComment[];
}
+export type BookComment = {
+ id: APIBook['bookId'];
+ writer: Writer;
+ createdAt: APIBookComment['createdAt'];
+ content: APIBookComment['contents'];
+};
export interface APIBestSeller {
isbn: string;
title: string;
diff --git a/src/types/group.ts b/src/types/group.ts
index f9d8b596..998f138a 100644
--- a/src/types/group.ts
+++ b/src/types/group.ts
@@ -1,5 +1,5 @@
import { APIBook } from './book';
-import { APIUser } from './user';
+import { APIUser, Writer } from './user';
import { Pagination } from './common';
type APIGroupOwner = {
@@ -91,11 +91,7 @@ export type BookGroupDetail = {
export type BookGroupComment = {
id: APIGroup['bookGroupId'];
- writer: {
- id: APIUser['userId'];
- profileImageSrc: APIUser['profileImage'];
- name: APIUser['nickname'];
- };
+ writer: Writer;
createdAt: APIGroupComment['createdAt'];
content: APIGroupComment['contents'];
};
diff --git a/src/types/user.ts b/src/types/user.ts
index 1c441a71..8420e898 100644
--- a/src/types/user.ts
+++ b/src/types/user.ts
@@ -18,3 +18,9 @@ export interface APIMyProfile extends Omit {
}
export type APIUser = APIUserProfile & { name: string | null };
+
+export type Writer = {
+ id: APIUser['userId'];
+ profileImageSrc: APIUser['profileImage'];
+ name: APIUser['nickname'];
+};
diff --git a/src/v1/bookGroup/detail/CommentList.tsx b/src/v1/bookGroup/detail/CommentList.tsx
deleted file mode 100644
index 9c3eafc9..00000000
--- a/src/v1/bookGroup/detail/CommentList.tsx
+++ /dev/null
@@ -1,74 +0,0 @@
-import { IconHamburger } from '@public/icons';
-import Avatar from '@/v1/base/Avatar';
-import { useBookGroupComments } from '@/queries/group/useBookGroupCommentsQuery';
-import { useMyProfileId } from '@/queries/user/useMyProfileQuery';
-import { isAuthed } from '@/utils/helpers';
-import { useBookGroup } from '@/queries/group/useBookGroupQuery';
-
-const CommentList = ({ groupId }: { groupId: number }) => {
- const { data: bookGroupInfo } = useBookGroup(groupId);
- const { data: comments } = useBookGroupComments(groupId);
- const { data: myId } = useMyProfileId({ enabled: isAuthed() });
- const { isPublic, isMember } = bookGroupInfo;
-
- if (!isPublic && !isMember) {
- return (
-
- {`멤버만 볼 수 있어요 🥲`}
-
- );
- }
-
- if (comments.length === 0) {
- return (
-
- {`아직 게시글이 없어요.
- 가장 먼저 게시글을 남겨보세요!`}
-
- );
- }
-
- return (
-
- {comments.map(({ id, writer, createdAt, content }) => (
-
-
-
-
-
-
-
- {writer.id === myId &&
}
-
-
-
- ))}
-
- );
-};
-
-export default CommentList;
-
-const Name = ({ name }: { name: string }) => (
- {name}
-);
-
-const Date = ({ date }: { date: string }) => (
- {date}
-);
-
-const MenuButton = () => {
- return (
-
-
-
- );
-};
-
-const Comment = ({ content }: { content: string }) => (
- {content}
-);
diff --git a/src/v1/comment/BookCommentList.tsx b/src/v1/comment/BookCommentList.tsx
new file mode 100644
index 00000000..6d44e6d1
--- /dev/null
+++ b/src/v1/comment/BookCommentList.tsx
@@ -0,0 +1,22 @@
+import { useMyProfileId } from '@/queries/user/useMyProfileQuery';
+import { useBookComments } from '@/queries/book/useBookCommentsQuery';
+import { isAuthed } from '@/utils/helpers';
+
+import CommentList from './CommentList';
+
+const BookCommentList = ({ bookId }: { bookId: number }) => {
+ const { data: comments } = useBookComments(bookId);
+ const { data: myId } = useMyProfileId({ enabled: isAuthed() });
+
+ return (
+ writer.id === myId}
+ emptyText={`아직 코멘트가 없어요.
+ 가장 먼저 코멘트를 남겨보세요!`}
+ />
+ );
+};
+
+export default BookCommentList;
diff --git a/src/v1/comment/BookGroupCommentList.tsx b/src/v1/comment/BookGroupCommentList.tsx
new file mode 100644
index 00000000..4eee893d
--- /dev/null
+++ b/src/v1/comment/BookGroupCommentList.tsx
@@ -0,0 +1,29 @@
+import { useBookGroupComments } from '@/queries/group/useBookGroupCommentsQuery';
+import { useMyProfileId } from '@/queries/user/useMyProfileQuery';
+import { useBookGroup } from '@/queries/group/useBookGroupQuery';
+import { isAuthed } from '@/utils/helpers';
+
+import CommentList from './CommentList';
+
+const BookGroupCommentList = ({ groupId }: { groupId: number }) => {
+ const { data: bookGroupInfo } = useBookGroup(groupId);
+ const { data: comments } = useBookGroupComments(groupId);
+ const { data: myId } = useMyProfileId({ enabled: isAuthed() });
+ const { isPublic, isMember } = bookGroupInfo;
+
+ const isHidden = !isPublic && !isMember;
+
+ return (
+ writer.id === myId}
+ isHidden={isHidden}
+ hiddenText={`멤버만 볼 수 있어요 🥲`}
+ emptyText={`아직 게시글이 없어요.
+ 가장 먼저 게시글을 남겨보세요!`}
+ />
+ );
+};
+
+export default BookGroupCommentList;
diff --git a/src/v1/comment/CommentList.tsx b/src/v1/comment/CommentList.tsx
new file mode 100644
index 00000000..1edf6555
--- /dev/null
+++ b/src/v1/comment/CommentList.tsx
@@ -0,0 +1,237 @@
+import { useMemo } from 'react';
+
+import type { Writer } from '@/types/user';
+import useDisclosure from '@/hooks/useDisclosure';
+
+import Avatar from '@/v1/base/Avatar';
+import Menu from '@/v1/base/Menu';
+import Button from '@/v1/base/Button';
+import Drawer from '@/v1/base/Drawer';
+import Modal from '@/v1/base/Modal';
+
+type Comment = {
+ id: number;
+ writer: Writer;
+ createdAt: string;
+ content: string;
+};
+
+interface CommentListProps {
+ comments: Comment[];
+ name?: string;
+ isHidden?: boolean;
+ hiddenText?: string;
+ emptyText?: string;
+ isEditableComment?: (comment: Comment) => boolean;
+ onEditConfirm?: (commentId: Comment['id']) => void;
+ onDeleteConfirm?: (commentId: Comment['id']) => void;
+}
+
+const CommentList = ({
+ name = '코멘트',
+ comments,
+ isHidden,
+ hiddenText,
+ emptyText,
+ isEditableComment,
+ onEditConfirm,
+ onDeleteConfirm,
+}: CommentListProps) => {
+ const titleOnCommentEdit = useMemo(
+ () => [name, '수정하기'].join(' '),
+ [name]
+ );
+
+ if (isHidden) {
+ return {hiddenText}
;
+ }
+
+ if (comments.length === 0) {
+ return (
+
+ {emptyText}
+
+ );
+ }
+
+ return (
+
+ {comments.map(comment => {
+ const { id, writer, createdAt, content } = comment;
+ return (
+ -
+
+
+
+
+
+
+ {isEditableComment && isEditableComment(comment) && (
+
+ )}
+
+
+
+ );
+ })}
+
+ );
+};
+
+export default CommentList;
+
+const Name = ({ name }: { name: string }) => (
+ {name}
+);
+
+const Date = ({ date }: { date: string }) => (
+ {date}
+);
+
+const CommentContent = ({ content }: { content: string }) => (
+ {content}
+);
+
+const CommentActionMenu = ({
+ comment,
+ titleOnCommentEdit,
+ onEditConfirm,
+ onDeleteConfirm,
+}: {
+ comment: Comment;
+ titleOnCommentEdit?: string;
+ onEditConfirm?: (commentId: Comment['id']) => void;
+ onDeleteConfirm?: (commentId: Comment['id']) => void;
+}) => {
+ const { id: commentId, content } = comment;
+
+ const {
+ isOpen: isDrawerOpen,
+ onOpen: onDrawerOpen,
+ onClose: onDrawerClose,
+ } = useDisclosure();
+
+ const {
+ isOpen: isModalOpen,
+ onOpen: onModalOpen,
+ onClose: onModalClose,
+ } = useDisclosure();
+
+ const handleChangeConfirm = () => {
+ onEditConfirm && onEditConfirm(commentId);
+ };
+
+ const handleDeleteConfirm = () => {
+ onDeleteConfirm && onDeleteConfirm(commentId);
+ };
+
+ return (
+ <>
+
+
+
+ >
+ );
+};
+
+const EditCommentDrawer = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ defaultComment,
+ drawerTitle = '수정하기',
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm?: () => void;
+ drawerTitle?: string;
+ defaultComment?: string;
+}) => {
+ const handleConfirm = () => {
+ onConfirm && onConfirm();
+ onClose();
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const DeleteCommentModal = ({
+ isOpen,
+ onClose,
+ onConfirm,
+}: {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm?: () => void;
+}) => {
+ const handleConfirm = () => {
+ onConfirm && onConfirm();
+ onClose();
+ };
+
+ return (
+
+
+ 정말 삭제할까요?
+
+ 한번 삭제하면 되돌릴 수 없어요.
+
+
+
+
+
+
+
+ );
+};