From 0187e6a9bb203777dc10021c179e4fc83c182b58 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:00:27 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20TanStack=20Query=20=ED=9B=85=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=20=ED=8C=A8=ED=84=B4=20=EB=B0=8F=20=EB=A0=88?= =?UTF-8?q?=EA=B1=B0=EC=8B=9C=20=EB=B0=A9=EC=8B=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 38ff92bf..96f73cf7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,64 @@ yarn generate:api 생성된 파일에서 API 인스턴스를 사용해 TanStack Query 훅을 작성. +#### TanStack Query 훅 작성 패턴 + +**useQuery (조회):** + +```typescript +export const useGetMissions = ({ groupStudyId, page = 1 }: GetMissionsParams) => { + return useQuery({ + queryKey: ['missions', groupStudyId, page], // 리소스명 + 파라미터 + queryFn: async () => { + const { data } = await missionApi.getMissions(groupStudyId, page); + return data.content; // content 추출 + }, + enabled: !!groupStudyId, // 조건부 실행 (선택) + }); +}; +``` + +**useMutation (생성/수정/삭제):** + +```typescript +export const useCreateMission = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ groupStudyId, request }: CreateMissionParams) => { + const { data } = await missionApi.createMission(groupStudyId, request); + return data.content; + }, + onSuccess: async (_, variables) => { + await queryClient.invalidateQueries({ + queryKey: ['missions', variables.groupStudyId], // 관련 쿼리 무효화 + }); + }, + }); +}; +``` + +**queryKey 컨벤션:** + +- 단일 리소스: `['mission', missionId]` +- 목록 리소스: `['missions', groupStudyId, page, size]` +- 무효화 시 상위 키 사용: `queryKey: ['missions']` (해당 리소스 전체 무효화) + +#### 레거시 방식 (features 내부 API) + +`src/features/<도메인>/api/` 디렉토리에 직접 axios 함수 작성: + +```typescript +import { axiosInstance } from '@/api/client/axios'; + +export const getArchive = async (params: GetArchiveParams) => { + const { data } = await axiosInstance.get<{ content: ArchiveResponse }>('/archive', { params }); + return data.content; +}; +``` + +레거시 방식은 기존 코드 유지보수용. 신규 API는 OpenAPI 방식 권장. + ### 상태 관리 - **Zustand** (`src/stores/`): 전역 클라이언트 상태. `useUserStore` (유저 정보 persist), `useLeaderStore`. From 47bc35ec36f872c7109f0eaee386762891dd8e88 Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:34:33 +0900 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20=EB=82=A0=EC=A7=9C=20=EC=9C=A0?= =?UTF-8?q?=ED=9A=A8=EC=84=B1=20=EA=B2=80=EC=82=AC=20=EB=B0=8F=20=EC=B5=9C?= =?UTF-8?q?=EC=86=8C=20=EB=82=A0=EC=A7=9C=20=EC=84=A4=EC=A0=95=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../group/model/group-study-form.schema.ts | 38 +++++++++++++++++++ .../study/group/ui/step/step1-group.tsx | 10 +++-- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index fa3b61b9..889582fd 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -100,6 +100,44 @@ export const GroupStudyFormSchema = z path: ['price'], }); } + + + const parseDate = (s: string) => { + const [y, m, d] = s.split('-').map(Number); + + return new Date(y, m - 1, d); + }; + + if (ISO_DATE_REGEX.test(data.startDate)) { + const start = parseDate(data.startDate); + const today = new Date(); + const tomorrow = new Date( + today.getFullYear(), + today.getMonth(), + today.getDate() + 1, + ); + + if (start < tomorrow) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '스터디 시작일은 내일부터 설정할 수 있습니다.', + path: ['startDate'], + }); + } + } + + // 종료일은 시작일과 같거나 이후여야 함 + if (ISO_DATE_REGEX.test(data.startDate) && ISO_DATE_REGEX.test(data.endDate)) { + const start = parseDate(data.startDate); + const end = parseDate(data.endDate); + if (end < start) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '종료일은 시작일과 같거나 이후여야 합니다.', + path: ['endDate'], + }); + } + } }); // 사진 상태 저장을 위한 로컬용 state diff --git a/src/features/study/group/ui/step/step1-group.tsx b/src/features/study/group/ui/step/step1-group.tsx index 06d351f3..86e7006b 100644 --- a/src/features/study/group/ui/step/step1-group.tsx +++ b/src/features/study/group/ui/step/step1-group.tsx @@ -1,5 +1,6 @@ 'use client'; +import { addDays } from 'date-fns'; import { Controller, useController, @@ -12,7 +13,7 @@ import FormField from '@/components/ui/form/form-field'; import { BaseInput } from '@/components/ui/input'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio'; import { GroupItems } from '@/components/ui/toggle'; -import { formatKoreaYMD } from '@/utils/time'; +import { formatKoreaYMD, getKoreaDate } from '@/utils/time'; import { TargetRole } from '../../api/group-study-types'; import { STUDY_TYPES, @@ -236,7 +237,7 @@ export default function Step1OpenGroupStudy() { type="date" value={field.value} onChange={field.onChange} - min={formatKoreaYMD()} + min={formatKoreaYMD(addDays(getKoreaDate(), 1))} max={watch('endDate') || undefined} /> )} @@ -250,7 +251,10 @@ export default function Step1OpenGroupStudy() { type="date" value={field.value} onChange={field.onChange} - min={watch('startDate') || formatKoreaYMD()} + min={ + watch('startDate') || + formatKoreaYMD(addDays(getKoreaDate(), 1)) + } /> )} /> From 0c5acacbdac9309ff0781baefb57106a85eff68a Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:38:46 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20prettier=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/features/study/group/model/group-study-form.schema.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/study/group/model/group-study-form.schema.ts b/src/features/study/group/model/group-study-form.schema.ts index 889582fd..ada46944 100644 --- a/src/features/study/group/model/group-study-form.schema.ts +++ b/src/features/study/group/model/group-study-form.schema.ts @@ -101,7 +101,6 @@ export const GroupStudyFormSchema = z }); } - const parseDate = (s: string) => { const [y, m, d] = s.split('-').map(Number); @@ -127,7 +126,10 @@ export const GroupStudyFormSchema = z } // 종료일은 시작일과 같거나 이후여야 함 - if (ISO_DATE_REGEX.test(data.startDate) && ISO_DATE_REGEX.test(data.endDate)) { + if ( + ISO_DATE_REGEX.test(data.startDate) && + ISO_DATE_REGEX.test(data.endDate) + ) { const start = parseDate(data.startDate); const end = parseDate(data.endDate); if (end < start) { From f089b420560855ba663517a49468984a6c8be01d Mon Sep 17 00:00:00 2001 From: Jeong Ha Seung <88266129+HA-SEUNG-JEONG@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:40:32 +0900 Subject: [PATCH 4/4] =?UTF-8?q?style:=20=EC=BD=94=EB=93=9C=20=ED=8F=AC?= =?UTF-8?q?=EB=A7=B7=ED=8C=85=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 96f73cf7..c11a6756 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,14 +53,17 @@ yarn generate:api **useQuery (조회):** ```typescript -export const useGetMissions = ({ groupStudyId, page = 1 }: GetMissionsParams) => { +export const useGetMissions = ({ + groupStudyId, + page = 1, +}: GetMissionsParams) => { return useQuery({ - queryKey: ['missions', groupStudyId, page], // 리소스명 + 파라미터 + queryKey: ['missions', groupStudyId, page], // 리소스명 + 파라미터 queryFn: async () => { const { data } = await missionApi.getMissions(groupStudyId, page); - return data.content; // content 추출 + return data.content; // content 추출 }, - enabled: !!groupStudyId, // 조건부 실행 (선택) + enabled: !!groupStudyId, // 조건부 실행 (선택) }); }; ``` @@ -78,7 +81,7 @@ export const useCreateMission = () => { }, onSuccess: async (_, variables) => { await queryClient.invalidateQueries({ - queryKey: ['missions', variables.groupStudyId], // 관련 쿼리 무효화 + queryKey: ['missions', variables.groupStudyId], // 관련 쿼리 무효화 }); }, }); @@ -99,7 +102,10 @@ export const useCreateMission = () => { import { axiosInstance } from '@/api/client/axios'; export const getArchive = async (params: GetArchiveParams) => { - const { data } = await axiosInstance.get<{ content: ArchiveResponse }>('/archive', { params }); + const { data } = await axiosInstance.get<{ content: ArchiveResponse }>( + '/archive', + { params }, + ); return data.content; }; ```