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/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..f9cabce0 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; @@ -76,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/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..9b1aa11c 100644 --- a/src/features/study/group/channel/ui/comment-section.tsx +++ b/src/features/study/group/channel/ui/comment-section.tsx @@ -1,27 +1,54 @@ +import { useQueries } from '@tanstack/react-query'; 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'; import { ThreadReaction } from './thread-reaction'; +import { getComments } from '../api/get-comments'; import { usePostThreadMutation, 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); // 👈 변경 + 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(); @@ -38,7 +65,26 @@ export default function CommentSection({ groupStudyId }: CommentProps) { ); }; - if (isLoading) return null; // 👈 안전 반환 + // threads 로딩 중이거나 답글 로딩 중이면 대기 + if (isLoading || !allCommentsLoaded) return null; + + if (isError) { + return ( +
+ + 댓글을 불러오는 중 오류가 발생했습니다. + + +
+ ); + } + + const hasComments = (data?.content?.length ?? 0) > 0; return (
@@ -46,12 +92,20 @@ export default function CommentSection({ groupStudyId }: CommentProps) { 댓글 - {data?.length ?? 0}개 + {totalCommentCount}개
- {data?.map((comment) => { + {!hasComments && ( +
+ + 아직 댓글이 없습니다. 첫 번째 댓글을 남겨보세요! + +
+ )} + + {data?.content?.map((comment) => { const isOpen = openThreadId === comment.threadId; const toggle = () => setOpenThreadId((prev) => @@ -89,8 +143,8 @@ export default function CommentSection({ groupStudyId }: CommentProps) {
@@ -104,6 +158,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..6e56a7f8 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; @@ -24,9 +25,12 @@ export default function SubComments({ const { data, isLoading, + isError, refetch: commentsRefetch, } = useCommentsQuery(groupStudyId, threadId); + console.log(data, 'subcomment'); + const [commentText, setCommentText] = useState(''); const { mutate: createComment } = usePostCommentMutation(); @@ -48,9 +52,27 @@ export default function SubComments({ if (isLoading) return null; + if (isError) { + return ( +
+ + 답글을 불러오지 못했습니다. + + +
+ ); + } + return (
- {data.map((subComment) => ( + {data?.content?.map((subComment) => (