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
49 changes: 49 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 1. Tech Stack & Environment (Default)
- Frontend: React (Functional Components, Hooks ์ค‘์‹ฌ. ํ•จ์ˆ˜ ์„ ์–ธ ์Šคํƒ€์ผ์€ Coding Standards์˜ ํ•จ์ˆ˜ ์„ ์–ธ ์Šคํƒ€์ผ ๊ทœ์น™์„ ๋”ฐ๋ฅธ๋‹ค.)
- Style: Tailwind CSS + Shadcn UI (Maintain Design Consistency)
- Language: TypeScript (Strict mode, No `any`)
- Routing: React Router v7
- State: Zustand (UI State), TanStack Query (Server State)
- Networking: Axios (with Interceptors), MSW (Mocking Sync required)

# 2. Coding Standards (Quality & Readability)
- "์ฝ๊ธฐ ์‰ฌ์šด ์ฝ”๋“œ" ์ตœ์šฐ์„ : ๋ณ€์ˆ˜๋ช…์€ ์ง๊ด€์ ์œผ๋กœ, ๋กœ์ง์€ ๋‹จ์ˆœํ•˜๊ฒŒ ์ž‘์„ฑํ•  ๊ฒƒ.
- **์ฃผ์„ ์ž‘์„ฑ ์›์น™**:
- ์ฃผ์„์€ ์ฝ”๋“œ๊ฐ€ "๋ฌด์—‡์„ ํ•˜๋Š”์ง€"๋ณด๋‹ค "์™œ ์ด๋ ‡๊ฒŒ ์ฒ˜๋ฆฌํ•˜๋Š”์ง€"๋ฅผ ์„ค๋ช…ํ•œ๋‹ค.
- ์ฝ”๋“œ๋งŒ์œผ๋กœ ์˜๋„๊ฐ€ ๋ถ„๋ช…ํ•œ ๋‹จ์ˆœ ๋กœ์ง์—๋Š” ์ฃผ์„์„ ๋‹ฌ์ง€ ์•Š๋Š”๋‹ค.
- ํ™”๋ฉด, ํ›…, mock, API์ฒ˜๋Ÿผ ํ๋ฆ„์ด ๊ธด ํŒŒ์ผ์€ ๊ธฐ์กด ์ฝ”๋“œ์ฒ˜๋Ÿผ ๋ฒˆํ˜ธ๋‚˜ ์งง์€ ์ œ๋ชฉ์œผ๋กœ ๊ตฌํš์„ ๋‚˜๋ˆˆ๋‹ค.
- ์˜ˆ: `// 1. ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ ๋ฐ ๊ถŒํ•œ ํ™•์ธ`, `{/* ์ฐธ์—ฌ์ž ๋ช…๋‹จ ์„น์…˜ */}`
- ์ œํ’ˆ ์ •์ฑ…, ์„œ๋ฒ„ ์‘๋‹ต ๋ณด์™„, ์บ์‹œ ๋™๊ธฐํ™”, ์˜ˆ์™ธ ์ฒ˜๋ฆฌ, mock์ด ์‹ค์ œ ๋ฐฑ์—”๋“œ ์ •์ฑ…์„ ํ‰๋‚ด ๋‚ด๋Š” ๋ถ€๋ถ„์—๋Š” ์งง์€ ํ•œ๊ตญ์–ด ์ฃผ์„์„ ๋‚จ๊ธด๋‹ค.
- ์ฃผ์„์€ ๊ธฐ๋ณธ์ ์œผ๋กœ 1~2์ค„๋กœ ์ž‘์„ฑํ•˜๊ณ , ๊ธด ์„ค๋ช…์€ Plan ๋ฌธ์„œ๋‚˜ ๋ณ„๋„ ๋ฌธ์„œ์— ๋‚จ๊ธด๋‹ค.
- ์˜ค๋ž˜๋œ ์ฃผ์„, ์ฃผ์„ ์ฒ˜๋ฆฌ๋œ ๋ฏธ์‚ฌ์šฉ ์ฝ”๋“œ, ์ฝ”๋“œ์™€ ๋งž์ง€ ์•Š๋Š” ์„ค๋ช…์€ ์ˆ˜์ • ๊ณผ์ •์—์„œ ํ•จ๊ป˜ ์ •๋ฆฌํ•œ๋‹ค.
- ๋ชจ๋“ˆํ™”: ํ•˜๋‚˜์˜ ํŒŒ์ผ์ด ๋„ˆ๋ฌด ๊ธธ์–ด์ง€์ง€ ์•Š๊ฒŒ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌํ•  ๊ฒƒ.
- ์—๋Ÿฌ ์ฒ˜๋ฆฌ: ์˜ˆ์™ธ ์ƒํ™ฉ(Error Handling)์„ ํ•ญ์ƒ ๊ณ ๋ คํ•˜์—ฌ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ๊ฒƒ.
- **Strict Typing**: `any` ์‚ฌ์šฉ์„ ์—„๊ฒฉํžˆ ๊ธˆ์ง€ํ•˜๋ฉฐ, ๋ชจ๋“  API ์‘๋‹ต๊ณผ Props์— ๋Œ€ํ•ด ๊ตฌ์ฒด์ ์ธ interface/type์„ ์ •์˜ํ•  ๊ฒƒ.
- **๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์›์น™**: ์„œ๋ฒ„ ๋ฐ์ดํ„ฐ(Server State)๋Š” TanStack Query๋ฅผ, ์ˆœ์ˆ˜ UI ์ƒํƒœ๋‚˜ ์ „์—ญ ์บ์‹ฑ(Client State)์€ Zustand๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์—ญํ• ์„ ์—„๊ฒฉํžˆ ๋ถ„๋ฆฌํ•  ๊ฒƒ.
- **MSW Sync**: ์ƒˆ๋กœ์šด API ์—ฐ๋™์ด๋‚˜ ์ˆ˜์ • ์‹œ, ๋ฐ˜๋“œ์‹œ `src/mocks` ๋‚ด์˜ ๋ชจํ‚น ํ•ธ๋“ค๋Ÿฌ์™€ ๋ฐ์ดํ„ฐ๋„ ์„ธํŠธ๋กœ ์—…๋ฐ์ดํŠธํ•  ๊ฒƒ.
- **์ฝ”๋“œ ๊ฒ€ํ†  ๋ฐ ํ’ˆ์งˆ ๊ด€๋ฆฌ**:
- ์ฝ”๋“œ ๊ฒ€ํ† (ํƒ€์ž… ์ฒดํฌ, ํฌ๋งคํŒ… ๊ฒ€์‚ฌ ๋“ฑ)๊ฐ€ ํ•„์š”ํ•  ๋•Œ๋Š” `yarn check-all` ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ.
- ์ฝ”๋“œ ํฌ๋งคํŒ…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ `yarn fix` ๋ช…๋ น์–ด๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ.
- **ํ•จ์ˆ˜ ์„ ์–ธ ์Šคํƒ€์ผ**:
- React ์ปดํฌ๋„ŒํŠธ๋Š” `function` ํ‚ค์›Œ๋“œ๋กœ ์„ ์–ธํ•œ๋‹ค.
- ์˜ˆ: `export default function EventMain() { ... }`
- ์ปค์Šคํ…€ ํ›…์€ ๊ธฐ์กด ์ฝ”๋“œ๋ฒ ์ด์Šค ํŒจํ„ด์— ๋งž์ถฐ `function` ํ‚ค์›Œ๋“œ๋กœ ์„ ์–ธํ•œ๋‹ค.
- ์˜ˆ: `export default function useEventDetail(...) { ... }`
- ์ปดํฌ๋„ŒํŠธ/ํ›… ๋‚ด๋ถ€์˜ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง, ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋Š” ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋กœ ์ž‘์„ฑํ•œ๋‹ค.
- ์˜ˆ: `const handleSubmit = async () => { ... }`
- ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์•„๋‹Œ export ์œ ํ‹ธ๋ฆฌํ‹ฐ๋Š” ํ™”์‚ดํ‘œ ํ•จ์ˆ˜๋กœ ์ž‘์„ฑํ•œ๋‹ค.
- ์˜ˆ: `export const formatDate = (...) => { ... }`


# 3. Work Process (Mandatory File-based Planning)
- **Step-by-Step Approach**: ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•˜๊ธฐ ์ „, ๋ฐ˜๋“œ์‹œ ๋‹ค์Œ ๋‘ ํŒŒ์ผ์„ ์ƒ์„ฑ/์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ œ์‹œํ•œ๋‹ค.
1. **[Implementation Plan]**: ๊ตฌ์ฒด์ ์ธ ์ˆ˜์ • ๋ฒ”์œ„์™€ ๋กœ์ง์„ ํ•œ๊ตญ์–ด๋กœ ๊ธฐ์ˆ ํ•œ ๋ฌธ์„œ.
2. **[Task]**: ์ฒดํฌ๋ฐ•์Šค(`- [ ]`) ํ˜•ํƒœ์˜ ์„ธ๋ถ€ ์ž‘์—… ๋ฆฌ์ŠคํŠธ ํŒŒ์ผ.
- **Permission Required**: ์œ„ ๋‘ ํŒŒ์ผ์ด ์ƒ์„ฑ๋˜๊ณ , ์‚ฌ์šฉ์ž์˜ **์Šน์ธ(Confirmation)**์„ ๋ฐ›์€ ํ›„์—๋งŒ ์‹ค์ œ ์ฝ”๋“œ ์ˆ˜์ •์„ ์‹œ์ž‘ํ•œ๋‹ค.
- **Progress Tracking**: ์ž‘์—…์ด ์ง„ํ–‰๋จ์— ๋”ฐ๋ผ [Task] ํŒŒ์ผ์˜ ์ฒดํฌ๋ฐ•์Šค๋ฅผ ์—…๋ฐ์ดํŠธํ•˜์—ฌ ์ง„ํ–‰ ์ƒํ™ฉ์„ ๊ณต์œ ํ•œ๋‹ค.

# ๏ผ”. Communication & Persona
- ์–ธ์–ด: ๋ชจ๋“  ์„ค๋ช…๊ณผ ์ฃผ์„, ์ž‘์—… ๊ณ„ํš(Plan)์€ **'ํ•œ๊ตญ์–ด'**๋กœ ์ž‘์„ฑ.
- ์„ค๋ช… ๋ฐฉ์‹: ์ดˆ๋ณด์ž๋„ ์ดํ•ดํ•  ์ˆ˜ ์žˆ๊ฒŒ ์‰ฝ๊ฒŒ ์„ค๋ช…ํ•˜๋˜, ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ ๊ตฌ์กฐ๋ฅผ ๋ช…ํ™•ํžˆ ์งš์–ด์ค„ ๊ฒƒ.
- ํƒœ๋„: ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๋งŒ ์งœ์ง€ ๋ง๊ณ , ๋‚ด ์š”์ฒญ์— ์ž ์žฌ๋œ '๋ฆฌ์Šคํฌ'๋‚˜ ๋” ์ข‹์€ '๋Œ€์•ˆ'์ด ์žˆ๋‹ค๋ฉด ๋จผ์ € ์ œ์•ˆํ•ด์ฃผ๋Š” ํŒŒํŠธ๋„ˆ๊ฐ€ ๋  ๊ฒƒ.
- ๋‹ต๋ณ€ ํ˜•์‹: [๊ฒฐ๋ก /ํ•ด๊ฒฐ์ฑ…] -> [์ฝ”๋“œ] -> [์ƒ์„ธ ์„ค๋ช…] ์ˆœ์„œ๋กœ ๋‘๊ด„์‹์œผ๋กœ ๋‹ต๋ณ€ํ•  ๊ฒƒ.
1 change: 1 addition & 0 deletions CLAUDE.md
7 changes: 7 additions & 0 deletions src/hooks/useAuthStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ interface AuthState {
setGuestRegistration: (eventId: string, registrationId: string) => void;
// ์‹ ์ฒญ ์ทจ์†Œ ์‹œ ์ •๋ณด๋ฅผ ์‚ญ์ œํ•˜๋Š” ์•ก์…˜
removeGuestRegistration: (eventId: string) => void;
// ์ธ์ฆ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€” ๋•Œ ๋น„๋กœ๊ทธ์ธ ์‹ ์ฒญ ์ •๋ณด๋ฅผ ๋ชจ๋‘ ์‚ญ์ œํ•˜๋Š” ์•ก์…˜
clearGuestRegistrations: () => void;
// ์†Œ์…œ ๋กœ๊ทธ์ธ ๋ฐ ํšŒ์›๊ฐ€์ž… ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋ฅผ ์œ„ํ•œ ์ƒํƒœ
redirectUrl: string | null;
redirectTimestamp: number | null;
Expand All @@ -40,12 +42,16 @@ const useAuthStore = create<AuthState>()(
user,
token,
isLoggedIn: true,
// ๋กœ๊ทธ์ธ ๊ณ„์ • ์ƒํƒœ์™€ ๋ธŒ๋ผ์šฐ์ €์— ๋‚จ์€ ๋น„๋กœ๊ทธ์ธ ์‹ ์ฒญ ์ƒํƒœ๊ฐ€ ์„ž์ด์ง€ ์•Š๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค.
guestRegistrations: {},
}),
logout: () =>
set({
user: null,
token: null,
isLoggedIn: false,
// ๋กœ๊ทธ์•„์›ƒ ์ดํ›„์—๋Š” ์ด์ „ ๊ณ„์ •/๋ธŒ๋ผ์šฐ์ € ์‹ ์ฒญ ์ƒํƒœ๋ฅผ ์ด์–ด ๋ณด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
guestRegistrations: {},
}),
updateUser: (user) => set({ user }),
setGuestRegistration: (eventId, registrationId) =>
Expand All @@ -65,6 +71,7 @@ const useAuthStore = create<AuthState>()(
delete newRegistrations[eventId];
return { guestRegistrations: newRegistrations };
}),
clearGuestRegistrations: () => set({ guestRegistrations: {} }),

setRedirectUrl: (url) =>
set({
Expand Down
50 changes: 42 additions & 8 deletions src/hooks/useEventDetail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,17 @@ interface JoinEventVariables {
data: JoinEventRequest;
}

interface MergeEventDetailOptions {
onInvalidRegId?: () => void;
}

const shouldClearGuestRegId = (error: unknown): boolean =>
isAxiosError(error) && [401, 403, 404].includes(error.response?.status ?? 0);

const getMergedEventDetail = async (
eventId: string,
effectiveRegId?: string | null
effectiveRegId?: string | null,
options?: MergeEventDetailOptions
): Promise<EventDetailResponse> => {
const eventRes = await getEventDetail(eventId);
let mergedData = eventRes.data;
Expand All @@ -36,7 +44,7 @@ const getMergedEventDetail = async (
regRes.status === 'CONFIRMED' || regRes.status === 'WAITLISTED';

// ๋น„๋กœ๊ทธ์ธ ์‹ ์ฒญ์ž๋Š” ์ด๋ฒคํŠธ ์ƒ์„ธ ์‘๋‹ต๋งŒ์œผ๋กœ ๋ณธ์ธ ์ƒํƒœ๋ฅผ ์•Œ ์ˆ˜ ์—†์–ด์„œ
// ์ €์žฅํ•ด ๋‘” regId๋กœ ์‹ ์ฒญ ์ƒ์„ธ๋ฅผ ๋ณ‘ํ•ฉํ•ด ๊ฐ™์€ ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
// URL ๋˜๋Š” ์ €์žฅํ•ด ๋‘” regId๋กœ ์‹ ์ฒญ ์ƒ์„ธ๋ฅผ ๋ณ‘ํ•ฉํ•ด ๊ฐ™์€ ํ™”๋ฉด ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค.
mergedData = {
...mergedData,
viewer: {
Expand All @@ -56,28 +64,50 @@ const getMergedEventDetail = async (
};
} catch (regError) {
// regId๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๊ฑฐ๋‚˜ ์ž˜๋ชป๋œ ๊ฒฝ์šฐ์—๋Š” ๊ณต๊ฐœ ์ƒ์„ธ๋งŒ ๋ณด์—ฌ์ค˜์•ผ ํ•ฉ๋‹ˆ๋‹ค.
if (shouldClearGuestRegId(regError)) {
options?.onInvalidRegId?.();
}
console.error('Failed to fetch guest registration info:', regError);
}

return mergedData;
};

export default function useEventDetail(id?: string) {
const [searchParams] = useSearchParams();
const [searchParams, setSearchParams] = useSearchParams();
const queryClient = useQueryClient();
const invalidateEventRegistration = useInvalidateEventRegistration();

const isLoggedIn = useAuthStore((state) => state.isLoggedIn);
const urlRegId = searchParams.get('regId');
const guestRegId = useAuthStore((state) =>
id ? state.guestRegistrations[id] : null
);
const effectiveRegId = urlRegId || guestRegId;
const canUseGuestRegId = !isLoggedIn;
const effectiveRegId = canUseGuestRegId ? urlRegId || guestRegId : null;

Comment thread
jun-0411 marked this conversation as resolved.
const setGuestRegistration = useAuthStore(
(state) => state.setGuestRegistration
);

// 1. ์ด๋ฒคํŠธ ์ƒ์„ธ๋Š” regId์— ๋”ฐ๋ผ viewer ์ƒํƒœ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด key์— ํ•จ๊ป˜ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
const removeGuestRegistration = useAuthStore(
(state) => state.removeGuestRegistration
);

const removeRegIdQuery = () => {
if (!searchParams.has('regId')) return;

const nextSearchParams = new URLSearchParams(searchParams);
nextSearchParams.delete('regId');
setSearchParams(nextSearchParams, { replace: true });
};

const clearGuestRegId = (eventId: string) => {
removeGuestRegistration(eventId);
removeRegIdQuery();
};

// 1. ๋น„๋กœ๊ทธ์ธ regId์— ๋”ฐ๋ผ viewer ์ƒํƒœ๊ฐ€ ๋‹ฌ๋ผ์งˆ ์ˆ˜ ์žˆ์–ด key์— ํ•จ๊ป˜ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.
const eventDetailQuery = useQuery<EventDetailResponse, Error>({
queryKey: id
? queryKeys.events.detail(id, effectiveRegId)
Expand All @@ -86,7 +116,9 @@ export default function useEventDetail(id?: string) {
if (!id) {
throw new Error('EVENT_ID_REQUIRED');
}
return getMergedEventDetail(id, effectiveRegId);
return getMergedEventDetail(id, effectiveRegId, {
onInvalidRegId: () => clearGuestRegId(id),
});
},
enabled: Boolean(id),
retry: (failureCount, error) => {
Expand Down Expand Up @@ -114,8 +146,9 @@ export default function useEventDetail(id?: string) {
onSuccess: async (response, { eventId }) => {
const regId = response.data.registrationPublicId;

if (regId) {
if (!isLoggedIn && regId) {
setGuestRegistration(eventId, regId);
removeRegIdQuery();
}

await invalidateEventRegistration(eventId);
Expand All @@ -126,7 +159,8 @@ export default function useEventDetail(id?: string) {
mutationFn: (registrationId: string) => deleteRegistration(registrationId),
onSuccess: async () => {
if (id) {
// ๋น„๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž์˜ regId๋Š” ์œ ์ง€ํ•˜๊ณ , ์„œ๋ฒ„์˜ CANCELED ์ƒํƒœ๋ฅผ ๋‹ค์‹œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.
// ์ง์ ‘ ์ทจ์†Œํ•œ ์‹ ์ฒญ์€ ๋” ์ด์ƒ ์†Œ์œ ์ž ์ƒํƒœ๋กœ ๋ณ‘ํ•ฉํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
clearGuestRegId(id);
await invalidateEventRegistration(id);
}
},
Expand Down
4 changes: 2 additions & 2 deletions src/routes/EventMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,10 +104,10 @@ export default function EventMain() {
};

const onCancelClick = async () => {
// API ์šฐ์„ , ์—†์œผ๋ฉด Zustand ์Šคํ† ์–ด ํ™•์ธ
// ๋กœ๊ทธ์ธ ์ƒํƒœ์—์„œ๋Š” ๋น„๋กœ๊ทธ์ธ ๋ธŒ๋ผ์šฐ์ €์— ๋‚จ์€ regId๋ฅผ ์ทจ์†Œ ๋Œ€์ƒ์œผ๋กœ ์“ฐ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
const regId =
viewer.registrationPublicId ||
useAuthStore.getState().guestRegistrations[id];
(!isLoggedIn ? useAuthStore.getState().guestRegistrations[id] : null);
if (!regId) return;

const success = await handleCancelEvent(regId);
Expand Down