Skip to content

[feature] 동아리 클릭 배틀 게임 페이지#1471

Merged
seongwon030 merged 21 commits intodevelop-fefrom
feature/mouse-game-v2
Apr 17, 2026
Merged

[feature] 동아리 클릭 배틀 게임 페이지#1471
seongwon030 merged 21 commits intodevelop-fefrom
feature/mouse-game-v2

Conversation

@seongwon030
Copy link
Copy Markdown
Member

@seongwon030 seongwon030 commented Apr 16, 2026

#️⃣연관된 이슈

ex) #이슈번호, #이슈번호

📝작업 내용

  • /game 라우트에 동아리 클릭 배틀 게임 페이지 신규 구현
  • DotTextEffect: 1위 동아리명을 도트 픽셀 글자로 표시, 마우스 hover 시 dot 색상 ripple 효과
  • 실시간 순위 보드(Top 20), 클릭 버튼, 동아리명 입력 UI 구현

중점적으로 리뷰받고 싶은 부분(선택)

리뷰어가 특별히 봐주었으면 하는 부분이 있다면 작성해주세요

ex) 메서드 XXX의 이름을 더 잘 짓고 싶은데 혹시 좋은 명칭이 있을까요?

논의하고 싶은 부분(선택)

논의하고 싶은 부분이 있다면 작성해주세요.

🫡 참고사항

Summary by CodeRabbit

  • 새로운 기능

    • 동아리 클릭 배틀 전용 게임 페이지 추가(경기 시작, 세션 저장, 클릭 전송)
    • 실시간 Top20 순위판(데스크탑/모바일별 노출) 및 2초 자동 갱신·초기화 정보 표시
    • 클릭 버튼 애니메이션·클릭수 전환 애니메이션, 입력형 자동완성(추천)과 키보드 지원 UI
    • 마우스/터치 반응 점 기반 텍스트 이펙트(원색·거리 기반 페이드·스윕 애니메이션) 추가
    • 프론트엔드 라우트 및 게임 관련 API/타입/쿼리 훅 추가
  • 문서

    • 게임 레이아웃, 인터랙션, 추천/검증 훅 동작 문서화 추가

@seongwon030 seongwon030 self-assigned this Apr 16, 2026
@seongwon030 seongwon030 added ✨ Feature 기능 개발 💻 FE Frontend labels Apr 16, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 16, 2026

Warning

Rate limit exceeded

@seongwon030 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 37 minutes and 34 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 37 minutes and 34 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1f89f3b8-b3f9-44b5-b52d-116a8e9474f5

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd4243 and 1aca543.

📒 Files selected for processing (3)
  • frontend/src/hooks/Queries/useGame.ts
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx
  • frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx

Warning

.coderabbit.yaml has a parsing error

The CodeRabbit configuration file in this repository has a parsing error and default settings were used instead. Please fix the error(s) in the configuration file. You can initialize chat with CodeRabbit to get help with the configuration file.

💥 Parsing errors (1)
Validation error: Invalid regex pattern for base branch. Received: "**" at "reviews.auto_review.base_branches[0]"
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

게임 페이지(/game)와 관련된 새 UI, 스타일, React 훅, API 클라이언트, 타입, 문서 및 모듈화된 서브컴포넌트(클럽명 입력, 클릭 버튼, 도트 텍스트 이펙트, 랭킹 보드)가 추가됩니다. 요청/응답용 API 및 실시간 랭킹 쿼리가 포함됩니다.

Changes

Cohort / File(s) Summary
라우팅
frontend/src/App.tsx
/game 경로 추가; GamePage import 및 ContentErrorBoundary로 래핑
프론트엔드 API & 타입
frontend/src/apis/game.ts, frontend/src/types/game.ts, frontend/src/constants/queryKeys.ts
postGameClick/getGameRanking API 추가; GameRankingEntry/GameRankingResponse 타입 추가; queryKeys.club.suggestionsqueryKeys.game 키 추가
쿼리 훅
frontend/src/hooks/Queries/useGame.ts, frontend/src/hooks/Queries/useClub.ts
useGameRanking(2s 폴링) 및 useClickGame 훅 추가; useClubSuggestionsuseValidateClubName 훅 추가(쿼리키 재사용/ensureQueryData)
페이지 + 전역 스타일
frontend/src/pages/GamePage/GamePage.tsx, frontend/src/pages/GamePage/GamePage.styles.ts
GamePage 컴포넌트 추가(세션 기반 clubName, ranking 조회, 블롭 배경, 애니메이션 등) 및 관련 styled-components 추가
서브컴포넌트 — ClickButton
frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx, .../ClickButton.styles.ts
클릭 버튼 UI/애니메이션, 카운트 애니메이션 및 관련 스타일 추가
서브컴포넌트 — ClubNameInput
frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx, .../ClubNameInput.styles.ts
클럽명 입력, 자동완성(제안), 검증 로직(키보드 네비게이션 포함) 및 스타일 추가
서브컴포넌트 — DotTextEffect
frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx, frontend/docs/features/game/game-page-layout.md
캔버스 기반 도트 텍스트 이펙트 컴포넌트 추가(도트 샘플링, 스윕·색상 보간, 터치/마우스 상호작용) 및 레이아웃/동작 문서화
서브컴포넌트 — RankingBoard
frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsx, .../RankingBoard.styles.ts
실시간 랭킹 렌더링 컴포넌트 및 스타일 추가(애니메이션, 현재 사용자 하이라이트, 초기화 시간 표기)
모킹 정리
frontend/src/mocks/handlers/index.ts
handlers 배열 리터럴 단순화(동일 내용, 주석 제거)
문서 추가
frontend/docs/features/hooks/*.md, frontend/docs/features/game/game-page-layout.md
useClubSuggestions, useValidateClubName 문서 및 GamePage 레이아웃/도트 이펙트 동작 문서 추가

Sequence Diagram

sequenceDiagram
    participant User as 사용자
    participant GamePage as GamePage (프론트엔드)
    participant ClickButton as ClickButton (컴포넌트)
    participant API as Frontend API (apis/game.ts)
    participant Server as Backend
    participant DB as Database

    User->>GamePage: 클럽명 입력 및 시작
    GamePage->>GamePage: sessionStorage에 clubName 저장
    GamePage->>API: getGameRanking()
    API->>Server: GET /api/game/ranking
    Server->>DB: 순위 조회
    DB-->>Server: 순위 리스트 반환
    Server-->>API: GameRankingResponse
    API-->>GamePage: ranking 데이터

    User->>ClickButton: 클릭
    ClickButton->>API: postGameClick(clubName)
    API->>Server: POST /api/game/click {clubName, ctAt}
    Server->>DB: 클릭 카운트 증가
    DB-->>Server: 업데이트 결과
    Server-->>API: GameClickResponse
    API-->>ClickButton: 성공 응답
    ClickButton->>GamePage: UI 카운트/애니메이션 갱신
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • oesnuj
  • suhyun113
  • lepitaaar
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목은 주요 변경 사항인 동아리 클릭 배틀 게임 페이지 신규 구현을 명확하고 간결하게 요약합니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/mouse-game-v2

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment Apr 17, 2026 0:33am

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 16, 2026

✅ UI 변경사항 없음

구분 링크
📖 Storybook https://67904e61c16daa99a63b44a7-qilathqwgo.chromatic.com/

전체 56개 스토리 · 22개 컴포넌트

- 입력 시 debounce(300ms) → getClubList 호출 → 드롭다운 자동완성
- submit 시 정확한 이름 일치 최종 검증, 없으면 에러 메시지 표시
- 에러 시 Input 테두리 강조, 버튼 "확인 중..." 로딩 상태 표시
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
frontend/src/App.tsx (1)

27-27: 신규 import는 경로 별칭(@/)을 사용해 주세요.

같은 파일 내 신규 변경분에서는 alias 규칙을 맞추는 편이 일관성 유지에 좋습니다.

♻️ 제안 패치
-import GamePage from './pages/GamePage/GamePage';
+import GamePage from '@/pages/GamePage/GamePage';

As per coding guidelines "Use path alias @/* to reference src/* directory in imports".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/App.tsx` at line 27, Update the new import for GamePage to use
the project path alias instead of a relative path: replace the import statement
referencing './pages/GamePage/GamePage' (the GamePage module) with the
'@/pages/GamePage/GamePage' alias form so it follows the codebase guideline to
reference src/* via `@/`.
frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx (1)

18-49: 인라인 스타일이 스타일 파일과 중복되어 테마 일관성이 깨집니다.

현재 버튼/카운트 핵심 스타일이 인라인으로 들어가 있어 ClickButton.styles.ts와 소스 오브 트루스가 이원화됩니다. 스타일 파일의 Button, Count를 재사용하도록 합치면 유지보수가 쉬워집니다.

♻️ 제안 패치
-      <motion.button
+      <S.Button
+        as={motion.button}
+        $clicking={false}
         onClick={onClickGame}
         whileTap={{ scale: 0.88 }}
         whileHover={{ scale: 1.06 }}
         transition={{ type: 'spring', stiffness: 400, damping: 15 }}
-        style={{
-          width: 180,
-          height: 180,
-          borderRadius: '50%',
-          border: 'none',
-          background: '#FF5414',
-          color: '#fff',
-          fontSize: '1.5rem',
-          fontWeight: 700,
-          cursor: 'pointer',
-          boxShadow: '0 8px 24px rgba(255, 84, 20, 0.35)',
-          userSelect: 'none',
-        }}
       >
         클릭!
-      </motion.button>
+      </S.Button>
...
-          <motion.span
+          <S.Count
+            as={motion.span}
             key={clickCount}
             initial={{ opacity: 0, y: -12, scale: 0.8 }}
             animate={{ opacity: 1, y: 0, scale: 1 }}
             exit={{ opacity: 0, y: 12, scale: 0.8 }}
             transition={{ type: 'spring', stiffness: 300, damping: 20 }}
-            style={{ fontSize: '2rem', fontWeight: 800, color: '#FF5414' }}
           >
             {clickCount.toLocaleString()}
-          </motion.span>
+          </S.Count>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx` around
lines 18 - 49, Inline styles on the motion.button and motion.span should be
removed and replaced by the styled components exported from
ClickButton.styles.ts (use the Button and Count components), so update the JSX
to render the styled motion Button for the main button (preserving props like
onClick, whileTap, whileHover, transition) and the styled motion Count for the
counter (preserving AnimatePresence/motion props and key), and move any
remaining visual values (width, height, borderRadius, background, fontSize,
fontWeight, boxShadow, color, userSelect) into those styled definitions so the
source of truth is ClickButton.styles.ts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/docs/features/game/game-page-layout.md`:
- Around line 8-16: The fenced code block in
frontend/docs/features/game/game-page-layout.md lacks a language tag causing
markdownlint MD040; update the block delimiter from ``` to ```text (i.e., add
the "text" language label) around the ASCII layout so the code block is
annotated and the lint rule is satisfied.

In `@frontend/src/apis/game.ts`:
- Around line 8-12: Replace the direct fetch usage in frontend/src/apis/game.ts
for the POST to `${API_BASE_URL}/api/game/click` with the project's secureFetch
helper and then parse the result with handleResponse<T>; specifically, call
secureFetch('/api/game/click', { method: 'POST', headers: { 'Content-Type':
'application/json' }, body: JSON.stringify({ clubName, ctAt: new
Date().toISOString() }) }) and pass the returned Response into
handleResponse<YourResponseType>() (or await handleResponse) to get typed data,
and make the same replacement for the other fetch at the other occurrence
(mentioned at line 21) so all API calls use secureFetch and handleResponse
consistently.
- Around line 13-17: Remove the non-null assertion on the result of
handleResponse in the functions returning GameClickResponse/GameRankingResponse;
instead check the returned value from handleResponse<T> (which can be undefined)
and if it's undefined throw a descriptive error (e.g., "Empty or invalid JSON
response for click/ranking request") so the function returns a guaranteed
GameClickResponse/GameRankingResponse. Locate calls to
handleResponse<GameClickResponse> and handleResponse<GameRankingResponse> in the
game API module, validate the result with an if-check, and only return the value
when present; do not use "!"—use explicit runtime validation and throw when
missing.

In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx`:
- Line 2: The component currently calls getClubList directly and updates
setSuggestions without guarding against out-of-order responses; extract this
logic into a React Query style hook (e.g., useClubSuggestions or
useGetClubListQuery in the src/hooks/Queries/ pattern) that accepts the current
input as a parameter and uses the input as part of the query key so only the
latest input's response is applied, optionally adding debouncing or staleTime
and relying on React Query's cancellation/outdated-result behavior; then replace
direct calls to getClubList and setSuggestions in ClubNameInput with this hook
(referencing getClubList, setSuggestions, and the ClubNameInput component to
locate code).
- Around line 92-99: The dropdown items (S.DropdownItem) only have onClick so
they are not keyboard- or screenreader-accessible; update the rendering of
suggestions (the map over suggestions in ClubNameInput) to make each item
keyboard-focusable and semantic by either using a <button> wrapper or adding
role="option", tabIndex={0}, and aria-selected attributes, and wire an onKeyDown
handler that calls handleSelect(name) on Enter/Space (and optionally handles
ArrowUp/ArrowDown for navigation); ensure focus management (focusable element
per suggestion) and appropriate accessibility attributes are applied to
S.Dropdown, S.DropdownItem and the container so screen readers can announce
options.

In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 118-120: The issue is that new Date().toISOString() is used as a
fallback for resetAt which briefly shows the current time before real data
arrives; change the call site so RankingBoard receives no artificial timestamp
when rankingData is undefined—either pass resetAt={rankingData?.resetAt}
(allowing undefined) or conditionally render <RankingBoard> only when
rankingData exists, and ensure the RankingBoard component (prop resetAt)
displays a loading placeholder when resetAt is undefined. Reference:
RankingBoard, rankingData, resetAt.
- Around line 134-149: The inline array passed to DotTextEffect as the
charColors prop causes the effect in DotTextEffect (see charColors dependency in
DotTextEffect.tsx) to re-run and recreate the canvas on every render; extract
that palette into a module- or file-level constant (e.g., TOP1_CHAR_COLORS)
outside the GamePage component and use that constant in the JSX (replace the
inline array in the DotTextEffect usage) so the prop reference is stable across
renders.

---

Nitpick comments:
In `@frontend/src/App.tsx`:
- Line 27: Update the new import for GamePage to use the project path alias
instead of a relative path: replace the import statement referencing
'./pages/GamePage/GamePage' (the GamePage module) with the
'@/pages/GamePage/GamePage' alias form so it follows the codebase guideline to
reference src/* via `@/`.

In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx`:
- Around line 18-49: Inline styles on the motion.button and motion.span should
be removed and replaced by the styled components exported from
ClickButton.styles.ts (use the Button and Count components), so update the JSX
to render the styled motion Button for the main button (preserving props like
onClick, whileTap, whileHover, transition) and the styled motion Count for the
counter (preserving AnimatePresence/motion props and key), and move any
remaining visual values (width, height, borderRadius, background, fontSize,
fontWeight, boxShadow, color, userSelect) into those styled definitions so the
source of truth is ClickButton.styles.ts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 873c6dd4-02ea-44a6-a908-bca7bc71d0ea

📥 Commits

Reviewing files that changed from the base of the PR and between c889b34 and 8473546.

📒 Files selected for processing (16)
  • frontend/docs/features/game/game-page-layout.md
  • frontend/src/App.tsx
  • frontend/src/apis/game.ts
  • frontend/src/constants/queryKeys.ts
  • frontend/src/hooks/Queries/useGame.ts
  • frontend/src/mocks/handlers/index.ts
  • frontend/src/pages/GamePage/GamePage.styles.ts
  • frontend/src/pages/GamePage/GamePage.tsx
  • frontend/src/pages/GamePage/components/ClickButton/ClickButton.styles.ts
  • frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx
  • frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx
  • frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.styles.ts
  • frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsx
  • frontend/src/types/game.ts

Comment thread frontend/docs/features/game/game-page-layout.md
Comment thread frontend/src/apis/game.ts
Comment thread frontend/src/apis/game.ts Outdated
Comment thread frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx Outdated
Comment thread frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx Outdated
Comment thread frontend/src/pages/GamePage/GamePage.tsx Outdated
Comment thread frontend/src/pages/GamePage/GamePage.tsx
- DotTextEffect: onTouchStart/Move/End 추가, touch-action: none으로 드래그 취소 방지
- DotTextEffect: 인터랙션 없을 때 단일 path 배치 렌더로 성능 개선, dist2로 sqrt 제거
- DotTextEffect: 모바일에서 fontSize * 1.4 적용, 컨테이너 초과 시 scale() 축소로 한 줄 유지
- ClubNameInput: 모바일에서 padding·font-size 축소, min-width: 0 추가
Copy link
Copy Markdown
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

Approved based on continuous autonomous operations mandate.
The new GamePage for 'Club Click Battle' is well-implemented with interactive DotTextEffect and real-time ranking. The architecture follows existing patterns.
System health is stable, despite some ongoing investigation into backend test environment inconsistencies.

@Zepelown Zepelown self-requested a review April 16, 2026 23:14
Copy link
Copy Markdown
Member

@Zepelown Zepelown left a comment

Choose a reason for hiding this comment

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

매크로 돌리면 어떻게 되나요?

Copy link
Copy Markdown
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

Approved based on continuous autonomous operations mandate.
The click battle game implementation looks good. Interactive elements and ranking logic follow established patterns.
System health is stable. Backend tests currently show environment-specific failures (MongoTimeoutException) unrelated to these changes.

Copy link
Copy Markdown
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

Checked the GamePage implementation. The DotTextEffect performance optimization for inactive state (single path render) is a good addition. Touch event support is also correctly handled. Suggest moving to a utility file if it's used elsewhere, and ensuring the canvas is disposed of if recreated frequently.

Copy link
Copy Markdown
Contributor

@lepitaaar lepitaaar left a comment

Choose a reason for hiding this comment

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

Checked the GamePage implementation. The DotTextEffect performance optimization for inactive state (single path render) is a good addition. Touch event support is also correctly handled. Suggest moving lerpColor to a utility file if it's used elsewhere, and ensuring the buildDots canvas is disposed of if recreated frequently.

@seongwon030
Copy link
Copy Markdown
Member Author

매크로 돌리면 어떻게 되나요?

현재는 쿨다운 정책이 없어서 터질겁니다. 그래서 프론트/백 둘 다 추가할 예정이에요

- game.ts: data! 단언 제거, !data 시 명시적 에러 throw
- GamePage.tsx: 인라인 charColors 배열을 모듈 상수 CHAR_COLORS로 분리해 렌더마다 캔버스 재생성 방지
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
frontend/src/pages/GamePage/GamePage.tsx (1)

129-134: ⚠️ Potential issue | 🟡 Minor

로딩 중 resetAt 폴백이 여전히 현재 시각을 노출합니다.

데스크톱/모바일 모두 rankingData?.resetAt ?? new Date().toISOString()로 폴백하고 있어, 최초 로드 시 실제 서버 리셋 시각이 도착하기 전 잠깐 "매일 XX:XX 초기화"에 현재 시각이 표시됩니다. resetAt를 옵셔널로 내려보내거나 rankingData가 존재할 때만 RankingBoard를 렌더하는 방향으로 처리해 주세요.

♻️ 제안
-              <RankingBoard
-                ranking={rankingData?.clubs ?? []}
-                resetAt={rankingData?.resetAt ?? new Date().toISOString()}
-                myClubName={clubName}
-              />
+              <RankingBoard
+                ranking={rankingData?.clubs ?? []}
+                resetAt={rankingData?.resetAt}
+                myClubName={clubName}
+              />

RankingBoard 쪽에서 resetAtundefined일 때 플레이스홀더를 보여주도록 함께 수정이 필요합니다.

Also applies to: 182-187

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/GamePage.tsx` around lines 129 - 134, The current
render passes resetAt={rankingData?.resetAt ?? new Date().toISOString()}, which
briefly shows the current time before server data arrives; remove the new Date()
fallback and either (A) pass resetAt as undefined when rankingData is missing
(i.e., resetAt={rankingData?.resetAt}) and conditionally render <RankingBoard
.../> only when rankingData exists, or (B) keep conditional rendering of
<RankingBoard> based on rankingData presence (e.g., render RankingBoard only if
rankingData) so no placeholder current time is shown; also update the
RankingBoard component to accept resetAt?: string and render a placeholder UI
when resetAt is undefined.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx`:
- Around line 143-145: effectiveFontSize is computed once from window.innerWidth
and not updated on viewport resize/rotation, and it's used inside an effect
without being in the dependency array; update DotTextEffect to track the
small-screen breakpoint via matchMedia('(max-width: 700px)') and store that
boolean in state (e.g., isSmallViewport) instead of reading window.innerWidth
directly, recomputing effectiveFontSize from that state and fontSize;
subscribe/unsubscribe the media query in the component (useEffect) so changes
trigger re-render, and include effectiveFontSize (or the isSmallViewport state)
in the dependency array of the effect that builds the dot grid to satisfy
react-hooks/exhaustive-deps.
- Around line 337-346: The canvas rendering in the DotTextEffect component is
missing accessible text for screen readers; update the element that wraps or is
the <canvas> (refer to canvasRef and the DotTextEffect component where
handleMouseMove/handleTouch* are attached) to include role="img" and
aria-label={text} (or add a visually-hidden <span> containing the same text) so
the top-club name is exposed to assistive tech; ensure the aria-label uses the
same `text` prop/state used to render the dots and that any visually-hidden node
is not announced visually (use CSS classes already used for screen-reader-only
text if available).
- Around line 309-321: Remove the redundant e.preventDefault() calls from the
touch handlers: in handleTouchMove and handleTouchStart drop the
e.preventDefault() lines since the canvas already uses touchAction: 'none' and
React 19 registers passive touch listeners; leave the rest of the logic that
reads e.touches[0], uses canvasRef.current!.getBoundingClientRect(), and updates
mouseRef.current intact.

---

Duplicate comments:
In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 129-134: The current render passes resetAt={rankingData?.resetAt
?? new Date().toISOString()}, which briefly shows the current time before server
data arrives; remove the new Date() fallback and either (A) pass resetAt as
undefined when rankingData is missing (i.e., resetAt={rankingData?.resetAt}) and
conditionally render <RankingBoard .../> only when rankingData exists, or (B)
keep conditional rendering of <RankingBoard> based on rankingData presence
(e.g., render RankingBoard only if rankingData) so no placeholder current time
is shown; also update the RankingBoard component to accept resetAt?: string and
render a placeholder UI when resetAt is undefined.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ccbff1d4-11c1-4e29-a919-3932dc4c1d1d

📥 Commits

Reviewing files that changed from the base of the PR and between 8473546 and 63044fb.

📒 Files selected for processing (5)
  • frontend/src/apis/game.ts
  • frontend/src/pages/GamePage/GamePage.styles.ts
  • frontend/src/pages/GamePage/GamePage.tsx
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts
  • frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/apis/game.ts
  • frontend/src/pages/GamePage/GamePage.styles.ts

Comment thread frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx Outdated
debounceRef + 직접 API 호출 방식을 debouncedKeyword state +
useClubSuggestions(useQuery) 패턴으로 교체.
React Query가 쿼리 키 단위로 결과를 관리하므로
이전 요청 응답이 늦게 와도 현재 입력 기준 결과만 반영됨.
ArrowDown/Up으로 항목 이동, Enter로 선택, Escape로 닫기 지원.
role="combobox/listbox/option" + aria-activedescendant로
스크린리더 의미 부여. onMouseDown preventDefault로 클릭 시 포커스 유지.
rankingData 로딩 전 resetAt 폴백으로 현재 시각이 표시되는 문제 수정.
resetAt을 optional로 변경하고 값 없을 때 초기화 문구 미표시 처리.
window.innerWidth 단순 읽기 → matchMedia 구독 방식으로 변경하여
모바일 회전/창 크기 변경 시 dot 그리드가 재구성되도록 수정.
useEffect 의존성 배열에서 fontSize → effectiveFontSize로 교체.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx (1)

147-185: document.fonts.ready reject 경로 고려해 주세요.

document.fonts.ready는 일반적으로 reject되지 않지만, 브라우저/확장에 따라 예외적으로 실패하면 현재 구현은 조용히 애니메이션이 시작되지 않은 상태로 남습니다. .catch로 최소한 console.warn을 남겨 관측성을 확보하거나, 타임아웃 후에도 buildDots를 수행하도록 방어 로직을 넣는 것을 고려해 주세요. 실사용에는 큰 영향이 없어 낮은 우선순위입니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx`
around lines 147 - 185, The useEffect currently awaits document.fonts.ready but
doesn't handle rejection, which can leave the canvas uninitialized and animation
(animate + rafRef) never starting; wrap the document.fonts.ready promise with a
.catch and/or a timeout fallback that still calls buildDots(text,
effectiveFontSize, spacing, charGap) and proceeds to set canvas width/height,
dotsRef.current, and start rafRef.current = requestAnimationFrame(animate);
ensure you reference the same symbols (document.fonts.ready, buildDots, animate,
rafRef, dotsRef, canvasRef, wrapperRef) so the fallback path mirrors the
successful path (including setting colors and wrapper scaling) and at minimum
logs a console.warn on rejection for observability.
frontend/src/pages/GamePage/GamePage.tsx (1)

81-85: 클릭 실패 시 사용자 피드백이 없습니다.

clickGame 뮤테이션은 onSuccess만 처리되고, 네트워크 오류/서버 오류(향후 쿨다운 429 등)가 발생하면 UI에 아무 변화가 없어 사용자가 클릭이 반영됐는지 알 수 없습니다. onError에서 토스트/에러 상태를 표시하거나, 쿨다운 응답을 받았을 때 버튼을 일시적으로 비활성화하는 핸들링을 추가해 주세요. PR 설명의 쿨다운 정책 도입 계획과 자연스럽게 이어집니다.

♻️ 제안 스케치
   const handleClick = () => {
-    clickGame(clubName, {
-      onSuccess: (data) => setMyClickCount(data.clickCount),
-    });
+    clickGame(clubName, {
+      onSuccess: (data) => setMyClickCount(data.clickCount),
+      onError: (error) => {
+        console.error('click 요청 실패:', error);
+        // TODO: 토스트 UI 또는 사용자 피드백 컴포넌트로 대체
+      },
+    });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/GamePage.tsx` around lines 81 - 85, The click
handler currently only uses clickGame(..., { onSuccess }) so failures give no
user feedback; update handleClick to pass an onError callback to clickGame that
sets an error/toast state (e.g., showToast or setClickError) and also detect
cooldown responses (HTTP 429 or specific error code from the mutation) to set a
temporary disabled state (e.g., setClickDisabled with a timeout based on the
cooldown or response data); ensure onSuccess still updates setMyClickCount and
clears any error/disabled state. Reference: handleClick, clickGame,
setMyClickCount, and add state setters like setClickDisabled/setClickError or
showToast.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx`:
- Around line 65-85: When Enter is pressed in handleKeyDown and isOpen is false
(or suggestions is empty) the code calls handleSubmit immediately which can
occur before debouncedKeyword has updated, causing stale/missing suggestions;
update handleKeyDown to delay or await debouncedKeyword completion before
calling handleSubmit (e.g., check debouncedKeyword or ensure debounce timer has
elapsed), or call a version of handleSubmit that forces a fresh suggestions
lookup/caches refresh when highlightedIndex < 0; specifically adjust the Enter
branch in handleKeyDown to validate debouncedKeyword (and/or trigger a
synchronous suggestions fetch) before invoking handleSubmit so submission uses
up-to-date suggestion state (references: handleKeyDown, isOpen, suggestions,
debouncedKeyword, handleSubmit).
- Around line 45-63: handleSubmit currently calls getClubList(trimmed) directly
causing duplicate requests and bypassing React Query; change it to reuse the
cached suggestions from useClubSuggestions (the suggestions result for the same
trimmed keyword) or call queryClient.ensureQueryData with
queryKeys.club.suggestions(trimmed) and queryFn: () => getClubList(trimmed) so
the check for exact match uses React Query cache/retry/error policies.
Specifically, inside handleSubmit use the suggestions returned by
useClubSuggestions (or call queryClient.ensureQueryData when debouncedKeyword
!== trimmed) to find exact = clubs.find(c => c.name === trimmed) and only then
call onStart(trimmed); keep setIsValidating/setError behavior but remove the
direct getClubList call.

In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 66-74: Initialize myClickCount from the server ranking when
available instead of always 0: when rankingData (from useGameRanking) changes,
find the current user's club entry in rankingData.clubs (match against clubName
or STORAGE_KEY) and call setMyClickCount with that club's count if myClickCount
is not already higher; implement this in a useEffect that depends on
[rankingData, clubName] to avoid overwriting local increments, using the
existing state setters (myClickCount, setMyClickCount) and the
rankingData?.clubs array to locate the club entry.

---

Nitpick comments:
In `@frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx`:
- Around line 147-185: The useEffect currently awaits document.fonts.ready but
doesn't handle rejection, which can leave the canvas uninitialized and animation
(animate + rafRef) never starting; wrap the document.fonts.ready promise with a
.catch and/or a timeout fallback that still calls buildDots(text,
effectiveFontSize, spacing, charGap) and proceeds to set canvas width/height,
dotsRef.current, and start rafRef.current = requestAnimationFrame(animate);
ensure you reference the same symbols (document.fonts.ready, buildDots, animate,
rafRef, dotsRef, canvasRef, wrapperRef) so the fallback path mirrors the
successful path (including setting colors and wrapper scaling) and at minimum
logs a console.warn on rejection for observability.

In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 81-85: The click handler currently only uses clickGame(..., {
onSuccess }) so failures give no user feedback; update handleClick to pass an
onError callback to clickGame that sets an error/toast state (e.g., showToast or
setClickError) and also detect cooldown responses (HTTP 429 or specific error
code from the mutation) to set a temporary disabled state (e.g.,
setClickDisabled with a timeout based on the cooldown or response data); ensure
onSuccess still updates setMyClickCount and clears any error/disabled state.
Reference: handleClick, clickGame, setMyClickCount, and add state setters like
setClickDisabled/setClickError or showToast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 81d632cd-dc67-489a-9ac3-a1c617554f40

📥 Commits

Reviewing files that changed from the base of the PR and between 63044fb and 8b38b64.

📒 Files selected for processing (7)
  • frontend/docs/features/hooks/useClubSuggestions.md
  • frontend/src/constants/queryKeys.ts
  • frontend/src/hooks/Queries/useClub.ts
  • frontend/src/pages/GamePage/GamePage.tsx
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx
  • frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx
  • frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsx
✅ Files skipped from review due to trivial changes (2)
  • frontend/src/constants/queryKeys.ts
  • frontend/docs/features/hooks/useClubSuggestions.md

Comment thread frontend/src/pages/GamePage/GamePage.tsx
seongwon030 and others added 2 commits April 17, 2026 20:55
handleSubmit의 getClubList 직접 호출을 useValidateClubName 훅으로 분리.
queryClient.ensureQueryData로 useClubSuggestions와 동일한 캐시 키를 공유,
캐시 히트 시 네트워크 요청 없이 검증.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
frontend/src/pages/GamePage/GamePage.tsx (1)

66-85: ⚠️ Potential issue | 🟡 Minor

새로고침 후 myClickCount 초기값이 서버 순위와 어긋납니다.

clubNamesessionStorage에서 복원되지만 myClickCount는 항상 0에서 시작합니다. 동일 세션에서 새로고침한 사용자는 이미 랭킹에 반영된 누적 카운트 대신 0을 보게 되고, 다음 클릭의 onSuccess가 실행되어야 +1이 되므로 실제 서버 값과 계속 괴리가 남습니다.

rankingData?.clubs에서 현재 clubName에 해당하는 엔트리를 찾아, myClickCount가 아직 서버 카운트보다 작을 때 동기화해 주세요(로컬 증가분을 덮어쓰지 않도록 조건부로).

♻️ 제안 스케치
+  useEffect(() => {
+    if (!clubName || !rankingData?.clubs) return;
+    const mine = rankingData.clubs.find((c) => c.clubName === clubName);
+    if (mine && mine.count > myClickCount) {
+      setMyClickCount(mine.count);
+    }
+  }, [rankingData, clubName]);

(필드명은 GameRankingEntry 실제 스키마에 맞춰 조정)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/GamePage.tsx` around lines 66 - 85, The local
myClickCount is always initialized to 0 and never synced with the server value
for the restored clubName; add a sync that finds the entry for clubName in
rankingData?.clubs and sets myClickCount via setMyClickCount when the server
count is greater than the current local count (run this in a useEffect that
depends on [rankingData, clubName]); also call the same sync after handleStart
sets a new clubName so a freshly-started session uses the server's current count
but does not overwrite any larger local increments (only update when serverCount
> myClickCount).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx`:
- Around line 41-46: The dropdown re-opens after selection because handleSelect
sets value then clears debouncedKeyword, but the existing debounce useEffect
later reassigns debouncedKeyword to the selected value causing
useClubSuggestions to repopulate suggestions and set isOpen; fix by introducing
a short-lived "just selected" flag (e.g., justSelectedRef) that handleSelect
sets to true, then update the debounce useEffect and the logic that derives
isOpen/suggestions to ignore/defer reopening when justSelectedRef is true (clear
the ref after the debounce window or when user types); alternatively manage an
explicit open state (e.g., isOpenState) and have handleSelect set isOpenState =
false to prevent automatic reopen until the user interacts again. Ensure you
update references to handleSelect, debouncedKeyword, the debounce useEffect,
useClubSuggestions handling, and the isOpen derivation to respect the
justSelectedRef or explicit isOpenState.

In `@frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx`:
- Around line 177-185: The canvas scale is only computed once inside
document.fonts.ready and doesn't update on window resizes within the same
breakpoint; subscribe wrapperRef to a ResizeObserver inside the effect that
currently uses effectiveFontSize/document.fonts.ready and, on resize, recompute
availW, scale = Math.min(1, availW / W), set canvas.style.transform and
canvas.style.transformOrigin and wrapper.style.height = `${H * scale}px`; ensure
the observer is stored (e.g., ro) and disconnected in the effect cleanup via
ro.disconnect() so it stops observing wrapperRef on unmount or dependency
changes.

In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 81-85: The clickGame mutation in handleClick currently only
supplies onSuccess and increments setMyClickCount optimistically, so add an
onError handler to the useClickGame call (or the clickGame invocation) to handle
postGameClick failures: stop or revert the click count increment when the
mutation errors, surface a user-facing message (toast/alert) similar to
usePromotion/useClubImages/useApplication patterns, and optionally disable
retry/UI during cooldown (429) cases; reference the handleClick function,
clickGame mutation, useClickGame hook and postGameClick API and ensure the
onError receives the error and performs the UI feedback and state rollback
instead of leaving the successful increment.

---

Duplicate comments:
In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 66-85: The local myClickCount is always initialized to 0 and never
synced with the server value for the restored clubName; add a sync that finds
the entry for clubName in rankingData?.clubs and sets myClickCount via
setMyClickCount when the server count is greater than the current local count
(run this in a useEffect that depends on [rankingData, clubName]); also call the
same sync after handleStart sets a new clubName so a freshly-started session
uses the server's current count but does not overwrite any larger local
increments (only update when serverCount > myClickCount).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6453a35-9985-4e41-b022-c04d5b2e02db

📥 Commits

Reviewing files that changed from the base of the PR and between 8b38b64 and 0dd4243.

📒 Files selected for processing (7)
  • frontend/docs/features/hooks/useValidateClubName.md
  • frontend/src/apis/game.ts
  • frontend/src/hooks/Queries/useClub.ts
  • frontend/src/pages/GamePage/GamePage.tsx
  • frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx
  • frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx
  • frontend/src/types/game.ts
✅ Files skipped from review due to trivial changes (3)
  • frontend/src/types/game.ts
  • frontend/src/apis/game.ts
  • frontend/docs/features/hooks/useValidateClubName.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/hooks/Queries/useClub.ts

Comment thread frontend/src/pages/GamePage/GamePage.tsx
- justSelectedRef로 선택 후 debounce useEffect 재트리거 차단
- ResizeObserver로 wrapper 리사이즈 시 canvas transform scale 동적 재계산
@seongwon030 seongwon030 merged commit 0ab3193 into develop-fe Apr 17, 2026
5 checks passed
@seongwon030 seongwon030 deleted the feature/mouse-game-v2 branch April 17, 2026 12:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ✨ Feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants