diff --git a/CLAUDE.md b/CLAUDE.md index 38ff92bf..c11a6756 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,6 +48,70 @@ 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`. 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..ada46944 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,46 @@ 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)) + } /> )} />