From 59514c00fdd2e6c8a3fb9fc9d5da684ff40d12ee Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 2 Feb 2026 11:41:40 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EC=8A=A4=EB=A0=88=EB=93=9C=20?= =?UTF-8?q?=EB=B0=8F=20=EB=8C=93=EA=B8=80=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0,=20=ED=8E=98=EC=9D=B4=EC=A7=80?= =?UTF-8?q?=EB=84=A4=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20=EB=A9=94=EC=8B=9C?= =?UTF-8?q?=EC=A7=80=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/channel/api/get-threads.ts | 13 ++--- src/features/study/group/channel/api/types.ts | 11 ++++- .../group/channel/model/use-channel-query.ts | 21 ++++++-- .../group/channel/ui/comment-section.tsx | 49 +++++++++++++++++-- .../study/group/channel/ui/sub-comments.tsx | 19 ++++++- 5 files changed, 96 insertions(+), 17 deletions(-) diff --git a/src/features/study/group/channel/api/get-threads.ts b/src/features/study/group/channel/api/get-threads.ts index 5297a279..8c7b3c09 100644 --- a/src/features/study/group/channel/api/get-threads.ts +++ b/src/features/study/group/channel/api/get-threads.ts @@ -1,23 +1,24 @@ import { axiosInstance } from '@/api/client/axios'; -import { GetThreadsRequest, GetThreadsResponse } from './types'; +import { GetThreadsRequest, PaginatedThreadsResponse } from './types'; export const getThreads = async ( param: GetThreadsRequest, -): Promise => { - const { groupStudyId } = param; +): Promise => { + const { groupStudyId, page = 0, size = 10 } = param; try { const { data } = await axiosInstance.get( `group-studies/${groupStudyId}/threads`, + { params: { page, size } }, ); if (data.statusCode !== 200) { - throw new Error('Failed to fetch post'); + throw new Error('Failed to fetch threads'); } - return data.content.content; + return data.content; } catch (err) { - console.error('Error fetching post:', err); + console.error('Error fetching threads:', err); throw err; } }; diff --git a/src/features/study/group/channel/api/types.ts b/src/features/study/group/channel/api/types.ts index 50358b63..dd67311f 100644 --- a/src/features/study/group/channel/api/types.ts +++ b/src/features/study/group/channel/api/types.ts @@ -28,7 +28,16 @@ export interface DeleteCommentRequest extends GetCommentsRequest { commentId: number; } -export type GetThreadsRequest = GroupStudyIdParam; +export interface GetThreadsRequest extends GroupStudyIdParam { + page?: number; + size?: number; +} + +export interface PaginatedThreadsResponse { + content: GetThreadsResponse[]; + totalElements: number; + totalPages: number; +} export interface PostThreadRequest extends GroupStudyIdParam { content: string; diff --git a/src/features/study/group/channel/model/use-channel-query.ts b/src/features/study/group/channel/model/use-channel-query.ts index e7599094..3ff07fae 100644 --- a/src/features/study/group/channel/model/use-channel-query.ts +++ b/src/features/study/group/channel/model/use-channel-query.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery } from '@tanstack/react-query'; +import { keepPreviousData, useMutation, useQuery } from '@tanstack/react-query'; import { deleteComment } from '../api/delete-comment'; import { deleteThread } from '../api/delete-thread'; import { getComments } from '../api/get-comments'; @@ -31,11 +31,22 @@ export const usePostQuery = (groupStudyId: number) => { }; // thread -export const useThreadsQuery = (groupStudyId: number) => { +interface UseThreadsQueryParams { + groupStudyId: number; + page?: number; + size?: number; +} + +export const useThreadsQuery = ({ + groupStudyId, + page = 1, + size = 10, +}: UseThreadsQueryParams) => { return useQuery({ - queryKey: ['get-threads', groupStudyId], - queryFn: () => getThreads({ groupStudyId }), - enabled: !!groupStudyId, // id가 존재할 때만 실행 + queryKey: ['get-threads', groupStudyId, page, size], + queryFn: () => getThreads({ groupStudyId, page: page - 1, size }), + enabled: !!groupStudyId, + placeholderData: keepPreviousData, }); }; diff --git a/src/features/study/group/channel/ui/comment-section.tsx b/src/features/study/group/channel/ui/comment-section.tsx index e9b2f070..e134e144 100644 --- a/src/features/study/group/channel/ui/comment-section.tsx +++ b/src/features/study/group/channel/ui/comment-section.tsx @@ -1,5 +1,6 @@ import { MessageCircle } from 'lucide-react'; import { useState } from 'react'; +import Pagination from '@/components/ui/pagination'; import Comment from './comment'; import CommentInput from './comment-input'; import SubComments from './sub-comments'; @@ -9,16 +10,20 @@ import { useThreadsQuery, } from '../model/use-channel-query'; +const COMMENTS_PAGE_SIZE = 10; + interface CommentProps { groupStudyId: number; } export default function CommentSection({ groupStudyId }: CommentProps) { + const [page, setPage] = useState(1); const { data, isLoading, + isError, refetch: threadRefetch, - } = useThreadsQuery(groupStudyId); + } = useThreadsQuery({ groupStudyId, page, size: COMMENTS_PAGE_SIZE }); const [threadText, setThreadText] = useState(''); const [openThreadId, setOpenThreadId] = useState(null); // 👈 변경 @@ -30,6 +35,7 @@ export default function CommentSection({ groupStudyId }: CommentProps) { { groupStudyId, content }, { onSuccess: async () => { + setPage(1); await threadRefetch(); setThreadText(''); }, @@ -38,7 +44,25 @@ export default function CommentSection({ groupStudyId }: CommentProps) { ); }; - if (isLoading) return null; // 👈 안전 반환 + if (isLoading) return null; + + if (isError) { + return ( +
+ + 댓글을 불러오는 중 오류가 발생했습니다. + + +
+ ); + } + + const hasComments = (data?.content?.length ?? 0) > 0; return (
@@ -46,12 +70,20 @@ export default function CommentSection({ groupStudyId }: CommentProps) { 댓글 - {data?.length ?? 0}개 + {data?.totalElements ?? 0}개
- {data?.map((comment) => { + {!hasComments && ( +
+ + 아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요! + +
+ )} + + {data?.content?.map((comment) => { const isOpen = openThreadId === comment.threadId; const toggle = () => setOpenThreadId((prev) => @@ -104,6 +136,15 @@ export default function CommentSection({ groupStudyId }: CommentProps) { onConfirm={() => handleThreadSubmit(groupStudyId, threadText)} onCancel={() => setThreadText('')} /> + + {(data?.totalPages ?? 0) > 1 && ( + + )}
); diff --git a/src/features/study/group/channel/ui/sub-comments.tsx b/src/features/study/group/channel/ui/sub-comments.tsx index b3cb7b59..48e8b888 100644 --- a/src/features/study/group/channel/ui/sub-comments.tsx +++ b/src/features/study/group/channel/ui/sub-comments.tsx @@ -24,6 +24,7 @@ export default function SubComments({ const { data, isLoading, + isError, refetch: commentsRefetch, } = useCommentsQuery(groupStudyId, threadId); @@ -48,9 +49,25 @@ export default function SubComments({ if (isLoading) return null; + if (isError) { + return ( +
+ + 답글을 불러오지 못했습니다. + + +
+ ); + } + return (
- {data.map((subComment) => ( + {data?.map((subComment) => (
Date: Mon, 2 Feb 2026 11:41:46 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=B2=84=ED=8A=BC=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= =?UTF-8?q?=ED=95=98=EC=97=AC=20=EC=98=A4=EB=A5=98=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=EC=97=90=20=EB=8B=A4=EC=8B=9C=20?= =?UTF-8?q?=EC=8B=9C=EB=8F=84=20=EB=B2=84=ED=8A=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/channel/ui/sub-comments.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/features/study/group/channel/ui/sub-comments.tsx b/src/features/study/group/channel/ui/sub-comments.tsx index 48e8b888..3b95396f 100644 --- a/src/features/study/group/channel/ui/sub-comments.tsx +++ b/src/features/study/group/channel/ui/sub-comments.tsx @@ -7,6 +7,7 @@ import { useCommentsQuery, usePostCommentMutation, } from '../model/use-channel-query'; +import Button from '@/components/ui/button'; interface SubCommentsProps { threadId: number; @@ -55,12 +56,14 @@ export default function SubComments({ 답글을 불러오지 못했습니다. - +
); } From dd67f7494d4ec6c078b62ab5b34af595342edeaa Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:44:38 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20=ED=9B=84=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=A5=BC=20?= =?UTF-8?q?=EC=B4=88=EA=B8=B0=ED=99=94=ED=95=98=EB=8A=94=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/channel/ui/comment-section.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/features/study/group/channel/ui/comment-section.tsx b/src/features/study/group/channel/ui/comment-section.tsx index e134e144..0687773b 100644 --- a/src/features/study/group/channel/ui/comment-section.tsx +++ b/src/features/study/group/channel/ui/comment-section.tsx @@ -35,7 +35,6 @@ export default function CommentSection({ groupStudyId }: CommentProps) { { groupStudyId, content }, { onSuccess: async () => { - setPage(1); await threadRefetch(); setThreadText(''); }, From c906fc5bffa7a71292b7c866743ecdcbac73423c Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Mon, 2 Feb 2026 14:44:16 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=EB=8C=93=EA=B8=80=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=EB=84=A4=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20=EB=8C=93=EA=B8=80=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=20=ED=83=80=EC=9E=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../study/group/channel/api/get-comments.ts | 10 +++--- src/features/study/group/channel/api/types.ts | 6 ++++ .../group/channel/ui/comment-section.tsx | 33 ++++++++++++++++--- .../study/group/channel/ui/sub-comments.tsx | 4 ++- 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/features/study/group/channel/api/get-comments.ts b/src/features/study/group/channel/api/get-comments.ts index a05f6193..fc554df5 100644 --- a/src/features/study/group/channel/api/get-comments.ts +++ b/src/features/study/group/channel/api/get-comments.ts @@ -1,9 +1,9 @@ import { axiosInstance } from '@/api/client/axios'; -import { GetCommentsRequest, GetCommentsResponse } from './types'; +import { GetCommentsRequest, PaginatedCommentsResponse } from './types'; export const getComments = async ( param: GetCommentsRequest, -): Promise => { +): Promise => { const { groupStudyId, threadId } = param; try { @@ -12,12 +12,12 @@ export const getComments = async ( ); if (data.statusCode !== 200) { - throw new Error('Failed to fetch post'); + throw new Error('Failed to fetch comments'); } - return data.content.content; + return data.content; } catch (err) { - console.error('Error fetching post:', err); + console.error('Error fetching comments:', err); throw err; } }; diff --git a/src/features/study/group/channel/api/types.ts b/src/features/study/group/channel/api/types.ts index dd67311f..f9cabce0 100644 --- a/src/features/study/group/channel/api/types.ts +++ b/src/features/study/group/channel/api/types.ts @@ -85,3 +85,9 @@ export interface GetPostResponse { export interface GetCommentsResponse extends GetThreadsResponse { commentId: number; } + +export interface PaginatedCommentsResponse { + content: GetCommentsResponse[]; + totalElements: number; + totalPages: number; +} diff --git a/src/features/study/group/channel/ui/comment-section.tsx b/src/features/study/group/channel/ui/comment-section.tsx index 0687773b..9b1aa11c 100644 --- a/src/features/study/group/channel/ui/comment-section.tsx +++ b/src/features/study/group/channel/ui/comment-section.tsx @@ -1,3 +1,4 @@ +import { useQueries } from '@tanstack/react-query'; import { MessageCircle } from 'lucide-react'; import { useState } from 'react'; import Pagination from '@/components/ui/pagination'; @@ -5,6 +6,7 @@ import Comment from './comment'; import CommentInput from './comment-input'; import SubComments from './sub-comments'; import { ThreadReaction } from './thread-reaction'; +import { getComments } from '../api/get-comments'; import { usePostThreadMutation, useThreadsQuery, @@ -26,7 +28,27 @@ export default function CommentSection({ groupStudyId }: CommentProps) { } = useThreadsQuery({ groupStudyId, page, size: COMMENTS_PAGE_SIZE }); const [threadText, setThreadText] = useState(''); - const [openThreadId, setOpenThreadId] = useState(null); // 👈 변경 + const [openThreadId, setOpenThreadId] = useState(null); + + // 모든 스레드의 답글을 병렬로 조회 + const commentQueries = useQueries({ + queries: (data?.content ?? []).map((thread) => ({ + queryKey: ['comments', groupStudyId, thread.threadId], + queryFn: () => getComments({ groupStudyId, threadId: thread.threadId }), + enabled: !!data, + })), + }); + + // 모든 답글 쿼리 로딩 완료 여부 + const allCommentsLoaded = commentQueries.every((q) => !q.isLoading); + + // 총 댓글 수 (메인 댓글 + 답글) + const totalReplyCount = commentQueries.reduce( + (sum, q) => sum + (q.data?.totalElements ?? 0), + 0, + ); + console.log(data.content, 'data'); + const totalCommentCount = (data?.totalElements ?? 0) + totalReplyCount; const { mutate: createThread } = usePostThreadMutation(); @@ -43,7 +65,8 @@ export default function CommentSection({ groupStudyId }: CommentProps) { ); }; - if (isLoading) return null; + // threads 로딩 중이거나 답글 로딩 중이면 대기 + if (isLoading || !allCommentsLoaded) return null; if (isError) { return ( @@ -69,7 +92,7 @@ export default function CommentSection({ groupStudyId }: CommentProps) { 댓글 - {data?.totalElements ?? 0}개 + {totalCommentCount}개
@@ -120,8 +143,8 @@ export default function CommentSection({ groupStudyId }: CommentProps) { diff --git a/src/features/study/group/channel/ui/sub-comments.tsx b/src/features/study/group/channel/ui/sub-comments.tsx index 3b95396f..6e56a7f8 100644 --- a/src/features/study/group/channel/ui/sub-comments.tsx +++ b/src/features/study/group/channel/ui/sub-comments.tsx @@ -29,6 +29,8 @@ export default function SubComments({ refetch: commentsRefetch, } = useCommentsQuery(groupStudyId, threadId); + console.log(data, 'subcomment'); + const [commentText, setCommentText] = useState(''); const { mutate: createComment } = usePostCommentMutation(); @@ -70,7 +72,7 @@ export default function SubComments({ return (
- {data?.map((subComment) => ( + {data?.content?.map((subComment) => (