Skip to content

Commit

Permalink
댓글 페이지네이션을 구현한다. (SWYP-team-2th#143)
Browse files Browse the repository at this point in the history
* 댓글 페이지네이션을 구현한다.

* 파일 분리

* 로딩 처리
  • Loading branch information
YOOJS1205 committed Feb 28, 2025
1 parent 3e32c33 commit ba029c9
Show file tree
Hide file tree
Showing 15 changed files with 177 additions and 97 deletions.
8 changes: 0 additions & 8 deletions src/api/useAddComment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,9 @@ export default function useAddComment() {

return useMutation<AddCommentResponse, Error, AddCommentVariables>({
mutationFn: ({ postId, content }) => {
const accessToken =
localStorage.getItem('accessToken') ||
'eyJhbGciOiJIUzI1NiJ9.eyJpZCI6IjEiLCJpYXQiOjE3NDAyOTQyMzEsImlzcyI6InN3eXA4dGVhbTIiLCJleHAiOjMzMjc2Mjk0MjMxfQ.gqA245tRiBQB9owKRWIpX1we1T362R-xDTt4YT9AhRY';

return request({
method: 'POST',
url: `/posts/${postId}/comments`,
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json;charset=UTF-8',
},
data: {
content,
},
Expand Down
40 changes: 29 additions & 11 deletions src/api/useGetComment.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { UseQueryOptions, useSuspenseQuery } from '@tanstack/react-query';
import {
InfiniteData,
useInfiniteQuery,
UseInfiniteQueryOptions,
} from '@tanstack/react-query';
import { request } from './config';
import { Pageable } from '@/types/pageable';

interface AuthorType {
userId: number;
Expand All @@ -15,23 +20,36 @@ interface CommentType {
createdAt: string;
}

interface CommentsResponse {
nextCursor: number;
hasNext: boolean;
data: CommentType[];
}

export default function useGetComment(
postId: number,
options?: Omit<UseQueryOptions<CommentsResponse>, 'queryKey' | 'queryFn'>,
size: number,
options?: Partial<
UseInfiniteQueryOptions<
Pageable<CommentType>,
Error,
InfiniteData<Pageable<CommentType>, unknown>
>
>,
) {
return useSuspenseQuery<CommentsResponse>({
queryFn: () =>
return useInfiniteQuery<Pageable<CommentType>>({
queryFn: ({ pageParam = null }) =>
request({
method: 'GET',
url: `/posts/${postId}/comments`,
params: {
cursor: pageParam,
size,
},
}),
queryKey: ['comments', postId],
queryKey: ['comments', postId, size],
initialPageParam: null,
getNextPageParam: (lastPage) => {
if (!lastPage.hasNext || lastPage.data.length === 0) {
return undefined;
}

return lastPage.nextCursor;
},
...options,
});
}
29 changes: 29 additions & 0 deletions src/components/comment-detail/CommentList/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import useCommentDetail from '../hooks';
import Loading from '@/components/common/Loading';
import CommentItem from '@/components/vote-detail/Comment/CommentItems';

export default function CommentList() {
const { comments, observerRef, isLoading } = useCommentDetail();

return (
<div className="h-full overflow-y-auto">
{isLoading ? (
<Loading />
) : (
<>
<div className="text-title-large mt-lg pl-sm pb-[9px]">
한마디 ({comments?.length})
</div>
<hr className="text-gray-300 mb-[20px]" />

<div>
{comments?.map((comment) => (
<CommentItem key={comment.commentId} comment={comment} />
))}
<div ref={observerRef} style={{ height: '10px' }} />
</div>
</>
)}
</div>
);
}
1 change: 1 addition & 0 deletions src/components/comment-detail/CommentList/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CommentList';
44 changes: 44 additions & 0 deletions src/components/comment-detail/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useEffect, useRef } from 'react';
import useComment from '../vote-detail/Comment/CommentList/hooks';

export default function useCommentDetail() {
const {
commentsData,
hasNextPage,
isLoading,
isFetchingNextPage,
fetchNextPage,
} = useComment(20);

const comments = commentsData?.pages.flatMap((page) => page.data);

const observerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasNextPage && !isFetchingNextPage) {
fetchNextPage();
}
},
{ threshold: 0.1 },
);

const currentObserver = observerRef.current;
if (currentObserver) {
observer.observe(currentObserver);
}

return () => {
if (currentObserver) {
observer.unobserve(currentObserver);
}
};
}, [hasNextPage, fetchNextPage, isFetchingNextPage]);

return {
comments,
observerRef,
isLoading,
};
}
65 changes: 36 additions & 29 deletions src/components/vote-detail/Comment/CommentList/CommentList.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,54 @@
import { useNavigate, useParams } from 'react-router-dom';
import pencilImage from '@/assets/images/vote-detail/pencil.png';
import Loading from '@/components/common/Loading';
import CommentItem from '@/components/vote-detail/Comment/CommentItems/CommentItem';
import useComment from '@/components/vote-detail/Comment/CommentList/hooks';

export default function CommentList() {
const { postId } = useParams<{ postId: string }>();
const { commentsData } = useComment();
const { shareUrl } = useParams<{ shareUrl: string }>();
const { commentsData, isLoading } = useComment(3);
const navigate = useNavigate();

const visibleComments = commentsData.data.slice(0, 3);
const comments = commentsData?.pages.flatMap((page) => page.data);

return (
<div className="max-h-[400px]">
<div className="text-title-large mt-lg pl-sm pb-[9px]">
한마디 ({commentsData.data.length})
</div>
<hr className="text-gray-300 mb-[20px]" />

{commentsData.data.length === 0 ? (
<div className="flex items-center justify-center text-center flex-col mb-[50px]">
<img src={pencilImage} className="w-16 h-16 opacity-50" />
<p className="text-title-small">
아직 댓글이 없어요 <br />
가장 먼저 한마디를 남겨보세요.
</p>
</div>
<div>
{isLoading ? (
<Loading />
) : (
<>
<div className="overflow-y-auto max-h-[267px]">
{visibleComments.map((comment) => (
<CommentItem key={comment.commentId} comment={comment} />
))}
<div className="text-title-large mt-lg pl-sm pb-[9px]">
한마디 ({comments?.length})
</div>
<hr className="text-gray-300 mb-[20px]" />

{commentsData.data.length > 3 && (
<div className="text-center text-accent-800">
<button
onClick={() => navigate(`/votes/${postId}/comments`)}
className="cursor-pointer"
>
더보기
</button>
{comments?.length === 0 ? (
<div className="flex items-center justify-center text-center flex-col mb-[50px]">
<img src={pencilImage} className="w-16 h-16 opacity-50" />
<p className="text-title-small">
아직 댓글이 없어요 <br />
가장 먼저 한마디를 남겨보세요.
</p>
</div>
) : (
<>
<div className="overflow-y-auto">
{comments?.map((comment) => (
<CommentItem key={comment.commentId} comment={comment} />
))}
</div>

{comments?.length && comments.length === 3 && (
<div className="text-center text-accent-800">
<button
onClick={() => navigate(`/votes/${shareUrl}/comments`)}
className="cursor-pointer"
>
더보기
</button>
</div>
)}
</>
)}
</>
)}
Expand Down
18 changes: 15 additions & 3 deletions src/components/vote-detail/Comment/CommentList/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,26 @@ import { useParams } from 'react-router-dom';
import useGetComment from '@/api/useGetComment';
import useGetVoteDetail from '@/api/useGetVoteDetail';

export default function useComment() {
export default function useComment(size: number) {
const { shareUrl } = useParams<{ shareUrl: string }>();
const { data: voteDetail } = useGetVoteDetail(shareUrl ?? '');
const { data: commentsData } = useGetComment(voteDetail.id, {
const { data: voteDetail, isLoading: isVoteDetailLoading } = useGetVoteDetail(
shareUrl ?? '',
);
const {
data: commentsData,
hasNextPage,
isFetchingNextPage,
isLoading: isCommentsLoading,
fetchNextPage,
} = useGetComment(voteDetail.id, size, {
enabled: !!voteDetail.id,
});

return {
commentsData,
hasNextPage,
isFetchingNextPage,
isLoading: isVoteDetailLoading || isCommentsLoading,
fetchNextPage,
};
}

This file was deleted.

Empty file.
Empty file.
21 changes: 13 additions & 8 deletions src/components/vote-detail/Input/CommentInput.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { SetStateAction, useRef, useState } from 'react';
import { useParams } from 'react-router-dom';
import useAddComment from '@/api/useAddComment';
import useGetVoteDetail from '@/api/useGetVoteDetail';
import Icon from '@/components/common/Icon';
import TextInput from '@/components/common/TextInput';

export default function CommentInput() {
const [content, setContent] = useState('');
const contentRef = useRef<HTMLInputElement>(null);
const { postId } = useParams<{ postId: string }>();
const { shareUrl } = useParams<{ shareUrl: string }>();

const { data: voteDetail } = useGetVoteDetail(shareUrl ?? '');

const { mutate: addComment } = useAddComment();

Expand All @@ -28,14 +31,16 @@ export default function CommentInput() {
return;
}

addComment(
{ postId: Number(postId), content },
{
onSuccess: () => {
setContent('');
if (voteDetail.id) {
addComment(
{ postId: voteDetail.id, content },
{
onSuccess: () => {
setContent('');
},
},
},
);
);
}
};

return (
Expand Down
4 changes: 1 addition & 3 deletions src/components/vote-detail/Vote/VoteSection/VoteSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ export default function VoteSection() {
<Suspense fallback={<Loading />}>
<VoteCardList />
</Suspense>
<Suspense fallback={<Loading />}>
<VoteResultList />
</Suspense>
<VoteResultList />
</>
);
}
29 changes: 6 additions & 23 deletions src/pages/Vote/VoteCommentDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
import { Suspense } from 'react';
import { useNavigate } from 'react-router-dom';
import Logo from '@/assets/icons/logo.svg?react';
import CommentList from '@/components/comment-detail/CommentList';
import { Header } from '@/components/common/Header/Header';
import Icon from '@/components/common/Icon';
import Loading from '@/components/common/Loading';
import CommentItem from '@/components/vote-detail/Comment/CommentItems';
import useComment from '@/components/vote-detail/Comment/CommentList/hooks';
import CommentInput from '@/components/vote-detail/Input';

export default function VoteCommentDetailPage() {
const { commentsData } = useComment();
const navigate = useNavigate();

return (
<div className="bg-gray-200 w-full h-screen flex items-center flex-col pt-[85px] relative">
<div className="bg-gray-200 w-full h-screen flex items-center flex-col py-[85px] relative">
<Header
leftNode={
<Icon
Expand All @@ -35,23 +31,10 @@ export default function VoteCommentDetailPage() {
}
/>

<div className="w-full">
<Suspense fallback={<Loading />}>
<div className="mx-[15px] px-[10px] pt-[18px] pb-1 bg-gray-100 rounded-2xl shadow-[0px_2px_20px_0px_rgba(0,0,0,0.03),0px_20px_15px_0px_rgba(0,0,0,0.02),0px_8px_25px_0px_rgba(0,0,0,0.04)]">
<div className="h-[791px] overflow-y-auto">
<div className="text-title-large mt-lg pl-sm pb-[9px]">
한마디 ({commentsData.data.length})
</div>
<hr className="text-gray-300 mb-[20px]" />

<div>
{commentsData.data.map((comment) => (
<CommentItem key={comment.commentId} comment={comment} />
))}
</div>
</div>
</div>
</Suspense>
<div className="w-full h-full">
<div className="h-full mx-[15px] px-[10px] pt-[18px] pb-1 bg-gray-100 rounded-2xl shadow-[0px_2px_20px_0px_rgba(0,0,0,0.03),0px_20px_15px_0px_rgba(0,0,0,0.02),0px_8px_25px_0px_rgba(0,0,0,0.04)]">
<CommentList />
</div>
<CommentInput />
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/pages/Vote/VotePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
import Logo from '@/assets/icons/logo.svg?react';
import { Header } from '@/components/common/Header/Header';
import Icon from '@/components/common/Icon';
import CommentSection from '@/components/vote-detail/Comment/CommentSection/CommentSection';
import CommentList from '@/components/vote-detail/Comment/CommentList';
import CommentInput from '@/components/vote-detail/Input/CommentInput';
import VoteTopSection from '@/components/vote-detail/Top/VoteTopSection/VoteTopSection';
import VoteSection from '@/components/vote-detail/Vote/VoteSection';
Expand Down Expand Up @@ -38,7 +38,7 @@ export default function VotePage() {
>
<VoteTopSection />
<VoteSection />
<CommentSection />
<CommentList />
</div>
<CommentInput />
</div>
Expand Down
Loading

0 comments on commit ba029c9

Please sign in to comment.