From 80ee9cb62cb7bad603ba047c7b34ffcb001ef94f Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 10:13:45 +0900 Subject: [PATCH 01/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=B4=88=EA=B8=B0=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20SSR=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/_component/MyGatheringList.tsx | 7 +++- src/app/(main)/mypage/page.tsx | 6 ++-- .../api/actions/gatherings/getMyGatherings.ts | 34 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 src/app/api/actions/gatherings/getMyGatherings.ts diff --git a/src/app/(main)/mypage/_component/MyGatheringList.tsx b/src/app/(main)/mypage/_component/MyGatheringList.tsx index 5dcd832a..69d618c2 100644 --- a/src/app/(main)/mypage/_component/MyGatheringList.tsx +++ b/src/app/(main)/mypage/_component/MyGatheringList.tsx @@ -5,8 +5,13 @@ import Card from '@/app/components/Card/Card'; import InfiniteScroll from '@/app/components/InfiniteScroll/InfiniteScroll'; import ReviewModal from '@/app/components/Modal/ReviewModal'; import { useState } from 'react'; +import { UserJoinedGatheringsData } from '@/types/data.type'; -const MyGatheringList = () => { +interface MyGatheringListProps { + initData: UserJoinedGatheringsData[]; +} + +const MyGatheringList = ({ initData }: MyGatheringListProps) => { const [isModalOpen, setIsModalOpen] = useState(false); const [cardId, setCardId] = useState(0); diff --git a/src/app/(main)/mypage/page.tsx b/src/app/(main)/mypage/page.tsx index 3b3fc690..ed2d2e53 100644 --- a/src/app/(main)/mypage/page.tsx +++ b/src/app/(main)/mypage/page.tsx @@ -1,13 +1,15 @@ import { Metadata } from 'next'; import MyGatheringList from './_component/MyGatheringList'; +import getMyGatherings from '@/app/api/actions/gatherings/getMyGatherings'; export const metadata: Metadata = { title: '나의 모임 | Soothe With Me', description: 'Soothe With Me 나의 모임 페이지입니다.', }; -const Mygatherings = () => { - return ; +const Mygatherings = async () => { + const myGatherings = await getMyGatherings(); + return ; }; export default Mygatherings; diff --git a/src/app/api/actions/gatherings/getMyGatherings.ts b/src/app/api/actions/gatherings/getMyGatherings.ts new file mode 100644 index 00000000..549d0f41 --- /dev/null +++ b/src/app/api/actions/gatherings/getMyGatherings.ts @@ -0,0 +1,34 @@ +'use server'; + +import { getCookie } from '@/actions/auth/cookie/cookie'; +import { GatheringType } from '@/types/data.type'; + +const getMyGatherings = async () => { + const token = await getCookie('token'); + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_BASE_URL}/gatherings/joined?limit=10&offset=0`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${token}`, + }, + }, + ); + + if (!res.ok) { + throw new Error('모임을 불러오지 못했습니다.'); + } + + const data: GatheringType[] = await res.json(); + + return data; + } catch (error) { + throw new Error( + error instanceof Error ? error.message : '모임을 불러오지 못했습니다.', + ); + } +}; + +export default getMyGatherings; From 26101786ab7e51373282c8febcc6ab1f0bc54709 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 11:28:25 +0900 Subject: [PATCH 02/11] =?UTF-8?q?[KAN-117]=20refactor:=20=EB=AC=B4?= =?UTF-8?q?=ED=95=9C=EC=8A=A4=ED=81=AC=EB=A1=A4=20initData=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mypage/_component/MyGatheringList.tsx | 1 + src/app/api/gatherings/joined/route.ts | 6 ++--- .../api/gatherings/service/getMyGathergins.ts | 5 +++-- .../InfiniteScroll/InfiniteScroll.tsx | 22 ++++++++++++++----- src/constants/common.ts | 2 +- 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/app/(main)/mypage/_component/MyGatheringList.tsx b/src/app/(main)/mypage/_component/MyGatheringList.tsx index 69d618c2..adea7776 100644 --- a/src/app/(main)/mypage/_component/MyGatheringList.tsx +++ b/src/app/(main)/mypage/_component/MyGatheringList.tsx @@ -27,6 +27,7 @@ const MyGatheringList = ({ initData }: MyGatheringListProps) => { return ( <> => { const response = await fetch( `/api/gatherings/joined?offset=${offset}&limit=${limit}`, diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index e0168c18..ed9c1246 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -1,7 +1,8 @@ import { useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; -import { DEFAULT_LIMIT } from '@/constants/common'; +import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; +import { UserJoinedGatheringsData } from '@/types/data.type'; interface ItemWithId { id: number; } @@ -13,6 +14,7 @@ interface InfiniteQueryResponse { } interface InfiniteScrollProps { + initData: T[]; queryKey: string[]; queryFn: (offset?: number) => Promise>; limit?: number; @@ -21,16 +23,14 @@ interface InfiniteScrollProps { } const InfiniteScroll = ({ + initData, queryKey, queryFn, limit = DEFAULT_LIMIT, emptyText, renderItem, }: InfiniteScrollProps) => { - const { ref, inView } = useInView({ - triggerOnce: false, - threshold: 1.0, - }); + const { ref, inView } = useInView({}); const { data, isError, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -39,7 +39,17 @@ const InfiniteScroll = ({ getNextPageParam: (lastPage) => { return lastPage.hasNextPage ? lastPage.offset + limit : undefined; }, - initialPageParam: 0, + initialPageParam: DEFAULT_OFFSET, + initialData: { + pages: [ + { + hasNextPage: true, + offset: 0, + data: initData, + }, + ], + pageParams: [DEFAULT_OFFSET], + }, }); useEffect(() => { diff --git a/src/constants/common.ts b/src/constants/common.ts index 87cc27e3..5c07f379 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -25,7 +25,7 @@ export const MIN_PARTICIPANTS = 5; // 무한스크롤 관련 데이터 패칭 상수 export const DEFAULT_OFFSET = 0; -export const DEFAULT_LIMIT = 5; +export const DEFAULT_LIMIT = 10; export const LIMIT_PER_REQUEST = 10; From 6080a197e151c8ccbbe421cc1b8bec94f28184bf Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 11:38:40 +0900 Subject: [PATCH 03/11] =?UTF-8?q?[KAN-117]=20fix:=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EA=B0=80=20=EC=97=86=EC=9D=84=20=EB=95=8C=20emptyText?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=20=EC=95=88=EB=90=98=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/InfiniteScroll/InfiniteScroll.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index ed9c1246..a5c87116 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -2,7 +2,6 @@ import { useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; -import { UserJoinedGatheringsData } from '@/types/data.type'; interface ItemWithId { id: number; } @@ -59,7 +58,7 @@ const InfiniteScroll = ({ }, [inView]); // 데이터가 없거나 비어있을 때 emptyText를 표시 - if (!data || data.pages.length === 0) { + if (!data || data.pages[0].data.length === 0) { return (
{emptyText} From db314cfe81f9dfa62950ea37ca06f4857f8ef7b2 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 13:06:09 +0900 Subject: [PATCH 04/11] =?UTF-8?q?[KAN-117]=20feat:=20loader=20=EC=BB=B4?= =?UTF-8?q?=ED=8F=AC=EB=84=8C=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/mypage/loading.tsx | 13 +++++++++++ .../InfiniteScroll/InfiniteScroll.tsx | 22 +++++++++---------- 2 files changed, 23 insertions(+), 12 deletions(-) create mode 100644 src/app/(main)/mypage/loading.tsx diff --git a/src/app/(main)/mypage/loading.tsx b/src/app/(main)/mypage/loading.tsx new file mode 100644 index 00000000..3bb70580 --- /dev/null +++ b/src/app/(main)/mypage/loading.tsx @@ -0,0 +1,13 @@ +'use client'; + +import Loader from '@/app/components/Loader/Loader'; + +const Loading = () => { + return ( +
+ ; +
+ ); +}; + +export default Loading; diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index a5c87116..f67e09cc 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -2,6 +2,7 @@ import { useEffect } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; +import Loader from '../Loader/Loader'; interface ItemWithId { id: number; } @@ -42,8 +43,8 @@ const InfiniteScroll = ({ initialData: { pages: [ { - hasNextPage: true, - offset: 0, + hasNextPage: initData.length === DEFAULT_LIMIT, + offset: DEFAULT_OFFSET, data: initData, }, ], @@ -73,21 +74,18 @@ const InfiniteScroll = ({
); + const allItems = data.pages.flatMap((page) => page.data); return ( <>
    - {data && - data.pages.map((page) => - page.data.map((item, index: number) => ( -
  • {renderItem(item, index)}
  • // 사용자 정의 컴포넌트를 렌더링 - )), - )} + {allItems.map((item, index) => ( +
  • {renderItem(item, index)}
  • + ))}
{isFetching && ( -
-
- - Loading... +
+ +
)} From 972c6001d4ed2880e59e614584c9eb9a38593768 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 13:18:45 +0900 Subject: [PATCH 05/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=20=EA=B7=B8=EB=9D=BC=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../InfiniteScroll/InfiniteScroll.tsx | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index f67e09cc..54838e3f 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; @@ -30,7 +30,16 @@ const InfiniteScroll = ({ emptyText, renderItem, }: InfiniteScrollProps) => { - const { ref, inView } = useInView({}); + const [topGradientVisible, setTopGradientVisible] = useState(false); + const [bottomGradientVisible, setBottomGradientVisible] = useState(false); + + const { ref, inView } = useInView({ threshold: 0 }); + const { ref: firstGatheringRef, inView: firstInView } = useInView({ + threshold: 0, + }); + const { ref: lastGatheringRef, inView: lastInView } = useInView({ + threshold: 0, + }); const { data, isError, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ @@ -53,10 +62,12 @@ const InfiniteScroll = ({ }); useEffect(() => { + setTopGradientVisible(!firstInView); + setBottomGradientVisible(!lastInView); if (inView && hasNextPage) { fetchNextPage(); } - }, [inView]); + }, [inView, firstInView, lastInView]); // 데이터가 없거나 비어있을 때 emptyText를 표시 if (!data || data.pages[0].data.length === 0) { @@ -77,9 +88,34 @@ const InfiniteScroll = ({ const allItems = data.pages.flatMap((page) => page.data); return ( <> + {/* Top gradient */} +
+ + {/* Bottom gradient */} +
+
    {allItems.map((item, index) => ( -
  • {renderItem(item, index)}
  • +
  • + {renderItem(item, index)} +
  • ))}
{isFetching && ( From d1a2e15cf45e16057f03584186c3a9ad37b22fb8 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 14:15:11 +0900 Subject: [PATCH 06/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=20=EC=B0=B8=EC=97=AC=EC=B7=A8=EC=86=8C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../(main)/mypage/_component/MyGatheringList.tsx | 8 ++++++-- src/app/(main)/mypage/page.tsx | 4 +++- src/app/layout.tsx | 4 ++-- src/hooks/useParticipation.ts | 13 +++++++++++++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/app/(main)/mypage/_component/MyGatheringList.tsx b/src/app/(main)/mypage/_component/MyGatheringList.tsx index adea7776..cfc60534 100644 --- a/src/app/(main)/mypage/_component/MyGatheringList.tsx +++ b/src/app/(main)/mypage/_component/MyGatheringList.tsx @@ -6,14 +6,18 @@ import InfiniteScroll from '@/app/components/InfiniteScroll/InfiniteScroll'; import ReviewModal from '@/app/components/Modal/ReviewModal'; import { useState } from 'react'; import { UserJoinedGatheringsData } from '@/types/data.type'; +import useParticipation from '@/hooks/useParticipation'; +import { UserData } from '@/types/client.type'; interface MyGatheringListProps { + user: UserData | null; initData: UserJoinedGatheringsData[]; } -const MyGatheringList = ({ initData }: MyGatheringListProps) => { +const MyGatheringList = ({ initData, user }: MyGatheringListProps) => { const [isModalOpen, setIsModalOpen] = useState(false); const [cardId, setCardId] = useState(0); + const { handleWithdrawClickWithId } = useParticipation(user); const handleOpenModal = (id: number) => { setCardId(id); @@ -42,7 +46,7 @@ const MyGatheringList = ({ initData }: MyGatheringListProps) => { handleButtonClick={() => { item.isCompleted ? handleOpenModal(item.id) - : console.log('Cancel gathering'); + : handleWithdrawClickWithId(item.id); }} /> diff --git a/src/app/(main)/mypage/page.tsx b/src/app/(main)/mypage/page.tsx index ed2d2e53..80af5d6d 100644 --- a/src/app/(main)/mypage/page.tsx +++ b/src/app/(main)/mypage/page.tsx @@ -1,6 +1,7 @@ import { Metadata } from 'next'; import MyGatheringList from './_component/MyGatheringList'; import getMyGatherings from '@/app/api/actions/gatherings/getMyGatherings'; +import { getUserData } from '@/app/api/actions/mypage/getUserData'; export const metadata: Metadata = { title: '나의 모임 | Soothe With Me', @@ -9,7 +10,8 @@ export const metadata: Metadata = { const Mygatherings = async () => { const myGatherings = await getMyGatherings(); - return ; + const user = await getUserData(); + return ; }; export default Mygatherings; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 79da0c09..35dd522b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -25,13 +25,13 @@ export default async function RootLayout({ }: Readonly<{ children: ReactNode; }>) { - const userData = await getUserData(); + const user = await getUserData(); const token = await getCookie('token'); return ( - +
{children}
diff --git a/src/hooks/useParticipation.ts b/src/hooks/useParticipation.ts index 60ff73b1..19c9e4f4 100644 --- a/src/hooks/useParticipation.ts +++ b/src/hooks/useParticipation.ts @@ -48,6 +48,18 @@ export default function useParticipation(user: UserData | null) { } }; + const handleWithdrawClickWithId = async (id: number) => { + const { success, message } = await deleteGatheringToWithdraw(id); + + if (!success) { + toast.error(message); + return; + } + + toast.success(message); + setHasParticipated(false); + }; + return { hasParticipated, setHasParticipated, @@ -55,5 +67,6 @@ export default function useParticipation(user: UserData | null) { setIsShowPopup, handleJoinClick, handleWithdrawClick, + handleWithdrawClickWithId, }; } From a07f4b241ed755b231d21a03971a04cec161ba93 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 15:43:54 +0900 Subject: [PATCH 07/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=EC=8B=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/mypage/_component/MyGatheringList.tsx | 7 ++----- src/hooks/useParticipation.ts | 6 +++++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/app/(main)/mypage/_component/MyGatheringList.tsx b/src/app/(main)/mypage/_component/MyGatheringList.tsx index cfc60534..9e0cba52 100644 --- a/src/app/(main)/mypage/_component/MyGatheringList.tsx +++ b/src/app/(main)/mypage/_component/MyGatheringList.tsx @@ -36,17 +36,14 @@ const MyGatheringList = ({ initData, user }: MyGatheringListProps) => { queryFn={getMyGathergins} emptyText='아직 참여한 모임이 없습니다.' renderItem={(item, index) => ( - console.log('Save Discard')} - data={item} - > + { item.isCompleted ? handleOpenModal(item.id) - : handleWithdrawClickWithId(item.id); + : handleWithdrawClickWithId(item.id, ['myGatherings']); }} /> diff --git a/src/hooks/useParticipation.ts b/src/hooks/useParticipation.ts index 19c9e4f4..45ec32de 100644 --- a/src/hooks/useParticipation.ts +++ b/src/hooks/useParticipation.ts @@ -6,9 +6,11 @@ import deleteGatheringToWithdraw from '@/app/api/actions/gatherings/deleteGather import { UserData } from '@/types/client.type'; import toast from 'react-hot-toast'; +import { useQueryClient } from '@tanstack/react-query'; export default function useParticipation(user: UserData | null) { const params = useParams(); + const queryClient = useQueryClient(); const [hasParticipated, setHasParticipated] = useState(false); const [isShowPopup, setIsShowPopup] = useState(false); @@ -48,13 +50,15 @@ export default function useParticipation(user: UserData | null) { } }; - const handleWithdrawClickWithId = async (id: number) => { + const handleWithdrawClickWithId = async (id: number, queryKey: string[]) => { const { success, message } = await deleteGatheringToWithdraw(id); if (!success) { toast.error(message); return; } + // 쿼리 무효화 함수 + await queryClient.invalidateQueries({ queryKey }); // querykey 무효화시켜서 취소한 모임 반영하여 최신화 toast.success(message); setHasParticipated(false); From f3d02ec48a490acd3016b021baf88f47a4b41536 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 16:14:58 +0900 Subject: [PATCH 08/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=AC=B4=ED=95=9C?= =?UTF-8?q?=EC=8A=A4=ED=81=AC=EB=A1=A4=ED=9B=85=20errorText=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/(main)/mypage/_component/MyGatheringList.tsx | 1 + src/app/components/InfiniteScroll/InfiniteScroll.tsx | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/(main)/mypage/_component/MyGatheringList.tsx b/src/app/(main)/mypage/_component/MyGatheringList.tsx index 9e0cba52..01ecbaf5 100644 --- a/src/app/(main)/mypage/_component/MyGatheringList.tsx +++ b/src/app/(main)/mypage/_component/MyGatheringList.tsx @@ -35,6 +35,7 @@ const MyGatheringList = ({ initData, user }: MyGatheringListProps) => { queryKey={['myGatherings']} queryFn={getMyGathergins} emptyText='아직 참여한 모임이 없습니다.' + errorText='모임을 불러오지 못했습니다.' renderItem={(item, index) => ( diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index 54838e3f..3ee99864 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -19,6 +19,7 @@ interface InfiniteScrollProps { queryFn: (offset?: number) => Promise>; limit?: number; emptyText: string; + errorText: string; renderItem: (item: T, index: number) => JSX.Element; } @@ -28,6 +29,7 @@ const InfiniteScroll = ({ queryFn, limit = DEFAULT_LIMIT, emptyText, + errorText, renderItem, }: InfiniteScrollProps) => { const [topGradientVisible, setTopGradientVisible] = useState(false); @@ -81,7 +83,7 @@ const InfiniteScroll = ({ if (isError) return (
- 모임을 불러오지 못했습니다. + {errorText}
); From 0b812fe2ce5abad5dc406a515b73a8d34414ccf1 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Fri, 11 Oct 2024 17:06:38 +0900 Subject: [PATCH 09/11] =?UTF-8?q?[KAN-117]=20test:=20BottomFloatingBar=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=BD=94=EB=93=9C=20QueryClient=20?= =?UTF-8?q?=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BottomFloatingBar.test.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx b/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx index a3386b56..fa9aa0b1 100644 --- a/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx +++ b/src/app/components/BottomFloatingBar/BottomFloatingBar.test.tsx @@ -1,10 +1,12 @@ 'use client'; -import { render, screen } from '@testing-library/react'; +import { render, screen, RenderOptions } from '@testing-library/react'; import { useRouter } from 'next/navigation'; import BottomFloatingBar from './BottomFloatingBar'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { UserData } from '@/types/client.type'; import { GatheringParticipantsType } from '@/types/data.type'; +import { ReactNode } from 'react'; import '@testing-library/jest-dom'; // 유저데이터 모킹 @@ -24,6 +26,16 @@ jest.mock('next/navigation', () => ({ useParams: jest.fn(() => ({ id: '1' })), })); +// QueryClient 인스턴스 생성 +const queryClient = new QueryClient(); + +// QueryClientProvider로 감싸주는 헬퍼 함수 정의 +const renderWithQueryClient = (ui: ReactNode, options?: RenderOptions) => + render( + {ui}, + options, + ); + describe('BottomFloatingBar 컴포넌트 테스트', () => { const mockRouter = { push: jest.fn() }; @@ -33,7 +45,8 @@ describe('BottomFloatingBar 컴포넌트 테스트', () => { // 참가자일 때 기본 렌더링 확인 it('renders correctly when the user is a participant', () => { - render( + renderWithQueryClient( + // 헬퍼 함수 사용 { // 주최자일 때 기본 렌더링 확인 it('renders correctly when the user is the organizer', () => { - render( + renderWithQueryClient( + // 헬퍼 함수 사용 Date: Fri, 11 Oct 2024 17:48:57 +0900 Subject: [PATCH 10/11] =?UTF-8?q?[KAN-117]=20feat:=20=EB=A6=AC=EB=B7=B0?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20disabled=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/components/Modal/ReviewModal.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/Modal/ReviewModal.tsx b/src/app/components/Modal/ReviewModal.tsx index 0672e1b6..55300ad8 100644 --- a/src/app/components/Modal/ReviewModal.tsx +++ b/src/app/components/Modal/ReviewModal.tsx @@ -61,6 +61,7 @@ const ReviewModal = ({ gatheringId, onClose }: ReviewModalProps) => { name='리뷰 등록' type='button' variant={score !== 0 && comment ? 'default' : 'gray'} + disabled={score === 0 || comment.length === 0} onClick={handleSubmit} />
From bb7ed06b8d4f4fd79607a8b41662103bc9950052 Mon Sep 17 00:00:00 2001 From: hakyoung12 Date: Mon, 14 Oct 2024 09:11:23 +0900 Subject: [PATCH 11/11] =?UTF-8?q?[KAN-117]=20chore:=20offset,=20limit=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=83=81=EC=88=98=20=EC=9D=B4=EB=A6=84=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/gatherings/joined/route.ts | 10 +++++++--- .../api/gatherings/service/getMyGathergins.ts | 9 ++++++--- .../InfiniteScroll/InfiniteScroll.tsx | 18 +++++++++++------- src/constants/common.ts | 4 ++-- src/hooks/useReviews/useReveiws.ts | 11 +++++++---- src/hooks/useUserCreated.ts | 8 ++++---- 6 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/app/api/gatherings/joined/route.ts b/src/app/api/gatherings/joined/route.ts index 3d4b2a2f..fa370f72 100644 --- a/src/app/api/gatherings/joined/route.ts +++ b/src/app/api/gatherings/joined/route.ts @@ -1,11 +1,15 @@ import { getCookie } from '@/actions/auth/cookie/cookie'; -import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; +import { + DEFAULT_GATHERINGS_LIMIT, + DEFAULT_GATHERINGS_OFFSET, +} from '@/constants/common'; import { NextResponse } from 'next/server'; export async function GET(req: Request) { const { searchParams } = new URL(req.url); - const offset = Number(searchParams.get('offset')) || DEFAULT_OFFSET; - const limit = Number(searchParams.get('limit')) || DEFAULT_LIMIT; + const offset = + Number(searchParams.get('offset')) || DEFAULT_GATHERINGS_OFFSET; + const limit = Number(searchParams.get('limit')) || DEFAULT_GATHERINGS_LIMIT; const token = await getCookie('token'); diff --git a/src/app/api/gatherings/service/getMyGathergins.ts b/src/app/api/gatherings/service/getMyGathergins.ts index e5a2f982..9720c77d 100644 --- a/src/app/api/gatherings/service/getMyGathergins.ts +++ b/src/app/api/gatherings/service/getMyGathergins.ts @@ -1,11 +1,14 @@ 'use client'; -import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; +import { + DEFAULT_GATHERINGS_LIMIT, + DEFAULT_GATHERINGS_OFFSET, +} from '@/constants/common'; import { FetchGatheringsResponse } from '@/types/data.type'; const getMyGathergins = async ( - offset = DEFAULT_OFFSET, - limit = DEFAULT_LIMIT, + offset = DEFAULT_GATHERINGS_OFFSET, + limit = DEFAULT_GATHERINGS_LIMIT, ): Promise => { const response = await fetch( `/api/gatherings/joined?offset=${offset}&limit=${limit}`, diff --git a/src/app/components/InfiniteScroll/InfiniteScroll.tsx b/src/app/components/InfiniteScroll/InfiniteScroll.tsx index 3ee99864..49d8e022 100644 --- a/src/app/components/InfiniteScroll/InfiniteScroll.tsx +++ b/src/app/components/InfiniteScroll/InfiniteScroll.tsx @@ -1,7 +1,10 @@ import { useEffect, useState } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { useInView } from 'react-intersection-observer'; -import { DEFAULT_LIMIT, DEFAULT_OFFSET } from '@/constants/common'; +import { + DEFAULT_GATHERINGS_LIMIT, + DEFAULT_GATHERINGS_OFFSET, +} from '@/constants/common'; import Loader from '../Loader/Loader'; interface ItemWithId { id: number; @@ -27,7 +30,7 @@ const InfiniteScroll = ({ initData, queryKey, queryFn, - limit = DEFAULT_LIMIT, + limit = DEFAULT_GATHERINGS_LIMIT, emptyText, errorText, renderItem, @@ -46,20 +49,21 @@ const InfiniteScroll = ({ const { data, isError, isFetching, fetchNextPage, hasNextPage } = useInfiniteQuery({ queryKey, - queryFn: async ({ pageParam = DEFAULT_LIMIT }) => queryFn(pageParam), + queryFn: async ({ pageParam = DEFAULT_GATHERINGS_LIMIT }) => + queryFn(pageParam), getNextPageParam: (lastPage) => { return lastPage.hasNextPage ? lastPage.offset + limit : undefined; }, - initialPageParam: DEFAULT_OFFSET, + initialPageParam: DEFAULT_GATHERINGS_OFFSET, initialData: { pages: [ { - hasNextPage: initData.length === DEFAULT_LIMIT, - offset: DEFAULT_OFFSET, + hasNextPage: initData.length === DEFAULT_GATHERINGS_LIMIT, + offset: DEFAULT_GATHERINGS_OFFSET, data: initData, }, ], - pageParams: [DEFAULT_OFFSET], + pageParams: [DEFAULT_GATHERINGS_OFFSET], }, }); diff --git a/src/constants/common.ts b/src/constants/common.ts index 5c07f379..6ba8db23 100644 --- a/src/constants/common.ts +++ b/src/constants/common.ts @@ -24,8 +24,8 @@ export const REVIEWS_PER_PAGE = 4; export const MIN_PARTICIPANTS = 5; // 무한스크롤 관련 데이터 패칭 상수 -export const DEFAULT_OFFSET = 0; -export const DEFAULT_LIMIT = 10; +export const DEFAULT_GATHERINGS_OFFSET = 0; +export const DEFAULT_GATHERINGS_LIMIT = 10; export const LIMIT_PER_REQUEST = 10; diff --git a/src/hooks/useReviews/useReveiws.ts b/src/hooks/useReviews/useReveiws.ts index 84957e15..9574cc0e 100644 --- a/src/hooks/useReviews/useReveiws.ts +++ b/src/hooks/useReviews/useReveiws.ts @@ -5,7 +5,10 @@ import getReviewScore from '@/app/api/actions/reviews/getReviewScore'; import useFilterState from './useFilterState'; import buildParams from './buildParams'; import { ReviewScoreType, ReviewsType } from '@/types/data.type'; -import { DEFAULT_OFFSET, LIMIT_PER_REQUEST } from '@/constants/common'; +import { + DEFAULT_GATHERINGS_OFFSET, + LIMIT_PER_REQUEST, +} from '@/constants/common'; const useReviews = ( initialReviewsData: ReviewsType[], @@ -57,7 +60,7 @@ const useReviews = ( error: reviewsError, } = useInfiniteQuery({ queryKey: ['reviews', filteringOptions, activeTab, selectedChip], - queryFn: async ({ pageParam = DEFAULT_OFFSET }) => { + queryFn: async ({ pageParam = DEFAULT_GATHERINGS_OFFSET }) => { try { const params = getParams(); const response = await getReviewList({ @@ -78,10 +81,10 @@ const useReviews = ( ? allPages.length * LIMIT_PER_REQUEST : undefined; }, - initialPageParam: DEFAULT_OFFSET, + initialPageParam: DEFAULT_GATHERINGS_OFFSET, initialData: { pages: [initialReviewsData], - pageParams: [DEFAULT_OFFSET], + pageParams: [DEFAULT_GATHERINGS_OFFSET], }, }); diff --git a/src/hooks/useUserCreated.ts b/src/hooks/useUserCreated.ts index a6b349ab..fe870112 100644 --- a/src/hooks/useUserCreated.ts +++ b/src/hooks/useUserCreated.ts @@ -2,7 +2,7 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import getGatherings from '@/app/api/actions/gatherings/getGatherings'; import { GatheringType } from '@/types/data.type'; import { - DEFAULT_OFFSET, + DEFAULT_GATHERINGS_OFFSET, LIMIT_PER_REQUEST, SORT_OPTIONS_MAP, } from '@/constants/common'; @@ -20,7 +20,7 @@ export const useUserCreated = ( error, } = useInfiniteQuery({ queryKey: ['created', createdBy], - queryFn: async ({ pageParam = DEFAULT_OFFSET }) => { + queryFn: async ({ pageParam = DEFAULT_GATHERINGS_OFFSET }) => { try { const response = await getGatherings({ createdBy: createdBy, @@ -42,10 +42,10 @@ export const useUserCreated = ( ? allPages.length * LIMIT_PER_REQUEST : undefined; }, - initialPageParam: DEFAULT_OFFSET, + initialPageParam: DEFAULT_GATHERINGS_OFFSET, initialData: { pages: [initialGatheringList], - pageParams: [DEFAULT_OFFSET], + pageParams: [DEFAULT_GATHERINGS_OFFSET], }, });