[feat] 동아리 클릭 게임 폭죽·다크모드 추가 및 resetAt 안내 제거#1682
Conversation
자동 초기화가 관리자 수동 초기화로 변경되어 resetAt이 무의미한 표시값이 됨. '매일 OO 초기화' 안내 문구와 연관 prop/스타일 제거.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedPull request was closed or merged during review Warning
|
| Layer / File(s) | Summary |
|---|---|
useBatchedClick 훅 신규 도입 frontend/src/pages/GamePage/hooks/useBatchedClick.ts |
클릭을 pendingRef에 누적하고 임계치(5회) 도달 시 즉시 flush, 미달 시 500ms 디바운스로 전송하는 훅을 신규 추가했다. 언마운트 cleanup에서 ref 미러링으로 미전송 클릭을 처리한다. |
BackgroundFirework 컴포넌트 신규 추가 frontend/src/pages/GamePage/components/BackgroundFirework/BackgroundFirework.tsx |
framer-motion 기반 70개 파티클 폭죽 컴포넌트를 신규 추가했다. position: fixed + zIndex: 0으로 콘텐츠 뒤에 렌더링되며 memo로 최적화됐다. |
ClickButton 클릭 파티클 이펙트 및 다크모드 지원 frontend/src/pages/GamePage/components/ClickButton/ClickButton.styles.ts, frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx |
bursts 상태와 1200ms 타이머로 클릭 파티클 오버레이를 표시·제거한다. isDark prop이 추가되어 ClubLabel/CountLabel 색상이 조건부로 적용되고, ButtonArea styled 컴포넌트가 신규 추가됐다. |
DotTextEffect 다크모드 색상 지원 frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx |
dotColor prop을 추가하여 기존 고정 상수 대신 동적 색상을 받도록 변경했다. 도트 렌더링 경로 모두에서 fillStyle이 dotColor로 적용된다. |
RankingBoard 다크모드 지원 및 resetAt 제거 frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.styles.ts, frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsx |
resetAt prop 제거, isDark prop 추가, Title/Item/ClubName/EmptyMessage가 $dark 조건부 색상으로 렌더링되도록 변경했다. Wrapper에 margin: 0 auto 중앙 정렬이 추가됐다. |
GamePage 스타일 업데이트 및 오케스트레이션 frontend/src/pages/GamePage/GamePage.styles.ts, frontend/src/pages/GamePage/GamePage.tsx |
Blob 스타일 제거, ToggleBar/ToggleSwitch/ToggleKnob 신규 추가, 미디어 기준 media.laptop 변경을 적용했다. useBatchedClick 위임, sessionStorage 다크모드 상태, 마일스톤 기반 BackgroundFirework 트리거 effect, $dark prop 전파를 구현했다. |
ClubNameInput 다크모드 및 게임 시작 이벤트 추적 frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts, frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx |
ClubNameInput 스타일에 $dark 조건부 색상을 적용했다. isDark prop 추가 후 게임 시작 버튼 성공 시 trackEvent(USER_EVENT.GAME_START_BUTTON_CLICKED, { clubName })을 발송한다. |
이벤트 상수 및 기능 문서화 frontend/src/constants/eventName.ts, frontend/docs/features/game/game-effects-darkmode.md, frontend/docs/features/game/game-tracking.md |
eventName.ts에 GAME_START_BUTTON_CLICKED 이벤트와 GAME_PAGE 페이지뷰를 추가했다. 게임 이펙트/다크모드 및 게임 추적 정책을 신규 문서에 정리했다. |
Sequence Diagram(s)
sequenceDiagram
participant User
participant GamePage
participant sessionStorage
participant ClickButton
participant BackgroundFirework
rect rgba(99, 179, 237, 0.5)
Note over User,sessionStorage: 다크모드 토글 흐름
User->>GamePage: 토글 버튼 클릭
GamePage->>sessionStorage: game_dark_mode 저장
GamePage->>GamePage: isDark 상태 갱신
GamePage->>ClickButton: isDark prop 전달
end
rect rgba(154, 230, 180, 0.5)
Note over User,BackgroundFirework: 클릭 및 파티클 폭죽 흐름
User->>ClickButton: 클릭
ClickButton->>ClickButton: handleClick 호출 (useBatchedClick)
ClickButton->>ClickButton: pendingRef 증가 / threshold 도달 시 즉시 flush<br/>미달 시 500ms 디바운스
ClickButton->>ClickButton: bursts에 id 추가 → Firework 파티클 렌더
ClickButton->>ClickButton: 1200ms 후 burst 제거
end
rect rgba(251, 211, 141, 0.5)
Note over GamePage,BackgroundFirework: 배경 폭죽 마일스톤 트리거 흐름
GamePage->>GamePage: 2초 폴링으로 랭킹 clickCount 감지
GamePage->>GamePage: MILESTONE_UNIT(100) 단위 돌파 확인
alt 마일스톤 돌파 감지
GamePage->>BackgroundFirework: bgBursts에 ID 추가하여 position:fixed 렌더
BackgroundFirework->>BackgroundFirework: 70개 파티클 x/y 경로, opacity, rotate 애니메이션
BackgroundFirework-->>GamePage: 애니메이션 완료 후 ID 제거
end
end
Estimated code review effort
🎯 4 (Complex) | ⏱️ ~60 minutes
Possibly related PRs
- Moadong/moadong#1487:
clickGame카운트 기반 배칭 전송 목표가 이번 PR의useBatchedClick훅 및GamePage.tsx클릭 처리 리팩터링과 직접 연관된다. - Moadong/moadong#1471:
DotTextEffect,RankingBoard,ClickButton컴포넌트의 초기 구현이 이번 PR에서 변경되는 동일한 파일들과 직접 연관된다.
Suggested labels
✨ Feature, 🎨 Design
Suggested reviewers
- suhyun113
- oesnuj
- lepitaaar
🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
| Check name | Status | Explanation | Resolution |
|---|---|---|---|
| Docstring Coverage | 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 (4 passed)
| Check name | Status | Explanation |
|---|---|---|
| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check | ✅ Passed | PR 제목이 주요 변경 사항을 정확하게 반영하고 있습니다. '폭죽(confetti effects)', '다크모드(dark mode toggle)', 'resetAt 안내 제거(removal of resetAt guidance)'라는 세 가지 핵심 변경 사항이 명확하고 간결하게 표현되어 있습니다. |
| Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
✏️ Tip: You can configure your own custom pre-merge checks in the settings.
✨ Finishing Touches
📝 Generate docstrings
- Create stacked PR
- Commit on current branch
🧪 Generate unit tests (beta)
- Create PR with unit tests
- Commit unit tests in branch
refactor/#1681-remove-game-ranking-reset-info-MOA-977
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 @coderabbitai help to get the list of available commands and usage tips.
✅ UI 변경사항 없음
전체 60개 스토리 · 23개 컴포넌트 |
There was a problem hiding this comment.
Code Review
This pull request introduces a mobile-responsive settings tab (SettingsTab) for the admin page, extracts admin tab configurations, and modularizes logout logic into a reusable useLogout hook. It also removes daily reset time displays from the game ranking board and refines global and component styles. Feedback focuses on ensuring local logout cleanup runs in a finally block to handle API failures gracefully, utilizing currentColor in SVG icons for dynamic color styling, and adhering to the design system by using typography tokens instead of hardcoded styles.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
I am having trouble creating individual review comments. Click here to see my feedback.
frontend/src/hooks/useLogout.ts (10-22)
logout() API 호출이 실패하더라도 클라이언트 측의 토큰 삭제 및 로그인 페이지로의 이동은 항상 수행되어야 합니다. 현재 구현에서는 API 호출이 실패하면 catch 블록으로 빠져 에러 메시지만 보여주고 로컬 로그아웃 처리가 되지 않아, 세션이 만료되었거나 서버 에러가 발생했을 때 사용자가 로그아웃할 수 없는 상태에 빠질 수 있습니다. finally 블록을 사용하여 항상 로컬 로그아웃 처리가 완료되도록 개선하는 것을 권장합니다.
const handleLogout = async () => {
const confirmed = window.confirm('정말 로그아웃하시겠습니까?');
if (!confirmed) return;
try {
await logout();
trackEvent(ADMIN_EVENT.LOGOUT_BUTTON_CLICKED);
} catch {
alert('로그아웃에 실패했습니다.');
} finally {
localStorage.removeItem('accessToken');
navigate('/admin/login', { replace: true });
}
};
frontend/src/assets/images/icons/right_arrow_icon.svg (2)
SVG 내부의 stroke 값을 currentColor로 설정하면, styled-components에서 color 속성을 통해 아이콘 색상을 동적으로 제어할 수 있어 유지보수성이 향상됩니다. 이렇게 변경하면 SettingsTab.styles.ts나 SettingsCard.styles.ts에서 nested path 선택자를 사용하지 않고 부모 컴포넌트의 color 지정만으로 색상을 변경할 수 있습니다.
<path d="M9 18L15 12L9 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
frontend/src/pages/AdminPage/tabs/SettingsTab/SettingsTab.styles.ts (23-28)
PageTitle 컴포넌트에 font-size, font-weight, line-height가 하드코딩되어 있습니다. 프로젝트의 디자인 시스템 일관성을 유지하기 위해, 하드코딩 대신 setTypography 유틸리티와 정의된 타이틀 토큰(예: typography.title 계열)을 사용하도록 수정해 주세요. 스타일을 구현할 때는 토큰 이름이 의미상 맞지 않는 것처럼 보이더라도 디자인 시스템(예: Figma)에 지정된 타이틀 토큰과 일치시키는 것을 우선시하고, 불일치 사항은 임의의 코드 변경 대신 디자인 팀과의 논의를 통해 해결해야 합니다.
References
- 스타일을 구현할 때, 토큰 이름이 의미상 맞지 않는 것처럼 보이더라도 디자인 시스템(예: Figma)에 지정된 타이틀 토큰과 일치시키는 것을 우선시하고, 불일치 사항은 임의의 코드 변경 대신 디자인 팀과의 논의를 통해 해결해야 합니다.
frontend/src/pages/AdminPage/tabs/SettingsTab/SettingsTab.styles.ts (91-99)
right_arrow_icon.svg 파일의 stroke 속성을 currentColor로 변경하면, nested path 선택자를 사용할 필요 없이 color 속성만으로 간단하게 색상을 지정할 수 있습니다.
export const RightArrowIcon = styled(RightArrowSvg)`
width: 24px;
height: 24px;
flex-shrink: 0;
color: ${colors.base.white};
`;GamePage에 인라인으로 있던 클릭 배칭(5회 또는 500ms 디바운스로 모아 전송) 로직을 별도 훅으로 추출해 페이지 컴포넌트 가독성 확보
- 순위표 반응형 겹침 수정 (전환 기준 tablet→laptop, 순위표 중앙 정렬) - 클릭 버튼 폭죽 파티클 이펙트 추가 - 랭킹 100회 단위 돌파 시 화면 전체 배경 대형 폭죽 발사 - GamePage 한정 다크모드 토글(SVG 슬라이딩 스위치) 추가 - DotTextEffect dotColor prop화, 배경 blob 그라데이션 제거
- PAGE_VIEW.GAME_PAGE, USER_EVENT.GAME_START_BUTTON_CLICKED 추가 - GamePage 진입 시 useTrackPageView 적용 - ClubNameInput 동아리명 검증 성공 시 게임 시작 이벤트 전송
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)
frontend/src/pages/GamePage/GamePage.tsx (1)
191-200:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win모바일 순위표에 다크모드 prop 전달이 누락되었습니다.
Line 197-200의
RankingBoard는isDark를 받지 않아 모바일에서는Title/EmptyMessage가 항상 라이트 모드 색상으로 렌더링됩니다. 데스크톱(Line 144-148)과 동작이 불일치합니다.🔧 제안 수정안
<RankingBoard ranking={rankingData?.clubs ?? []} myClubName={clubName} + isDark={isDark} />🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/pages/GamePage/GamePage.tsx` around lines 191 - 200, The RankingBoard component in the mobile view (inside the S.MobileOnly section at lines 197-200) is missing the isDark prop that is present in the desktop version (lines 144-148). Add the isDark prop to the mobile RankingBoard component to ensure consistent dark mode styling between desktop and mobile views. The isDark value should be sourced from the same state or props that is being used for the desktop RankingBoard component.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx`:
- Around line 93-95: The setTimeout callback that filters bursts (line 93-95) is
not being cleaned up when the component unmounts, which can cause the callback
to execute after unmounting and trigger state updates on an unmounted component.
Store the timeout ID returned by setTimeout in a useRef, then add a useEffect
hook with a cleanup function that calls clearTimeout with that ref to ensure the
timer is cancelled when the component unmounts. Apply this same pattern to all
other setTimeout or async operations in the component that modify state.
- Around line 105-124: Move the inline style object from the motion.button
component (lines 105-124) into a new styled-component in ClickButton.styles.ts
following the styled-components pattern. Create a styled motion.button component
that contains all the CSS properties currently defined inline (width, height,
borderRadius, border, background, color, fontSize, fontWeight, cursor,
boxShadow, userSelect, position, zIndex), then replace the motion.button in
ClickButton.tsx with this new styled component while preserving the onClick
handler and motion attributes (whileTap, whileHover, transition). This
consolidation will enable responsive style rules to be applied consistently and
align with the project's styled-components coding guidelines.
In `@frontend/src/pages/GamePage/hooks/useBatchedClick.ts`:
- Around line 20-24: The click count in pendingRef.current is being reset to 0
before the clickGame request is sent on line 22-23, so if the request fails, the
count is lost and cannot be recovered. Move the pendingRef.current reset to only
happen after the clickGame request succeeds. Wrap the clickGame call to handle
failures by restoring pendingRef.current back to the original count value, and
consider scheduling a retry of the failed batch.
---
Outside diff comments:
In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Around line 191-200: The RankingBoard component in the mobile view (inside the
S.MobileOnly section at lines 197-200) is missing the isDark prop that is
present in the desktop version (lines 144-148). Add the isDark prop to the
mobile RankingBoard component to ensure consistent dark mode styling between
desktop and mobile views. The isDark value should be sourced from the same state
or props that is being used for the desktop RankingBoard component.
🪄 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: 3470479b-1c6f-481f-8335-ab5c3c83c031
📒 Files selected for processing (10)
frontend/docs/features/game/game-effects-darkmode.mdfrontend/src/pages/GamePage/GamePage.styles.tsfrontend/src/pages/GamePage/GamePage.tsxfrontend/src/pages/GamePage/components/BackgroundFirework/BackgroundFirework.tsxfrontend/src/pages/GamePage/components/ClickButton/ClickButton.styles.tsfrontend/src/pages/GamePage/components/ClickButton/ClickButton.tsxfrontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsxfrontend/src/pages/GamePage/components/RankingBoard/RankingBoard.styles.tsfrontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsxfrontend/src/pages/GamePage/hooks/useBatchedClick.ts
- ClubNameInput 다크모드 미적용 보강 (제목/입력창/드롭다운) - RankingBoard 순위 카드 다크 톤 전환 (내 동아리 카드는 주황 강조 유지) - 라이트모드에서 페이지 배경과 동일색이던 일반 카드를 흰색+보더+그림자로 분리
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx (1)
58-76:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win중복 제출 방지 가드가 필요합니다.
Line 58에서
isValidating재진입 방어가 없어 Enter 연타 시 검증/이벤트/시작 로직이 중복 실행될 수 있습니다. 버튼disabled만으로는 키보드 경로를 막지 못합니다.수정 예시
const handleSubmit = async () => { + if (isValidating) return; const trimmed = value.trim(); if (!trimmed) return; setIsValidating(true); try {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx` around lines 58 - 76, The handleSubmit function lacks a guard against duplicate submissions when users rapidly press Enter, allowing validation and onStart logic to execute multiple times concurrently. Add an early return guard at the beginning of the handleSubmit function that checks if isValidating is already true, returning immediately before any validation or event tracking occurs. This prevents re-entrance even when triggered via keyboard input that bypasses the disabled button state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx`:
- Around line 86-104: The timersRef.current array accumulates timer IDs
indefinitely because timers are only pushed to the array but never removed after
they execute. In the handleClick function, after the setTimeout callback
completes and updates the bursts state, the timer ID should be removed from
timersRef.current to prevent unbounded memory growth. Modify the setTimeout
callback to also remove the timer ID from the timersRef array once it has
executed, ensuring completed timers do not persist in memory during long gaming
sessions.
---
Outside diff comments:
In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx`:
- Around line 58-76: The handleSubmit function lacks a guard against duplicate
submissions when users rapidly press Enter, allowing validation and onStart
logic to execute multiple times concurrently. Add an early return guard at the
beginning of the handleSubmit function that checks if isValidating is already
true, returning immediately before any validation or event tracking occurs. This
prevents re-entrance even when triggered via keyboard input that bypasses the
disabled button state.
🪄 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: 94928a40-65bb-453f-a00b-df99195222c8
📒 Files selected for processing (10)
frontend/docs/features/game/game-effects-darkmode.mdfrontend/docs/features/game/game-tracking.mdfrontend/src/constants/eventName.tsfrontend/src/pages/GamePage/GamePage.tsxfrontend/src/pages/GamePage/components/ClickButton/ClickButton.tsxfrontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.tsfrontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsxfrontend/src/pages/GamePage/components/RankingBoard/RankingBoard.styles.tsfrontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsxfrontend/src/pages/GamePage/hooks/useBatchedClick.ts
✅ Files skipped from review due to trivial changes (2)
- frontend/docs/features/game/game-tracking.md
- frontend/docs/features/game/game-effects-darkmode.md
🚧 Files skipped from review as they are similar to previous changes (2)
- frontend/src/pages/GamePage/components/RankingBoard/RankingBoard.tsx
- frontend/src/pages/GamePage/GamePage.tsx
배경
동아리 클릭 게임(GamePage) 관련 작업을 모았습니다. 백엔드의 랭킹 자동 초기화 제거에 따른 안내 문구 정리와 함께, 게임 페이지의 인터랙션/반응형/테마를 개선했습니다.
변경 사항
1. resetAt 기반 랭킹 초기화 안내 제거 (refactor, MOA-977)
백엔드에서 랭킹 매주 월요일 자정 자동 초기화가 제거되고 관리자 수동 초기화로 변경됨.
resetAt는 더 이상 '실제 초기화 예정 시각'이 아니라 무의미한 표시값이 되어 안내 문구를 제거.RankingBoard.tsx—매일 ○○ 초기화렌더 +resetTime계산 제거, 미사용resetAtprop 제거GamePage.tsx— orphan 된resetAtprop 2곳 제거RankingBoard.styles.ts— orphan 된ResetInfostyled 제거types/game.ts의resetAt필드는 API 응답 스펙 유지 위해 보존2. 클릭 배칭 로직 훅 분리 (refactor)
GamePage.tsx에 인라인으로 있던 클릭 배칭(5회 또는 500ms 디바운스) 로직을useBatchedClick훅으로 분리3. 폭죽 이펙트 및 다크모드 토글 (feat)
tablet→laptop, 순위표 중앙 정렬), 배경 blob 그라데이션 제거BackgroundFirework, 신규)sessionStorage영속화.DotTextEffect는dotColorprop화검증
npm run typecheck통과npm run format적용Resolves MOA-977 (#1681)
Summary by CodeRabbit
릴리스 노트
New Features
Bug Fixes
Documentation
Style