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 ( + + + + + + + +