Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,70 @@ yarn generate:api <swagger-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`.
Expand Down
40 changes: 40 additions & 0 deletions src/features/study/group/model/group-study-form.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
});
}
Comment on lines 105 to 139
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the actual schema file
cat -n src/features/study/group/model/group-study-form.schema.ts | head -150

Repository: code-zero-to-one/study-platform-client

Length of output: 5392


🏁 Script executed:

# Check if getKoreaDate utility exists and how it's implemented
fd -t f "time" src/utils --exec cat -n {} \;

Repository: code-zero-to-one/study-platform-client

Length of output: 5638


🏁 Script executed:

# Search for getKoreaDate usage in the UI
rg "getKoreaDate|formatKoreaYMD" src/features/study/group -B 2 -A 2

Repository: code-zero-to-one/study-platform-client

Length of output: 1441


🏁 Script executed:

# Check the full schema file to understand context
wc -l src/features/study/group/model/group-study-form.schema.ts

Repository: code-zero-to-one/study-platform-client

Length of output: 143


🏁 Script executed:

# Verify the endDate comparison logic in the proposed fix
rg "endDate.*startDate" src/features/study/group -B 3 -A 3

Repository: code-zero-to-one/study-platform-client

Length of output: 64


스키마의 로컬 타임존 해석으로 인한 KST 검증 불일치 해결 필요

UI에서 formatKoreaYMD(addDays(getKoreaDate(), 1))로 KST 기준 내일 날짜를 min 속성으로 설정하지만, 스키마의 parseDate()new Date(y, m-1, d)로 브라우저의 로컬 타임존으로 해석합니다. 따라서 KST가 아닌 다른 타임존의 사용자에게서는 검증 결과가 어긋납니다.

YYYY-MM-DD 문자열 직접 비교로 타임존 해석을 완전히 제거하는 것이 안전합니다.

권장 수정안
    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) {
+      const todayKst = getKoreaDate();
+      const tomorrow = new Date(todayKst);
+      tomorrow.setDate(todayKst.getDate() + 1);
+      const toYmd = (d: Date) =>
+        `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(
+          d.getDate(),
+        ).padStart(2, '0')}`;
+      const tomorrowYmd = toYmd(tomorrow);
+
+      if (data.startDate < tomorrowYmd) {
         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) {
+      if (data.endDate < data.startDate) {
         ctx.addIssue({
           code: z.ZodIssueCode.custom,
           message: '종료일은 시작일과 같거나 이후여야 합니다.',
           path: ['endDate'],
         });
       }
     }

상단에 import { getKoreaDate } from '@/utils/time'; 추가 필요.

🤖 Prompt for AI Agents
In `@src/features/study/group/model/group-study-form.schema.ts` around lines 105 -
139, The validation wrongly parses YYYY-MM-DD into local Date via parseDate
causing timezone drift; replace the date arithmetic with pure string comparisons
using KST-based reference strings: import getKoreaDate (and use the same format
function used by the UI, e.g., formatKoreaYMD) to compute tomorrowKstYmd, then
for the startDate check (ISO_DATE_REGEX and data.startDate) compare
data.startDate < tomorrowKstYmd and call ctx.addIssue if true; likewise, for the
endDate vs startDate check (ISO_DATE_REGEX on both) compare data.endDate <
data.startDate and call ctx.addIssue if true, removing reliance on parseDate/new
Date for validation.

}
});

// 사진 상태 저장을 위한 로컬용 state
Expand Down
10 changes: 7 additions & 3 deletions src/features/study/group/ui/step/step1-group.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { addDays } from 'date-fns';
import {
Controller,
useController,
Expand All @@ -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,
Expand Down Expand Up @@ -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}
/>
)}
Expand All @@ -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))
}
/>
)}
/>
Expand Down
Loading