From 4aad21551f34b5d447fc398dcff482f0a58b9eda Mon Sep 17 00:00:00 2001 From: woohyun Date: Mon, 10 Aug 2026 02:17:19 +0900 Subject: [PATCH] =?UTF-8?q?[Feat]=20=EB=A7=88=EC=9D=B4=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20=EB=82=98=EC=9D=98=20=ED=99=9C=EB=8F=99=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/layouts/Layout.tsx | 8 +- src/pages/my-page/index.tsx | 2 +- src/pages/my-page/my-comments-page.tsx | 90 +++++++++- src/pages/my-page/my-posts-page.tsx | 92 +++++++++- src/pages/my-page/reward-use-history-page.tsx | 158 +++++++++++++----- 5 files changed, 288 insertions(+), 62 deletions(-) diff --git a/src/layouts/Layout.tsx b/src/layouts/Layout.tsx index 22b93a4..e50a051 100644 --- a/src/layouts/Layout.tsx +++ b/src/layouts/Layout.tsx @@ -86,10 +86,10 @@ const Layout = () => { {isCommunityWrite && navigate('/')} bgColor='bg-bg-green1' />} {isCommunityModify && navigate('/')} bgColor='bg-bg-green1' />} - {isMyPosts && navigate('/')} bgColor='bg-my' />} - {isMyfavorites && navigate('/')} bgColor='bg-my' />} - {isMyhistory && navigate('/')} bgColor='bg-my' />} - {isMycomments && navigate('/')} bgColor='bg-my' />} + {isMyPosts && navigate('/')} bgColor='bg-bg-my' />} + {isMyfavorites && navigate('/')} bgColor='bg-bg-my' />} + {isMyhistory && navigate('/')} bgColor='bg-bg-my' />} + {isMycomments && navigate('/')} bgColor='bg-bg-my' />} {isReward && navigate('/')} bgColor='bg-bg-green1' />} {isRewardHistory && navigate('/')} bgColor='bg-bg-green1' />} diff --git a/src/pages/my-page/index.tsx b/src/pages/my-page/index.tsx index 1124919..0d2ec57 100644 --- a/src/pages/my-page/index.tsx +++ b/src/pages/my-page/index.tsx @@ -160,7 +160,7 @@ const MyPage = () => { return ( <> -
+
{/* 프로필 */}
diff --git a/src/pages/my-page/my-comments-page.tsx b/src/pages/my-page/my-comments-page.tsx index da78ba7..5bd5937 100644 --- a/src/pages/my-page/my-comments-page.tsx +++ b/src/pages/my-page/my-comments-page.tsx @@ -1,10 +1,14 @@ import type { MouseEvent } from 'react'; import { + useInfiniteQuery, useMutation, - useQuery, useQueryClient, } from '@tanstack/react-query'; -import { useState } from 'react'; +import { + useEffect, + useRef, + useState, +} from 'react'; import { useNavigate } from 'react-router-dom'; import { @@ -21,8 +25,12 @@ const formatCreatedAt = (createdAt: string) => { const date = new Date(createdAt); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String( + date.getMonth() + 1, + ).padStart(2, '0'); + const day = String( + date.getDate(), + ).padStart(2, '0'); return `${year}.${month}.${day}`; }; @@ -39,16 +47,71 @@ const MyCommentsPage = () => { const [deleteTarget, setDeleteTarget] = useState(null); + const loadMoreRef = + useRef(null); + const { data, isPending, isError, - } = useQuery({ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ queryKey: ['myComments'], - queryFn: () => getMyComments(0, 10), + initialPageParam: 0, + + queryFn: ({ pageParam }) => + getMyComments(pageParam, 10), + + getNextPageParam: (lastPage) => { + const result = lastPage.result; + + if (!result || !result.hasNext) { + return undefined; + } + + return result.page + 1; + }, }); - const comments = data?.result?.items ?? []; + const comments = + data?.pages.flatMap( + (page) => page.result?.items ?? [], + ) ?? []; + + useEffect(() => { + const target = loadMoreRef.current; + + if (!target) return; + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + + if ( + entry?.isIntersecting && + hasNextPage && + !isFetchingNextPage + ) { + void fetchNextPage(); + } + }, + { + rootMargin: '100px', + }, + ); + + observer.observe(target); + + return () => { + observer.disconnect(); + }; + }, [ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + ]); const deleteMutation = useMutation({ mutationFn: ({ @@ -126,7 +189,7 @@ const MyCommentsPage = () => { return (
-
+
{comments.length > 0 ? (
{comments.map((comment) => ( @@ -168,6 +231,17 @@ const MyCommentsPage = () => {
))} + +
+ + {isFetchingNextPage && ( +
+ +
+ )}
) : (
diff --git a/src/pages/my-page/my-posts-page.tsx b/src/pages/my-page/my-posts-page.tsx index d49a58c..84b13fc 100644 --- a/src/pages/my-page/my-posts-page.tsx +++ b/src/pages/my-page/my-posts-page.tsx @@ -1,10 +1,14 @@ import type { MouseEvent } from 'react'; import { + useInfiniteQuery, useMutation, - useQuery, useQueryClient, } from '@tanstack/react-query'; -import { useState } from 'react'; +import { + useEffect, + useRef, + useState, +} from 'react'; import { useNavigate } from 'react-router-dom'; import LoadingSpinner from '../../components/common/LoadingSpinner'; @@ -21,8 +25,12 @@ const formatCreatedAt = (createdAt: string) => { const date = new Date(createdAt); const year = date.getFullYear(); - const month = String(date.getMonth() + 1).padStart(2, '0'); - const day = String(date.getDate()).padStart(2, '0'); + const month = String( + date.getMonth() + 1, + ).padStart(2, '0'); + const day = String( + date.getDate(), + ).padStart(2, '0'); return `${year}.${month}.${day}`; }; @@ -34,19 +42,75 @@ const MyPostsPage = () => { const [deletePostId, setDeletePostId] = useState(null); + const loadMoreRef = + useRef(null); + const { data, isPending, isError, - } = useQuery({ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ queryKey: ['myCommunities'], - queryFn: () => getMyCommunities(0, 10), + initialPageParam: 0, + + queryFn: ({ pageParam }) => + getMyCommunities(pageParam, 10), + + getNextPageParam: (lastPage) => { + const result = lastPage.result; + + if (!result || !result.hasNext) { + return undefined; + } + + return result.page + 1; + }, }); - const posts = data?.result?.items ?? []; + const posts = + data?.pages.flatMap( + (page) => page.result?.items ?? [], + ) ?? []; + + useEffect(() => { + const target = loadMoreRef.current; + + if (!target) return; + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + + if ( + entry?.isIntersecting && + hasNextPage && + !isFetchingNextPage + ) { + void fetchNextPage(); + } + }, + { + rootMargin: '100px', + }, + ); + + observer.observe(target); + + return () => { + observer.disconnect(); + }; + }, [ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + ]); const deleteMutation = useMutation({ mutationFn: deleteCommunity, + onSuccess: async () => { await queryClient.invalidateQueries({ queryKey: ['myCommunities'], @@ -70,6 +134,7 @@ const MyPostsPage = () => { communityId: number, ) => { event.stopPropagation(); + setDeletePostId(communityId); }; @@ -108,7 +173,7 @@ const MyPostsPage = () => { return (
-
+
{posts.length > 0 ? (
{posts.map((post) => ( @@ -149,6 +214,17 @@ const MyPostsPage = () => {
))} + +
+ + {isFetchingNextPage && ( +
+ +
+ )} ) : (
diff --git a/src/pages/my-page/reward-use-history-page.tsx b/src/pages/my-page/reward-use-history-page.tsx index c773138..4e9b2a6 100644 --- a/src/pages/my-page/reward-use-history-page.tsx +++ b/src/pages/my-page/reward-use-history-page.tsx @@ -1,4 +1,9 @@ -import { useQuery } from '@tanstack/react-query'; +import { useInfiniteQuery } from '@tanstack/react-query'; +import { + useEffect, + useRef, +} from 'react'; + import LoadingSpinner from '../../components/common/LoadingSpinner'; import ShippingIcon from '../../assets/icons/shipping.svg?react'; @@ -38,60 +43,120 @@ const formatRedeemedAt = ( }; const RewardUseHistoryPage = () => { + const loadMoreRef = + useRef(null); + const { data, isPending, isError, - } = useQuery({ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = useInfiniteQuery({ queryKey: ['rewardRedemptions'], - queryFn: () => + + initialPageParam: + undefined as number | undefined, + + queryFn: ({ pageParam }) => getRewardRedemptions({ + cursor: pageParam, size: 10, }), + + getNextPageParam: (lastPage) => { + if ( + !lastPage.hasNext || + lastPage.nextCursor === null + ) { + return undefined; + } + + return lastPage.nextCursor; + }, }); const rewardUseHistory = - data?.content ?? []; - - const renderStatusIcon = ( - type: FulfillmentType, - status: FulfillmentStatus, -) => { - if (status === 'READY' || status === 'SENT') { - return ( - + data?.pages.flatMap( + (page) => page.content ?? [], + ) ?? []; + + useEffect(() => { + const target = loadMoreRef.current; + + if (!target) return; + + const observer = new IntersectionObserver( + (entries) => { + const entry = entries[0]; + + if ( + entry?.isIntersecting && + hasNextPage && + !isFetchingNextPage + ) { + void fetchNextPage(); + } + }, + { + rootMargin: '100px', + }, ); - } - if ( - type === 'DELIVERY' && - status === 'COMPLETED' - ) { - return ( - - ); - } + observer.observe(target); - if ( - type === 'COUPON' && - status === 'COMPLETED' - ) { - return ( - - ); - } + return () => { + observer.disconnect(); + }; + }, [ + fetchNextPage, + hasNextPage, + isFetchingNextPage, + ]); - return null; -}; + const renderStatusIcon = ( + type: FulfillmentType, + status: FulfillmentStatus, + ) => { + if ( + status === 'READY' || + status === 'SENT' + ) { + return ( + + ); + } + + if ( + type === 'DELIVERY' && + status === 'COMPLETED' + ) { + return ( + + ); + } + + if ( + type === 'COUPON' && + status === 'COMPLETED' + ) { + return ( + + ); + } + + return null; + }; if (isPending) { return ( @@ -153,12 +218,23 @@ const RewardUseHistoryPage = () => { {/* 배송 상태 */}
{renderStatusIcon( - item.fulfillmentType, + item.fulfillmentType, item.fulfillmentStatus, )}
))} + +
+ + {isFetchingNextPage && ( +
+ +
+ )} ) : (