Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions frontend/docs/features/game/game-effects-darkmode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# GamePage 폭죽 이펙트 및 다크모드 토글

## 반응형 레이아웃 수정

순위표(`RankingBoard`)는 데스크탑에서 `position: absolute`로 우측 상단에 떠 있다.
전환 기준이 `media.tablet`(≤700px)이던 탓에 700~1280px 구간에서 중앙 정렬된 타이틀과 겹쳤다.

- `DesktopOnly`/`MobileOnly` 전환 기준을 `media.laptop`(≤1280px)로 변경 → 태블릿·랩탑 폭에서는 순위표가 하단으로 내려감
- `RankingBoard.Wrapper`에 `margin: 0 auto`를 추가해 좁은 폭에서 중앙 정렬
- 배경 blob 그라데이션(`BLOBS`, `Blob`)은 제거해 배경을 단순화

## 클릭 폭죽 이펙트

클릭 버튼을 누를 때마다 버튼 중심에서 파티클이 방사형으로 터진다.

- 매 클릭마다 `bursts` state에 id를 추가하고 일정 시간 후 자동 제거
- 파티클은 원형 빛 입자 + 회전하는 색종이(confetti) 혼합, `box-shadow`로 글로우
- 포물선 궤적(중력) + `rotate`로 떨어지는 느낌

## 100회 단위 배경 폭죽

랭킹의 **어떤 동아리든** 누적 클릭수가 100단위를 넘기면 화면 전체에 대형 폭죽을 발사한다.

- 2초 폴링마다 직전 카운트와 비교: `floor(cur/100) > floor(prev/100)`이면 트리거
- 첫 로드 시에는 베이스라인만 기록(기존 누적값으로 오발 방지)
- `BackgroundFirework`는 `position: fixed`로 화면 전체를 덮고 `zIndex: 0`으로 콘텐츠 뒤에 깔림
- 폴링 간격 사이의 다중/동시 돌파는 1회 발사로 합쳐짐(의도된 단순화)

## 다크모드 토글

GamePage 한정 다크모드. 전역 테마 오버라이드 대신 `$dark` prop을 내려 색을 반전한다.

- 토글 상태는 `sessionStorage`(`game_dark_mode`)에 영속화
- 배경 위에 직접 올라가는 요소를 반전: 배경, 타이틀, 설명, dot(`dotColor` prop), 랭킹 제목/EmptyMessage, 클럽 라벨/카운트 라벨
- `ClubNameInput`(제목/입력창/드롭다운)과 `RankingBoard` 순위 카드도 다크 서피스로 전환 — 내 동아리 카드만 주황 틴트+보더 유지
- 토글 UI는 인라인 SVG 해/달 아이콘 + 슬라이딩 knob 스위치(`translateX`)

### 라이트모드 리더보드 카드 구분

페이지 배경과 일반 순위 카드 배경이 둘 다 `gray[100]`(#F5F5F5)로 동일해 카드가 배경에 묻혔다.
→ 일반 카드를 흰색(`#FFFFFF`) + `gray[300]` 보더 + 옅은 그림자로 분리. 내 동아리 카드는 기존 핑크+주황 보더 유지.

## 관련 코드

- `src/pages/GamePage/GamePage.tsx` — 다크모드 state·토글, SVG 아이콘, 100회 배경 폭죽 트리거, blob 제거
- `src/pages/GamePage/GamePage.styles.ts` — `$dark` 스타일, 토글 스위치, 반응형 기준 변경, blob 제거
- `src/pages/GamePage/components/BackgroundFirework/BackgroundFirework.tsx` — 화면 전체 대형 폭죽 (신규)
- `src/pages/GamePage/components/ClickButton/` — 클릭 폭죽 파티클, `isDark` prop
- `src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx` — `dotColor` prop화
- `src/pages/GamePage/components/RankingBoard/` — `margin: 0 auto`, `$dark` prop
26 changes: 26 additions & 0 deletions frontend/docs/features/game/game-tracking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# GamePage 트래킹 추가

릴리즈 전 점검 결과 GamePage에는 페이지뷰를 포함해 트래킹이 전혀 없었다. 다른 페이지와 동일한 패턴으로 최소한의 트래킹을 추가했다.

## 페이지뷰

다른 페이지들(`PromotionListPage`, `BuskingPage` 등)과 동일하게 `useTrackPageView`로 진입·체류시간을 추적한다.

- `PAGE_VIEW.GAME_PAGE` 추가
- `GamePage.tsx`에서 `useTrackPageView(PAGE_VIEW.GAME_PAGE)` 호출

## 게임 시작 버튼

`ClubNameInput`에서 동아리명 검증(`validateClubName`)이 **성공한 경우에만** 이벤트를 보낸다. 존재하지 않는 동아리를 입력한 실패 케이스는 노이즈라 제외했다.

- `USER_EVENT.GAME_START_BUTTON_CLICKED` 추가, `clubName`을 함께 전송해 어떤 동아리로 유입됐는지 추적

## 제외한 항목

- 클릭 버튼(`ClickButton`)과 추천 동아리 목록 선택(`handleSelect`)은 의도적으로 트래킹하지 않음 (클릭 버튼은 과도한 이벤트 발생 우려로 제외 요청)

## 관련 코드

- `src/constants/eventName.ts` — `PAGE_VIEW.GAME_PAGE`, `USER_EVENT.GAME_START_BUTTON_CLICKED`
- `src/pages/GamePage/GamePage.tsx` — 페이지뷰 트래킹
- `src/pages/GamePage/components/ClubNameInput/ClubNameInput.tsx` — 게임 시작 버튼 트래킹
4 changes: 4 additions & 0 deletions frontend/src/constants/eventName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export const USER_EVENT = {
PROMOTION_CLUB_CTA_CLICKED: 'Promotion Club CTA Clicked',

WEBVIEW_SUBSCRIBE_TOGGLED: 'Webview Subscribe Toggled',

// 클릭배틀 게임
GAME_START_BUTTON_CLICKED: 'Game Start Button Clicked',
} as const;

export const WEBVIEW_LINK_TARGET = {
Expand Down Expand Up @@ -135,6 +138,7 @@ export const PAGE_VIEW = {
DAEDONG2026_BUSKING_PAGE: '2026 대동제 버스킹 시간표 페이지',
PROMOTION_LIST_PAGE: '홍보 목록 페이지',
PROMOTION_DETAIL_PAGE: '홍보 상세 페이지',
GAME_PAGE: 'GamePage',

WEBVIEW_MAIN_PAGE: 'WebviewMainPage',

Expand Down
75 changes: 52 additions & 23 deletions frontend/src/pages/GamePage/GamePage.styles.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,65 @@
import styled from 'styled-components';
import { media } from '@/styles/mediaQuery';

export const PageContainer = styled.div`
export const PageContainer = styled.div<{ $dark: boolean }>`
position: relative;
overflow: hidden;
min-height: 100vh;
background: ${({ theme }) => theme.colors.gray[100]};
background: ${({ $dark, theme }) =>
$dark ? '#111111' : theme.colors.gray[100]};
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 20px 80px;
transition: background 0.3s;

${media.mobile} {
padding: 32px 16px 60px;
}
`;

export const Blob = styled.div<{
$size: number;
$top: string;
$left: string;
$color: string;
}>`
export const ToggleBar = styled.div`
display: flex;
justify-content: flex-end;
width: 100%;
margin-bottom: 12px;
`;

export const ToggleSwitch = styled.button<{ $dark: boolean }>`
position: relative;
width: 60px;
height: 32px;
flex-shrink: 0;
padding: 0;
border-radius: 999px;
border: 1px solid
${({ $dark, theme }) => ($dark ? '#3A3A4A' : theme.colors.gray[300])};
background: ${({ $dark, theme }) =>
$dark ? '#262633' : theme.colors.gray[200]};
cursor: pointer;
transition:
background 0.25s,
border-color 0.25s;
`;

export const ToggleKnob = styled.span<{ $dark: boolean }>`
position: absolute;
width: ${({ $size }) => $size}px;
height: ${({ $size }) => $size}px;
top: ${({ $top }) => $top};
left: ${({ $left }) => $left};
background: ${({ $color }) => $color};
top: 3px;
left: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 50%;
filter: blur(60px);
pointer-events: none;
z-index: 0;
background: ${({ $dark }) => ($dark ? '#111111' : '#FFFFFF')};
color: ${({ $dark }) => ($dark ? '#FFD432' : '#FF9500')};
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
transform: translateX(${({ $dark }) => ($dark ? '28px' : '0')});
transition:
transform 0.25s ease,
background 0.25s,
color 0.25s;
`;

export const Content = styled.div`
Expand All @@ -44,20 +72,21 @@ export const Content = styled.div`
max-width: 1400px;
`;

export const PageTitle = styled.h1`
export const PageTitle = styled.h1<{ $dark: boolean }>`
font-size: 1.75rem;
font-weight: 800;
color: ${({ theme }) => theme.colors.gray[900]};
color: ${({ $dark, theme }) => ($dark ? '#FFFFFF' : theme.colors.gray[900])};
margin-bottom: 4px;

${media.mobile} {
font-size: 1.4rem;
}
`;

export const PageDescription = styled.p`
export const PageDescription = styled.p<{ $dark: boolean }>`
font-size: 0.95rem;
color: ${({ theme }) => theme.colors.gray[600]};
color: ${({ $dark, theme }) =>
$dark ? theme.colors.gray[500] : theme.colors.gray[600]};
margin-top: 4px;
`;

Expand All @@ -66,17 +95,17 @@ export const DesktopOnly = styled.div`
right: 0;
top: 0;

${media.tablet} {
${media.laptop} {
display: none;
}
`;

export const MobileOnly = styled.div`
display: none;
width: 100%;
margin-top: 32px;
margin-top: 64px;

${media.tablet} {
${media.laptop} {
display: block;
}
`;
Expand Down
Loading
Loading