Feat/#107 이벤트 흐름 변경에 따른 기능 구현 및 리팩토링#119
Conversation
- '응모 하기' → '진행중인 이벤트'로 변경 - '응모 결과' → '종료된 이벤트'로 변경
- participation → inProgress (진행중인 이벤트) - result → finished (종료된 이벤트) - StepType 타입 정의 및 모든 참조 업데이트
Participation을 InProgressEvent로 변경하면서 하위 컴포넌트들도 Participation 접두사를 제거하고 각 역할을 명확히 표현하도록 변경 - Participation → InProgressEvent (진행중인 이벤트) - ParticipationAction → EventEntryAction (이벤트 참가 액션) - ParticipationCountdown → EventCountdown (카운트다운) - ParticipationModal → EntryTicketModal (응모권 선택 모달) - ParticipationPrize → PrizeInfo (상품 정보)
- SuccessModalContent 수정
- 종료된 이벤트 목록에 스크롤 가능하도록 overflow-y-auto 추가 - flex 컨테이너 오버플로우 방지를 위해 min-h-0 추가
- 이벤트 스키마에 nullable 타입 추가 (eventId, prize, eventEndDate) - 진행 중인 이벤트가 없을 때 EmptyEventState 컴포넌트 표시
구조 분해 할당을 null 체크 이후로 이동하여 prize, eventEndDate가 null일 때 발생하는 에러 해결
서로 다른 이벤트 결과가 같은 캐시를 공유하는 문제 해결
- MSW handler에서 동적 라우트 파라미터 처리 - EventWelcome에서 nullable prize 처리 및 리다이렉트 추가
- console.log 제거 - 미사용 변수 경고 해결 (eslint-disable 주석 추가) - AGENTS.md 규칙에 따라 import 순서 정리 (Node/built-in → External → Absolute → Relative)
- 에러 로깅 추가 (console.error) - AxiosError를 통한 서버 에러 메시지 표시 - 중복 color/severity prop 제거 - queryKey spread 연산자 제거
- WinnerInfoForm 컴포넌트로 전화번호 입력 폼 구현 - useSubmitWinnerPhoneNumber mutation hook 추가 - WinnerPhoneNumberSchema로 전화번호 유효성 검증 (010-XXXX-XXXX 형식) - submitWinnerPhoneNumber API 서비스 함수 추가 - ResultModal에 eventId prop 전달 및 폼 통합 - 성공/실패 시 토스트 메시지 표시
- MemberView 폴더 내부로 이동
- /events/lucky-draw/result/:path* 추가
|
Caution Review failedThe pull request is closed. ℹ️ Recent review infoConfiguration used: Repository UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
Walkthrough이 PR은 lucky-draw 이벤트 기능을 재구조화합니다. API 쿼리/서비스를 공개/비공개/항목 엔드포인트로 분리하고, UI를 GuestView/MemberView로 분할하며, 이벤트 결과 페이지와 당첨자 제출 양식(및 관련 뮤테이션/스키마/경로)을 추가합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser as Browser
participant ClientComp as EventResultClient
participant Modal as ResultModal/WinnerInfoForm
participant API as Server(API)
Browser->>ClientComp: 페이지 로드 (eventId)
ClientComp->>API: GET /events/{eventId}/entries (useSuspenseQuery)
API-->>ClientComp: EventResult 데이터
Browser->>ClientComp: 사용자 클릭 "추첨 시작"
ClientComp->>Modal: 모달 열기 (애니메이션 시작)
alt 당첨 & 전화 미제출
Modal->>Browser: WinnerInfoForm 표시
Browser->>Modal: 폼 제출(phone, agreements)
Modal->>API: POST /events/{eventId}/apply (submitWinnerForm)
API-->>Modal: 200 OK
Modal-->>ClientComp: onAnimationStop / 닫기
else 비당첨 또는 제출 완료
Modal-->>ClientComp: onAnimationStop / 닫기
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx (1)
26-57:⚠️ Potential issue | 🟠 Major모달 재오픈 또는 잔여 응모권 변경 시
ticketsCount가 동기화되지 않습니다.Line 26의
useState(remainingTicketsCount)는 초기 렌더에서만 반영됩니다. 이후 잔여 수량이 바뀌어도 이전 값이 남아 잘못된ticketsCount가 Line 30에서 전송될 수 있습니다.🔧 제안 수정안
-import { useState } from 'react' +import { useEffect, useState } from 'react' @@ const [ticketsCount, setTicketsCount] = useState(remainingTicketsCount) const { mutate: participationEvent } = useParticipationEvent() + + useEffect(() => { + if (isOpen) { + setTicketsCount(remainingTicketsCount) + } + }, [isOpen, remainingTicketsCount]) @@ <NumberInput - defaultValue={remainingTicketsCount} minValue={1} maxValue={remainingTicketsCount} label={'응모권 개수'}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx` around lines 26 - 57, The local state initialized with useState(remainingTicketsCount) (ticketsCount / setTicketsCount) is not kept in sync when remainingTicketsCount or the modal open state changes; add an effect in EntryTicketModal that calls setTicketsCount(remainingTicketsCount) whenever remainingTicketsCount or isOpen changes (or when the modal opens) so the NumberInput value and the payload sent by participationEvent({ eventId, ticketsCount }) are always current; ensure you still keep controlled value prop on NumberInput and avoid relying only on defaultValue.
🧹 Nitpick comments (14)
apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EventCountdown.tsx (1)
7-7: 반환 타입을 명시해 주세요.Line 7의
EventCountdown도 반환 타입이 빠져 있습니다. 컴포넌트 시그니처를 명시하면 타입 안정성이 높아집니다.🔧 제안 수정안
-export const EventCountdown = ({ eventEndDate }: { eventEndDate: string }) => { +export const EventCountdown = ({ + eventEndDate, +}: { + eventEndDate: string +}): JSX.Element => {As per coding guidelines
**/*.{ts,tsx}: Use TypeScript in strict mode with noUncheckedIndexedAccess enabled and return types required on functions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EventCountdown.tsx` at line 7, The EventCountdown function component lacks an explicit return type; update the component signature (export const EventCountdown = ({ eventEndDate }: { eventEndDate: string }) => ...) to include a return type such as : JSX.Element or : React.ReactElement (e.g., export const EventCountdown = (...) : JSX.Element => ...) so the component has an explicit TypeScript return type and satisfies strict/noUncheckedIndexedAccess rules; keep the existing prop type for eventEndDate and ensure any necessary React types are imported if not already.apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx (1)
20-30: 컴포넌트/핸들러 반환 타입을 명시해 주세요.Line 20의
EntryTicketModal과 Line 29의onPress에 반환 타입을 명시하면 strict 환경에서 시그니처 안정성이 좋아집니다.🔧 제안 수정안
-export const EntryTicketModal = ({ +export const EntryTicketModal = ({ isOpen, onOpenChange, eventId, remainingTicketsCount, -}: Props) => { +}: Props): JSX.Element => { @@ - const onPress = () => { + const onPress = (): void => { participationEvent({ eventId, ticketsCount }) }As per coding guidelines
**/*.{ts,tsx}: Use TypeScript in strict mode with noUncheckedIndexedAccess enabled and return types required on functions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx` around lines 20 - 30, Add explicit return types: annotate the EntryTicketModal component to return JSX.Element (e.g., change the component signature to return JSX.Element) and annotate the onPress handler to return void (e.g., onPress: () => void). Update the function signatures for EntryTicketModal and onPress (referencing the EntryTicketModal declaration and the onPress function) so TypeScript strict mode knows the expected return types.apps/web/app/events/lucky-draw/layout.tsx (1)
8-8: 컴포넌트 반환 타입을 명시해 주세요.Line 8의
LuckyDrawLayout은 반환 타입이 생략되어 있습니다. strict 설정에서 추론 변화로 인한 회귀를 줄이기 위해 명시 타입을 두는 편이 안전합니다.🔧 제안 수정안
-const LuckyDrawLayout = ({ children }: { children: React.ReactNode }) => { +const LuckyDrawLayout = ({ + children, +}: { + children: React.ReactNode +}): JSX.Element => {As per coding guidelines
**/*.{ts,tsx}: Use TypeScript in strict mode with noUncheckedIndexedAccess enabled and return types required on functions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/layout.tsx` at line 8, The LuckyDrawLayout component is missing an explicit return type; update its signature (function LuckyDrawLayout) to declare a return type (e.g., : React.ReactElement or : JSX.Element) while keeping the existing props type ({ children }: { children: React.ReactNode }) so the function has an explicit React return type to satisfy strict/noUncheckedIndexedAccess rules.apps/web/app/_mocks/handlers/eventHandlers.ts (1)
7-18: 주석 처리된 핸들러 정리 고려주석 처리된 mock 핸들러들이 여러 개 있습니다. 나중에 사용할 계획이라면 TODO 주석을 추가하고, 그렇지 않다면 삭제하는 것이 좋습니다.
♻️ 제안된 정리
export const EventHandlers = [ - // http.get(addBaseUrl(API_PATH.EVENT.INFO), () => { - // return HttpResponse.json(event) - // }), - // http.post(addBaseUrl(API_PATH.EVENT.ENTRIES), () => { - // return HttpResponse.json({ message: '성공' }) - // }), - // http.get(addBaseUrl('/events/:eventId/entries'), () => { - // return HttpResponse.json(eventResult) - // }), http.get(addBaseUrl(API_PATH.EVENT.ENTRIES), () => { return HttpResponse.json(EVENT_ENTRIES) }), + // TODO: 추가 핸들러 필요시 구현 ]🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/_mocks/handlers/eventHandlers.ts` around lines 7 - 18, The file contains several commented-out mock handlers (the http.get/http.post blocks referencing API_PATH.EVENT.INFO, API_PATH.EVENT.ENTRIES, '/events/:eventId/entries' and variables event/eventResult) that should be either removed or annotated; decide whether you need them later and if not delete these commented handlers, otherwise replace each commented block with a concise TODO comment explaining its intended future use (e.g., "TODO: re-enable mock for EVENT.INFO when implementing X") next to the related symbol (http.get/http.post, API_PATH.EVENT.INFO, API_PATH.EVENT.ENTRIES, '/events/:eventId/entries', event, eventResult) so the purpose is clear.apps/web/app/events/lucky-draw/_components/Pages/EmptyEventState.tsx (1)
9-27: JSX 속성에 일관된 따옴표 사용 권장코딩 가이드라인에 따르면
jsxSingleQuote: true가 설정되어 있어 JSX 속성값에 작은따옴표를 사용해야 합니다. 일부className에서 큰따옴표가 사용되고 있습니다.♻️ 제안된 수정
<Flex className={'gap-1'}> - <Text variant='title1' className='text-gray-300'> + <Text variant={'title1'} className={'text-gray-300'}> 현재 진행 중인 럭키드로우가 없습니다 </Text> <Icon type={'cry'} /> </Flex> <Column className={'items-center'}> <Text - variant='body1' - className='whitespace-pre-wrap break-words text-center text-gray-300' + variant={'body1'} + className={'whitespace-pre-wrap break-words text-center text-gray-300'} > 맛집 리뷰를 작성하고 응모권을 모아보세요 </Text> <Text - variant='body1' - className='whitespace-pre-wrap break-words text-center text-gray-300' + variant={'body1'} + className={'whitespace-pre-wrap break-words text-center text-gray-300'} > 다음 럭키드로우 이벤트에서 행운의 주인공이 되실 수 있습니다! </Text> </Column>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/EmptyEventState.tsx` around lines 9 - 27, Some JSX attributes in EmptyEventState.tsx use double quotes for className while project enforces jsxSingleQuote: true; update all JSX attribute quotes to single quotes (e.g., change className="..." to className='...') across the components in this file—specifically adjust the className props on <Flex>, <Column>, and the two <Text> elements (and any other JSX attributes like Icon type if inconsistent) to use single quotes, then run your formatter/ESLint to confirm consistency.apps/web/app/_apis/mutations/useSubmitWinnerForm.ts (2)
4-6: import 순서를 가이드라인 순서로 맞춰주세요.현재 상대 경로 import가 절대 경로 import보다 먼저 와 있습니다. 정렬 규칙에 맞추면 파일 가독성이 올라갑니다.
As per coding guidelines
**/*.{ts,tsx,js,jsx}: Organize imports in the following order: Node/built-in, external packages, absolute imports, relative imports.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/_apis/mutations/useSubmitWinnerForm.ts` around lines 4 - 6, Reorder the import statements in useSubmitWinnerForm.ts so they follow the repository guideline (Node/built-in, external packages, absolute imports, then relative imports): move the absolute imports (EventWinnerForm from '@/_apis/schemas/event' and EventQueryKeys from '@/_apis/queries/event') above the relative import (submitWinnerForm from '../services/event'), keeping the same imported symbols and names.
18-20:queryKey는 스프레드 없이 키 팩토리 반환값을 그대로 사용하세요.
EventQueryKeys.result(eventId)는 이미 올바른 키 튜플이라, 스프레드는 불필요합니다.✍️ 제안 수정
queryClient.invalidateQueries({ - queryKey: [...EventQueryKeys.result(eventId)], + queryKey: EventQueryKeys.result(eventId), })🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/_apis/mutations/useSubmitWinnerForm.ts` around lines 18 - 20, The invalidate call is spreading the key tuple unnecessarily; update the call to pass the key factory result directly by replacing queryClient.invalidateQueries({ queryKey: [...EventQueryKeys.result(eventId)] }) with queryClient.invalidateQueries({ queryKey: EventQueryKeys.result(eventId) }), referencing the existing queryClient.invalidateQueries and EventQueryKeys.result symbols.apps/web/app/events/lucky-draw/page.tsx (1)
5-14: 페이지 레벨에서 HydrationBoundaryPage 패턴을 유지하는 게 안전합니다.
GuestView/MemberView가 suspense query를 사용하는 구조라면, 이 페이지에서도 SSR prefetch + hydration 경계를 두는 쪽이 로딩/일관성 측면에서 더 안정적입니다.Based on learnings
Applies to **/(page|layout).{ts,tsx} : Use HydrationBoundaryPage for server-side rendering with prefetching.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/page.tsx` around lines 5 - 14, The page currently returns GuestView or MemberView directly in Page, which breaks the HydrationBoundaryPage SSR+prefetch pattern; wrap the conditional render in the HydrationBoundaryPage component and perform any server-side prefetching before returning so both MemberView and GuestView are hydrated consistently—update the Page function to fetch cookies/accessToken as now, run required prefetches for data used by MemberView/GuestView, then return <HydrationBoundaryPage>{accessToken ? <MemberView/> : <GuestView/>}</HydrationBoundaryPage> (use the HydrationBoundaryPage wrapper and call the components' prefetch hooks or loaders where applicable).apps/web/app/events/lucky-draw/result/[id]/page.tsx (1)
3-7: 페이지 함수에 명시적 반환 타입을 추가해주세요.현재 시그니처에 반환 타입이 없어, 프로젝트 타입 규칙과 어긋납니다.
✍️ 제안 수정
-const EventResultPage = async ({ +const EventResultPage = async ({ params, }: { params: Promise<{ id: string }> -}) => { +}): Promise<JSX.Element> => {As per coding guidelines
**/*.{ts,tsx}: Use TypeScript in strict mode with noUncheckedIndexedAccess enabled and return types required on functions.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/result/`[id]/page.tsx around lines 3 - 7, The EventResultPage async function lacks an explicit return type; update its signature to include a Promise return type (e.g., Promise<JSX.Element> or Promise<React.ReactElement>) so it conforms to the project's strict TypeScript rules. Locate the EventResultPage constant (the async function taking params) and annotate it with the chosen return type, ensuring any JSX returned inside matches that type and imports React types if necessary.apps/web/app/events/lucky-draw/_components/Pages/GuestView/GuestView.tsx (1)
39-86:Title/PrizeUI는 공용 컴포넌트 추출을 고려해주세요.
apps/web/app/places/new/_components/Step/EventWelcome/EventWelcome.tsx의 동일한 블록과 중복이 커서, 문구/스타일 변경 시 동시 수정 포인트가 늘어납니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/GuestView/GuestView.tsx` around lines 39 - 86, Duplicate UI blocks Title and Prize should be extracted into a shared component to avoid duplicated maintenance; create a reusable component (e.g., EventHeader or GiftPrize) that accepts props for texts and imageUrl and move the JSX from Title and Prize into that component, export it, then replace the local Title and Prize with imports and prop usages in GuestView (symbols: Title, Prize) and the other file where the same block appears (symbol: EventWelcome) so both use the single shared component; ensure props cover variant/styles and accessibility attributes (alt, priority) and update imports/exports accordingly.apps/web/app/events/lucky-draw/result/[id]/EventResultClient.tsx (2)
14-14: 타입 전용 import에type키워드를 사용하세요.
EventResult는 타입으로만 사용되므로, strict TypeScript 모드에서는import type을 사용하는 것이 권장됩니다.♻️ 제안된 수정
-import { EventResult } from '@/_apis/schemas/event' +import type { EventResult } from '@/_apis/schemas/event'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/result/`[id]/EventResultClient.tsx at line 14, The import of EventResult in EventResultClient.tsx is used only as a type; change the statement `import { EventResult } from '@/_apis/schemas/event'` to a type-only import by using `import type { EventResult } from '@/_apis/schemas/event'` so it compiles under strict TypeScript settings and avoids runtime import emissions.
41-46: 컴포넌트 언마운트 시 setTimeout 정리가 필요할 수 있습니다.
onClick핸들러의setTimeout이 컴포넌트 언마운트 후에도 실행될 수 있어, 언마운트된 컴포넌트의 상태를 업데이트하려고 시도할 수 있습니다. 현재 800ms의 짧은 지연이므로 실제 문제 발생 가능성은 낮지만, 방어적 코딩을 위해 cleanup을 고려할 수 있습니다.♻️ useRef를 사용한 cleanup 예시
const timerRef = useRef<NodeJS.Timeout | null>(null) useEffect(() => { return () => { if (timerRef.current) clearTimeout(timerRef.current) } }, []) const onClick = () => { setIsRunning(true) timerRef.current = setTimeout(() => { onOpen() }, LOTTERY_ANIMATION_DURATION_MS) }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/result/`[id]/EventResultClient.tsx around lines 41 - 46, The setTimeout in the onClick handler can fire after the component unmounts and should be cleaned up: add a timer ref (e.g., timerRef via useRef<NodeJS.Timeout | null>) in the EventResultClient component, assign the timeout ID to timerRef when calling setTimeout inside onClick, and add a useEffect cleanup that calls clearTimeout(timerRef.current) (and sets it to null) on unmount; also consider clearing any existing timer before setting a new one in onClick to avoid overlapping timers.apps/web/app/events/lucky-draw/_components/Pages/MemberView/MemberView.tsx (1)
35-40: 탭 전환 시 Suspense 경계 구조를 개선할 수 있습니다.현재 두 개의 Suspense 경계가 항상 렌더링되고 내부에서 조건부로 컴포넌트를 마운트합니다. 탭 전환 시 컴포넌트 상태를 유지할 필요가 없다면,
keyprop을 사용하여 단일 Suspense로 단순화할 수 있습니다:♻️ 제안된 리팩토링
- <Suspense fallback={<Spinner className={'m-auto'} />}> - {currentTab === 'inProgress' && <InProgressEvent />} - </Suspense> - <Suspense fallback={<Spinner className={'m-auto'} />}> - {currentTab === 'finished' && <FinishedEvent />} - </Suspense> + <Suspense key={currentTab} fallback={<Spinner className={'m-auto'} />}> + {currentTab === 'inProgress' ? <InProgressEvent /> : <FinishedEvent />} + </Suspense>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/MemberView.tsx` around lines 35 - 40, Two separate Suspense boundaries always render and conditionally mount InProgressEvent/FinishedEvent; simplify by using a single Suspense wrapping the tab content and use currentTab as a key so the correct component is mounted/cleared on tab switch. Replace the two Suspense blocks with one Suspense fallback={<Spinner className={'m-auto'} />} that renders either <InProgressEvent /> or <FinishedEvent /> based on currentTab and assign key={currentTab} to the rendered child to ensure the non-active tab is unmounted and state is not preserved.apps/web/app/events/lucky-draw/_components/Pages/MemberView/FinishedEvent/FinishedEvent.tsx (1)
32-62: EventSummary 컴포넌트 중복에 대한 TODO가 있습니다.TODO 주석에서 언급한 대로,
EventSummary가EventResultClient.tsx에도 유사하게 구현되어 있습니다. 날짜 포맷팅 로직(eventEndDate.slice(0, 10).replace(/-/g, '.'))도 동일합니다. 리팩토링 시 공통 컴포넌트와 날짜 포맷 유틸리티 함수로 추출하는 것을 권장합니다.공통
EventSummary컴포넌트와 날짜 포맷팅 유틸리티를 생성하는 것을 도와드릴까요? 또는 이 작업을 추적하기 위한 이슈를 생성할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/FinishedEvent/FinishedEvent.tsx` around lines 32 - 62, EventSummary is duplicated (also in EventResultClient.tsx) and repeats date formatting logic; extract a shared EventSummary component (replace the local EventSummary in FinishedEvent.tsx and the one in EventResultClient.tsx) and move the date formatting into a small utility (e.g., formatEventDate) used by both; update FinishedEvent's EventSummary usage to import the shared component and call formatEventDate(eventEndDate) instead of eventEndDate.slice(...).replace(...) so both files share the same UI and formatting logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/app/_apis/schemas/event.ts`:
- Around line 31-35: EventResultSchema currently extends BaseEventSchema which
forces result responses to include BaseEventSchema fields (e.g., prize,
totalWinnersCount, participantsCount, eventEndDate) and causes parsing/type
failures against the eventResult mock; change EventResultSchema to be a
standalone schema that only validates result-specific fields (isWinner,
usedTicketsCount, isPhoneSubmitted) or create a separate Result-only schema and
use that for response parsing instead of BaseEventSchema.extend; update
references to EventResultSchema where result responses are validated so they
consume the new result-only schema and ensure compatibility with
apps/web/app/_mocks/data/event.ts's eventResult shape.
In `@apps/web/app/_constants/path.ts`:
- Around line 39-41: Add explicit return types to the arrow functions in the
path constants: change RESULT and APPLY to include a : string return type (i.e.,
RESULT: (eventId: string): string => `/events/${eventId}/entries` and APPLY:
(eventId: string): string => `/events/${eventId}/apply`) so the TypeScript files
comply with the rule that functions must declare their return type.
In
`@apps/web/app/events/lucky-draw/result/`[id]/_components/WinnerInfoForm/WinnerInfoForm.tsx:
- Line 56: In WinnerInfoForm, the HeroUI Input uses the wrong prop name: replace
the Input prop "disabled={isSubmitting}" with "isDisabled={isSubmitting}" so it
matches the component API and stays consistent with the existing "isInvalid"
usage; update the Input instance in the WinnerInfoForm component (where
isSubmitting is referenced) to use isDisabled.
---
Outside diff comments:
In
`@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx`:
- Around line 26-57: The local state initialized with
useState(remainingTicketsCount) (ticketsCount / setTicketsCount) is not kept in
sync when remainingTicketsCount or the modal open state changes; add an effect
in EntryTicketModal that calls setTicketsCount(remainingTicketsCount) whenever
remainingTicketsCount or isOpen changes (or when the modal opens) so the
NumberInput value and the payload sent by participationEvent({ eventId,
ticketsCount }) are always current; ensure you still keep controlled value prop
on NumberInput and avoid relying only on defaultValue.
---
Nitpick comments:
In `@apps/web/app/_apis/mutations/useSubmitWinnerForm.ts`:
- Around line 4-6: Reorder the import statements in useSubmitWinnerForm.ts so
they follow the repository guideline (Node/built-in, external packages, absolute
imports, then relative imports): move the absolute imports (EventWinnerForm from
'@/_apis/schemas/event' and EventQueryKeys from '@/_apis/queries/event') above
the relative import (submitWinnerForm from '../services/event'), keeping the
same imported symbols and names.
- Around line 18-20: The invalidate call is spreading the key tuple
unnecessarily; update the call to pass the key factory result directly by
replacing queryClient.invalidateQueries({ queryKey:
[...EventQueryKeys.result(eventId)] }) with queryClient.invalidateQueries({
queryKey: EventQueryKeys.result(eventId) }), referencing the existing
queryClient.invalidateQueries and EventQueryKeys.result symbols.
In `@apps/web/app/_mocks/handlers/eventHandlers.ts`:
- Around line 7-18: The file contains several commented-out mock handlers (the
http.get/http.post blocks referencing API_PATH.EVENT.INFO,
API_PATH.EVENT.ENTRIES, '/events/:eventId/entries' and variables
event/eventResult) that should be either removed or annotated; decide whether
you need them later and if not delete these commented handlers, otherwise
replace each commented block with a concise TODO comment explaining its intended
future use (e.g., "TODO: re-enable mock for EVENT.INFO when implementing X")
next to the related symbol (http.get/http.post, API_PATH.EVENT.INFO,
API_PATH.EVENT.ENTRIES, '/events/:eventId/entries', event, eventResult) so the
purpose is clear.
In `@apps/web/app/events/lucky-draw/_components/Pages/EmptyEventState.tsx`:
- Around line 9-27: Some JSX attributes in EmptyEventState.tsx use double quotes
for className while project enforces jsxSingleQuote: true; update all JSX
attribute quotes to single quotes (e.g., change className="..." to
className='...') across the components in this file—specifically adjust the
className props on <Flex>, <Column>, and the two <Text> elements (and any other
JSX attributes like Icon type if inconsistent) to use single quotes, then run
your formatter/ESLint to confirm consistency.
In `@apps/web/app/events/lucky-draw/_components/Pages/GuestView/GuestView.tsx`:
- Around line 39-86: Duplicate UI blocks Title and Prize should be extracted
into a shared component to avoid duplicated maintenance; create a reusable
component (e.g., EventHeader or GiftPrize) that accepts props for texts and
imageUrl and move the JSX from Title and Prize into that component, export it,
then replace the local Title and Prize with imports and prop usages in GuestView
(symbols: Title, Prize) and the other file where the same block appears (symbol:
EventWelcome) so both use the single shared component; ensure props cover
variant/styles and accessibility attributes (alt, priority) and update
imports/exports accordingly.
In
`@apps/web/app/events/lucky-draw/_components/Pages/MemberView/FinishedEvent/FinishedEvent.tsx`:
- Around line 32-62: EventSummary is duplicated (also in EventResultClient.tsx)
and repeats date formatting logic; extract a shared EventSummary component
(replace the local EventSummary in FinishedEvent.tsx and the one in
EventResultClient.tsx) and move the date formatting into a small utility (e.g.,
formatEventDate) used by both; update FinishedEvent's EventSummary usage to
import the shared component and call formatEventDate(eventEndDate) instead of
eventEndDate.slice(...).replace(...) so both files share the same UI and
formatting logic.
In
`@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsx`:
- Around line 20-30: Add explicit return types: annotate the EntryTicketModal
component to return JSX.Element (e.g., change the component signature to return
JSX.Element) and annotate the onPress handler to return void (e.g., onPress: ()
=> void). Update the function signatures for EntryTicketModal and onPress
(referencing the EntryTicketModal declaration and the onPress function) so
TypeScript strict mode knows the expected return types.
In
`@apps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EventCountdown.tsx`:
- Line 7: The EventCountdown function component lacks an explicit return type;
update the component signature (export const EventCountdown = ({ eventEndDate }:
{ eventEndDate: string }) => ...) to include a return type such as : JSX.Element
or : React.ReactElement (e.g., export const EventCountdown = (...) : JSX.Element
=> ...) so the component has an explicit TypeScript return type and satisfies
strict/noUncheckedIndexedAccess rules; keep the existing prop type for
eventEndDate and ensure any necessary React types are imported if not already.
In `@apps/web/app/events/lucky-draw/_components/Pages/MemberView/MemberView.tsx`:
- Around line 35-40: Two separate Suspense boundaries always render and
conditionally mount InProgressEvent/FinishedEvent; simplify by using a single
Suspense wrapping the tab content and use currentTab as a key so the correct
component is mounted/cleared on tab switch. Replace the two Suspense blocks with
one Suspense fallback={<Spinner className={'m-auto'} />} that renders either
<InProgressEvent /> or <FinishedEvent /> based on currentTab and assign
key={currentTab} to the rendered child to ensure the non-active tab is unmounted
and state is not preserved.
In `@apps/web/app/events/lucky-draw/layout.tsx`:
- Line 8: The LuckyDrawLayout component is missing an explicit return type;
update its signature (function LuckyDrawLayout) to declare a return type (e.g.,
: React.ReactElement or : JSX.Element) while keeping the existing props type ({
children }: { children: React.ReactNode }) so the function has an explicit React
return type to satisfy strict/noUncheckedIndexedAccess rules.
In `@apps/web/app/events/lucky-draw/page.tsx`:
- Around line 5-14: The page currently returns GuestView or MemberView directly
in Page, which breaks the HydrationBoundaryPage SSR+prefetch pattern; wrap the
conditional render in the HydrationBoundaryPage component and perform any
server-side prefetching before returning so both MemberView and GuestView are
hydrated consistently—update the Page function to fetch cookies/accessToken as
now, run required prefetches for data used by MemberView/GuestView, then return
<HydrationBoundaryPage>{accessToken ? <MemberView/> :
<GuestView/>}</HydrationBoundaryPage> (use the HydrationBoundaryPage wrapper and
call the components' prefetch hooks or loaders where applicable).
In `@apps/web/app/events/lucky-draw/result/`[id]/EventResultClient.tsx:
- Line 14: The import of EventResult in EventResultClient.tsx is used only as a
type; change the statement `import { EventResult } from '@/_apis/schemas/event'`
to a type-only import by using `import type { EventResult } from
'@/_apis/schemas/event'` so it compiles under strict TypeScript settings and
avoids runtime import emissions.
- Around line 41-46: The setTimeout in the onClick handler can fire after the
component unmounts and should be cleaned up: add a timer ref (e.g., timerRef via
useRef<NodeJS.Timeout | null>) in the EventResultClient component, assign the
timeout ID to timerRef when calling setTimeout inside onClick, and add a
useEffect cleanup that calls clearTimeout(timerRef.current) (and sets it to
null) on unmount; also consider clearing any existing timer before setting a new
one in onClick to avoid overlapping timers.
In `@apps/web/app/events/lucky-draw/result/`[id]/page.tsx:
- Around line 3-7: The EventResultPage async function lacks an explicit return
type; update its signature to include a Promise return type (e.g.,
Promise<JSX.Element> or Promise<React.ReactElement>) so it conforms to the
project's strict TypeScript rules. Locate the EventResultPage constant (the
async function taking params) and annotate it with the chosen return type,
ensuring any JSX returned inside matches that type and imports React types if
necessary.
ℹ️ Review info
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (43)
apps/web/app/_apis/mutations/useParticipationEvent.tsapps/web/app/_apis/mutations/useSubmitWinnerForm.tsapps/web/app/_apis/queries/event.tsapps/web/app/_apis/schemas/event.tsapps/web/app/_apis/services/event.tsapps/web/app/_constants/path.tsapps/web/app/_mocks/data/event.tsapps/web/app/_mocks/handlers/eventHandlers.tsapps/web/app/events/lucky-draw/LuckyDraw.tsxapps/web/app/events/lucky-draw/_components/Pages/EmptyEventState.tsxapps/web/app/events/lucky-draw/_components/Pages/GuestView/GuestView.tsxapps/web/app/events/lucky-draw/_components/Pages/GuestView/index.tsapps/web/app/events/lucky-draw/_components/Pages/MemberView/FinishedEvent/FinishedEvent.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/FinishedEvent/index.tsapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EntryTicketModal.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EventCountdown.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/EventEntryAction.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/InProgressEvent.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/PrizeInfo.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/RemainingTickets.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/InProgressEvent/index.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/MemberView.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/NavBarItem/NavBarItem.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/NavBarItem/index.tsxapps/web/app/events/lucky-draw/_components/Pages/MemberView/index.tsapps/web/app/events/lucky-draw/_components/Pages/Participation/index.tsxapps/web/app/events/lucky-draw/_components/Pages/Result/NoResult.tsxapps/web/app/events/lucky-draw/_components/Pages/Result/Result.tsxapps/web/app/events/lucky-draw/_components/Pages/Result/index.tsxapps/web/app/events/lucky-draw/_components/Pages/index.tsxapps/web/app/events/lucky-draw/layout.tsxapps/web/app/events/lucky-draw/page.tsxapps/web/app/events/lucky-draw/result/[id]/EventResultClient.tsxapps/web/app/events/lucky-draw/result/[id]/ResultModal.tsxapps/web/app/events/lucky-draw/result/[id]/_components/LottoBalls/LottoBalls.tsxapps/web/app/events/lucky-draw/result/[id]/_components/LottoBalls/index.tsxapps/web/app/events/lucky-draw/result/[id]/_components/WinnerInfoForm/TermAgreementCheckbox.tsxapps/web/app/events/lucky-draw/result/[id]/_components/WinnerInfoForm/WinnerInfoForm.tsxapps/web/app/events/lucky-draw/result/[id]/_components/WinnerInfoForm/constants.tsapps/web/app/events/lucky-draw/result/[id]/_components/WinnerInfoForm/index.tsapps/web/app/events/lucky-draw/result/[id]/page.tsxapps/web/app/places/new/_components/Step/EventWelcome/EventWelcome.tsxapps/web/middleware.ts
💤 Files with no reviewable changes (6)
- apps/web/app/events/lucky-draw/_components/Pages/index.tsx
- apps/web/app/events/lucky-draw/_components/Pages/Result/Result.tsx
- apps/web/app/events/lucky-draw/_components/Pages/Result/NoResult.tsx
- apps/web/app/events/lucky-draw/LuckyDraw.tsx
- apps/web/app/events/lucky-draw/_components/Pages/Participation/index.tsx
- apps/web/app/events/lucky-draw/_components/Pages/Result/index.tsx
| export const EventResultSchema = BaseEventSchema.extend({ | ||
| isWinner: z.boolean(), | ||
| participantsCount: z.number(), | ||
| usedTicketsCount: z.number(), | ||
| isPhoneSubmitted: z.boolean(), | ||
| }) |
There was a problem hiding this comment.
EventResultSchema가 결과 응답 계약을 과도하게 강제하고 있습니다.
Line 31에서 BaseEventSchema.extend(...)를 사용하면 결과 응답에 prize, totalWinnersCount, participantsCount, eventEndDate가 모두 필수가 됩니다. 현재 PR의 apps/web/app/_mocks/data/event.ts의 eventResult 형태와도 충돌해서 타입/파싱 실패 위험이 큽니다. 결과 전용 스키마를 별도로 두는 게 안전합니다.
🔧 제안 수정안
-export const EventResultSchema = BaseEventSchema.extend({
- isWinner: z.boolean(),
- usedTicketsCount: z.number(),
- isPhoneSubmitted: z.boolean(),
-})
+export const EventResultSchema = z.object({
+ eventId: z.number().transform(String),
+ isWinner: z.boolean(),
+ participantsCount: z.number(),
+ usedTicketsCount: z.number(),
+ isPhoneSubmitted: z.boolean().default(false),
+})📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const EventResultSchema = BaseEventSchema.extend({ | |
| isWinner: z.boolean(), | |
| participantsCount: z.number(), | |
| usedTicketsCount: z.number(), | |
| isPhoneSubmitted: z.boolean(), | |
| }) | |
| export const EventResultSchema = z.object({ | |
| eventId: z.number().transform(String), | |
| isWinner: z.boolean(), | |
| participantsCount: z.number(), | |
| usedTicketsCount: z.number(), | |
| isPhoneSubmitted: z.boolean().default(false), | |
| }) |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/_apis/schemas/event.ts` around lines 31 - 35, EventResultSchema
currently extends BaseEventSchema which forces result responses to include
BaseEventSchema fields (e.g., prize, totalWinnersCount, participantsCount,
eventEndDate) and causes parsing/type failures against the eventResult mock;
change EventResultSchema to be a standalone schema that only validates
result-specific fields (isWinner, usedTicketsCount, isPhoneSubmitted) or create
a separate Result-only schema and use that for response parsing instead of
BaseEventSchema.extend; update references to EventResultSchema where result
responses are validated so they consume the new result-only schema and ensure
compatibility with apps/web/app/_mocks/data/event.ts's eventResult shape.
| ENTRIES: '/events/entries', | ||
| RESULT: (eventId: string) => `/events/${eventId}/entries`, | ||
| APPLY: (eventId: string) => `/events/${eventId}/apply`, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n apps/web/app/_constants/path.tsRepository: knu-matzip/Matzip-Client
Length of output: 2991
🏁 Script executed:
rg -n "API_PATH\.EVENT\.RESULT" --type ts --type tsx -A 2 -B 2Repository: knu-matzip/Matzip-Client
Length of output: 95
🏁 Script executed:
rg -n "API_PATH\.EVENT\.RESULT[^(]" --type ts --type tsxRepository: knu-matzip/Matzip-Client
Length of output: 95
🏁 Script executed:
rg "API_PATH\.EVENT\.RESULT" -A 2 -B 2Repository: knu-matzip/Matzip-Client
Length of output: 450
🏁 Script executed:
rg "EVENT\.RESULT" --type tsRepository: knu-matzip/Matzip-Client
Length of output: 179
🏁 Script executed:
fd -t ts -t tsx . | head -20Repository: knu-matzip/Matzip-Client
Length of output: 237
🏁 Script executed:
rg "EVENT\.RESULT" -iRepository: knu-matzip/Matzip-Client
Length of output: 179
🏁 Script executed:
cat -n apps/web/app/_apis/services/event.tsRepository: knu-matzip/Matzip-Client
Length of output: 1842
함수에 명시적 반환 타입 추가 필요
코딩 가이드라인에서 "TypeScript 파일의 함수는 반환 타입이 필수"라고 명시되어 있으나, 현재 파일의 모든 매개변수를 받는 함수들이 명시적 반환 타입을 선언하지 않고 있습니다.
예시:
- Line 40:
RESULT: (eventId: string) =>/events/${eventId}/entries`` →RESULT: (eventId: string): string => ...필요 - Line 41:
APPLY: (eventId: string) =>/events/${eventId}/apply`` →APPLY: (eventId: string): string => ...필요
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/app/_constants/path.ts` around lines 39 - 41, Add explicit return
types to the arrow functions in the path constants: change RESULT and APPLY to
include a : string return type (i.e., RESULT: (eventId: string): string =>
`/events/${eventId}/entries` and APPLY: (eventId: string): string =>
`/events/${eventId}/apply`) so the TypeScript files comply with the rule that
functions must declare their return type.
#️⃣연관된 이슈
📝작업 내용
이벤트 도메인의 전반적인 비즈니스 요구사항 및 기획 변경에 맞춰 프론트엔드 로직과 UI를 대대적으로 개편했습니다
1. 당첨자 정보 입력 기능 구현
당첨 시 전화번호와 필수 약관 동의(서비스 이용, 개인정보 수집)를 받는 WinnerInfoForm 컴포넌트 추가
약관 내용 모달 컴포넌트(TermAgreementRow) 추가 및 가독성 높은 UI 적용
당첨자 정보 제출(Submit)을 위한 Mutation 로직 연동
2. 종료된 이벤트 목록 기능 추가
기존 진행 중인 이벤트 외에, 응모 내역 기반으로 '종료된 이벤트'를 조회할 수 있는 기능 추가
3. 이벤트 상세 뷰 리팩토링 (회원/비회원 분리)
middleware.ts에 luckdraw 페이지 제외하고, 인증 상태에 따라 보여지는 UI(뷰)를 명확히 분리하여 렌더링하도록 구조 변경스크린샷 (선택)
💬리뷰 요구사항(선택)
Summary by CodeRabbit
릴리스 노트
새로운 기능
버그 수정