Conversation
[FEAT] 같은 동아리 찾기 게임 api 연동 및 애니메이션 적용
[REFACTOR] 짝 맞추기 게임 리팩토링
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
동아리 카드 짝 맞추기 게임 기능 구현Walkthrough동아리 카드 짝 맞추기 게임의 완전한 기능을 구현합니다. API 뮤테이션/쿼리, 폼 관리, 라운드 진행, 단계별 네비게이션, 자산 프리로드 기능을 추가하고 컨텍스트 기반 상태 관리를 재구성합니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant Browser as 브라우저
participant GamePage as GamePage
participant API as API Server
participant DB as Database
User->>Browser: 게임 시작
Browser->>GamePage: IntroStep 렌더링
GamePage->>GamePage: 자산 프리로드
User->>Browser: 게임 시작 클릭
Browser->>GamePage: onGameStart 호출
GamePage->>GamePage: currentRound = 0, gameInstanceId++
GamePage->>GamePage: PairGamePlayingProvider 마운트
GamePage->>GamePage: useCardState 초기화 (카드 생성)
GamePage->>GamePage: useRoundPhase 시작 (preview -> playing -> ended)
User->>Browser: 카드 클릭
Browser->>GamePage: selectCard(cardId)
GamePage->>GamePage: 카드 플립, 매칭 체크
GamePage->>GamePage: 성공 시: onRoundComplete 호출
GamePage->>API: RoundResult 전달
GamePage->>GamePage: PlayingStep → CompletedStep
GamePage->>API: appliersAmount 쿼리
API->>DB: SELECT appliers count
DB-->>API: 참여자 수 반환
API-->>GamePage: 참여자 수 업데이트
User->>Browser: 폼 작성 및 제출
Browser->>GamePage: SubmitStep handleSubmit
GamePage->>API: createPairGameApplier mutation
API->>DB: INSERT applier
DB-->>API: 성공
API-->>GamePage: 제출 완료
GamePage->>GamePage: CompletedStep 전환
sequenceDiagram
participant User as 사용자
participant SubmitStep as SubmitStep<br/>컴포넌트
participant usePairGameForm as usePairGameForm<br/>훅
participant usePairGameSubmitAction as usePairGameSubmitAction<br/>훅
participant validateFn as validatePairGame<br/>SubmitData
participant API as API<br/>(createPairGameApplier)
participant Toast as Toast<br/>알림
User->>SubmitStep: 폼 입력 (이름, 학번, 학과, 번호)
SubmitStep->>usePairGameForm: handleChange 호출
usePairGameForm->>usePairGameForm: formData 상태 업데이트
User->>SubmitStep: 영수증 파일 선택
SubmitStep->>usePairGameForm: handleFileChange 호출
usePairGameForm->>usePairGameForm: receiptFile 상태 저장
User->>SubmitStep: 제출 버튼 클릭
SubmitStep->>usePairGameForm: handleSubmit 호출
usePairGameForm->>validateFn: validatePairGameSubmitData 호출
validateFn->>validateFn: Zod 스키마 검증
alt 검증 실패
validateFn->>Toast: 에러 메시지 표시
Toast-->>User: 검증 실패 알림
else 검증 성공
validateFn-->>usePairGameForm: true 반환
usePairGameForm->>usePairGameSubmitAction: submit 호출
usePairGameSubmitAction->>API: createPairGameApplier mutation
API->>API: FormData 생성 (request + file)
API->>API: POST /pair-game/appliers
alt API 성공
API-->>usePairGameSubmitAction: 성공 응답
usePairGameSubmitAction->>usePairGameSubmitAction: sessionStorage 업데이트
usePairGameSubmitAction->>Toast: 성공 알림
Toast-->>User: 제출 완료 메시지
else API 오류
API-->>usePairGameSubmitAction: 오류 응답
usePairGameSubmitAction->>Toast: 오류 메시지 표시
Toast-->>User: 오류 알림
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (1 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: 21
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/pair_game/prize/page.tsx (1)
49-51:⚠️ Potential issue | 🟡 Minor오타 수정 필요: "네이버페 이" → "네이버페이"
Line 50에서 "네이버페이"가 "네이버페 이"로 잘못 띄어쓰기 되어 있습니다.
🔧 수정 제안
- 네이버페 이 1만원 권 (10명) + 네이버페이 1만원권 (10명)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/pair_game/prize/page.tsx` around lines 49 - 51, Fix the whitespace typo in the Caption1 JSX text: replace "네이버페 이 1만원 권 (10명)" with "네이버페이 1만원 권 (10명)" in the page component where Caption1 is rendered so the brand name is correctly joined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/_api/mutations/pair_game.ts`:
- Around line 14-19: The POST call that sets headers: { 'Content-Type':
undefined } (in the function making instance.post('pair-game/appliers') in
src/app/_api/mutations/pair_game.ts) causes ky to send "Content-Type: undefined"
and breaks FormData; remove that header usage and instead add a
hooks.beforeRequest handler that deletes the Content-Type header from the
outgoing Request (request.headers.delete('Content-Type')) so ky can auto-set the
multipart boundary; apply the same fix to the uploadMemberExcel function in
src/app/_api/mutations/member.ts.
In `@src/app/pair_game/_components/steps/IntroStepMobile.tsx`:
- Around line 160-163: The BridgeMaruMari usage includes an invalid Tailwind
class "pt-15" in the className prop; update the className on the BridgeMaruMari
component to use a valid spacing utility (e.g., pt-16 or pt-14) or an arbitrary
value like pt-[60px], or add a matching spacing key to tailwind.config.ts if you
need a custom token; locate the BridgeMaruMari element (props: animateMaru,
driveDistance) and replace "pt-15" with the chosen valid spacing class.
In `@src/app/pair_game/_components/steps/PlayingStep.tsx`:
- Around line 117-122: The current className passed to ProgressBar in
PlayingStep.tsx uses internal child selectors (`[&>div]:!duration-100
[&>div]:!ease-linear`) which couples styling to ProgressBar's DOM; instead,
update the code so PlayingStep stops relying on internal selectors and either
(a) pass explicit styling props or named class props supported by ProgressBar
(e.g., an innerClassName/transitionDuration/transitionTiming prop) or (b) add
those props to the ProgressBar component itself (modify ProgressBar to accept
and apply innerClassName or transition props to its internal div). Locate
ProgressBar usage in PlayingStep.tsx and the ProgressBar component definition
and implement the chosen API so transitions are applied robustly without using
child selectors.
In `@src/app/pair_game/_components/ui/BridgeMaruMari.tsx`:
- Around line 18-23: maruClassName builds a class string from two always-present
string entries, so remove the unnecessary .filter(Boolean) call and join the
array directly; update the expression that constructs maruClassName (which
references animateMaru) to use .join(' ') on the array without filtering to
simplify the code.
In `@src/app/pair_game/_constants/roundConfigs.ts`:
- Around line 9-14: ROUND_CONFIGS currently contains a redundant manual
totalCards field that can drift from rows*cols; update the config creation so
totalCards is derived (rows * cols) instead of hard-coded. Locate the exported
ROUND_CONFIGS array and either remove the explicit totalCards entries and
compute totalCards on construction or map the array into a derived array where
each item sets totalCards = rows * cols (preserving previewTime and gameTime);
ensure any type uses (RoundConfig) still accept the derived totalCards value.
In `@src/app/pair_game/_contexts/PairGamePlayingContext.tsx`:
- Around line 76-87: The while loop in tryFlipNextCards can hang if flipCard
repeatedly returns false (e.g., all remaining pending cards are invalid) because
selectedCardIds won't increase; add a defensive iteration cap (e.g.,
maxIterations) inside tryFlipNextCards and increment it each loop, breaking when
the cap is reached so the loop always terminates; reference
pendingCardIds.current.shift(), flipCard(nextId), and selectedCardIds.current to
implement the check and log or handle the capped-break case appropriately.
- Around line 57-60: Add a defensive effect that resets the refs when the round
changes: when currentRound updates, set selectedCardIds.current = [],
pendingCardIds.current = [], isProcessingMatch.current = false, and
isRoundCompleted.current = false so the Provider can handle cases where it isn't
remounted; implement this in PairGamePlayingContext by adding a useEffect that
depends on currentRound and performs those assignments (referencing the existing
refs selectedCardIds, pendingCardIds, isProcessingMatch, isRoundCompleted).
In `@src/app/pair_game/_hooks/useCardState.ts`:
- Around line 36-48: The current checkMatch returns a single boolean that
conflates whether the two picked cards matched and whether the game is now fully
matched; update checkMatch to return an explicit result object (e.g. { isMatch,
isAllMatched }) so callers can distinguish "these two cards matched" from "all
cards are matched after this move." Inside checkMatch, call
processCardMatch(cardsRef.current, firstId, secondId) to get updatedCards and
isMatch, compute isAllMatched = areAllCardsMatched(updatedCards), update
cardsRef.current and setCards(updatedCards), then return the object containing
both isMatch and isAllMatched (or similarly named properties) so the meaning is
unambiguous.
In `@src/app/pair_game/_hooks/useDelayedAction.ts`:
- Around line 22-25: In cancelAll, the forEach callback is implicitly returning
clearTimeout’s return value; change the arrow callback to a block statement that
calls clearTimeout without returning (e.g., timeoutIds.current.forEach((id) => {
clearTimeout(id); });) so the callback returns undefined, then keep the
subsequent timeoutIds.current.clear() call; this removes the unintended implicit
return flagged by the linter.
In `@src/app/pair_game/_hooks/useGameProgress.ts`:
- Around line 25-27: completeRound currently calls preloadGameAssets and the
intro stage in src/app/pair_game/page.tsx also triggers a preload, causing
duplicate preloads; remove one call and centralize preload logic by moving the
single responsibility to a shared place (e.g., the hook useGameProgress) or a
dedicated function that both page.tsx and completeRound use: eliminate
preloadGameAssets() from completeRound (or from the page intro) and instead
expose and call a single public preload function from useGameProgress (or import
that shared preload helper) so only that function performs preloading and both
the intro stage and round completion invoke the same centralized preload path.
In `@src/app/pair_game/_hooks/usePairGameSubmitAction.ts`:
- Around line 17-33: The submit handler can fire createApplier multiple times on
quick clicks and weakens type-safety with "as File"; add a pending guard (e.g.,
a useRef or state like isSubmitting) in the hook and in submit: return early if
isSubmitting is true, set isSubmitting = true before calling createApplier and
reset it in finally to prevent duplicate requests; also remove the unsafe cast
by either changing the payload to pass file: receiptFile (allowing File | null)
or ensure validatePairGameSubmitData guarantees receiptFile is non-null and only
then pass receiptFile (no "as File"); update references to submit,
validatePairGameSubmitData, and createApplier accordingly.
In `@src/app/pair_game/_hooks/useRoundPhase.ts`:
- Around line 15-63: The two near-identical useEffect timer blocks (the preview
and playing effects) should be extracted into a reusable hook (e.g.,
usePhaseTimer) to remove duplication: create a hook that accepts activePhase,
currentPhase, duration (seconds) and onComplete, implements the performance.now
timing and setInterval logic, and returns the remaining time; then replace the
preview effect with a call to usePhaseTimer('preview', phase, PREVIEW_TIME, ()
=> setPhase('playing')) and wire its return to setPreviewTimer (or have the hook
expose the remaining value directly), and similarly replace the playing effect
with usePhaseTimer('playing', phase, GAME_TIME, () => setPhase('ended')) and
connect to setGameTimer; ensure the hook clears intervals on cleanup and depends
on currentPhase, activePhase, duration, and onComplete.
In `@src/app/pair_game/_utils/cardStyles.ts`:
- Around line 7-14: The function getCardSizeStyleForConfig can produce
Infinity/invalid vw values when config.cols or config.rows are 0 or negative;
add defensive validation at the start of getCardSizeStyleForConfig to
coerce/validate inputs (e.g., const safeCols = Math.max(1, config.cols); const
safeRows = Math.max(1, config.rows)); use safeCols/safeRows when building key
and computing vwW (replace config.cols with safeCols) and guard against
base.width === 0 by using Math.max(1, base.width) when computing vwH so vwW and
vwH are always finite; keep CARD_SIZES lookup using the safe key.
In `@src/app/pair_game/_utils/cardUtils.ts`:
- Around line 21-23: The createCards function must validate totalCards before
computing pairCount: ensure totalCards is a positive even integer and that
totalCards/2 does not exceed CLUBS.length (38); if validation fails, either
throw a clear Error or return a controlled fallback. In practice, in createCards
check Number.isInteger(totalCards) && totalCards > 0 && totalCards % 2 === 0,
compute pairCount = totalCards / 2, then assert pairCount <= CLUBS.length (or
clamp/raise) before using shuffle(CLUBS).slice(0, pairCount) so you never
request more pairs than available and never use a fractional pairCount.
In `@src/app/pair_game/_utils/clubImages.ts`:
- Around line 4-7: getClubImageSrc currently builds a path from any numeric
clubId and can cause 404s; add validation inside getClubImageSrc to check the id
against known CLUBS (or allowed id range) and return a safe fallback path like
`${PAIR_GAME_PATH}/clubs/default.webp` when the id is invalid or missing, and
ensure CLUB_IMAGE_SRCS continues to call getClubImageSrc(c.id) so the mapping
uses the new defensive logic; refer to getClubImageSrc, CLUB_IMAGE_SRCS, CLUBS,
and PAIR_GAME_PATH when making the change.
In `@src/app/pair_game/_utils/clubs.ts`:
- Line 5: The getClubById function currently omits an explicit return type
causing ambiguity; update its signature to declare the return type as Club |
undefined (e.g., export const getClubById = (id: number): Club | undefined =>
CLUB_MAP.get(id);), ensuring callers know Map.get can return undefined and
adjust any call sites if they assume a non-null Club; reference getClubById and
CLUB_MAP when making this change.
In `@src/app/pair_game/_utils/timerDisplay.ts`:
- Around line 12-13: The percent calculation (variable percent in
timerDisplay.ts using gameTime, displaySeconds and remaining) can exceed 100 and
must be clamped to the 0–100 range; update the percent assignment so after
computing (remaining / gameTime) * 100 it is clamped to Math.max(0,
Math.min(100, ...)) (or use an existing clamp utility) so percent never goes
below 0 or above 100.
In `@src/constants/navItems.ts`:
- Around line 43-49: The new nav item under '동아리박람회 이벤트' has id: 7 which breaks
the existing id sequence (1-5); update the id to 6 in src/constants/navItems.ts
for the object with href '/pair_game' and content '동아리 카드 짝 맞추기 게임' to restore
the sequential IDs (leave the href '/pair_game' unchanged as noted).
In `@src/styles/globals.css`:
- Around line 120-132: 애니메이션의 translateY에 하드코딩된 vh 단위가 iOS Safari에서 주소창 표시/숨김으로
인해 뷰포트 높이가 변동되며 동작이 달라질 수 있으므로 `@keyframes` float-up의 translateY(35vh) 및
translateY(-45vh)을 동적 뷰포트 단위로 전환하세요; 예를 들어 dvh를 우선 사용하고 구형 브라우저 대응을 위해 CSS
변수(--float-dvh) 또는 calc()와 함께 폴백(예: var(--float-dvh, 35vh))을 도입해 float-up 정의와
관련된 transform 값에 적용하여 예상한 애니메이션 동작을 확보하도록 수정하세요.
In `@src/types/schemas/pairGameSubmitSchema.ts`:
- Around line 9-11: The phoneNumber zod rule in pairGameSubmitSchema.ts
currently only accepts hyphenated input; update the phoneNumber schema to accept
both hyphenated and non-hyphenated formats by either (A) replacing the regex
with a flexible one such as /^010-?\d{4}-?\d{4}$/ (or
/^(?:010-?\d{4}-?\d{4}|010\d{8})$/ to be explicit) or (B) normalize input first
(e.g., use z.string().transform(s => s.replace(/-/g, '')) and then validate with
/^\d{10}$/ or /^010\d{8}$/), keeping the same error message text ('올바른 전화번호 형식이
아닙니다.') and updating the phoneNumber declaration accordingly.
---
Outside diff comments:
In `@src/app/pair_game/prize/page.tsx`:
- Around line 49-51: Fix the whitespace typo in the Caption1 JSX text: replace
"네이버페 이 1만원 권 (10명)" with "네이버페이 1만원 권 (10명)" in the page component where
Caption1 is rendered so the brand name is correctly joined.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (52)
public/pair_game/background_heart.webpsrc/app/_api/mutations/pair_game.tssrc/app/_api/queries/pair_game.tssrc/app/_api/types/pair_game.tssrc/app/pair_game/_components/steps/CompletedStep.tsxsrc/app/pair_game/_components/steps/IntroStep.tsxsrc/app/pair_game/_components/steps/IntroStepDesktop.tsxsrc/app/pair_game/_components/steps/IntroStepMobile.tsxsrc/app/pair_game/_components/steps/PlayingStep.tsxsrc/app/pair_game/_components/steps/SubmitStep.tsxsrc/app/pair_game/_components/ui/BellAnimation.tsxsrc/app/pair_game/_components/ui/BridgeMaruMari.tsxsrc/app/pair_game/_components/ui/EventCard.tsxsrc/app/pair_game/_components/ui/GameStartModal.tsxsrc/app/pair_game/_components/ui/RoundResultModal.tsxsrc/app/pair_game/_constants/RoundResultModalContent.tssrc/app/pair_game/_constants/categoryStyles.tssrc/app/pair_game/_constants/clubs.tssrc/app/pair_game/_constants/gameImages.tssrc/app/pair_game/_constants/heart.tssrc/app/pair_game/_constants/roundConfigs.tssrc/app/pair_game/_contexts/PairGamePlayingContext.tsxsrc/app/pair_game/_hooks/useCardState.tssrc/app/pair_game/_hooks/useDelayedAction.tssrc/app/pair_game/_hooks/useGameLayoutBg.tssrc/app/pair_game/_hooks/useGameProgress.tssrc/app/pair_game/_hooks/useGameTimer.tssrc/app/pair_game/_hooks/usePairGameForm.tssrc/app/pair_game/_hooks/usePairGamePage.tssrc/app/pair_game/_hooks/usePairGameSubmitAction.tssrc/app/pair_game/_hooks/useRoundPhase.tssrc/app/pair_game/_hooks/useRoundResultModal.tssrc/app/pair_game/_hooks/useTimer.tssrc/app/pair_game/_utils/cardStyles.tssrc/app/pair_game/_utils/cardUtils.tssrc/app/pair_game/_utils/clubImages.tssrc/app/pair_game/_utils/clubs.tssrc/app/pair_game/_utils/gameConstants.tssrc/app/pair_game/_utils/gameImages.tssrc/app/pair_game/_utils/preloadGameAssets.tssrc/app/pair_game/_utils/roundResultModal.tssrc/app/pair_game/_utils/shareLink.tssrc/app/pair_game/_utils/timerDisplay.tssrc/app/pair_game/_utils/validatePairGameSubmitData.tssrc/app/pair_game/guide/page.tsxsrc/app/pair_game/page.tsxsrc/app/pair_game/prize/page.tsxsrc/components/layout/GameLayout.tsxsrc/constants/navItems.tssrc/styles/globals.csssrc/types/schemas/pairGameSubmitSchema.tssrc/utils/userAgent.ts
💤 Files with no reviewable changes (6)
- src/app/pair_game/_hooks/usePairGamePage.ts
- src/app/pair_game/_hooks/useTimer.ts
- src/app/pair_game/_hooks/useGameTimer.ts
- src/app/pair_game/_hooks/useRoundResultModal.ts
- src/app/pair_game/_utils/gameConstants.ts
- src/app/pair_game/_utils/gameImages.ts
| <BridgeMaruMari | ||
| className="relative bottom-0 mt-auto w-screen pt-15" | ||
| animateMaru={animateMaru} | ||
| driveDistance="53vw" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find Tailwind config
fd 'tailwind.config\.(js|cjs|mjs|ts)$' --type fRepository: COW-dev/ddingdong-fe
Length of output: 82
🏁 Script executed:
# Check if the file exists and view the relevant lines
cat -n src/app/pair_game/_components/steps/IntroStepMobile.tsx | sed -n '155,170p'Repository: COW-dev/ddingdong-fe
Length of output: 408
🏁 Script executed:
# Search for pt-15 usage in the codebase
rg 'pt-15' --type tsx --type ts --type jsx --type jsRepository: COW-dev/ddingdong-fe
Length of output: 91
🏁 Script executed:
# Search for spacing configuration in tailwind config
rg -A 20 'theme.*spacing|extend.*spacing' --type js --type ts --type cjsRepository: COW-dev/ddingdong-fe
Length of output: 91
🏁 Script executed:
cat -n tailwind.config.tsRepository: COW-dev/ddingdong-fe
Length of output: 1535
🏁 Script executed:
# Search for pt-15 usage in the entire codebase
rg 'pt-15'Repository: COW-dev/ddingdong-fe
Length of output: 180
🏁 Script executed:
# Check spacing configuration more broadly
rg 'spacing' tailwind.config.ts -A 5 -B 2Repository: COW-dev/ddingdong-fe
Length of output: 46
pt-15 클래스는 유효하지 않으므로 수정이 필요합니다.
Line 161의 pt-15는 Tailwind의 기본 spacing 스케일에 없고 tailwind.config.ts에 확장 설정도 없어 패딩이 적용되지 않습니다. pt-16 (64px), pt-14 (56px) 또는 pt-[60px] 같은 유효한 값으로 변경하거나, 필요시 Tailwind 설정에 spacing을 확장하세요.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/pair_game/_components/steps/IntroStepMobile.tsx` around lines 160 -
163, The BridgeMaruMari usage includes an invalid Tailwind class "pt-15" in the
className prop; update the className on the BridgeMaruMari component to use a
valid spacing utility (e.g., pt-16 or pt-14) or an arbitrary value like
pt-[60px], or add a matching spacing key to tailwind.config.ts if you need a
custom token; locate the BridgeMaruMari element (props: animateMaru,
driveDistance) and replace "pt-15" with the chosen valid spacing class.
| const handleSubmit = () => { | ||
| onCloseModal(); | ||
| window.sessionStorage.setItem('pairGameCanSubmit', '1'); | ||
| setStep('submit'); | ||
| window.history.replaceState(null, '', PAIR_GAME_PATH); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "pairGameCanSubmit" --type=ts --type=tsxRepository: COW-dev/ddingdong-fe
Length of output: 91
🏁 Script executed:
rg "pairGameCanSubmit" -nRepository: COW-dev/ddingdong-fe
Length of output: 301
🏁 Script executed:
cat -n src/app/pair_game/_hooks/usePairGameSubmitAction.tsRepository: COW-dev/ddingdong-fe
Length of output: 2101
🏁 Script executed:
cat -n src/app/pair_game/_components/steps/PlayingStep.tsxRepository: COW-dev/ddingdong-fe
Length of output: 6049
🏁 Script executed:
rg "step.*submit|submit.*step" -n --context 3 | head -100Repository: COW-dev/ddingdong-fe
Length of output: 46
🏁 Script executed:
find src/app/pair_game -name "*[Ss]ubmit*" -o -name "*[Ss]tep*" | grep -v node_modulesRepository: COW-dev/ddingdong-fe
Length of output: 526
🏁 Script executed:
cat -n src/app/pair_game/_components/steps/SubmitStep.tsxRepository: COW-dev/ddingdong-fe
Length of output: 7078
🏁 Script executed:
find src/app/pair_game -name "*usePairGameForm*" -o -name "*PairGameForm*"Repository: COW-dev/ddingdong-fe
Length of output: 107
🏁 Script executed:
cat -n src/app/pair_game/_hooks/usePairGameForm.tsRepository: COW-dev/ddingdong-fe
Length of output: 1989
🏁 Script executed:
find src/app/pair_game -name "*Context*" -o -name "*context*"Repository: COW-dev/ddingdong-fe
Length of output: 196
🏁 Script executed:
cat -n src/app/pair_game/_contexts/GameFunnelContext.tsxRepository: COW-dev/ddingdong-fe
Length of output: 1557
🏁 Script executed:
find src/app/pair_game -maxdepth 2 -name "page.tsx" -o -name "layout.tsx"Repository: COW-dev/ddingdong-fe
Length of output: 156
🏁 Script executed:
cat -n src/app/pair_game/page.tsxRepository: COW-dev/ddingdong-fe
Length of output: 3601
sessionStorage 검증 로직 누락 및 상태 업데이트 순서 확인 필요
pairGameCanSubmit 플래그를 설정하지만 SubmitStep에서 이 값을 검증하지 않습니다. 폼 제출 전에 이 플래그 존재 여부를 확인하는 로직이 없으므로, 게임을 진행하지 않고 직접 제출 단계에 접근할 수 있습니다. 현재 코드에서 이 플래그는 설정만 되고 검증되지 않고 있습니다.
또한 setStep('submit')의 비동기 상태 업데이트와 window.history.replaceState() 간의 실행 순서 문제가 있습니다. history.replaceState는 동기적으로 즉시 실행되지만, setStep은 비동기이므로 URL 변경이 React 상태 업데이트 완료 전에 적용될 수 있습니다.
| <div className="min-w-0 flex-1 overflow-hidden"> | ||
| <ProgressBar | ||
| color="pink" | ||
| percent={progressPercent} | ||
| className="w-full max-w-full [&>div]:!duration-100 [&>div]:!ease-linear" | ||
| /> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
ProgressBar에 전달되는 className 검증 필요
[&>div]:!duration-100 [&>div]:!ease-linear 선택자가 ProgressBar 내부 구조에 의존합니다. ProgressBar 컴포넌트의 DOM 구조가 변경되면 이 스타일이 적용되지 않을 수 있습니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/pair_game/_components/steps/PlayingStep.tsx` around lines 117 - 122,
The current className passed to ProgressBar in PlayingStep.tsx uses internal
child selectors (`[&>div]:!duration-100 [&>div]:!ease-linear`) which couples
styling to ProgressBar's DOM; instead, update the code so PlayingStep stops
relying on internal selectors and either (a) pass explicit styling props or
named class props supported by ProgressBar (e.g., an
innerClassName/transitionDuration/transitionTiming prop) or (b) add those props
to the ProgressBar component itself (modify ProgressBar to accept and apply
innerClassName or transition props to its internal div). Locate ProgressBar
usage in PlayingStep.tsx and the ProgressBar component definition and implement
the chosen API so transitions are applied robustly without using child
selectors.
| const maruClassName = [ | ||
| 'h-auto w-[28%] max-w-[140px] object-contain object-bottom', | ||
| animateMaru ? 'maru-slow-drive' : '-scale-x-100', | ||
| ] | ||
| .filter(Boolean) | ||
| .join(' '); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
filter(Boolean) 불필요
배열의 두 요소가 항상 문자열이므로 filter(Boolean)이 필요하지 않습니다.
♻️ 간소화 제안
- const maruClassName = [
- 'h-auto w-[28%] max-w-[140px] object-contain object-bottom',
- animateMaru ? 'maru-slow-drive' : '-scale-x-100',
- ]
- .filter(Boolean)
- .join(' ');
+ const maruClassName = `h-auto w-[28%] max-w-[140px] object-contain object-bottom ${animateMaru ? 'maru-slow-drive' : '-scale-x-100'}`;📝 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.
| const maruClassName = [ | |
| 'h-auto w-[28%] max-w-[140px] object-contain object-bottom', | |
| animateMaru ? 'maru-slow-drive' : '-scale-x-100', | |
| ] | |
| .filter(Boolean) | |
| .join(' '); | |
| const maruClassName = `h-auto w-[28%] max-w-[140px] object-contain object-bottom ${animateMaru ? 'maru-slow-drive' : '-scale-x-100'}`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/pair_game/_components/ui/BridgeMaruMari.tsx` around lines 18 - 23,
maruClassName builds a class string from two always-present string entries, so
remove the unnecessary .filter(Boolean) call and join the array directly; update
the expression that constructs maruClassName (which references animateMaru) to
use .join(' ') on the array without filtering to simplify the code.
| const safeCount = Math.min(count, CLUB_IDS.length); | ||
| return shuffled.slice(0, safeCount); | ||
| }; | ||
| export const getClubById = (id: number) => CLUB_MAP.get(id); |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
반환 타입 명시 권장
getClubById의 반환 타입이 명시되지 않았습니다. Map.get은 Club | undefined를 반환하므로, 호출자가 undefined 케이스를 처리해야 함을 명확히 하는 것이 좋습니다.
♻️ 타입 명시 제안
+import { CLUBS, type Club } from '../_constants/clubs';
-import { CLUBS } from '../_constants/clubs';
const CLUB_MAP = new Map(CLUBS.map((c) => [c.id, c]));
-export const getClubById = (id: number) => CLUB_MAP.get(id);
+export const getClubById = (id: number): Club | undefined => CLUB_MAP.get(id);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/pair_game/_utils/clubs.ts` at line 5, The getClubById function
currently omits an explicit return type causing ambiguity; update its signature
to declare the return type as Club | undefined (e.g., export const getClubById =
(id: number): Club | undefined => CLUB_MAP.get(id);), ensuring callers know
Map.get can return undefined and adjust any call sites if they assume a non-null
Club; reference getClubById and CLUB_MAP when making this change.
| const percent = | ||
| gameTime > 0 && displaySeconds > 0 ? (remaining / gameTime) * 100 : 0; |
There was a problem hiding this comment.
진행률 상한 clamp가 없어 100% 초과 가능성이 있습니다.
Line 12-13 계산 결과가 100을 넘으면 progress UI가 깨질 수 있습니다. 0~100 범위로 고정해 주세요.
🔧 제안 수정안
- const percent =
- gameTime > 0 && displaySeconds > 0 ? (remaining / gameTime) * 100 : 0;
+ const percent =
+ gameTime > 0 && displaySeconds > 0
+ ? Math.min(100, (remaining / gameTime) * 100)
+ : 0;📝 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.
| const percent = | |
| gameTime > 0 && displaySeconds > 0 ? (remaining / gameTime) * 100 : 0; | |
| const percent = | |
| gameTime > 0 && displaySeconds > 0 | |
| ? Math.min(100, (remaining / gameTime) * 100) | |
| : 0; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/pair_game/_utils/timerDisplay.ts` around lines 12 - 13, The percent
calculation (variable percent in timerDisplay.ts using gameTime, displaySeconds
and remaining) can exceed 100 and must be clamped to the 0–100 range; update the
percent assignment so after computing (remaining / gameTime) * 100 it is clamped
to Math.max(0, Math.min(100, ...)) (or use an existing clamp utility) so percent
never goes below 0 or above 100.
| '동아리박람회 이벤트': [ | ||
| { | ||
| id: 7, | ||
| href: '/pair_game', | ||
| content: '동아리 카드 짝 맞추기 게임', | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, examine the navItems.ts file to understand the full context
cat -n src/constants/navItems.tsRepository: COW-dev/ddingdong-fe
Length of output: 1193
🏁 Script executed:
# Search for all references to 'pair_game' in the codebase
rg "pair_game" -t ts -t tsxRepository: COW-dev/ddingdong-fe
Length of output: 91
🏁 Script executed:
# Also search without type restriction to be thorough
rg -n "pair_game"Repository: COW-dev/ddingdong-fe
Length of output: 889
ID 값 누락 확인 필요 (경로 네이밍은 기존 구현으로 진행)
-
ID 누락: 기존 ID가 1-5까지 있는데, 새 항목의 ID가 6이 아닌 7입니다. 의도된 것인지 확인 부탁드립니다.
-
경로 네이밍:
/pair_game은 기존 경로들(/notice,/documents,/faq,/feeds)의 소문자 패턴과 달라 네이밍 컨벤션 불일치가 있습니다. 다만, 이 경로는 이미 애플리케이션 전체에 걸쳐 구현되어 있으므로(src/app/pair_game/ 디렉토리, 관련 API, 타입 정의 등) 현 단계에서 변경하면 광범위한 리팩토링이 필요합니다. 네이밍 일관성보다 기능 안정성을 우선시하여 현재 경로로 진행하는 것을 권장합니다.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/constants/navItems.ts` around lines 43 - 49, The new nav item under
'동아리박람회 이벤트' has id: 7 which breaks the existing id sequence (1-5); update the
id to 6 in src/constants/navItems.ts for the object with href '/pair_game' and
content '동아리 카드 짝 맞추기 게임' to restore the sequential IDs (leave the href
'/pair_game' unchanged as noted).
| @keyframes float-up { | ||
| 0% { | ||
| transform: translateY(35vh) scale(calc(0.85 * var(--float-size, 1))); | ||
| opacity: 0; | ||
| } | ||
| 15% { | ||
| opacity: 0.7; | ||
| } | ||
| 100% { | ||
| transform: translateY(-45vh) scale(var(--float-size, 1)); | ||
| opacity: 0; | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
모바일에서 vh 단위 사용 시 주의
translateY(35vh) 및 translateY(-45vh)는 iOS Safari에서 주소창 표시/숨김에 따라 뷰포트 높이가 변동되어 애니메이션이 예상과 다르게 보일 수 있습니다. dvh(dynamic viewport height) 사용을 고려해 보세요.
♻️ 수정 제안
`@keyframes` float-up {
0% {
- transform: translateY(35vh) scale(calc(0.85 * var(--float-size, 1)));
+ transform: translateY(35dvh) scale(calc(0.85 * var(--float-size, 1)));
opacity: 0;
}
15% {
opacity: 0.7;
}
100% {
- transform: translateY(-45vh) scale(var(--float-size, 1));
+ transform: translateY(-45dvh) scale(var(--float-size, 1));
opacity: 0;
}
}📝 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.
| @keyframes float-up { | |
| 0% { | |
| transform: translateY(35vh) scale(calc(0.85 * var(--float-size, 1))); | |
| opacity: 0; | |
| } | |
| 15% { | |
| opacity: 0.7; | |
| } | |
| 100% { | |
| transform: translateY(-45vh) scale(var(--float-size, 1)); | |
| opacity: 0; | |
| } | |
| } | |
| `@keyframes` float-up { | |
| 0% { | |
| transform: translateY(35dvh) scale(calc(0.85 * var(--float-size, 1))); | |
| opacity: 0; | |
| } | |
| 15% { | |
| opacity: 0.7; | |
| } | |
| 100% { | |
| transform: translateY(-45dvh) scale(var(--float-size, 1)); | |
| opacity: 0; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/styles/globals.css` around lines 120 - 132, 애니메이션의 translateY에 하드코딩된 vh
단위가 iOS Safari에서 주소창 표시/숨김으로 인해 뷰포트 높이가 변동되며 동작이 달라질 수 있으므로 `@keyframes` float-up의
translateY(35vh) 및 translateY(-45vh)을 동적 뷰포트 단위로 전환하세요; 예를 들어 dvh를 우선 사용하고 구형
브라우저 대응을 위해 CSS 변수(--float-dvh) 또는 calc()와 함께 폴백(예: var(--float-dvh, 35vh))을 도입해
float-up 정의와 관련된 transform 값에 적용하여 예상한 애니메이션 동작을 확보하도록 수정하세요.
| phoneNumber: z | ||
| .string() | ||
| .regex(/^010-\d{4}-\d{4}$/, '올바른 전화번호 형식이 아닙니다.'), |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
전화번호 입력 유연성 고려
현재 정규식 /^010-\d{4}-\d{4}$/는 하이픈이 포함된 형식만 허용합니다. 사용자가 하이픈 없이 입력할 가능성이 있다면, 입력 필드에서 자동 포맷팅을 적용하거나 정규식을 더 유연하게 수정하는 것을 고려해 주세요.
♻️ 유연한 정규식 예시
phoneNumber: z
.string()
- .regex(/^010-\d{4}-\d{4}$/, '올바른 전화번호 형식이 아닙니다.'),
+ .regex(/^010-?\d{4}-?\d{4}$/, '올바른 전화번호 형식이 아닙니다.')
+ .transform((val) => {
+ const digits = val.replace(/-/g, '');
+ return `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7)}`;
+ }),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/types/schemas/pairGameSubmitSchema.ts` around lines 9 - 11, The
phoneNumber zod rule in pairGameSubmitSchema.ts currently only accepts
hyphenated input; update the phoneNumber schema to accept both hyphenated and
non-hyphenated formats by either (A) replacing the regex with a flexible one
such as /^010-?\d{4}-?\d{4}$/ (or /^(?:010-?\d{4}-?\d{4}|010\d{8})$/ to be
explicit) or (B) normalize input first (e.g., use z.string().transform(s =>
s.replace(/-/g, '')) and then validate with /^\d{10}$/ or /^010\d{8}$/), keeping
the same error message text ('올바른 전화번호 형식이 아닙니다.') and updating the phoneNumber
declaration accordingly.
🔥 연관 이슈
🚀 작업 내용
🤔 고민했던 내용
💬 리뷰 중점사항
Summary by CodeRabbit
새로운 기능
동아리 카드 짝 맞추기 게임 추가
사용자 경험 향상