From 20afe6d56d38e06f20b57dde8d035b1be0854c7c Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:41:08 +0900 Subject: [PATCH 01/43] =?UTF-8?q?=EC=B1=84=ED=8C=85=EC=B0=BD=20=EB=86=92?= =?UTF-8?q?=EC=9D=B4=20=EC=A7=80=EC=A0=95=20(#307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/chat/[id]/page.tsx | 111 ++++++++++++++++++++----------------- 1 file changed, 60 insertions(+), 51 deletions(-) diff --git a/src/app/chat/[id]/page.tsx b/src/app/chat/[id]/page.tsx index 39860903..f1b2a8bf 100644 --- a/src/app/chat/[id]/page.tsx +++ b/src/app/chat/[id]/page.tsx @@ -178,61 +178,70 @@ function ChatRoomPage() { }; return ( -
-
-
-
-
- } - onClick={handleGoBack} - className="bg-gray-light-02" - /> -

채팅

- -
-
- } - onClick={() => {}} - className="bg-gray-light-02" - /> +
+
+
+
+
+
+ } + onClick={handleGoBack} + className="bg-gray-light-02" + /> +

채팅

+ +
+
+ } + onClick={() => {}} + className="bg-gray-light-02" + /> +
+ router.push(`/bookclub/${chatId}`), + }} + />
- router.push(`/bookclub/${chatId}`), - }} +
+ +
+ {}} />
-
-
- {}} - /> -
-
-
- - - } - aria-label="메시지 전송" - className="h-[52px] w-[52px] bg-green-light-01" - onClick={handleSubmit} - /> + +
+
+
+ +
+ } + aria-label="메시지 전송" + className="h-[52px] w-[52px] bg-green-light-01" + onClick={handleSubmit} + /> + +
); From 50180e40e2993c55706321de4a093a8bbeeb61c8 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:11:25 +0900 Subject: [PATCH 02/43] =?UTF-8?q?=ED=86=A0=EC=8A=A4=ED=8A=B8=EA=B0=80=20?= =?UTF-8?q?=EC=82=AC=EB=9D=BC=EC=A7=80=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20#309=20(#311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/layout.tsx | 1 - src/components/header/HeaderBar.tsx | 2 -- src/components/toast/toast.tsx | 6 +++++- src/features/auth/api/auth.ts | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a7ff9915..48a6dc59 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -31,7 +31,6 @@ export default function RootLayout({ {children} - ); diff --git a/src/components/header/HeaderBar.tsx b/src/components/header/HeaderBar.tsx index 34a905db..3cfba709 100644 --- a/src/components/header/HeaderBar.tsx +++ b/src/components/header/HeaderBar.tsx @@ -7,7 +7,6 @@ import { usePathname, useRouter } from 'next/navigation'; import { useAuthStore } from '@/store/authStore'; import DropDown from '../drop-down/DropDown'; import { logout } from '@/features/auth/api/auth'; -import { showToast } from '../toast/toast'; function HeaderBar() { const pathname = usePathname(); @@ -18,7 +17,6 @@ function HeaderBar() { if (value === 'LOGOUT') { try { await logout(); - showToast({ message: '로그아웃 되었습니다 ', type: 'success' }); router.replace('/bookclub'); } catch (error) { console.error('로그아웃 실패:', error); diff --git a/src/components/toast/toast.tsx b/src/components/toast/toast.tsx index 5ef1b2b4..a065313c 100644 --- a/src/components/toast/toast.tsx +++ b/src/components/toast/toast.tsx @@ -14,7 +14,11 @@ const defaultOptions: ToastOptions = { }; export const showToast = ({ message, type }: ToastProps) => { - toast[type](message, defaultOptions); + if (type === 'success') { + toast.success(message, defaultOptions); + } else if (type === 'error') { + toast.error(message, defaultOptions); + } }; export const Toast = () => { diff --git a/src/features/auth/api/auth.ts b/src/features/auth/api/auth.ts index 41db7216..0a7a8d54 100644 --- a/src/features/auth/api/auth.ts +++ b/src/features/auth/api/auth.ts @@ -62,6 +62,7 @@ export const logout = async () => { const { setIsLoggedIn, setUser } = useAuthStore.getState(); setIsLoggedIn(false); setUser(null); + showToast({ message: '로그아웃 되었습니다 ', type: 'success' }); return response; } catch (error) { console.error('로그아웃 에러:', error); From 6c3d3533355e0d6527b4fa55420b46ba4b709788 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:11:43 +0900 Subject: [PATCH 03/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=ED=86=A0?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=83=81?= =?UTF-8?q?=EC=88=98=EB=A1=9C=20=EA=B4=80=EB=A6=AC=20#312=20(#314)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/auth/react-query/customHooks.ts | 11 ++++- src/api/book-club/react-query/customHooks.ts | 16 +++++-- src/constants/messages/toast.ts | 45 +++++++++++++++++++ .../club-details/hooks/useJoinClub.ts | 5 ++- .../profile/container/MyJoinedClubList.tsx | 12 +++-- src/lib/hooks/useCancelClub.ts | 5 ++- src/lib/hooks/useLeaveClub.ts | 8 +++- src/lib/hooks/useLikeClub.ts | 5 ++- src/lib/hooks/useUnLikeClub.ts | 5 ++- 9 files changed, 93 insertions(+), 19 deletions(-) create mode 100644 src/constants/messages/toast.ts diff --git a/src/api/auth/react-query/customHooks.ts b/src/api/auth/react-query/customHooks.ts index 58834975..5b8ca5d6 100644 --- a/src/api/auth/react-query/customHooks.ts +++ b/src/api/auth/react-query/customHooks.ts @@ -1,5 +1,6 @@ import { useMutation } from '@tanstack/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; import { authClientAPI } from '../authClientAPI'; import { getUserInfo } from '@/features/auth/api/auth'; @@ -9,10 +10,16 @@ export function useEditInfoMutation() { mutationFn: (formData: FormData) => authClientAPI.editInfo(formData), onSuccess: () => { getUserInfo(); - showToast({ message: '프로필 수정이 완료되었습니다.', type: 'success' }); + showToast({ + message: TOAST_MESSAGES.SUCCESS.PROFILE_EDIT, + type: 'success', + }); }, onError: (error) => { - showToast({ message: '프로필 수정을 실패하였습니다', type: 'error' }); + showToast({ + message: TOAST_MESSAGES.ERROR.PROFILE_EDIT_FAILED, + type: 'error', + }); console.error(error); }, }); diff --git a/src/api/book-club/react-query/customHooks.ts b/src/api/book-club/react-query/customHooks.ts index dcfdc23e..b5d49fbc 100644 --- a/src/api/book-club/react-query/customHooks.ts +++ b/src/api/book-club/react-query/customHooks.ts @@ -1,6 +1,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query'; import { bookClubs } from './queries'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; import { bookClubLikeAPI, bookClubMainAPI, @@ -25,7 +26,10 @@ export function useBookClubCreateMutation() { }); }, onError: () => { - showToast({ message: '북클럽 생성에 실패했습니다.', type: 'error' }); + showToast({ + message: TOAST_MESSAGES.ERROR.CLUB_CREATE_FAILED, + type: 'error', + }); }, }); } @@ -74,12 +78,18 @@ export function useWriteReview() { queryClient.invalidateQueries({ queryKey: bookClubs.my()._ctx.reviews().queryKey, }); - showToast({ message: '리뷰 작성을 완료하였습니다', type: 'success' }); + showToast({ + message: TOAST_MESSAGES.SUCCESS.REVIEW_CREATE, + type: 'success', + }); }, onError: (error) => { console.error(error); - showToast({ message: '리뷰 작성을 실패하였습니다.', type: 'error' }); + showToast({ + message: TOAST_MESSAGES.ERROR.REVIEW_CREATE_FAILED, + type: 'error', + }); }, }); } diff --git a/src/constants/messages/toast.ts b/src/constants/messages/toast.ts new file mode 100644 index 00000000..5b166a5c --- /dev/null +++ b/src/constants/messages/toast.ts @@ -0,0 +1,45 @@ +export const TOAST_MESSAGES = { + SUCCESS: { + // 인증 관련 + LOGIN: '로그인에 성공했습니다.', + LOGOUT: '로그아웃되었습니다.', + SIGNUP: '회원가입이 완료되었습니다.', + + // 프로필 관련 + PROFILE_EDIT: '프로필 수정이 완료되었습니다.', + + // 북클럽 관련 + CLUB_CREATE: '북클럽이 생성되었습니다.', + CLUB_JOIN: '참여 완료! 함께하게 돼서 기뻐요🥰', + CLUB_CANCEL: '모임을 취소하였습니다.', + CLUB_LEAVE: '모임 참여를 취소하였습니다.', + CLUB_DELETE: '취소된 모임을 삭제하였습니다.', + CLUB_LIKE: '찜 완료! 찜한 모임은 찜 목록 페이지에서 확인하세요', + CLUB_UNLIKE: '찜이 취소되었습니다', + + // 리뷰 관련 + REVIEW_CREATE: '리뷰 작성을 완료하였습니다', + }, + + ERROR: { + // 인증 관련 + LOGIN_FAILED: '로그인에 실패했습니다.', + LOGOUT_FAILED: '로그아웃에 실패했습니다.', + + // 프로필 관련 + PROFILE_EDIT_FAILED: '프로필 수정을 실패하였습니다', + + // 북클럽 관련 + CLUB_CREATE_FAILED: '북클럽 생성에 실패했습니다.', + CLUB_JOIN_FAILED: '참여 요청 중 문제가 발생했습니다. 다시 시도해주세요.', + CLUB_CANCEL_FAILED: '모임 취소를 실패하였습니다.', + CLUB_LEAVE_FAILED: '모임 참여 취소를 실패하였습니다.', + + // 리뷰 관련 + REVIEW_CREATE_FAILED: '리뷰 작성을 실패하였습니다.', + REVIEW_VALIDATION: '점수와 리뷰 내용을 입력해주세요', + + // 일반 에러 + UNKNOWN: '알 수 없는 오류가 발생했습니다.', + }, +} as const; diff --git a/src/features/club-details/hooks/useJoinClub.ts b/src/features/club-details/hooks/useJoinClub.ts index 86deb3ea..09f71b05 100644 --- a/src/features/club-details/hooks/useJoinClub.ts +++ b/src/features/club-details/hooks/useJoinClub.ts @@ -1,5 +1,6 @@ import { useJoinBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; export const useJoinClub = () => { const { mutate: joinClub } = useJoinBookClub(); @@ -8,7 +9,7 @@ export const useJoinClub = () => { joinClub(clubId, { onSuccess: () => { showToast({ - message: '참여 완료! 함께하게 돼서 기뻐요🥰', + message: TOAST_MESSAGES.SUCCESS.CLUB_JOIN, type: 'success', }); }, @@ -20,7 +21,7 @@ export const useJoinClub = () => { }); } else { showToast({ - message: '참여 요청 중 문제가 발생했습니다. 다시 시도해주세요.', + message: TOAST_MESSAGES.ERROR.CLUB_JOIN_FAILED, type: 'error', }); } diff --git a/src/features/profile/container/MyJoinedClubList.tsx b/src/features/profile/container/MyJoinedClubList.tsx index 6176b401..30e6dcf0 100644 --- a/src/features/profile/container/MyJoinedClubList.tsx +++ b/src/features/profile/container/MyJoinedClubList.tsx @@ -16,6 +16,7 @@ import { useWriteReview, } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; import { BookClub } from '@/types/bookclubs'; import Loading from '@/components/loading/Loading'; import { useAuthStore } from '@/store/authStore'; @@ -58,7 +59,7 @@ export default function MyJoinedClubList({ order }: ClubListProps) { const res = await leaveClub(clubId); if (res) { showToast({ - message: '취소된 모임을 삭제하였습니다.', + message: TOAST_MESSAGES.SUCCESS.CLUB_DELETE, type: 'success', }); } @@ -80,7 +81,10 @@ export default function MyJoinedClubList({ order }: ClubListProps) { const onConfirmReview = (rating: number, content: string) => { //TODO: 토스트 메시지가 뜨더라도 모달이 열린 상태로 유지되도록 수정 if (!rating || !content) { - showToast({ message: '점수와 리뷰 내용을 입력해주세요', type: 'error' }); + showToast({ + message: TOAST_MESSAGES.ERROR.REVIEW_VALIDATION, + type: 'error', + }); return; } @@ -97,14 +101,14 @@ export default function MyJoinedClubList({ order }: ClubListProps) { const res = await leaveClub(selectedClubId); if (res) { showToast({ - message: '모임 참여를 취소하였습니다.', + message: TOAST_MESSAGES.SUCCESS.CLUB_LEAVE, type: 'success', }); } } } catch (error) { showToast({ - message: '모임 참여를 취소를 실패하였습니다.', + message: TOAST_MESSAGES.ERROR.CLUB_LEAVE_FAILED, type: 'error', }); console.error(error); diff --git a/src/lib/hooks/useCancelClub.ts b/src/lib/hooks/useCancelClub.ts index 68f81d4f..cd482b25 100644 --- a/src/lib/hooks/useCancelClub.ts +++ b/src/lib/hooks/useCancelClub.ts @@ -1,5 +1,6 @@ import { useCancelBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; import { useState } from 'react'; export function useCancelClub() { @@ -29,14 +30,14 @@ export function useCancelClub() { const res = await cancelClub(popUpState.selectedClubId); if (res) { showToast({ - message: '모임을 취소하였습니다.', + message: TOAST_MESSAGES.SUCCESS.CLUB_CANCEL, type: 'success', }); } } } catch (error) { showToast({ - message: '모임 취소를 실패하였습니다.', + message: TOAST_MESSAGES.ERROR.CLUB_CANCEL_FAILED, type: 'error', }); console.error(error); diff --git a/src/lib/hooks/useLeaveClub.ts b/src/lib/hooks/useLeaveClub.ts index fe37b393..ea27d67d 100644 --- a/src/lib/hooks/useLeaveClub.ts +++ b/src/lib/hooks/useLeaveClub.ts @@ -1,5 +1,6 @@ import { useLeaveBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; import { useState } from 'react'; export const useLeaveClub = () => { @@ -26,14 +27,17 @@ export const useLeaveClub = () => { try { if (popUpState.selectedClubId) { await leaveClub(popUpState.selectedClubId); - showToast({ message: '모임 참여를 취소하였습니다.', type: 'success' }); + showToast({ + message: TOAST_MESSAGES.SUCCESS.CLUB_LEAVE, + type: 'success', + }); } } catch (error) { if (error instanceof Error) { showToast({ message: error.message, type: 'error' }); } else { showToast({ - message: '알 수 없는 오류가 발생했습니다.', + message: TOAST_MESSAGES.ERROR.UNKNOWN, type: 'error', }); } diff --git a/src/lib/hooks/useLikeClub.ts b/src/lib/hooks/useLikeClub.ts index abab5c66..78f4368b 100644 --- a/src/lib/hooks/useLikeClub.ts +++ b/src/lib/hooks/useLikeClub.ts @@ -1,5 +1,6 @@ import { useLikeBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; export const useLikeClub = () => { const { mutate: likeClub } = useLikeBookClub(); @@ -8,7 +9,7 @@ export const useLikeClub = () => { likeClub(selectedClubId, { onSuccess: () => { showToast({ - message: '찜 완료! 찜한 모임은 찜 목록 페이지에서 확인하세요', + message: TOAST_MESSAGES.SUCCESS.CLUB_LIKE, type: 'success', }); }, @@ -23,7 +24,7 @@ export const useLikeClub = () => { message: error instanceof Error ? error.message - : '알 수 없는 오류가 발생했습니다. 다시 시도해주세요.', + : TOAST_MESSAGES.ERROR.UNKNOWN, type: 'error', }); } diff --git a/src/lib/hooks/useUnLikeClub.ts b/src/lib/hooks/useUnLikeClub.ts index dd6227e6..d65678ef 100644 --- a/src/lib/hooks/useUnLikeClub.ts +++ b/src/lib/hooks/useUnLikeClub.ts @@ -1,5 +1,6 @@ import { useUnLikeBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; +import { TOAST_MESSAGES } from '@/constants/messages/toast'; export const useUnLikeClub = () => { const { mutate: unLikeClub } = useUnLikeBookClub(); @@ -8,7 +9,7 @@ export const useUnLikeClub = () => { unLikeClub(selectedClubId, { onSuccess: () => { showToast({ - message: '찜이 취소되었습니다', + message: TOAST_MESSAGES.SUCCESS.CLUB_UNLIKE, type: 'success', }); }, @@ -23,7 +24,7 @@ export const useUnLikeClub = () => { message: error instanceof Error ? error.message - : '알 수 없는 오류가 발생했습니다. 다시 시도해주세요.', + : TOAST_MESSAGES.ERROR.UNKNOWN, type: 'error', }); } From 0fc6d1e96af60863ac9df4c4385fedba2ab4cb79 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:33:34 +0900 Subject: [PATCH 04/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20[Refactor]=20?= =?UTF-8?q?=ED=94=84=EB=A1=9C=EA=B7=B8=EB=A0=88=EC=8A=A4=EB=B0=94=20?= =?UTF-8?q?=EB=84=98=EC=B9=A8=20=ED=98=84=EC=83=81=20=ED=95=B4=EA=B2=B0=20?= =?UTF-8?q?(#313)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ♻️[Refactor] 프로그레스바 100% 안넘도록 수정 * ✅[Test] 테스트 코드 추가 --- src/components/progress-bar/ProgressBar.test.tsx | 9 +++++++++ src/components/progress-bar/ProgressBar.tsx | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/progress-bar/ProgressBar.test.tsx b/src/components/progress-bar/ProgressBar.test.tsx index 67aa3033..fb7d0079 100644 --- a/src/components/progress-bar/ProgressBar.test.tsx +++ b/src/components/progress-bar/ProgressBar.test.tsx @@ -16,4 +16,13 @@ describe('ProgressBar', () => { const fillBar = screen.getByRole('progressbar').children[0]; expect(fillBar).toHaveStyle({ width: '25%' }); }); + + it('percentage가 100을 초과할 경우 100%로 제한되는지 확인', () => { + render(); + const progressbar = screen.getByRole('progressbar'); + const fillBar = progressbar.children[0]; + + expect(progressbar).toHaveAttribute('aria-valuenow', '100'); + expect(fillBar).toHaveStyle({ width: '100%' }); + }); }); diff --git a/src/components/progress-bar/ProgressBar.tsx b/src/components/progress-bar/ProgressBar.tsx index 8041f380..3457c53f 100644 --- a/src/components/progress-bar/ProgressBar.tsx +++ b/src/components/progress-bar/ProgressBar.tsx @@ -16,10 +16,12 @@ function ProgressBar({ const fillColor = color || (isPast ? 'bg-gray-dark-02' : 'bg-green-normal-01'); + const limitedPercentage = Math.min(100, Math.max(0, percentage)); + return (
From 58426ac2fa93edfbbcf24f0aedc3a0cb188c22e5 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 13 Jan 2025 11:37:36 +0900 Subject: [PATCH 05/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20fetching=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/bookclub/page.tsx | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 91208bd1..c322215c 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,5 +1,35 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; -export default function Home() { - return ; +export default async function Home() { + console.log('SSR 함수 실행됨'); + + const DEFAULT_PAGE = 1; + const DEFAULT_SIZE = 10; + + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + const initialData = response.bookClubs; + + console.log('SSR로 가져온 데이터:', initialData); + + return ; + } catch (error) { + console.error('SSR 데이터 가져오기 실패:', error); + + return ; + } } From 8522c3060218376b08585b6826087a50268f84ac Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 14 Jan 2025 15:02:48 +0900 Subject: [PATCH 06/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20fetching?= =?UTF-8?q?=20=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20useQuery?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookclub/hooks/useFetchBookClubList.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 2b70ca45..5537816e 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { BookClubParams } from '@/types/bookclubs'; +import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -const useBookClubList = () => { +const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { + // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, error } = useQuery({ ...bookClubs.list(filters), + initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 + staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 }); const clubList = data?.bookClubs; + console.log(clubList); const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); From cbaa13519974d6ec5589e260af5f8d77a51c9911 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 15 Jan 2025 10:57:33 +0900 Subject: [PATCH 07/43] =?UTF-8?q?=E2=9C=A8[Feat]=20srr=EB=A1=9C=20data=20f?= =?UTF-8?q?etching=20=ED=9B=84=20csr=EB=A1=9C=20data=20fetching=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 +++++-- src/app/bookclub/page.tsx | 16 +++++----------- .../bookclub/components/BookClubMainPage.tsx | 11 ++++++++--- .../bookclub/hooks/useFetchBookClubList.ts | 14 +++++++++++--- src/lib/constants/filters.ts | 2 +- src/types/bookclubs.ts | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 92f84752..8b84c6b7 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,8 +6,11 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: [{ filters: filters || {} }], - queryFn: () => bookClubMainAPI.getBookClubs(filters), + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => { + console.log('Fetching data with filters:', filters); + bookClubMainAPI.getBookClubs(filters); + }, }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index c322215c..8937fbfc 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -3,19 +3,13 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; export default async function Home() { console.log('SSR 함수 실행됨'); - const DEFAULT_PAGE = 1; - const DEFAULT_SIZE = 10; - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index f434bbdf..07f31b3c 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -7,9 +7,14 @@ import ClubListSection from './ClubListSection'; import Button from '@/components/button/Button'; import { useRouter } from 'next/navigation'; import Loading from '@/components/loading/Loading'; +import { BookClub } from '@/types/bookclubs'; -function BookClubMainPage() { - const { clubList, isLoading, filters, updateFilters } = useBookClubList(); +function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { + const { clubList, isLoading, isFetching, filters, updateFilters } = + useBookClubList({ + initialData, + // defaultFilters, + }); const router = useRouter(); @@ -43,7 +48,7 @@ function BookClubMainPage() { } /> - {isLoading ? ( + {isLoading || isFetching ? (
diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 5537816e..74d3d60f 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,20 +3,27 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; +import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { - // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); - const { data, isLoading, error } = useQuery({ + const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), + //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => bookClubMainAPI.getBookClubs(filters), initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 + staleTime: 0, + // enabled: !!filters, // filters가 존재할 때만 요청 실행 }); const clubList = data?.bookClubs; console.log(clubList); + // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); + // console.log('filters: ',filters); + const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; @@ -24,6 +31,7 @@ const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { return { clubList, isLoading, + isFetching, error, filters, updateFilters, diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts index 98505373..0ee948a3 100644 --- a/src/lib/constants/filters.ts +++ b/src/lib/constants/filters.ts @@ -5,6 +5,6 @@ export const DEFAULT_FILTERS: BookClubParams = { meetingType: 'ALL', order: 'DESC', page: 1, - size: 100, + size: 10, searchKeyword: '', }; diff --git a/src/types/bookclubs.ts b/src/types/bookclubs.ts index 81b9c090..e043a785 100644 --- a/src/types/bookclubs.ts +++ b/src/types/bookclubs.ts @@ -42,7 +42,7 @@ export interface BookClub { averageScore?: number; - clubStatus: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 + clubStatus?: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 isLiked: boolean; isInactive: boolean; isJoined: boolean; From 86b7761a096f8b9415b1a900c9c3f525ab07fa34 Mon Sep 17 00:00:00 2001 From: cloud0406 <32586926+cloud0406@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:11:16 +0900 Subject: [PATCH 08/43] =?UTF-8?q?=08=F0=9F=93=9D[Docs]=20=EB=A6=AC?= =?UTF-8?q?=EB=93=9C=EB=AF=B8=20=ED=8F=AC=EB=A7=B7=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?(#319)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 📝[Docs] 리드미 포맷 적용 * 리드미 업데이트 --- README.md | 123 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 100 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index e215bc4c..d6c3cff8 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,113 @@ -This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). +
+image -## Getting Started +> 📖 당신의 독서 생활에 새로운 페이지를 열어보세요! +
새로운 사람들과 함께 읽고 나누는 특별한 독서 경험, **북코**가 함께합니다. +>
+
[![Bookco](https://img.shields.io/badge/BOOKCO.SITE-00a991?style=for-the-badge)](https://bookco.vercel.app/) +
+
+
-```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` +## 🎯 Bookco에서 할 수 있는 일 -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +- **👥 독서 모임** + + 비슷한 취향을 가진 사람들과 함께 책을 읽고 이야기를 나눌 수 있습니다. + - 정해진 책으로 독서 모임에 참여하거나, 직접 모임을 만들 수 있습니다. -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. +- 💬 **채팅하기** + + 다른 북코 유저들과 채팅 기능을 통해 소통할 수 있습니다. + - 모임의 호스트나 교환하고 싶은 책을 가진 유저와 대화를 나눌 수 있습니다. + +- **📚 교환하기 (추후 개발 예정..)** + + 안 보게 된 책을 등록하면, 다른 사람의 책과 바꿔 읽을 수 있습니다. + - 집에서 방치되던 책을 다른 유저와 공유할 수 있습니다. -This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. +
+
-## Learn More +## 📚 서비스 소개 -To learn more about Next.js, take a look at the following resources: +image +image +image +image +image +image +image +image +image -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. +
+
-You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! +## 🛠️ 기술스택 -## Deploy on Vercel +### 💻 Core +![Next.js](https://img.shields.io/badge/Next.js-000000?style=for-the-badge&logo=next.js&logoColor=white) +![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?style=for-the-badge&logo=typescript&logoColor=white) + +### 🔄 상태 관리 +![TanStack Query](https://img.shields.io/badge/TanStack_Query-FF4154?style=for-the-badge&logo=reactquery&logoColor=white) +![Zustand](https://img.shields.io/badge/Zustand-000000?style=for-the-badge) + +### 🌐 통신 +![Axios](https://img.shields.io/badge/Axios-5A29E4?style=for-the-badge&logo=axios&logoColor=white) +![SockJS](https://img.shields.io/badge/SockJS-000000?style=for-the-badge&logo=socket.io&logoColor=white) +![STOMP](https://img.shields.io/badge/STOMP-000000?style=for-the-badge) + +### 🎨 스타일링 +![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-06B6D4?style=for-the-badge&logo=tailwindcss&logoColor=white) + +### ⚙️ 유틸리티 +![Zod](https://img.shields.io/badge/Zod-3E67B1?style=for-the-badge&logo=zod&logoColor=white) + +### 🧪 테스팅 +![Jest](https://img.shields.io/badge/Jest-C21325?style=for-the-badge&logo=jest&logoColor=white) +![React Testing Library](https://img.shields.io/badge/React_Testing_Library-E33332?style=for-the-badge&logo=testing-library&logoColor=white) +![Storybook](https://img.shields.io/badge/Storybook-FF4785?style=for-the-badge&logo=storybook&logoColor=white) + +### 📋 코드 품질 +![ESLint](https://img.shields.io/badge/ESLint-4B32C3?style=for-the-badge&logo=eslint&logoColor=white) +![Prettier](https://img.shields.io/badge/Prettier-F7B93E?style=for-the-badge&logo=prettier&logoColor=black) +![Husky](https://img.shields.io/badge/Husky-000000?style=for-the-badge) + +
+
+ +## 🤝 팀 협업 방식, 브랜치 전략 + +### ✅ **PR 리뷰 방식** +- **2명 Approve** 방식 +- PR 확인 시간 고정: `09:00`, `13:00`, `18:00` +- **Pn 룰**과 **Dn 룰** 적용 +- **데일리 스크럼** 진행 + +### ✅ **브랜치 전략** +- **GitHub Flow** 적용 + - `feature` → `develop` → `main` + - `hotfix` 는 Main에서 급하게 수정할 일 있을 때 사용 + +### ✅ **CI/CD 전략** +- **Husky**를 통한 코드 품질 관리 + - 커밋시 린트 검사 +- **디스코드 웹훅 연결**로 실시간 알림 +- PR 작성시 Lint 검사, test 코드 실행, 스토리북 빌드, 프로덕션 빌드 실행하여 검사 + +
+
+ +## 👥 팀원 구성 + +|FE|FE|FE|FE| +|:---:|:---:|:---:|:---:| +||||| +|[김선구](https://github.com/haegu97)|[김민경](https://github.com/wynter24)|[신선](https://github.com/sunnwave)|[김정호](https://github.com/cloud0406)| +
-The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. From 9b61a24c9d82b629e04cae2c1d2d886ef11fbc5a Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 18 Jan 2025 15:37:10 +0900 Subject: [PATCH 09/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 ++--- src/app/bookclub/page.tsx | 27 +++---------------- src/app/page.tsx | 7 +++-- .../bookclub/components/BookClubMainPage.tsx | 1 - .../bookclub/hooks/useFetchBookClubList.ts | 19 ++++--------- src/lib/utils/fetchBookClubs.ts | 20 ++++++++++++++ 6 files changed, 35 insertions(+), 46 deletions(-) create mode 100644 src/lib/utils/fetchBookClubs.ts diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 8b84c6b7..92f84752 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,11 +6,8 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => { - console.log('Fetching data with filters:', filters); - bookClubMainAPI.getBookClubs(filters); - }, + queryKey: [{ filters: filters || {} }], + queryFn: () => bookClubMainAPI.getBookClubs(filters), }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 8937fbfc..3693cc9d 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,29 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; export default async function Home() { - console.log('SSR 함수 실행됨'); + const initialData = await fetchBookClubs(); - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - - const response = await res.json(); - const initialData = response.bookClubs; - - console.log('SSR로 가져온 데이터:', initialData); - - return ; - } catch (error) { - console.error('SSR 데이터 가져오기 실패:', error); - - return ; - } + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 91208bd1..3693cc9d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; -export default function Home() { - return ; +export default async function Home() { + const initialData = await fetchBookClubs(); + + return ; } diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 07f31b3c..6b864202 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -13,7 +13,6 @@ function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { const { clubList, isLoading, isFetching, filters, updateFilters } = useBookClubList({ initialData, - // defaultFilters, }); const router = useRouter(); diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 74d3d60f..57c9e31b 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,33 +3,24 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), - //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => bookClubMainAPI.getBookClubs(filters), - initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 0, - // enabled: !!filters, // filters가 존재할 때만 요청 실행 + initialData: { bookClubs: initialData }, + initialDataUpdatedAt: 0, + refetchOnMount: false, + refetchOnWindowFocus: false, }); - const clubList = data?.bookClubs; - console.log(clubList); - - // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); - // console.log('filters: ',filters); - const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; return { - clubList, + clubList: data?.bookClubs, isLoading, isFetching, error, diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts new file mode 100644 index 00000000..51ceb01b --- /dev/null +++ b/src/lib/utils/fetchBookClubs.ts @@ -0,0 +1,20 @@ +export async function fetchBookClubs() { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + return response.bookClubs; + } catch (error) { + console.error('데이터 가져오기 실패:', error); + return []; + } +} From 6d785dc22accfc2221203a9c6822d6a431374978 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 13 Jan 2025 11:37:36 +0900 Subject: [PATCH 10/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20fetching=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/bookclub/page.tsx | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 91208bd1..c322215c 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,5 +1,35 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; -export default function Home() { - return ; +export default async function Home() { + console.log('SSR 함수 실행됨'); + + const DEFAULT_PAGE = 1; + const DEFAULT_SIZE = 10; + + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + const initialData = response.bookClubs; + + console.log('SSR로 가져온 데이터:', initialData); + + return ; + } catch (error) { + console.error('SSR 데이터 가져오기 실패:', error); + + return ; + } } From b90275f4de34e6103dc43db9da385c635c55c985 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 14 Jan 2025 15:02:48 +0900 Subject: [PATCH 11/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20fetching?= =?UTF-8?q?=20=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20useQuery?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookclub/hooks/useFetchBookClubList.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 2b70ca45..5537816e 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { BookClubParams } from '@/types/bookclubs'; +import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -const useBookClubList = () => { +const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { + // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, error } = useQuery({ ...bookClubs.list(filters), + initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 + staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 }); const clubList = data?.bookClubs; + console.log(clubList); const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); From f22837a4326b113f754f08ea70f8501a423f9612 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 15 Jan 2025 10:57:33 +0900 Subject: [PATCH 12/43] =?UTF-8?q?=E2=9C=A8[Feat]=20srr=EB=A1=9C=20data=20f?= =?UTF-8?q?etching=20=ED=9B=84=20csr=EB=A1=9C=20data=20fetching=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 +++++-- src/app/bookclub/page.tsx | 16 +++++----------- .../bookclub/components/BookClubMainPage.tsx | 11 ++++++++--- .../bookclub/hooks/useFetchBookClubList.ts | 14 +++++++++++--- src/lib/constants/filters.ts | 2 +- src/types/bookclubs.ts | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 92f84752..8b84c6b7 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,8 +6,11 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: [{ filters: filters || {} }], - queryFn: () => bookClubMainAPI.getBookClubs(filters), + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => { + console.log('Fetching data with filters:', filters); + bookClubMainAPI.getBookClubs(filters); + }, }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index c322215c..8937fbfc 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -3,19 +3,13 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; export default async function Home() { console.log('SSR 함수 실행됨'); - const DEFAULT_PAGE = 1; - const DEFAULT_SIZE = 10; - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index f434bbdf..07f31b3c 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -7,9 +7,14 @@ import ClubListSection from './ClubListSection'; import Button from '@/components/button/Button'; import { useRouter } from 'next/navigation'; import Loading from '@/components/loading/Loading'; +import { BookClub } from '@/types/bookclubs'; -function BookClubMainPage() { - const { clubList, isLoading, filters, updateFilters } = useBookClubList(); +function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { + const { clubList, isLoading, isFetching, filters, updateFilters } = + useBookClubList({ + initialData, + // defaultFilters, + }); const router = useRouter(); @@ -43,7 +48,7 @@ function BookClubMainPage() { } /> - {isLoading ? ( + {isLoading || isFetching ? (
diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 5537816e..74d3d60f 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,20 +3,27 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; +import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { - // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); - const { data, isLoading, error } = useQuery({ + const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), + //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => bookClubMainAPI.getBookClubs(filters), initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 + staleTime: 0, + // enabled: !!filters, // filters가 존재할 때만 요청 실행 }); const clubList = data?.bookClubs; console.log(clubList); + // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); + // console.log('filters: ',filters); + const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; @@ -24,6 +31,7 @@ const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { return { clubList, isLoading, + isFetching, error, filters, updateFilters, diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts index 98505373..0ee948a3 100644 --- a/src/lib/constants/filters.ts +++ b/src/lib/constants/filters.ts @@ -5,6 +5,6 @@ export const DEFAULT_FILTERS: BookClubParams = { meetingType: 'ALL', order: 'DESC', page: 1, - size: 100, + size: 10, searchKeyword: '', }; diff --git a/src/types/bookclubs.ts b/src/types/bookclubs.ts index 81b9c090..e043a785 100644 --- a/src/types/bookclubs.ts +++ b/src/types/bookclubs.ts @@ -42,7 +42,7 @@ export interface BookClub { averageScore?: number; - clubStatus: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 + clubStatus?: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 isLiked: boolean; isInactive: boolean; isJoined: boolean; From 06344dafbcaa09066b3eeaba5a503c1209f47743 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 18 Jan 2025 15:37:10 +0900 Subject: [PATCH 13/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 ++--- src/app/bookclub/page.tsx | 27 +++---------------- src/app/page.tsx | 7 +++-- .../bookclub/components/BookClubMainPage.tsx | 1 - .../bookclub/hooks/useFetchBookClubList.ts | 19 ++++--------- src/lib/utils/fetchBookClubs.ts | 20 ++++++++++++++ 6 files changed, 35 insertions(+), 46 deletions(-) create mode 100644 src/lib/utils/fetchBookClubs.ts diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 8b84c6b7..92f84752 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,11 +6,8 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => { - console.log('Fetching data with filters:', filters); - bookClubMainAPI.getBookClubs(filters); - }, + queryKey: [{ filters: filters || {} }], + queryFn: () => bookClubMainAPI.getBookClubs(filters), }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 8937fbfc..3693cc9d 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,29 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; export default async function Home() { - console.log('SSR 함수 실행됨'); + const initialData = await fetchBookClubs(); - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - - const response = await res.json(); - const initialData = response.bookClubs; - - console.log('SSR로 가져온 데이터:', initialData); - - return ; - } catch (error) { - console.error('SSR 데이터 가져오기 실패:', error); - - return ; - } + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 91208bd1..3693cc9d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; -export default function Home() { - return ; +export default async function Home() { + const initialData = await fetchBookClubs(); + + return ; } diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 07f31b3c..6b864202 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -13,7 +13,6 @@ function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { const { clubList, isLoading, isFetching, filters, updateFilters } = useBookClubList({ initialData, - // defaultFilters, }); const router = useRouter(); diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 74d3d60f..57c9e31b 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,33 +3,24 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), - //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => bookClubMainAPI.getBookClubs(filters), - initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 0, - // enabled: !!filters, // filters가 존재할 때만 요청 실행 + initialData: { bookClubs: initialData }, + initialDataUpdatedAt: 0, + refetchOnMount: false, + refetchOnWindowFocus: false, }); - const clubList = data?.bookClubs; - console.log(clubList); - - // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); - // console.log('filters: ',filters); - const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; return { - clubList, + clubList: data?.bookClubs, isLoading, isFetching, error, diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts new file mode 100644 index 00000000..51ceb01b --- /dev/null +++ b/src/lib/utils/fetchBookClubs.ts @@ -0,0 +1,20 @@ +export async function fetchBookClubs() { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + return response.bookClubs; + } catch (error) { + console.error('데이터 가져오기 실패:', error); + return []; + } +} From 07a6a75b05bd40e5356069d2c09a935e8dbe6c95 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 20 Jan 2025 11:48:37 +0900 Subject: [PATCH 14/43] =?UTF-8?q?=F0=9F=92=84[Design]=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20size=3D100=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/constants/filters.ts | 2 +- src/lib/utils/fetchBookClubs.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts index 0ee948a3..98505373 100644 --- a/src/lib/constants/filters.ts +++ b/src/lib/constants/filters.ts @@ -5,6 +5,6 @@ export const DEFAULT_FILTERS: BookClubParams = { meetingType: 'ALL', order: 'DESC', page: 1, - size: 10, + size: 100, searchKeyword: '', }; diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index 51ceb01b..dafe472e 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,11 +1,14 @@ export async function fetchBookClubs() { try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); From 1887f7e5d697f4a8e1becedb6264147c69fc73fa Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 20 Jan 2025 14:28:54 +0900 Subject: [PATCH 15/43] =?UTF-8?q?=E2=9C=85[Test]=20fetchBookClub=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClub.test.ts | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/lib/utils/fetchBookClub.test.ts diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts new file mode 100644 index 00000000..f743473c --- /dev/null +++ b/src/lib/utils/fetchBookClub.test.ts @@ -0,0 +1,66 @@ +import { fetchBookClubs } from './fetchBookClubs'; + +describe('fetchBookClub', () => { + beforeEach(() => { + // fetch mocking + global.fetch = jest.fn(); + }); + + afterAll(() => { + // 테스트 후 모킹 초기화 + jest.resetAllMocks(); + }); + + it('요청 성공시 bookCluns를 반환해야 한다', async () => { + // 모의 응답 데이터 + const mockResponse = { + bookClubs: [ + { id: 1, title: 'bookclub 1' }, + { id: 2, title: 'bookclub 2' }, + ], + }; + + // fetch 모킹 + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockResponse), + }); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + expect(result).toEqual(mockResponse.bookClubs); + }); + + it('HTTP 에러가 발생하면 빈 배열을 반환해야 한다', async () => { + // fetch를 에러 상태로 모킹 + (global.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 500, + }); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + }); + + it('fetch 호출 중에 에러가 발생하면 빈 배열을 반환해야 한다', async () => { + // fetch 가 에러를 던지도록 모킹 + (global.fetch as jest.Mock).mockRejectedValue(new Error('Network Error')); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + }); +}); From 3e3ff44a9412930010034a7dafd23d7516d6b9f4 Mon Sep 17 00:00:00 2001 From: Sungu Kim <108677235+haegu97@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:12:16 +0900 Subject: [PATCH 16/43] =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#317=20(#318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../container/login-form/LoginForm.test.tsx | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/features/auth/container/login-form/LoginForm.test.tsx b/src/features/auth/container/login-form/LoginForm.test.tsx index 2add16e1..72f83f25 100644 --- a/src/features/auth/container/login-form/LoginForm.test.tsx +++ b/src/features/auth/container/login-form/LoginForm.test.tsx @@ -1,57 +1,64 @@ -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent from '@testing-library/user-event'; import LoginForm from './LoginForm'; -jest.mock('react-hook-form', () => ({ - useForm: () => ({ - register: () => ({}), - handleSubmit: (fn: any) => fn, - formState: { - isSubmitting: false, - errors: {}, - isValid: true, - }, - setError: jest.fn(), - reset: jest.fn(), - }), -})); - -// next/navigation mock jest.mock('next/navigation', () => ({ useRouter: () => ({ replace: jest.fn(), }), useSearchParams: () => null, })); - -describe('LoginForm', () => { - it('폼이 올바르게 렌더링되어야 한다', () => { +describe('LoginForm UI 테스트', () => { + it('로그인 폼의 모든 UI 요소가 올바르게 렌더링되어야 한다', () => { render(); - expect(screen.getByRole('heading', { name: '로그인' })).toBeInTheDocument(); expect(screen.getByLabelText('아이디')).toBeInTheDocument(); expect(screen.getByLabelText('비밀번호')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: '로그인' })).toBeInTheDocument(); + + expect(screen.getByText('회원가입')).toBeInTheDocument(); }); - it('이메일과 비밀번호를 입력할 수 있어야 한다', async () => { + it('입력 필드에 올바른 placeholder가 표시되어야 한다', () => { + render(); + + expect(screen.getByPlaceholderText('이메일')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('비밀번호')).toBeInTheDocument(); + }); +}); + +describe('LoginForm', () => { + it('이메일과 비밀번호를 입력했을 때 로그인 버튼이 활성화되어야 한다', async () => { render(); const emailInput = screen.getByLabelText('아이디'); const passwordInput = screen.getByLabelText('비밀번호'); + const submitButton = screen.getByRole('button', { name: '로그인' }); + + expect(submitButton).toBeDisabled(); await userEvent.type(emailInput, 'test@example.com'); await userEvent.type(passwordInput, 'password123'); expect(emailInput).toHaveValue('test@example.com'); expect(passwordInput).toHaveValue('password123'); + + expect(submitButton).toBeEnabled(); }); - it('로그인 버튼이 제출 가능한 상태여야 한다', () => { + it('유효하지 않은 이메일 형식을 입력하면 에러 메시지가 표시되어야 한다', async () => { render(); - const submitButton = screen.getByRole('button', { name: '로그인' }); - expect(submitButton).toBeEnabled(); + const emailInput = screen.getByLabelText('아이디'); + await userEvent.type(emailInput, 'invalid-email'); + await userEvent.tab(); + + await waitFor(() => { + expect( + screen.getByText('올바른 이메일 형식이 아닙니다.'), + ).toBeInTheDocument(); + }); }); }); From da9f989ddaf409aab9e1181ff1d5d1ae27c2069b Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 22 Jan 2025 22:51:30 +0900 Subject: [PATCH 17/43] =?UTF-8?q?=E2=9C=85[Test]=20useBookClubList=20?= =?UTF-8?q?=ED=9B=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(=EC=BF=BC=EB=A6=AC=ED=82=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EA=B2=80=EC=A6=9D)=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 3 + .../hooks/useFetchBookClubList.test.tsx | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/features/bookclub/hooks/useFetchBookClubList.test.tsx diff --git a/jest.config.js b/jest.config.js index 2d756f45..970b1b93 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,9 @@ const config = { coverageProvider: 'v8', testEnvironment: 'jsdom', // setupFilesAfterEnv: ['/src/setupTests.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/src/features/bookclub/hooks/useFetchBookClubList.test.tsx b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx new file mode 100644 index 00000000..127dc96f --- /dev/null +++ b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx @@ -0,0 +1,90 @@ +import { bookClubs } from '@/api/book-club/react-query'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import useBookClubList from './useFetchBookClubList'; +import { BookClubParams } from '@/types/bookclubs'; +import { mockBookClubs } from '@/mocks/mockDatas'; + +// API 호출 모킹 +jest.mock('@/api/book-club/react-query'); + +describe('useBookClubList', () => { + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + queryClient.clear(); + }); + + it('초기 데이터가 올바르게 설정되는지 테스트', () => { + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', { filters: {} }], + queryFn: jest.fn().mockResolvedValue({ bookClubs: mockBookClubs }), + }); + + const { result } = renderHook( + () => useBookClubList({ initialData: mockBookClubs }), + { + wrapper, + }, + ); + + expect(result.current.clubList).toEqual(mockBookClubs); + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetching).toBe(false); + }); + + it('필터 업데이트하고 다시 쿼리를 호출하는지 테스트', async () => { + const filters: BookClubParams = { bookClubType: 'FREE' }; + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', filters], + queryFn: jest.fn().mockResolvedValue({}), + }); + + const { result } = renderHook( + () => useBookClubList({ initialData: mockBookClubs }), + { + wrapper, + }, + ); + + act(() => { + result.current.updateFilters(filters); + }); + + await waitFor(() => + expect(bookClubs.list).toHaveBeenCalledWith( + expect.objectContaining(filters), + ), + ); + expect(result.current.filters.bookClubType).toBe('FREE'); + }); + + it('API 호출 실패 시 에러 반환', async () => { + const mockError = new Error('Network Error'); + const mockQueryFn = jest.fn().mockRejectedValue(mockError); + + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', {}], + queryFn: mockQueryFn, + }); + + const { result } = renderHook(() => useBookClubList({ initialData: [] }), { + wrapper, + }); + + await act(async () => { + await queryClient.prefetchQuery(bookClubs.list()); + }); + + await waitFor(() => { + expect(result.current.error).toBeDefined(); + }); + + expect(result.current.error?.message).toBe('Network Error'); + }); +}); From bc44663c1b961c22e6732fcdda569d2ffc3d68cc Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 13 Jan 2025 11:37:36 +0900 Subject: [PATCH 18/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20fetching=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/bookclub/page.tsx | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 91208bd1..c322215c 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,5 +1,35 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; -export default function Home() { - return ; +export default async function Home() { + console.log('SSR 함수 실행됨'); + + const DEFAULT_PAGE = 1; + const DEFAULT_SIZE = 10; + + try { + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + const initialData = response.bookClubs; + + console.log('SSR로 가져온 데이터:', initialData); + + return ; + } catch (error) { + console.error('SSR 데이터 가져오기 실패:', error); + + return ; + } } From ecd7a988af334f9c2f0da58323d11f4f74bb8331 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 14 Jan 2025 15:02:48 +0900 Subject: [PATCH 19/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20fetching?= =?UTF-8?q?=20=ED=95=9C=20=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A5=BC=20useQuery?= =?UTF-8?q?=20=EC=B4=88=EA=B8=B0=EB=8D=B0=EC=9D=B4=ED=84=B0=EB=A1=9C=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookclub/hooks/useFetchBookClubList.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 2b70ca45..5537816e 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -1,17 +1,21 @@ import { useState } from 'react'; -import { BookClubParams } from '@/types/bookclubs'; +import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -const useBookClubList = () => { +const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { + // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, error } = useQuery({ ...bookClubs.list(filters), + initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 + staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 }); const clubList = data?.bookClubs; + console.log(clubList); const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); From c37ebec9186d844efd0bbddc97646fec9281c220 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 15 Jan 2025 10:57:33 +0900 Subject: [PATCH 20/43] =?UTF-8?q?=E2=9C=A8[Feat]=20srr=EB=A1=9C=20data=20f?= =?UTF-8?q?etching=20=ED=9B=84=20csr=EB=A1=9C=20data=20fetching=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 +++++-- src/app/bookclub/page.tsx | 16 +++++----------- .../bookclub/components/BookClubMainPage.tsx | 11 ++++++++--- .../bookclub/hooks/useFetchBookClubList.ts | 14 +++++++++++--- src/lib/constants/filters.ts | 2 +- src/types/bookclubs.ts | 2 +- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 92f84752..8b84c6b7 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,8 +6,11 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: [{ filters: filters || {} }], - queryFn: () => bookClubMainAPI.getBookClubs(filters), + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => { + console.log('Fetching data with filters:', filters); + bookClubMainAPI.getBookClubs(filters); + }, }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index c322215c..8937fbfc 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -3,19 +3,13 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; export default async function Home() { console.log('SSR 함수 실행됨'); - const DEFAULT_PAGE = 1; - const DEFAULT_SIZE = 10; - try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?page=${DEFAULT_PAGE}&size=${DEFAULT_SIZE}`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index f434bbdf..07f31b3c 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -7,9 +7,14 @@ import ClubListSection from './ClubListSection'; import Button from '@/components/button/Button'; import { useRouter } from 'next/navigation'; import Loading from '@/components/loading/Loading'; +import { BookClub } from '@/types/bookclubs'; -function BookClubMainPage() { - const { clubList, isLoading, filters, updateFilters } = useBookClubList(); +function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { + const { clubList, isLoading, isFetching, filters, updateFilters } = + useBookClubList({ + initialData, + // defaultFilters, + }); const router = useRouter(); @@ -43,7 +48,7 @@ function BookClubMainPage() { } /> - {isLoading ? ( + {isLoading || isFetching ? (
diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 5537816e..74d3d60f 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,20 +3,27 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; +import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { - // const [filters, setFilters] = useState(defaultFilters); const [filters, setFilters] = useState(DEFAULT_FILTERS); - const { data, isLoading, error } = useQuery({ + const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), + //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => bookClubMainAPI.getBookClubs(filters), initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 1000 * 60 * 5, // 5분 동안 데이터를 Fresh 상태로 유지 + staleTime: 0, + // enabled: !!filters, // filters가 존재할 때만 요청 실행 }); const clubList = data?.bookClubs; console.log(clubList); + // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); + // console.log('filters: ',filters); + const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; @@ -24,6 +31,7 @@ const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { return { clubList, isLoading, + isFetching, error, filters, updateFilters, diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts index 98505373..0ee948a3 100644 --- a/src/lib/constants/filters.ts +++ b/src/lib/constants/filters.ts @@ -5,6 +5,6 @@ export const DEFAULT_FILTERS: BookClubParams = { meetingType: 'ALL', order: 'DESC', page: 1, - size: 100, + size: 10, searchKeyword: '', }; diff --git a/src/types/bookclubs.ts b/src/types/bookclubs.ts index 81b9c090..e043a785 100644 --- a/src/types/bookclubs.ts +++ b/src/types/bookclubs.ts @@ -42,7 +42,7 @@ export interface BookClub { averageScore?: number; - clubStatus: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 + clubStatus?: 'pending' | 'confirmed' | 'closed'; // TODO: 내가 만든 모임에서 '모임 완료' 상태 추가 isLiked: boolean; isInactive: boolean; isJoined: boolean; From 8b5f007d6da00274354cbdaf180f593bb0cff501 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 18 Jan 2025 15:37:10 +0900 Subject: [PATCH 21/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 ++--- src/app/bookclub/page.tsx | 27 +++---------------- src/app/page.tsx | 7 +++-- .../bookclub/components/BookClubMainPage.tsx | 1 - .../bookclub/hooks/useFetchBookClubList.ts | 19 ++++--------- src/lib/utils/fetchBookClubs.ts | 20 ++++++++++++++ 6 files changed, 35 insertions(+), 46 deletions(-) create mode 100644 src/lib/utils/fetchBookClubs.ts diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 8b84c6b7..92f84752 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,11 +6,8 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => { - console.log('Fetching data with filters:', filters); - bookClubMainAPI.getBookClubs(filters); - }, + queryKey: [{ filters: filters || {} }], + queryFn: () => bookClubMainAPI.getBookClubs(filters), }), detail: (bookClubId: number) => ({ diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 8937fbfc..3693cc9d 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,29 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; export default async function Home() { - console.log('SSR 함수 실행됨'); + const initialData = await fetchBookClubs(); - try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!res.ok) { - throw new Error(`HTTP error! status: ${res.status}`); - } - - const response = await res.json(); - const initialData = response.bookClubs; - - console.log('SSR로 가져온 데이터:', initialData); - - return ; - } catch (error) { - console.error('SSR 데이터 가져오기 실패:', error); - - return ; - } + return ; } diff --git a/src/app/page.tsx b/src/app/page.tsx index 91208bd1..3693cc9d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,5 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; -export default function Home() { - return ; +export default async function Home() { + const initialData = await fetchBookClubs(); + + return ; } diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 07f31b3c..6b864202 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -13,7 +13,6 @@ function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { const { clubList, isLoading, isFetching, filters, updateFilters } = useBookClubList({ initialData, - // defaultFilters, }); const router = useRouter(); diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 74d3d60f..57c9e31b 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,33 +3,24 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { const [filters, setFilters] = useState(DEFAULT_FILTERS); const { data, isLoading, isFetching, error } = useQuery({ ...bookClubs.list(filters), - //[데이터 그룹 식별, 세부 데이터 유형, 추가 정보(보통 객체로 전달)] - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => bookClubMainAPI.getBookClubs(filters), - initialData: { bookClubs: initialData }, // SSR로 전달받은 초기 데이터 설정 - staleTime: 0, - // enabled: !!filters, // filters가 존재할 때만 요청 실행 + initialData: { bookClubs: initialData }, + initialDataUpdatedAt: 0, + refetchOnMount: false, + refetchOnWindowFocus: false, }); - const clubList = data?.bookClubs; - console.log(clubList); - - // console.log('queryKey:', ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])]); - // console.log('filters: ',filters); - const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; return { - clubList, + clubList: data?.bookClubs, isLoading, isFetching, error, diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts new file mode 100644 index 00000000..51ceb01b --- /dev/null +++ b/src/lib/utils/fetchBookClubs.ts @@ -0,0 +1,20 @@ +export async function fetchBookClubs() { + try { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error(`HTTP error! status: ${res.status}`); + } + + const response = await res.json(); + return response.bookClubs; + } catch (error) { + console.error('데이터 가져오기 실패:', error); + return []; + } +} From 6c2a3ae135a9e83c522e3ba8428786fc66590e27 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 15 Jan 2025 10:57:33 +0900 Subject: [PATCH 22/43] =?UTF-8?q?=E2=9C=A8[Feat]=20srr=EB=A1=9C=20data=20f?= =?UTF-8?q?etching=20=ED=9B=84=20csr=EB=A1=9C=20data=20fetching=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EC=A4=91=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 +++++-- src/features/bookclub/hooks/useFetchBookClubList.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 92f84752..8b84c6b7 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,8 +6,11 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: [{ filters: filters || {} }], - queryFn: () => bookClubMainAPI.getBookClubs(filters), + queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], + queryFn: () => { + console.log('Fetching data with filters:', filters); + bookClubMainAPI.getBookClubs(filters); + }, }), detail: (bookClubId: number) => ({ diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 57c9e31b..3c90812d 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,6 +3,7 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; +import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { const [filters, setFilters] = useState(DEFAULT_FILTERS); From 27a8cb6f75cc2919805ebedf00595982afbd4db7 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 18 Jan 2025 15:37:10 +0900 Subject: [PATCH 23/43] =?UTF-8?q?=E2=9C=A8[Feat]=20ssr=EB=A1=9C=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A8=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A1=9C=20=EB=A0=8C=EB=8D=94=EB=A7=81=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/queries.ts | 7 ++----- src/features/bookclub/hooks/useFetchBookClubList.ts | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/api/book-club/react-query/queries.ts b/src/api/book-club/react-query/queries.ts index 8b84c6b7..92f84752 100644 --- a/src/api/book-club/react-query/queries.ts +++ b/src/api/book-club/react-query/queries.ts @@ -6,11 +6,8 @@ import { bookClubMainAPI } from '@/api/book-club/bookClubMainAPI'; export const bookClubs = createQueryKeys('bookClubs', { list: (filters?: BookClubParams) => ({ - queryKey: ['bookClubs', 'list', ...(filters ? Object.values(filters) : [])], - queryFn: () => { - console.log('Fetching data with filters:', filters); - bookClubMainAPI.getBookClubs(filters); - }, + queryKey: [{ filters: filters || {} }], + queryFn: () => bookClubMainAPI.getBookClubs(filters), }), detail: (bookClubId: number) => ({ diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 3c90812d..57c9e31b 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -3,7 +3,6 @@ import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useQuery } from '@tanstack/react-query'; import { bookClubs } from '@/api/book-club/react-query'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -import { bookClubMainAPI } from '@/api/book-club'; const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { const [filters, setFilters] = useState(DEFAULT_FILTERS); From 01575e7df8f504fd654bff1060101ba47b5fae16 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 20 Jan 2025 11:48:37 +0900 Subject: [PATCH 24/43] =?UTF-8?q?=F0=9F=92=84[Design]=20=EB=AA=A8=EC=9E=84?= =?UTF-8?q?=20=EC=B9=B4=EB=93=9C=20size=3D100=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/constants/filters.ts | 2 +- src/lib/utils/fetchBookClubs.ts | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/constants/filters.ts b/src/lib/constants/filters.ts index 0ee948a3..98505373 100644 --- a/src/lib/constants/filters.ts +++ b/src/lib/constants/filters.ts @@ -5,6 +5,6 @@ export const DEFAULT_FILTERS: BookClubParams = { meetingType: 'ALL', order: 'DESC', page: 1, - size: 10, + size: 100, searchKeyword: '', }; diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index 51ceb01b..dafe472e 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,11 +1,14 @@ export async function fetchBookClubs() { try { - const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', + const res = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); From e5455fadbd31a59e4d35d4dc2604a205a41a7cfe Mon Sep 17 00:00:00 2001 From: wynter24 Date: Mon, 20 Jan 2025 14:28:54 +0900 Subject: [PATCH 25/43] =?UTF-8?q?=E2=9C=85[Test]=20fetchBookClub=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClub.test.ts | 66 +++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/lib/utils/fetchBookClub.test.ts diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts new file mode 100644 index 00000000..f743473c --- /dev/null +++ b/src/lib/utils/fetchBookClub.test.ts @@ -0,0 +1,66 @@ +import { fetchBookClubs } from './fetchBookClubs'; + +describe('fetchBookClub', () => { + beforeEach(() => { + // fetch mocking + global.fetch = jest.fn(); + }); + + afterAll(() => { + // 테스트 후 모킹 초기화 + jest.resetAllMocks(); + }); + + it('요청 성공시 bookCluns를 반환해야 한다', async () => { + // 모의 응답 데이터 + const mockResponse = { + bookClubs: [ + { id: 1, title: 'bookclub 1' }, + { id: 2, title: 'bookclub 2' }, + ], + }; + + // fetch 모킹 + (global.fetch as jest.Mock).mockResolvedValue({ + ok: true, + json: jest.fn().mockResolvedValue(mockResponse), + }); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(fetch).toHaveBeenCalledWith( + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + expect(result).toEqual(mockResponse.bookClubs); + }); + + it('HTTP 에러가 발생하면 빈 배열을 반환해야 한다', async () => { + // fetch를 에러 상태로 모킹 + (global.fetch as jest.Mock).mockResolvedValue({ + ok: false, + status: 500, + }); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + }); + + it('fetch 호출 중에 에러가 발생하면 빈 배열을 반환해야 한다', async () => { + // fetch 가 에러를 던지도록 모킹 + (global.fetch as jest.Mock).mockRejectedValue(new Error('Network Error')); + + const result = await fetchBookClubs(); + + expect(fetch).toHaveBeenCalledTimes(1); + expect(result).toEqual([]); + }); +}); From 0361c7001cc17b3047c2f693e2ee2ddd4b87dc1c Mon Sep 17 00:00:00 2001 From: wynter24 Date: Wed, 22 Jan 2025 22:51:30 +0900 Subject: [PATCH 26/43] =?UTF-8?q?=E2=9C=85[Test]=20useBookClubList=20?= =?UTF-8?q?=ED=9B=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20(=EC=BF=BC=EB=A6=AC=ED=82=A4=EB=A1=9C=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EA=B8=B0=20=EA=B2=80=EC=A6=9D)=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jest.config.js | 3 + .../hooks/useFetchBookClubList.test.tsx | 90 +++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/features/bookclub/hooks/useFetchBookClubList.test.tsx diff --git a/jest.config.js b/jest.config.js index 2d756f45..970b1b93 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,6 +11,9 @@ const config = { coverageProvider: 'v8', testEnvironment: 'jsdom', // setupFilesAfterEnv: ['/src/setupTests.ts'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, }; // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async diff --git a/src/features/bookclub/hooks/useFetchBookClubList.test.tsx b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx new file mode 100644 index 00000000..127dc96f --- /dev/null +++ b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx @@ -0,0 +1,90 @@ +import { bookClubs } from '@/api/book-club/react-query'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, act, waitFor } from '@testing-library/react'; +import useBookClubList from './useFetchBookClubList'; +import { BookClubParams } from '@/types/bookclubs'; +import { mockBookClubs } from '@/mocks/mockDatas'; + +// API 호출 모킹 +jest.mock('@/api/book-club/react-query'); + +describe('useBookClubList', () => { + const queryClient = new QueryClient(); + + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + beforeEach(() => { + jest.clearAllMocks(); + queryClient.clear(); + }); + + it('초기 데이터가 올바르게 설정되는지 테스트', () => { + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', { filters: {} }], + queryFn: jest.fn().mockResolvedValue({ bookClubs: mockBookClubs }), + }); + + const { result } = renderHook( + () => useBookClubList({ initialData: mockBookClubs }), + { + wrapper, + }, + ); + + expect(result.current.clubList).toEqual(mockBookClubs); + expect(result.current.isLoading).toBe(false); + expect(result.current.isFetching).toBe(false); + }); + + it('필터 업데이트하고 다시 쿼리를 호출하는지 테스트', async () => { + const filters: BookClubParams = { bookClubType: 'FREE' }; + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', filters], + queryFn: jest.fn().mockResolvedValue({}), + }); + + const { result } = renderHook( + () => useBookClubList({ initialData: mockBookClubs }), + { + wrapper, + }, + ); + + act(() => { + result.current.updateFilters(filters); + }); + + await waitFor(() => + expect(bookClubs.list).toHaveBeenCalledWith( + expect.objectContaining(filters), + ), + ); + expect(result.current.filters.bookClubType).toBe('FREE'); + }); + + it('API 호출 실패 시 에러 반환', async () => { + const mockError = new Error('Network Error'); + const mockQueryFn = jest.fn().mockRejectedValue(mockError); + + (bookClubs.list as unknown as jest.Mock).mockReturnValue({ + queryKey: ['bookClubs', {}], + queryFn: mockQueryFn, + }); + + const { result } = renderHook(() => useBookClubList({ initialData: [] }), { + wrapper, + }); + + await act(async () => { + await queryClient.prefetchQuery(bookClubs.list()); + }); + + await waitFor(() => { + expect(result.current.error).toBeDefined(); + }); + + expect(result.current.error?.message).toBe('Network Error'); + }); +}); From 15fc754c7e084354b23c42151c8ffe84c0efa6af Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 10:09:25 +0900 Subject: [PATCH 27/43] =?UTF-8?q?=F0=9F=90=9B[Fix]=20SSR=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EC=84=9C=20=EC=84=9C=EB=B2=84=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98(API=5FURL)=20=EB=88=84=EB=9D=BD=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClubs.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index dafe472e..b1a484e0 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,14 +1,12 @@ export async function fetchBookClubs() { + const baseUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL; try { - const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, - { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - }, + const res = await fetch(`${baseUrl}/book-clubs?size=100`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', }, - ); + }); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); From f7b9b74f3628a695c7631c9f9bcb99f654829f1b Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 11:21:28 +0900 Subject: [PATCH 28/43] =?UTF-8?q?=F0=9F=92=AC[Comment]=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=B6=94=EA=B0=80=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClubs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index b1a484e0..5bbec747 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -15,7 +15,7 @@ export async function fetchBookClubs() { const response = await res.json(); return response.bookClubs; } catch (error) { - console.error('데이터 가져오기 실패:', error); + console.error('데이터 가져오기 실패: ', error); return []; } } From 24cf4e7be823eebfecad0c236d7c1f99b9e810c3 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 11:30:47 +0900 Subject: [PATCH 29/43] =?UTF-8?q?=E2=9C=85[Test]=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=97=90=EB=9F=AC=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClubs.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index 5bbec747..44fa9fa6 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,12 +1,15 @@ export async function fetchBookClubs() { - const baseUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL; + // const baseUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL; try { - const res = await fetch(`${baseUrl}/book-clubs?size=100`, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', + const res = await fetch( + `https://d3eoy4ym225l85.cloudfront.net/api/v1/book-clubs?size=100`, + { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, }, - }); + ); if (!res.ok) { throw new Error(`HTTP error! status: ${res.status}`); From 6018847707e1e7a67a7120f461104926d607bbe0 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 11:34:36 +0900 Subject: [PATCH 30/43] =?UTF-8?q?=E2=9C=85[Test]=20=ED=99=98=EA=B2=BD=20?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=97=90=EB=9F=AC=20=ED=99=95=EC=9D=B8=20?= =?UTF-8?q?#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClub.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts index f743473c..bf1b41a2 100644 --- a/src/lib/utils/fetchBookClub.test.ts +++ b/src/lib/utils/fetchBookClub.test.ts @@ -30,7 +30,7 @@ describe('fetchBookClub', () => { expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + `https://d3eoy4ym225l85.cloudfront.net/api/v1/book-clubs?size=100`, { method: 'GET', headers: { From c0373275910813ea0e7de48c0125ac1f0948f077 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 11:50:01 +0900 Subject: [PATCH 31/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EC=A3=BC=EC=86=8C=EB=A1=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClub.test.ts | 2 +- src/lib/utils/fetchBookClubs.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts index bf1b41a2..f743473c 100644 --- a/src/lib/utils/fetchBookClub.test.ts +++ b/src/lib/utils/fetchBookClub.test.ts @@ -30,7 +30,7 @@ describe('fetchBookClub', () => { expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith( - `https://d3eoy4ym225l85.cloudfront.net/api/v1/book-clubs?size=100`, + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, { method: 'GET', headers: { diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index 44fa9fa6..dafe472e 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,8 +1,7 @@ export async function fetchBookClubs() { - // const baseUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL; try { const res = await fetch( - `https://d3eoy4ym225l85.cloudfront.net/api/v1/book-clubs?size=100`, + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, { method: 'GET', headers: { @@ -18,7 +17,7 @@ export async function fetchBookClubs() { const response = await res.json(); return response.bookClubs; } catch (error) { - console.error('데이터 가져오기 실패: ', error); + console.error('데이터 가져오기 실패:', error); return []; } } From 72f52f7d95c8e6c29a0ae3fe4b6024e5e2076d33 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 14:36:15 +0900 Subject: [PATCH 32/43] =?UTF-8?q?=F0=9F=93=A6[Chore]=20next.config=20env?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/next.config.ts b/next.config.ts index 51ca252f..5eeb3438 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,6 +10,9 @@ const nextConfig: NextConfig = { instrumentation: { enabled: true, }, + env: { + NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + }, }; export default nextConfig; From 3eb1c529f997f06bfa88fa33bd244e5a2c2a15b9 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Fri, 24 Jan 2025 17:32:55 +0900 Subject: [PATCH 33/43] =?UTF-8?q?=F0=9F=93=A6[Chore]=20CI=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=84=A4=EC=A0=95:=20NEXT=5FPUBLIC=5FAPI=5FURL=20?= =?UTF-8?q?=EC=A0=84=EB=8B=AC=20=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 5 +++++ next.config.ts | 8 +++++--- src/app/bookclub/page.tsx | 7 +++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c239d990..e2d73509 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,9 @@ jobs: with: node-version: ${{ matrix.node-version }} + - name: Set environment variables + run: echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" >> .env + - name: Install dependencies run: npm install @@ -35,6 +38,8 @@ jobs: run: npm test - name: Build Next.js app + env: + NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }} run: npm run build - name: Build Storybook diff --git a/next.config.ts b/next.config.ts index 5eeb3438..03ded23d 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,9 +10,11 @@ const nextConfig: NextConfig = { instrumentation: { enabled: true, }, - env: { - NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, - }, + // env: { + // NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, + // }, }; +// console.log('Build-time NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL); + export default nextConfig; diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 3693cc9d..b0012414 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,8 +1,15 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; +// export const dynamic = 'force-dynamic'; + export default async function Home() { const initialData = await fetchBookClubs(); + // console.log( + // 'Server-side NEXT_PUBLIC_API_URL:', + // process.env.NEXT_PUBLIC_API_URL, + // ); + // console.log('something'); return ; } From d321aecfc389afed438364c21b30df1df47f15ff Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 25 Jan 2025 11:58:02 +0900 Subject: [PATCH 34/43] =?UTF-8?q?=F0=9F=92=AC[Comment]=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98=20test=20=EC=A3=BC=EC=84=9D=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- next.config.ts | 5 ----- src/app/bookclub/page.tsx | 7 ------- 2 files changed, 12 deletions(-) diff --git a/next.config.ts b/next.config.ts index 03ded23d..51ca252f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -10,11 +10,6 @@ const nextConfig: NextConfig = { instrumentation: { enabled: true, }, - // env: { - // NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL, - // }, }; -// console.log('Build-time NEXT_PUBLIC_API_URL:', process.env.NEXT_PUBLIC_API_URL); - export default nextConfig; diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index b0012414..3693cc9d 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,15 +1,8 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; -// export const dynamic = 'force-dynamic'; - export default async function Home() { const initialData = await fetchBookClubs(); - // console.log( - // 'Server-side NEXT_PUBLIC_API_URL:', - // process.env.NEXT_PUBLIC_API_URL, - // ); - // console.log('something'); return ; } From 00b888e9fbbe76c17927a1313b0fec1de2d076ac Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 25 Jan 2025 12:40:19 +0900 Subject: [PATCH 35/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20fetchBookC?= =?UTF-8?q?lub=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20mockBookClubs=20=EC=82=AC=EC=9A=A9=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClub.test.ts | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts index f743473c..c9c3cf33 100644 --- a/src/lib/utils/fetchBookClub.test.ts +++ b/src/lib/utils/fetchBookClub.test.ts @@ -1,3 +1,4 @@ +import { mockBookClubs } from '@/mocks/mockDatas'; import { fetchBookClubs } from './fetchBookClubs'; describe('fetchBookClub', () => { @@ -11,19 +12,11 @@ describe('fetchBookClub', () => { jest.resetAllMocks(); }); - it('요청 성공시 bookCluns를 반환해야 한다', async () => { - // 모의 응답 데이터 - const mockResponse = { - bookClubs: [ - { id: 1, title: 'bookclub 1' }, - { id: 2, title: 'bookclub 2' }, - ], - }; - + it('요청 성공시 bookClubs를 반환해야 한다', async () => { // fetch 모킹 (global.fetch as jest.Mock).mockResolvedValue({ ok: true, - json: jest.fn().mockResolvedValue(mockResponse), + json: jest.fn().mockResolvedValue({ bookClubs: mockBookClubs }), }); const result = await fetchBookClubs(); @@ -38,7 +31,7 @@ describe('fetchBookClub', () => { }, }, ); - expect(result).toEqual(mockResponse.bookClubs); + expect(result).toEqual(mockBookClubs); }); it('HTTP 에러가 발생하면 빈 배열을 반환해야 한다', async () => { From 169e0c58bf4e119f17f4e83e80342108df7f1954 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 25 Jan 2025 12:42:40 +0900 Subject: [PATCH 36/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20fetchBookC?= =?UTF-8?q?lubs=20=EC=97=90=EB=9F=AC=20=EA=B0=9C=EB=B0=9C=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=97=90=EC=84=9C=EB=A7=8C=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=9C=EB=A0=A5=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/utils/fetchBookClubs.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index dafe472e..a5bf275d 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -17,7 +17,9 @@ export async function fetchBookClubs() { const response = await res.json(); return response.bookClubs; } catch (error) { - console.error('데이터 가져오기 실패:', error); + if (process.env.NODE_ENV === 'development') { + console.error('Error:', error); // 개발 환경에서만 로그 출력 + } return []; } } From 7780462b322420d5cb44abcab42df42d5dfd981b Mon Sep 17 00:00:00 2001 From: wynter24 Date: Sat, 25 Jan 2025 14:38:07 +0900 Subject: [PATCH 37/43] =?UTF-8?q?=F0=9F=94=A5[Remove]=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20feature/bookclub/api=20=EC=82=AD=EC=A0=9C?= =?UTF-8?q?=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/bookclub/api/bookclubApi.ts | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 src/features/bookclub/api/bookclubApi.ts diff --git a/src/features/bookclub/api/bookclubApi.ts b/src/features/bookclub/api/bookclubApi.ts deleted file mode 100644 index ac7a7d9a..00000000 --- a/src/features/bookclub/api/bookclubApi.ts +++ /dev/null @@ -1,7 +0,0 @@ -import apiClient from '@/lib/utils/apiClient'; -import { BookClubParams } from '@/types/bookclubs'; - -export const getBookClubs = async (params?: BookClubParams) => { - const response = await apiClient.get('/book-clubs', { params }); - return response.data.bookClubs; -}; From 08c72b939d686378327420863ebe18b86129400a Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 4 Feb 2025 11:34:45 +0900 Subject: [PATCH 38/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20initialDat?= =?UTF-8?q?a=20=EB=B0=A9=EC=8B=9D=20=EB=8C=80=EC=8B=A0=20prefetchQuery?= =?UTF-8?q?=EB=A1=9C=20=EC=84=9C=EB=B2=84=EC=97=90=EC=84=9C=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EA=B0=80=EC=A0=B8=EC=98=A4=EA=B8=B0=20#30?= =?UTF-8?q?8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/bookclub/page.tsx | 19 +++++++++++++++++-- src/app/page.tsx | 19 +++++++++++++++++-- .../bookclub/components/BookClubMainPage.tsx | 19 ++++++++++--------- .../bookclub/hooks/useFetchBookClubList.ts | 18 ++---------------- src/lib/utils/fetchBookClubs.ts | 13 +++++++++++-- 5 files changed, 57 insertions(+), 31 deletions(-) diff --git a/src/app/bookclub/page.tsx b/src/app/bookclub/page.tsx index 3693cc9d..8e0d78c2 100644 --- a/src/app/bookclub/page.tsx +++ b/src/app/bookclub/page.tsx @@ -1,8 +1,23 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; export default async function Home() { - const initialData = await fetchBookClubs(); + const queryClient = new QueryClient(); - return ; + await queryClient.prefetchQuery({ + queryKey: ['bookClubs', 'list', DEFAULT_FILTERS], + queryFn: () => fetchBookClubs(DEFAULT_FILTERS), + }); + + return ( + + + + ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 3693cc9d..8e0d78c2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,8 +1,23 @@ import BookClubMainPage from '@/features/bookclub/components/BookClubMainPage'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; +import { + dehydrate, + HydrationBoundary, + QueryClient, +} from '@tanstack/react-query'; export default async function Home() { - const initialData = await fetchBookClubs(); + const queryClient = new QueryClient(); - return ; + await queryClient.prefetchQuery({ + queryKey: ['bookClubs', 'list', DEFAULT_FILTERS], + queryFn: () => fetchBookClubs(DEFAULT_FILTERS), + }); + + return ( + + + + ); } diff --git a/src/features/bookclub/components/BookClubMainPage.tsx b/src/features/bookclub/components/BookClubMainPage.tsx index 6b864202..cee9b1f3 100644 --- a/src/features/bookclub/components/BookClubMainPage.tsx +++ b/src/features/bookclub/components/BookClubMainPage.tsx @@ -7,18 +7,19 @@ import ClubListSection from './ClubListSection'; import Button from '@/components/button/Button'; import { useRouter } from 'next/navigation'; import Loading from '@/components/loading/Loading'; -import { BookClub } from '@/types/bookclubs'; +import { useQuery } from '@tanstack/react-query'; +import { fetchBookClubs } from '@/lib/utils/fetchBookClubs'; -function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { - const { clubList, isLoading, isFetching, filters, updateFilters } = - useBookClubList({ - initialData, - }); +function BookClubMainPage() { + const { filters, updateFilters } = useBookClubList(); + const { data, isLoading, isFetching } = useQuery({ + queryKey: ['bookClubs', 'list', filters], + queryFn: () => fetchBookClubs(filters), + staleTime: 1000 * 30, + }); const router = useRouter(); - const user = useAuthStore((state) => state.user); - const userName = user?.nickname || '북코'; const handleFilterChange = (newFilter: Partial) => { @@ -52,7 +53,7 @@ function BookClubMainPage({ initialData }: { initialData: BookClub[] }) { ) : ( - + )} ); diff --git a/src/features/bookclub/hooks/useFetchBookClubList.ts b/src/features/bookclub/hooks/useFetchBookClubList.ts index 57c9e31b..aa2940da 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.ts +++ b/src/features/bookclub/hooks/useFetchBookClubList.ts @@ -1,29 +1,15 @@ import { useState } from 'react'; -import { BookClub, BookClubParams } from '@/types/bookclubs'; -import { useQuery } from '@tanstack/react-query'; -import { bookClubs } from '@/api/book-club/react-query'; +import { BookClubParams } from '@/types/bookclubs'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -const useBookClubList = ({ initialData }: { initialData: BookClub[] }) => { +const useBookClubList = () => { const [filters, setFilters] = useState(DEFAULT_FILTERS); - const { data, isLoading, isFetching, error } = useQuery({ - ...bookClubs.list(filters), - initialData: { bookClubs: initialData }, - initialDataUpdatedAt: 0, - refetchOnMount: false, - refetchOnWindowFocus: false, - }); - const updateFilters = (newFilters: Partial) => { setFilters((prevFilters) => ({ ...prevFilters, ...newFilters })); }; return { - clubList: data?.bookClubs, - isLoading, - isFetching, - error, filters, updateFilters, }; diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index a5bf275d..5d42c56d 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -1,7 +1,16 @@ -export async function fetchBookClubs() { +import { BookClubParams } from '@/types/bookclubs'; + +export async function fetchBookClubs(filters: BookClubParams) { try { + // filters 객체를 URLSearchParams로 변환 + const queryParams = new URLSearchParams( + Object.entries(filters) + .filter(([, value]) => value !== undefined && value !== null) // undefined, null 값 제거 + .map(([key, value]) => [key, String(value)]), // 값들을 문자열로 변환 + ).toString(); + const res = await fetch( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?${queryParams}`, { method: 'GET', headers: { From 1054f49bf19b60e795e088d4489485848e831001 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 4 Feb 2025 11:42:45 +0900 Subject: [PATCH 39/43] =?UTF-8?q?=E2=99=BB=EF=B8=8F[Refactor]=20=EC=B0=9C?= =?UTF-8?q?=ED=95=98=EA=B8=B0=20=ED=9B=84=20mutate=EB=90=98=EB=8A=94=20?= =?UTF-8?q?=EB=AA=A8=EC=9E=84=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20=EC=BF=BC?= =?UTF-8?q?=EB=A6=AC=ED=82=A4=20=EB=B3=80=EA=B2=BD=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/book-club/react-query/customHooks.ts | 30 +++++++------- .../react-query/likeOptimisticUpdate.ts | 40 +++++++++---------- .../bookclub/components/ClubListSection.tsx | 11 +++-- src/lib/hooks/useLikeClub.ts | 6 ++- src/lib/hooks/useUnLikeClub.ts | 6 ++- 5 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/api/book-club/react-query/customHooks.ts b/src/api/book-club/react-query/customHooks.ts index b5d49fbc..0e8cd82f 100644 --- a/src/api/book-club/react-query/customHooks.ts +++ b/src/api/book-club/react-query/customHooks.ts @@ -11,6 +11,7 @@ import { import { WriteReviewParams } from '../types'; import { AxiosError } from 'axios'; import { likeOnError, likeOnMutate } from './likeOptimisticUpdate'; +import { BookClubParams } from '@/types/bookclubs'; export function useBookClubCreateMutation() { const queryClient = useQueryClient(); @@ -113,21 +114,22 @@ export function useCancelBookClub() { }); } -export function useLikeBookClub() { +export function useLikeBookClub(filter: BookClubParams) { const queryClient = useQueryClient(); return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.like(id), onMutate: async (id) => { - return likeOnMutate(queryClient, id, true); + return likeOnMutate(queryClient, id, true, filter); }, //TODO: 로직 확인 후 변경 필요 - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: bookClubs._def, - }); - }, + // onSuccess: () => { + // queryClient.invalidateQueries({ + // queryKey: ['bookClubs', 'list', DEFAULT_FILTERS], + // }); + // // console.log(bookClubs._def) + // }, onError: (_error, id, context) => { if (context) { @@ -137,21 +139,21 @@ export function useLikeBookClub() { }); } -export function useUnLikeBookClub() { +export function useUnLikeBookClub(filter: BookClubParams) { const queryClient = useQueryClient(); return useMutation, number>({ mutationFn: (id: number) => bookClubLikeAPI.unlike(id), onMutate: async (id) => { - return likeOnMutate(queryClient, id, false); + return likeOnMutate(queryClient, id, false, filter); }, //TODO: 로직 확인 후 변경 필요 - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: bookClubs._def, - }); - }, + // onSuccess: () => { + // queryClient.invalidateQueries({ + // queryKey: bookClubs._def, + // }); + // }, onError: (_error, id, context) => { if (context) { diff --git a/src/api/book-club/react-query/likeOptimisticUpdate.ts b/src/api/book-club/react-query/likeOptimisticUpdate.ts index 86a0a536..dce81855 100644 --- a/src/api/book-club/react-query/likeOptimisticUpdate.ts +++ b/src/api/book-club/react-query/likeOptimisticUpdate.ts @@ -1,5 +1,5 @@ import { QueryClient } from '@tanstack/react-query'; -import { BookClub } from '@/types/bookclubs'; +import { BookClub, BookClubParams } from '@/types/bookclubs'; import { bookClubs } from './queries'; import { DEFAULT_FILTERS } from '@/lib/constants/filters'; @@ -7,39 +7,39 @@ export const likeOnMutate = async ( queryClient: QueryClient, id: number, isLiked: boolean, + filter?: BookClubParams, ) => { - const listQueryKey = bookClubs.list(DEFAULT_FILTERS).queryKey; + const listQueryKey = ['bookClubs', 'list', filter || DEFAULT_FILTERS]; const detailQueryKey = bookClubs.detail(id).queryKey; - const previousBookClubs = queryClient.getQueryData<{ - bookClubs: BookClub[]; - }>(listQueryKey); + await queryClient.cancelQueries({ queryKey: listQueryKey }); + await queryClient.cancelQueries({ queryKey: detailQueryKey }); + + // console.log('🔍 수정된 listQueryKey:', listQueryKey); + // console.log('🔍 현재 활성화된 모든 쿼리키:', queryClient.getQueriesData({})); + const previousBookClubs = queryClient.getQueryData<{ bookClubs: BookClub[] }>( + listQueryKey, + ); const previousDetail = queryClient.getQueryData(detailQueryKey); - // 목록 캐시 업데이트 + // if (!previousBookClubs) { + // console.warn('⚠️ 캐시된 데이터가 없습니다. queryKey 확인 필요:', listQueryKey); + // queryClient.invalidateQueries({ queryKey: listQueryKey }); + // } + if (previousBookClubs) { - queryClient.setQueryData(listQueryKey, { - ...previousBookClubs, - bookClubs: previousBookClubs.bookClubs.map((club) => + queryClient.setQueryData(listQueryKey, (old: any) => + old?.map((club: BookClub) => club.id === id ? { ...club, isLiked } : club, ), - }); + ); } - // 상세 캐시 업데이트 if (previousDetail) { - queryClient.setQueryData(detailQueryKey, { - ...previousDetail, - isLiked, - }); + queryClient.setQueryData(detailQueryKey, { ...previousDetail, isLiked }); } - //TODO: 로직 확인 후 변경 필요 - queryClient.invalidateQueries({ - queryKey: bookClubs._def, - }); - return { previousBookClubs, previousDetail }; }; diff --git a/src/features/bookclub/components/ClubListSection.tsx b/src/features/bookclub/components/ClubListSection.tsx index 95b5231e..29bcd6ad 100644 --- a/src/features/bookclub/components/ClubListSection.tsx +++ b/src/features/bookclub/components/ClubListSection.tsx @@ -6,20 +6,23 @@ import { useRouter } from 'next/navigation'; import { useMemo } from 'react'; import EmptyState from '@/components/common-layout/EmptyState'; import { clubStatus } from '@/lib/utils/clubUtils'; -import { BookClub } from '@/types/bookclubs'; +import { BookClub, BookClubParams } from '@/types/bookclubs'; import { useLikeClub, useUnLikeClub } from '@/lib/hooks'; interface ClubListSectionProps { bookClubs: BookClub[]; + filter: BookClubParams; } -function ClubListSection({ bookClubs = [] }: ClubListSectionProps) { +function ClubListSection({ bookClubs = [], filter }: ClubListSectionProps) { const router = useRouter(); - const { onConfirmUnLike } = useUnLikeClub(); - const { onConfirmLike } = useLikeClub(); + const { onConfirmUnLike } = useUnLikeClub(filter); + const { onConfirmLike } = useLikeClub(filter); const today = useMemo(() => new Date(), []); + // console.log('🔍 ClubListSection 데이터:', bookClubs); + const handleLikeClub = (isLiked: boolean, id: number) => { isLiked ? onConfirmUnLike(id) : onConfirmLike(id); }; diff --git a/src/lib/hooks/useLikeClub.ts b/src/lib/hooks/useLikeClub.ts index 78f4368b..353278ec 100644 --- a/src/lib/hooks/useLikeClub.ts +++ b/src/lib/hooks/useLikeClub.ts @@ -1,9 +1,11 @@ import { useLikeBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; import { TOAST_MESSAGES } from '@/constants/messages/toast'; +import { BookClubParams } from '@/types/bookclubs'; +import { DEFAULT_FILTERS } from '../constants/filters'; -export const useLikeClub = () => { - const { mutate: likeClub } = useLikeBookClub(); +export const useLikeClub = (filter?: BookClubParams) => { + const { mutate: likeClub } = useLikeBookClub(filter || DEFAULT_FILTERS); const onConfirmLike = (selectedClubId: number) => { likeClub(selectedClubId, { diff --git a/src/lib/hooks/useUnLikeClub.ts b/src/lib/hooks/useUnLikeClub.ts index d65678ef..00bbda19 100644 --- a/src/lib/hooks/useUnLikeClub.ts +++ b/src/lib/hooks/useUnLikeClub.ts @@ -1,9 +1,11 @@ import { useUnLikeBookClub } from '@/api/book-club/react-query'; import { showToast } from '@/components/toast/toast'; import { TOAST_MESSAGES } from '@/constants/messages/toast'; +import { BookClubParams } from '@/types/bookclubs'; +import { DEFAULT_FILTERS } from '../constants/filters'; -export const useUnLikeClub = () => { - const { mutate: unLikeClub } = useUnLikeBookClub(); +export const useUnLikeClub = (filter?: BookClubParams) => { + const { mutate: unLikeClub } = useUnLikeBookClub(filter || DEFAULT_FILTERS); const onConfirmUnLike = (selectedClubId: number) => { unLikeClub(selectedClubId, { From 6af325f8c66fa798f05251c1e1bfc764533a3df1 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 4 Feb 2025 12:32:58 +0900 Subject: [PATCH 40/43] =?UTF-8?q?=E2=9C=85[Test]=20fetchBookClubs=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=20=EC=88=98=EC=A0=95=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=A5=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../hooks/useFetchBookClubList.test.tsx | 92 ++++--------------- src/lib/utils/fetchBookClub.test.ts | 24 ++--- src/lib/utils/fetchBookClubs.ts | 4 +- 3 files changed, 30 insertions(+), 90 deletions(-) diff --git a/src/features/bookclub/hooks/useFetchBookClubList.test.tsx b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx index 127dc96f..9c7d45bb 100644 --- a/src/features/bookclub/hooks/useFetchBookClubList.test.tsx +++ b/src/features/bookclub/hooks/useFetchBookClubList.test.tsx @@ -1,90 +1,34 @@ -import { bookClubs } from '@/api/book-club/react-query'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { renderHook, act, waitFor } from '@testing-library/react'; -import useBookClubList from './useFetchBookClubList'; -import { BookClubParams } from '@/types/bookclubs'; -import { mockBookClubs } from '@/mocks/mockDatas'; - -// API 호출 모킹 -jest.mock('@/api/book-club/react-query'); +import { renderHook, act } from '@testing-library/react'; +import useBookClubList from '@/features/bookclub/hooks/useFetchBookClubList'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; describe('useBookClubList', () => { - const queryClient = new QueryClient(); - - const wrapper = ({ children }: { children: React.ReactNode }) => ( - {children} - ); - - beforeEach(() => { - jest.clearAllMocks(); - queryClient.clear(); - }); - - it('초기 데이터가 올바르게 설정되는지 테스트', () => { - (bookClubs.list as unknown as jest.Mock).mockReturnValue({ - queryKey: ['bookClubs', { filters: {} }], - queryFn: jest.fn().mockResolvedValue({ bookClubs: mockBookClubs }), - }); - - const { result } = renderHook( - () => useBookClubList({ initialData: mockBookClubs }), - { - wrapper, - }, - ); + it('초기 필터 상태는 DEFAULT_FILTERS와 동일해야 한다', () => { + const { result } = renderHook(() => useBookClubList()); - expect(result.current.clubList).toEqual(mockBookClubs); - expect(result.current.isLoading).toBe(false); - expect(result.current.isFetching).toBe(false); + expect(result.current.filters).toEqual(DEFAULT_FILTERS); }); - it('필터 업데이트하고 다시 쿼리를 호출하는지 테스트', async () => { - const filters: BookClubParams = { bookClubType: 'FREE' }; - (bookClubs.list as unknown as jest.Mock).mockReturnValue({ - queryKey: ['bookClubs', filters], - queryFn: jest.fn().mockResolvedValue({}), - }); - - const { result } = renderHook( - () => useBookClubList({ initialData: mockBookClubs }), - { - wrapper, - }, - ); + it('updateFilters를 호출하면 필터 상태가 업데이트되어야 한다', () => { + const { result } = renderHook(() => useBookClubList()); act(() => { - result.current.updateFilters(filters); + result.current.updateFilters({ meetingType: 'ONLINE' }); }); - await waitFor(() => - expect(bookClubs.list).toHaveBeenCalledWith( - expect.objectContaining(filters), - ), - ); - expect(result.current.filters.bookClubType).toBe('FREE'); + expect(result.current.filters.meetingType).toBe('ONLINE'); }); - it('API 호출 실패 시 에러 반환', async () => { - const mockError = new Error('Network Error'); - const mockQueryFn = jest.fn().mockRejectedValue(mockError); - - (bookClubs.list as unknown as jest.Mock).mockReturnValue({ - queryKey: ['bookClubs', {}], - queryFn: mockQueryFn, - }); - - const { result } = renderHook(() => useBookClubList({ initialData: [] }), { - wrapper, - }); - - await act(async () => { - await queryClient.prefetchQuery(bookClubs.list()); - }); + it('updateFilters는 기존 필터 상태를 유지하면서 새로운 필터 값을 반영해야 한다', () => { + const { result } = renderHook(() => useBookClubList()); - await waitFor(() => { - expect(result.current.error).toBeDefined(); + act(() => { + result.current.updateFilters({ location: '서울' }); }); - expect(result.current.error?.message).toBe('Network Error'); + expect(result.current.filters.location).toBe('서울'); + expect(result.current.filters.meetingType).toBe( + DEFAULT_FILTERS.meetingType, + ); }); }); diff --git a/src/lib/utils/fetchBookClub.test.ts b/src/lib/utils/fetchBookClub.test.ts index c9c3cf33..b0f32e0e 100644 --- a/src/lib/utils/fetchBookClub.test.ts +++ b/src/lib/utils/fetchBookClub.test.ts @@ -1,29 +1,27 @@ import { mockBookClubs } from '@/mocks/mockDatas'; import { fetchBookClubs } from './fetchBookClubs'; +import { DEFAULT_FILTERS } from '@/lib/constants/filters'; -describe('fetchBookClub', () => { +describe('fetchBookClubs', () => { beforeEach(() => { - // fetch mocking global.fetch = jest.fn(); }); - afterAll(() => { - // 테스트 후 모킹 초기화 + afterEach(() => { jest.resetAllMocks(); }); - it('요청 성공시 bookClubs를 반환해야 한다', async () => { - // fetch 모킹 + it('요청 성공 시 bookClubs를 반환해야 한다', async () => { (global.fetch as jest.Mock).mockResolvedValue({ ok: true, json: jest.fn().mockResolvedValue({ bookClubs: mockBookClubs }), }); - const result = await fetchBookClubs(); + const result = await fetchBookClubs(DEFAULT_FILTERS); expect(fetch).toHaveBeenCalledTimes(1); expect(fetch).toHaveBeenCalledWith( - `${process.env.NEXT_PUBLIC_API_URL}/book-clubs?size=100`, + expect.stringContaining(`${process.env.NEXT_PUBLIC_API_URL}/book-clubs?`), { method: 'GET', headers: { @@ -34,24 +32,22 @@ describe('fetchBookClub', () => { expect(result).toEqual(mockBookClubs); }); - it('HTTP 에러가 발생하면 빈 배열을 반환해야 한다', async () => { - // fetch를 에러 상태로 모킹 + it('HTTP 에러 발생 시 빈 배열을 반환해야 한다', async () => { (global.fetch as jest.Mock).mockResolvedValue({ ok: false, status: 500, }); - const result = await fetchBookClubs(); + const result = await fetchBookClubs(DEFAULT_FILTERS); expect(fetch).toHaveBeenCalledTimes(1); expect(result).toEqual([]); }); - it('fetch 호출 중에 에러가 발생하면 빈 배열을 반환해야 한다', async () => { - // fetch 가 에러를 던지도록 모킹 + it('fetch 호출 중 에러 발생 시 빈 배열을 반환해야 한다', async () => { (global.fetch as jest.Mock).mockRejectedValue(new Error('Network Error')); - const result = await fetchBookClubs(); + const result = await fetchBookClubs(DEFAULT_FILTERS); expect(fetch).toHaveBeenCalledTimes(1); expect(result).toEqual([]); diff --git a/src/lib/utils/fetchBookClubs.ts b/src/lib/utils/fetchBookClubs.ts index 5d42c56d..e19d6c46 100644 --- a/src/lib/utils/fetchBookClubs.ts +++ b/src/lib/utils/fetchBookClubs.ts @@ -5,8 +5,8 @@ export async function fetchBookClubs(filters: BookClubParams) { // filters 객체를 URLSearchParams로 변환 const queryParams = new URLSearchParams( Object.entries(filters) - .filter(([, value]) => value !== undefined && value !== null) // undefined, null 값 제거 - .map(([key, value]) => [key, String(value)]), // 값들을 문자열로 변환 + .filter(([, value]) => value !== undefined && value !== null) + .map(([key, value]) => [key, String(value)]), ).toString(); const res = await fetch( From 86fcf7873dfd0dbbc9fef4aa4b6ce2919dee720c Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 4 Feb 2025 14:20:10 +0900 Subject: [PATCH 41/43] =?UTF-8?q?=F0=9F=90=9B[Test]=20Storybook=EC=97=90?= =?UTF-8?q?=EC=84=9C=20useRouter=20Mocking=ED=95=98=EC=97=AC=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=20=ED=95=B4=EA=B2=B0=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/preview.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index 4580843a..f3ec658e 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,6 +1,19 @@ import type { Preview } from '@storybook/react'; import '../src/styles/globals.css'; +// useRouter Mocking +jest.mock('next/router', () => ({ + useRouter: () => ({ + push: jest.fn(), + replace: jest.fn(), + back: jest.fn(), + prefetch: jest.fn(), + pathname: '/', + query: {}, + asPath: '/', + }), +})); + const preview: Preview = { parameters: { controls: { From 17a2e67c7a051d7963dfeb003b03cffe4b2064c7 Mon Sep 17 00:00:00 2001 From: wynter24 Date: Tue, 4 Feb 2025 14:54:08 +0900 Subject: [PATCH 42/43] =?UTF-8?q?=F0=9F=90=9B[Test]=20Storybook=EC=97=90?= =?UTF-8?q?=EC=84=9C=20useRouter=20Mocking=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=EA=B1=B0=20#308?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .storybook/preview.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.storybook/preview.ts b/.storybook/preview.ts index f3ec658e..4580843a 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -1,19 +1,6 @@ import type { Preview } from '@storybook/react'; import '../src/styles/globals.css'; -// useRouter Mocking -jest.mock('next/router', () => ({ - useRouter: () => ({ - push: jest.fn(), - replace: jest.fn(), - back: jest.fn(), - prefetch: jest.fn(), - pathname: '/', - query: {}, - asPath: '/', - }), -})); - const preview: Preview = { parameters: { controls: { From 280d3472d5bb6fd5ca265c849326920c3ca8a0c6 Mon Sep 17 00:00:00 2001 From: Jeongho Kim Date: Thu, 6 Feb 2025 11:03:37 +0900 Subject: [PATCH 43/43] =?UTF-8?q?=F0=9F=90=9B[Fix]=20storybook=20router=20?= =?UTF-8?q?=EC=97=90=EB=9F=AC=20fix=20(#334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChatBubbleList.stories.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/features/chat-room/container/chat-bubble-list/ChatBubbleList.stories.tsx b/src/features/chat-room/container/chat-bubble-list/ChatBubbleList.stories.tsx index 9eda641a..e2a6a9f2 100644 --- a/src/features/chat-room/container/chat-bubble-list/ChatBubbleList.stories.tsx +++ b/src/features/chat-room/container/chat-bubble-list/ChatBubbleList.stories.tsx @@ -4,6 +4,7 @@ import { GroupedMessage } from '@/features/chat-room/types/chatBubbleList'; import { useAuthStore } from '@/store/authStore'; import { useEffect } from 'react'; import { mockUser } from '@/mocks/mockDatas'; +import { useRouter } from 'next/navigation'; const AuthDecorator = (Story: React.ComponentType) => { useEffect(() => { @@ -16,10 +17,27 @@ const AuthDecorator = (Story: React.ComponentType) => { return ; }; +const MockNextRouter = (Story: React.ComponentType) => { + const mockRouter = { + push: () => Promise.resolve(), + replace: () => Promise.resolve(), + prefetch: () => Promise.resolve(), + back: () => Promise.resolve(), + forward: () => Promise.resolve(), + refresh: () => Promise.resolve(), + pathname: '/', + query: {}, + }; + + (useRouter as any).mockImplementation(() => mockRouter); + + return ; +}; + const meta: Meta = { title: 'Features/ChatRoom/ChatBubbleList', component: ChatBubbleList, - decorators: [AuthDecorator], + decorators: [AuthDecorator, MockNextRouter], parameters: { layout: 'centered', },