Skip to content

Develop/fe#1506

Merged
seongwon030 merged 198 commits into
mainfrom
develop-fe
May 3, 2026
Merged

Develop/fe#1506
seongwon030 merged 198 commits into
mainfrom
develop-fe

Conversation

@suhyun113
Copy link
Copy Markdown
Collaborator

@suhyun113 suhyun113 commented May 3, 2026

🚀 릴리즈 PR

📦 버전 정보

항목 내용
서비스 💾 BE / 💻 FE
Bump 타입 🚨 MAJOR / ➕ MINOR / 🔧 PATCH
예상 버전 vX.Y.Z

⚠️ 반드시 라벨을 지정해주세요: 서비스 라벨(💾 BE, 💻 FE)과 버전 라벨(🚨 MAJOR, ➕ MINOR, 🔧 PATCH)이 없으면 태그가 생성되지 않습니다.

📖 버전 라벨 선택 가이드 (Semantic Versioning)
라벨 버전 변화 선택 기준 예시
🚨 MAJOR v1.0.0v2.0.0 기존 API/기능이 호환되지 않는 변경 API 엔드포인트 삭제/변경, 요청/응답 스펙 변경, DB 스키마 대규모 변경
➕ MINOR v1.0.0v1.1.0 기존 기능은 유지하면서 새 기능 추가 새 API 엔드포인트 추가, 새 기능 도입, 기존 API에 선택적 필드 추가
🔧 PATCH v1.0.0v1.0.1 기능 변경 없이 버그 수정/내부 개선 버그 수정, 성능 개선, 리팩토링, 문서 수정

📋 포함된 변경사항

이번 릴리즈에 포함된 주요 변경사항을 요약합니다.

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능

    • 게임 페이지(클릭 게임 및 실시간 순위), 대화형 지도 뷰·지도 모달, 웹뷰 전용 메인 페이지, 웹뷰 구독 토글/동기화 기능 추가
  • 개선 사항

    • 모달/포털 컴포넌트 통합 및 재설계, 헤더 가시성 제어 개선, 버튼/탭/스타일 전반 개선, 클럽 상세에 지도·일정 노출 개선
  • 문서

    • 아키텍처·API·컨벤션·테스트·실험·디자인시스템 등 광범위한 문서 추가/갱신
  • 기타

    • 버전관리(ignore) 규칙 업데이트

suhyun113 and others added 30 commits March 27, 2026 01:58
- Google Calendar 및 Notion 이벤트를 통합 형식으로 변환하는 함수 추가
- formatDateOnly 함수에 대한 테스트 케이스 작성
- 한국어 로케일에 맞춘 날짜 포맷팅 테스트 케이스 추가
- 코드 블록 언어 태그를 'text'로 변경하여 가독성 향상
…-784

[docs] 클로드 mixpanel 커맨드 추가
- ButtonHTMLAttributes 확장으로 type, aria-*, data-* 등 자동 지원
- 하드코딩 색상을 테마 시스템(theme.colors)으로 교체
- transient props($animated, $width) 적용으로 DOM 전달 경고 제거
모바일 메뉴 닫기 Mixpanel 트래킹을 Header에서 useHeaderNavigation으로 이관
- DeviceType을 src/types/device.ts로 공통화
- useHeaderVisibility 훅으로 showOn/hideOn 판단 로직 분리
- Header.tsx에서 useDevice, isInAppWebView, shouldRender 제거
showOn/hideOn/webview/우선순위 등 10개 케이스 커버
폼 내부에서 의도치 않은 submit 방지
네비 링크 클릭이 아닌 실제 메뉴 버튼 닫기 시에만 이벤트가 발동되도록 수정.
setIsMenuOpen updater 내 prev 기반으로 열림→닫힘 전환 시에만 handleMenuClose 호출.
hideOn 존재 여부 대신 length로 체크해 hideOn=[]일 때 showOn이 정상 평가되도록 수정.
경계 조건 테스트 3개 추가.
- Add 5 OMC session JSON files recording session end reasons
- Add HUD state and stdin cache snapshots
- Add frontend/.claude hooks (stop.sh, post-compact.sh) for auto-commit and context re-injection
- Add frontend/.claude/settings.json with PostToolUse, Stop, Notification, PostCompact hooks
- Add frontend/.omc project memory and agent replay logs
@vercel
Copy link
Copy Markdown

vercel Bot commented May 3, 2026

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

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment May 3, 2026 9:45am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Warning

Rate limit exceeded

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

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ 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: 7488ca65-64a7-4b22-90e6-4d61a1a78fe9

📥 Commits

Reviewing files that changed from the base of the PR and between e61bce3 and 9cf9669.

📒 Files selected for processing (5)
  • frontend/src/hooks/useNavigator.test.ts
  • frontend/src/hooks/useNavigator.ts
  • frontend/src/pages/MainPage/components/Banner/Banner.tsx
  • frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx
  • frontend/src/pages/PromotionPage/components/list/PromotionCard/PromotionCard.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

지도(네이버맵) 통합, 게임 페이지(클릭 배치·실시간 순위·DotTextEffect), 웹뷰 브리지/웹뷰 전용 페이지, Modal/Portal 리팩토링, 헤더 가시성 훅·라우팅 재구성, 여러 UI 컴포넌트·타입·쿼리 훅 추가 및 문서·설정 파일 대거 추가/수정을 포함한 기능 확장 및 리팩토링.

Changes

지도 기능

Layer / File(s) Summary
타입 및 상수 정의
frontend/src/constants/clubLocation.ts, frontend/src/types/window.d.ts
ClubLocation 인터페이스와 clubLocations 정적 배열(495개), window.naver/window.navermap_authFailure 전역 선언 추가.
스크립트 로더 & DEV 초기화
frontend/src/utils/loadNaverMapScript.ts, frontend/src/index.tsx
네이버 맵 OpenAPI 스크립트 비동기 로더 추가 및 DEV 전용 navermap_authFailure 핸들러 등록.
맵 훅
frontend/src/hooks/Map/useNaverMap.ts, frontend/src/hooks/Map/useMapZoom.ts
useNaverMap으로 맵 초기화·마커 생성·cleanup, useMapZoom으로 zoomIn/zoomOut 제공 및 NaverMapInstance 타입 추가.
맵 컴포넌트
frontend/src/components/map/*, frontend/src/pages/ClubDetailPage/*
NaverMap, InteractiveMapView, MapClubInfoCard, MapModal, MapZoomControls 등 UI·스타일 컴포넌트 추가 및 ClubDetailPage에 맵 미리보기·MapModal 통합.

게임 기능

Layer / File(s) Summary
타입·쿼리키·API
frontend/src/types/game.ts, frontend/src/constants/queryKeys.ts, frontend/src/apis/game.ts
게임 타입(GameRankingEntry/GameRankingResponse), queryKeys.game, postGameClick/getGameRanking API 추가.

frontend/src/hooks/Queries/useGame.ts, frontend/src/hooks/Queries/useClub.ts
useGameRanking(폴링), useClickGame(뮤테이션), useClubSuggestions, useValidateClubName 훅 추가.
페이지·컴포넌트·스타일
frontend/src/pages/GamePage/*, frontend/src/pages/GamePage/components/*
GamePage 구현(클릭 배치·debounce·flush·unmount flush), DotTextEffect, ClickButton, ClubNameInput, RankingBoard 및 관련 스타일 추가.

웹뷰 지원

Layer / File(s) Summary
브리지 타입·요청 헬퍼
frontend/src/utils/webviewBridge.ts
웹→앱/앱→웹 메시지 타입 확장(구독 토글/상태, 웹뷰 네비게이션, 외부 URL 열기)과 requestSubscribeToggle/requestSubscribeState/requestNavigateWebview/requestOpenExternalUrl 헬퍼 추가; 외부 URL 프로토콜 검증 포함.
구독 훅·페이지·스타일
frontend/src/hooks/useWebviewSubscribe.ts, frontend/src/pages/WebviewMainPage/*, frontend/src/styles/WebviewGlobal.styles.ts
구독 상태 동기화 훅, WebviewMainPage·구독 버튼·웹뷰 전용 글로벌 스타일과 라우트(frontend/src/routes/webviewRoutes.tsx) 추가/통합.
웹뷰 관련 UI 변경
frontend/src/pages/MainPage/components/Banner/*, frontend/src/pages/PromotionPage/*, frontend/src/components/common/Filter/*
배너·프로모션 CTA·필터 동작을 웹뷰 분기와 연동(네비게이션 브리지 사용)하도록 수정.

모달 및 포털 리팩토링

Layer / File(s) Summary
Portal/Modal 구현
frontend/src/components/common/Portal/Portal.tsx, frontend/src/components/common/Modal/Modal.tsx
범용 Portal과 새 Modal 구현(Portal 사용, body scroll lock, Escape 처리, backdrop click 제어).
삭제·마이그레이션
frontend/src/components/common/Modal/PortalModal.tsx, frontend/src/.../*Modal
기존 PortalModal 제거 및 여러 모달 사용처(사진/관리자 개인정보 모달/지원 선택 모달 등)를 Modal로 마이그레이션.
스토리북 업데이트
frontend/src/components/common/Modal/Modal.stories.tsx
스토리 메타를 Modal로 전환.

헤더·네비게이션 개선

Layer / File(s) Summary
DeviceType·훅
frontend/src/types/device.ts, frontend/src/hooks/Header/useHeaderVisibility.ts, frontend/src/hooks/Header/useHeaderVisibility.test.ts
DeviceType(webview 포함) 추가, useHeaderVisibility(showOn, hideOn) 훅 및 단위 테스트 추가(우선순위: hideOn).
헤더 네비게이션 훅·컴포넌트
frontend/src/hooks/Header/useHeaderNavigation.ts, frontend/src/components/common/Header/Header.tsx
handleMenuClose 추적 추가, Header는 useHeaderVisibility 사용으로 렌더 조건 변경 및 메뉴 토글 로직 업데이트.
네비게이터 외부 URL
frontend/src/hooks/useNavigator.ts
웹뷰 환경에서 requestOpenExternalUrl 사용하고 실패 시 window.open 폴백.

라우팅·앱 구조

Layer / File(s) Summary
AppRoutes 분리
frontend/src/routes/AppRoutes.tsx
탑레벨 라우팅을 AppRoutes로 정의(메인, 클럽 상세·맵, 프로모션, 게임, 관리자, 웹뷰 서브루트 포함, DEV 전용 route 포함).
App 컴포넌트
frontend/src/App.tsx
인라인 <Routes> 제거, <AppRoutes /> 사용으로 라우팅 분리.
웹뷰 서브루트
frontend/src/routes/webviewRoutes.tsx
/webview 하위 라우트 집합 추가.

UI·타이포그래피·스타일 변화

Layer / File(s) Summary
공통 컴포넌트 수정
frontend/src/components/common/Button/Button.tsx, SearchField, UnderlineTabs, Filter
Button props를 HTMLAttributes로 확장해 포워딩, 반응형 SearchBox, UnderlineTabs className prop, Filter의 webview 옵션·스타일 조정 등.
클럽 상세 UI 업데이트
frontend/src/pages/ClubDetailPage/*
좌측 섹션·MapCard·MapDetailText 등 스타일 추가, Schedule 탭 항상 노출, 빈 상태 문구·프로필 카드에 위치·onMapClick prop 추가.
타이포그래피 헬퍼
frontend/src/styles/theme/typography.ts
title1~title6 값 조정 및 setTypography 유틸 추가; 여러 컴포넌트에서 사용으로 스타일 통합.

훅·쿼리·테스트

Layer / File(s) Summary
쿼리 훅
frontend/src/hooks/Queries/useClub.ts, frontend/src/hooks/Queries/useGame.ts
클럽 자동완성/검증 훅(useClubSuggestions, useValidateClubName)과 게임 관련 쿼리·뮤테이션 훅 추가.
헤더 훅 테스트
frontend/src/hooks/Header/useHeaderVisibility.test.ts
다양한 디바이스/웹뷰 조건을 모킹한 단위 테스트 추가.
테스트 보강
frontend/src/utils/calendarSyncUtils.test.ts
날짜 포맷 유닛 및 이벤트 변환 통합 테스트 보강.

실험·Mixpanel

Layer / File(s) Summary
실험 Mixpanel 등록
frontend/src/experiments/ExperimentRepository.ts
실험 배정 시 mixpanel.register() 호출로 super property 등록(기존 재사용 케이스도 등록).
이벤트 상수 추가
frontend/src/constants/eventName.ts
PROMOTION_CLUB_CTA_CLICKED, WEBVIEW_SUBSCRIBE_TOGGLED, WEBVIEW_MAIN_PAGE 상수 추가.

문서·설정

Layer / File(s) Summary
문서 추가/분리
frontend/docs/claude/*, frontend/.claude/*, docs/weekly-reports/*, frontend/docs/features/*
API/아키텍처/컨벤션/테스트/컴포넌트/게임·실험 문서 등 다수 추가.
gitignore / eslint
.gitignore, frontend/.gitignore, frontend/eslint.config.mjs
.omc, .gstack/, .claude/, AGENTS.md, storybook-static/** 등 무시 패턴 추가.
Design System spec
frontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.md
토큰 파이프라인·ESLint 룰·CI 워크플로우 설계 문서 추가.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Possibly related PRs

Suggested reviewers

  • lepitaaar
  • oesnuj
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop-fe

@suhyun113 suhyun113 added 💻 FE Frontend 📈 release 릴리즈 배포 labels May 3, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

🎨 UI 변경사항을 확인해주세요

변경된 스토리를 Chromatic에서 확인해주세요.

구분 링크
🔍 변경사항 리뷰 https://www.chromatic.com/build?appId=67904e61c16daa99a63b44a7&number=285
📖 Storybook https://67904e61c16daa99a63b44a7-qjrugngiml.chromatic.com/

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

@suhyun113 suhyun113 added the ➕ MINOR Minor 릴리즈 label May 3, 2026
Copy link
Copy Markdown
Member

@oesnuj oesnuj 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

@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: 20

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
frontend/src/experiments/ExperimentRepository.ts (1)

8-8: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

ASSIGNMENT_STORAGE_KEYstorageKeys.ts로 이동하세요.

localStorage 키('moadong_experiments')가 파일 로컬 상수로 선언되어 있습니다. resetAssignments()(Line 102)에서도 동일 키를 참조하므로, 중앙 관리가 되지 않으면 나중에 키 변경 시 누락 위험이 있습니다.

♻️ 제안된 리팩토링

src/constants/storageKeys.ts 에 추가:

+  EXPERIMENT_ASSIGNMENTS: 'moadong_experiments',

ExperimentRepository.ts:

-import mixpanel from 'mixpanel-browser';
+import mixpanel from 'mixpanel-browser';
+import { STORAGE_KEYS } from '@/constants/storageKeys';
 
-const ASSIGNMENT_STORAGE_KEY = 'moadong_experiments';
+const ASSIGNMENT_STORAGE_KEY = STORAGE_KEYS.EXPERIMENT_ASSIGNMENTS;

As per coding guidelines: "localStorage keys must be centrally managed in src/constants/storageKeys.ts".

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

In `@frontend/src/experiments/ExperimentRepository.ts` at line 8, Move the
localStorage key constant ASSIGNMENT_STORAGE_KEY out of ExperimentRepository.ts
into the centralized src/constants/storageKeys.ts and update all references
(including in resetAssignments() and any other usages within the
ExperimentRepository class/module) to import and use the exported key from
storageKeys.ts; ensure the exported name matches the original (or update uses
accordingly) so the repository reads/writes the same 'moadong_experiments' key
via the central constant.
frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx (1)

289-300: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

비-웹뷰 환경에서 target="_blank" 동작이 깨집니다

e.preventDefault()가 항상 호출되면서 브라우저가 href를 따르지 않습니다. 웹뷰가 아닌 일반 브라우저 사용자 입장에서 "일정 상세 보기" 링크가 기존 새 탭 대신 같은 탭으로 이동하게 되는 UX 퇴행입니다. hreftarget="_blank" 속성은 현재 사실상 무효화된 상태입니다.

isInAppWebView() 체크로 웹뷰일 때만 기본 동작을 막고 브릿지에 위임하고, 브라우저에서는 앵커 기본 동작을 그대로 유지해야 합니다.

+ import isInAppWebView from '@/utils/isInAppWebView';
🐛 수정 제안 — 웹뷰에서만 preventDefault 적용
                      onClick={(e) => {
-                       e.preventDefault();
-                       handleLink(event.url!);
+                       if (isInAppWebView()) {
+                         e.preventDefault();
+                         handleLink(event.url!);
+                       }
                      }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx`
around lines 289 - 300, The onClick handler currently always calls
e.preventDefault() which disables normal anchor behavior; change the onClick on
Styled.EventLink to only intercept clicks in webviews by checking
isInAppWebView(): inside the onClick, if (isInAppWebView()) {
e.preventDefault(); handleLink(event.url!); } else do nothing so the browser
follows href and target="_blank" normally; reference the onClick on
Styled.EventLink, handleLink, and isInAppWebView() when applying the change.
🧹 Nitpick comments (16)
.gitignore (1)

10-10: 💤 Low value

디렉토리 패턴에 후행 슬래시 추가 권장.

/.omc가 디렉토리를 무시하려는 의도라면 /.omc/로 명시하는 것이 더 명확합니다. 현재 frontend/.gitignore에서는 .omc/로 후행 슬래시를 사용하고 있어 일관성을 위해 루트에서도 동일한 패턴을 사용하는 것을 권장합니다.

♻️ 제안하는 수정
-/.omc
+/.omc/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.gitignore at line 10, Update the .gitignore entry '/.omc' to explicitly
target the directory by adding a trailing slash: change '/.omc' to '/.omc/' so
it consistently ignores the directory (matching the frontend/.gitignore pattern)
and makes the intent clear; edit the '/.omc' line in the .gitignore file to
'/.omc/'.
frontend/src/components/common/Portal/Portal.tsx (1)

1-15: ⚡ Quick win

코딩 가이드라인에 따라 Storybook 스토리 파일이 필요합니다.

src/components/의 공통 UI 컴포넌트에는 Storybook 스토리를 작성해야 합니다. Portal.stories.tsx를 같은 폴더에 추가해 주세요.

As per coding guidelines, "Storybook stories must be created for shared UI components in src/components/".

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

In `@frontend/src/components/common/Portal/Portal.tsx` around lines 1 - 15, Add a
Storybook story file named Portal.stories.tsx alongside the Portal component
that exports a default meta object and one or two stories demonstrating the
Portal component rendering children into a target root (use the Portal component
symbol) and showing the rootId prop variation; include setup to create a DOM
element with id 'modal-root' in the story's play or decorator so the portal can
mount, import Portal from './Portal', and ensure story names and args cover
default usage and a custom rootId case.
frontend/src/constants/eventName.ts (1)

67-74: 💤 Low value

WEBVIEW_LINK_TARGET를 별도 상수 파일로 분리하는 것을 고려하세요.

WEBVIEW_LINK_TARGET은 Mixpanel 이벤트명이 아닌 웹뷰 내비게이션의 링크 타겟 식별자입니다. eventName.ts에 혼재하면 파일의 단일 책임이 희석됩니다. src/constants/webviewConfig.ts 또는 유사한 파일로 이동을 고려해볼 수 있습니다.

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

In `@frontend/src/constants/eventName.ts` around lines 67 - 74, The
WEBVIEW_LINK_TARGET constant is not an event name and should be moved out of
eventName.ts to preserve single responsibility; extract WEBVIEW_LINK_TARGET (and
its members like CLUB_FESTIVAL) into a new constants file such as
webviewConfig.ts (or similar), update imports/usages throughout the codebase to
import from the new module, and ensure eventName.ts only contains Mixpanel event
constants like PROMOTION_CLUB_CTA_CLICKED and WEBVIEW_SUBSCRIBE_TOGGLED.
frontend/src/pages/GamePage/components/ClickButton/ClickButton.styles.ts (1)

26-29: ⚡ Quick win

원시 미디어 쿼리 대신 media.mobile 유틸리티를 사용해야 합니다.

ClubLabelButton 모두 @media (max-width: 500px)를 직접 사용하고 있습니다. 코딩 가이드라인에 따라 반응형 브레이크포인트는 src/styles/mediaQuery.ts의 유틸리티를 통해 관리해야 합니다.

♻️ 수정 제안
+import { media } from '@/styles/mediaQuery';
 ...
 export const ClubLabel = styled.p`
   ...
-  `@media` (max-width: 500px) {
+  ${media.mobile} {
     max-width: 140px;
     font-size: 0.875rem;
   }
 `;

 export const Button = styled.button<{ $clicking: boolean }>`
   ...
-  `@media` (max-width: 500px) {
+  ${media.mobile} {
     width: 140px;
     height: 140px;
     font-size: 1.2rem;
   }
 `;

As per coding guidelines: "Responsive breakpoints must be defined in src/styles/mediaQuery.ts".

🤖 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.styles.ts`
around lines 26 - 29, Replace the raw "@media (max-width: 500px)" queries in
ClickButton.styles.ts for the ClubLabel and Button components with the project's
media utility (use media.mobile), import the media symbol at the top of the
file, and move the max-width and font-size overrides into the media.mobile block
so the components use the centralized breakpoint helper (update ClubLabel and
Button styles accordingly).
frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx (1)

21-54: ⚡ Quick win

S.Button / S.Count가 데드 코드이며, 반응형 스타일과 테마 색상이 누락됩니다.

ClickButton.styles.ts에서 정의한 S.ButtonS.Count가 이 컴포넌트에서 전혀 사용되지 않습니다. 대신 motion.buttonmotion.span'#FF5414'를 하드코딩한 인라인 스타일을 사용하고 있어 두 가지 문제가 발생합니다.

  1. 반응형 스타일 누락: S.Button에 정의된 모바일(500px 이하) 140×140px 크기가 적용되지 않아 모바일에서 버튼이 항상 180×180px로 렌더링됩니다.
  2. 테마 색상 우회: theme.colors.primary[900] 대신 '#FF5414'가 직접 사용되어 테마 변경 시 이 컴포넌트만 업데이트되지 않습니다.

motion.create()를 사용하면 styled-component를 motion 컴포넌트로 래핑할 수 있습니다. ClickButton.styles.ts에서 styled(motion.button)으로 정의하고 S.Button / S.Count를 그대로 사용하는 방식을 권장합니다.

♻️ 수정 제안 (styles 파일)
-import styled, { keyframes } from 'styled-components';
+import styled from 'styled-components';
+import { motion } from 'framer-motion';
+import { media } from '@/styles/mediaQuery';

-const pop = keyframes`...`;

-export const Button = styled.button<{ $clicking: boolean }>`
+export const Button = styled(motion.button)`
   width: 180px;
   height: 180px;
   border-radius: 50%;
   border: none;
   background: ${({ theme }) => theme.colors.primary[900]};
   color: `#fff`;
   font-size: 1.5rem;
   font-weight: 700;
   cursor: pointer;
   box-shadow: 0 8px 24px rgba(255, 84, 20, 0.35);
   user-select: none;

   &:hover {
     box-shadow: 0 12px 32px rgba(255, 84, 20, 0.45);
   }

-  `@media` (max-width: 500px) {
+  ${media.mobile} {
     width: 140px;
     height: 140px;
     font-size: 1.2rem;
   }
`;

+export const Count = styled(motion.span)`
+  font-size: 2rem;
+  font-weight: 800;
+  color: ${({ theme }) => theme.colors.primary[900]};
+`;
// ClickButton.tsx
-      <motion.button
+      <S.Button
         onClick={handleClick}
         whileTap={{ scale: 0.88 }}
         whileHover={{ scale: 1.06 }}
         transition={{ type: 'spring', stiffness: 400, damping: 15 }}
-        style={{ width: 180, height: 180, ... background: '#FF5414', ... }}
       >
         클릭!
-      </motion.button>
+      </S.Button>
       ...
-          <motion.span
+          <S.Count
             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>

As per coding guidelines: "Use styled-components for styling React components".

🤖 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 21 - 54, The component is using inline hardcoded styles instead of the
styled motion components S.Button and S.Count, so replace the direct
motion.button and motion.span usage with the styled motion-wrapped components
defined in ClickButton.styles.ts (S.Button and S.Count) to restore responsive
sizes and theme colors; if the styles file hasn’t wrapped them for framer-motion
yet, change its exports to use styled(motion.button) and styled(motion.span) (or
motion.custom equivalent) so S.Button and S.Count can be used here, then remove
the hardcoded '#FF5414' and size values and rely on the styles in
S.Button/S.Count and theme.colors.primary[900], while keeping the motion props
(onClick, whileTap, whileHover, transition, AnimatePresence/keys) passed through
to those components.
frontend/src/apis/game.ts (2)

9-9: ⚡ Quick win

API 엔드포인트 경로를 api.ts 상수로 분리해야 합니다.

/api/game/click/api/game/ranking이 인라인 문자열로 하드코딩되어 있습니다. 코딩 가이드라인에 따르면 API 엔드포인트 URL은 반드시 src/constants/api.ts에 정의해야 합니다.

♻️ 제안: api.ts에 game 엔드포인트 상수 추가

src/constants/api.ts에 추가:

+ export const GAME_API = {
+   CLICK: '/api/game/click',
+   RANKING: '/api/game/ranking',
+ } as const;

game.ts에서 사용:

- const response = await fetch(`${API_BASE_URL}/api/game/click`, {
+ const response = await fetch(`${API_BASE_URL}${GAME_API.CLICK}`, {
- const response = await fetch(`${API_BASE_URL}/api/game/ranking`);
+ const response = await fetch(`${API_BASE_URL}${GAME_API.RANKING}`);

As per coding guidelines, "API endpoint URLs must be defined in src/constants/api.ts".

Also applies to: 18-18

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

In `@frontend/src/apis/game.ts` at line 9, Move the hardcoded endpoints
`/api/game/click` and `/api/game/ranking` into constants and reference them from
game API calls: add two named constants (e.g., GAME_CLICK_ENDPOINT and
GAME_RANKING_ENDPOINT) to src/constants/api.ts and export them, then update
frontend/src/apis/game.ts to import these constants and use them in the fetch
calls (replace the inline `'/api/game/click'`/`'/api/game/ranking'` usages while
keeping the existing API_BASE_URL concatenation). Ensure the constant names
match the imports in game.ts and update both locations (the fetch call that
currently uses API_BASE_URL + '/api/game/click' and the other call using
'/api/game/ranking').

5-14: ⚡ Quick win

인증이 필요한 엔드포인트에 secureFetch 사용을 검토하세요.

postGameClick은 클릭 수를 서버에 기록하는 API로, 인증 없이 호출되면 클릭 어뷰징에 취약할 수 있습니다. 가이드라인에 따르면 인증이 필요한 요청에는 403 응답 시 자동 토큰 갱신을 지원하는 secureFetch를 사용해야 합니다.

백엔드에서 /api/game/click 엔드포인트의 인증 요구 여부를 확인해 주세요. 인증이 필요하다면:

+ import { secureFetch } from './utils/apiHelpers';

  export const postGameClick = async (
    clubName: string,
    count: number,
  ): Promise<void> => {
-   const response = await fetch(`${API_BASE_URL}/api/game/click`, {
+   const response = await secureFetch(`${API_BASE_URL}/api/game/click`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ clubName, count, ctAt: new Date().toISOString() }),
    });
    if (!response.ok) throw new Error('클릭 요청에 실패했습니다.');
  };

As per coding guidelines, "Use secureFetch() for authenticated API requests and automatic token refresh on 403".

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

In `@frontend/src/apis/game.ts` around lines 5 - 14, postGameClick currently uses
plain fetch which bypasses the app's token-renew logic and may allow
unauthenticated abuses; if the backend requires auth for /api/game/click,
replace the fetch call in postGameClick with secureFetch (which handles 403 ->
token refresh) and preserve the same request shape (method POST, headers/body
with clubName, count, ctAt) and error handling when !response.ok; locate the
postGameClick function and swap the fetch invocation to secureFetch(API_BASE_URL
+ '/api/game/click', { method: 'POST', headers: {...}, body: JSON.stringify(...)
}) or conditionally call secureFetch after verifying the endpoint requires
authentication.
frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts (1)

1-118: ⚡ Quick win

테마 토큰 미사용 — colors, setTypography 적용 필요

이 파일은 colorssetTypography/typography를 임포트하지 않아, 동일한 디렉토리 내 다른 스타일 파일들(예: ClubIntroContent.styles.ts, UnderlineTabs.styles.ts)과 달리 하드코딩된 색상값·폰트 크기가 곳곳에 남아 있습니다:

라인 현재 값 권장 대체
60 color: #fff`` colors.white 또는 동등 테마 토큰
88 background: #fff`` 동일
14, 33, 52, 58, 79, 100, 115 font-size: Xrem 직접 지정 setTypography(typography.xxx)

As per coding guidelines, "Theme system must use styled-components ThemeProvider with colors, typography, and transitions defined in src/styles/theme/".

♻️ 수정 예시
 import styled from 'styled-components';
 import { media } from '@/styles/mediaQuery';
+import { colors } from '@/styles/theme/colors';
+import { setTypography, typography } from '@/styles/theme/typography';

 export const Title = styled.h2`
-  font-size: 1.25rem;
-  font-weight: 700;
-  color: ${({ theme }) => theme.colors.gray[900]};
+  ${setTypography(typography.title.title4)};
+  color: ${colors.gray[900]};
 `;

 export const StartButton = styled.button`
   ...
-  color: `#fff`;
+  color: ${colors.white};
   ...
 `;

 export const Dropdown = styled.ul`
   ...
-  background: `#fff`;
+  background: ${colors.white};
   ...
 `;

font-size 직접 지정 부분도 유사하게 setTypography(typography.paragraph.pX) 패턴으로 대체하고, colors를 직접 import하여 ThemeProvider 의존을 통일하는 것을 권장합니다.

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

In `@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts`
around lines 1 - 118, This file uses hardcoded color & font-size values instead
of the theme tokens; import colors, setTypography, and typography from your
theme and replace literal values in the styled components (Title, Input,
StartButton, Dropdown, ErrorMessage, etc.): swap "#fff" and other hex literals
for theme.colors.* tokens and replace direct font-size declarations with
setTypography(typography.xxx) calls for the appropriate text styles; ensure
imports are added at the top and that each component uses ${({ theme }) =>
theme...} or setTypography(typography...) consistently to match other files like
ClubIntroContent.styles.ts.
frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts (1)

59-73: ⚡ Quick win

테마 색상 토큰 미사용: background-color: #f2f2f2``

Line 68의 하드코딩된 hex 색상은 @/styles/theme/colors의 토큰으로 교체해야 합니다. 코딩 가이드라인에서 모든 색상을 src/styles/theme/에 정의된 값으로 관리하도록 명시하고 있습니다.

♻️ 제안 수정
-  background-color: `#f2f2f2`;
+  background-color: ${colors.gray[200]};

colors.gray[200]이 정확한 값인지 테마 파일에서 확인 후 적용하세요.

As per coding guidelines: "Theme system must use styled-components ThemeProvider with colors, typography, and transitions defined in src/styles/theme/"

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

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts` around lines 59 -
73, MapCard currently uses a hardcoded background-color "#f2f2f2"; replace it
with the theme color token (e.g. colors.gray[200]) from your theme module so all
colors come from src/styles/theme; update the import if missing and change the
background-color line in the MapCard styled component to use colors.gray[200]
(confirm the exact token value in the theme file before applying).
frontend/src/pages/PromotionPage/PromotionListPage.tsx (1)

8-8: ⚡ Quick win

@/ 경로 별칭 미사용 — 코딩 가이드라인 위반

같은 파일의 다른 임포트들은 모두 @/ 별칭을 사용하지만 Filter 임포트만 상대 경로를 사용하고 있습니다.

♻️ 제안 수정
-import Filter from '../../components/common/Filter/Filter';
+import Filter from '@/components/common/Filter/Filter';

As per coding guidelines: "Use path alias @/* mapped to src/* for imports".

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

In `@frontend/src/pages/PromotionPage/PromotionListPage.tsx` at line 8, The import
for the Filter component uses a relative path instead of the required path
alias; update the import statement that references Filter (import Filter from
'../../components/common/Filter/Filter') to use the project alias prefix (i.e.,
import from '@/components/common/Filter/Filter') so it follows the coding
guideline of using '@/...' mapped to src/*; ensure any other similar relative
imports in PromotionListPage.tsx are also converted to the '@/...' alias.
frontend/src/components/common/Header/Header.tsx (1)

55-61: 💤 Low value

prev && !next 조건을 단순화할 수 있습니다.

next = !prev이므로 !next === prev가 되어, prev && !nextprev와 동일합니다. 의미가 더 명확한 형태로 단순화할 수 있습니다.

♻️ 제안 리팩토링
  const toggleMenu = () => {
    setIsMenuOpen((prev) => {
      const next = !prev;
-     if (prev && !next) handleMenuClose();
+     if (prev) handleMenuClose();
      return next;
    });
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/common/Header/Header.tsx` around lines 55 - 61, The
toggleMenu closure uses a redundant condition (prev && !next) since next =
!prev; simplify it by replacing that check with a single prev check and call
handleMenuClose() when prev is true inside the setIsMenuOpen updater in the
toggleMenu function (referencing toggleMenu, setIsMenuOpen, handleMenuClose,
prev/next).
frontend/src/pages/WebviewMainPage/WebviewMainPage.styles.ts (1)

12-12: ⚡ Quick win

테마 토큰 미사용 — RetryButton과의 불일치

같은 파일의 RetryButton(Line 101)은 theme.colors.primary[900]을 올바르게 참조하는 반면, 여러 컴포넌트에서 하드코딩된 색상값을 사용하고 있습니다. 특히 SubscribeButton#FF5414theme.colors.primary[900]과 동일한 값일 가능성이 높아 불일치가 발생합니다.

♻️ 제안 수정 (주요 항목)
 export const SearchBarArea = styled.div`
   ...
-  background-color: `#fff`;
+  background-color: ${({ theme }) => theme.colors.base.white};
   ...
 `;

 export const SectionTitle = styled.span`
   ...
-  color: `#787878`;
+  color: ${({ theme }) => theme.colors.gray[600]};   /* 실제 토큰 값 확인 필요 */
 `;

 export const EmptyResult = styled.div`
   ...
-  color: `#555`;
+  color: ${({ theme }) => theme.colors.gray[700]};   /* 실제 토큰 값 확인 필요 */
   ...
 `;

 export const SubscribeButton = styled.button<{ $subscribed: boolean }>`
   ...
-  color: ${({ $subscribed }) => ($subscribed ? '#FF5414' : '#C5C5C5')};
+  color: ${({ $subscribed, theme }) =>
+    $subscribed ? theme.colors.primary[900] : theme.colors.gray[300]};  /* 실제 토큰 값 확인 필요 */
   ...
 `;

As per coding guidelines: "Theme system must use styled-components ThemeProvider with colors, typography, and transitions defined in src/styles/theme/".

Also applies to: 43-44, 49-50, 67-67, 83-83

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

In `@frontend/src/pages/WebviewMainPage/WebviewMainPage.styles.ts` at line 12,
Several components in WebviewMainPage.styles.ts use hardcoded color literals
(e.g., SubscribeButton's `#FF5414`) causing inconsistency with RetryButton which
correctly uses theme.colors.primary[900]; update SubscribeButton and all other
hardcoded color usages to read from the styled-components theme (e.g., replace
literals with theme.colors.primary[900] or the appropriate token like
theme.colors.primary[500]/[900]), ensure imports/types for ThemeProps remain
intact and adjust any styled-component props (e.g., in SubscribeButton,
RetryButton, and other affected styled components) to reference theme instead of
literal values so the ThemeProvider tokens control the colors.
frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts (1)

239-249: 💤 Low value

MapLink 색상에 하드코딩된 hex 값 대신 테마 토큰 사용을 고려해 보세요.

#3366bb가 직접 사용되고 있습니다. 프로젝트에 링크 색상 토큰이 있다면 테마 시스템 일관성을 위해 사용하는 것이 좋습니다.

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

In
`@frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts`
around lines 239 - 249, MapLink uses a hardcoded color "#3366bb"; replace that
literal with the theme link color token to maintain design-system consistency.
Locate the styled component MapLink in ClubProfileCard.styles.ts and change the
color line to reference the theme (e.g. use theme.colors.link or the project’s
token name) so the component consumes the theme token instead of the hex; keep
the rest of the styles (setTypography call, cursor, hover underline) unchanged.
frontend/src/hooks/Queries/useGame.ts (1)

5-12: 2초 폴링 주기의 서버 부하 고려 필요

refetchInterval: 2000은 게임 페이지에 접속한 모든 사용자가 2초마다 랭킹 API를 호출함을 의미합니다. 동시 접속자가 많아질 경우 서버 부하가 선형적으로 증가합니다. 실시간 랭킹이 반드시 필요한 경우라면 WebSocket/SSE 방식을 장기적으로 검토해 볼 수 있습니다.

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

In `@frontend/src/hooks/Queries/useGame.ts` around lines 5 - 12, The current
useGameRanking hook sets refetchInterval: 2000 causing every connected client to
poll getGameRanking every 2s (useQuery in useGameRanking with queryKey
queryKeys.game.ranking), which can overload the server; change to a
lower/default polling strategy: remove or increase refetchInterval, or make
polling conditional (e.g., only poll when the tab is visible or when a "live"
flag is true), and/or implement fallback to a push-based solution later
(WebSocket/SSE) — update useGameRanking to use a sensible interval or
visibility/enable gating around refetchInterval and document the change.
frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx (1)

70-86: 💤 Low value

중복된 탭 배열을 통합하세요.

tabstopBarTabs 배열이 동일합니다. 하나의 상수로 통합하면 유지보수성이 향상됩니다.

♻️ 제안된 수정
- const tabs = useMemo(
-   () => [
-     { key: TAB_TYPE.INTRO, label: '소개내용' },
-     { key: TAB_TYPE.PHOTOS, label: '활동사진' },
-     { key: TAB_TYPE.SCHEDULE, label: '행사일정' },
-   ],
-   [],
- );
-
- const topBarTabs = useMemo(
-   () => [
-     { key: TAB_TYPE.INTRO, label: '소개내용' },
-     { key: TAB_TYPE.PHOTOS, label: '활동사진' },
-     { key: TAB_TYPE.SCHEDULE, label: '행사일정' },
-   ],
-   [],
- );
+ const tabs = useMemo(
+   () => [
+     { key: TAB_TYPE.INTRO, label: '소개내용' },
+     { key: TAB_TYPE.PHOTOS, label: '활동사진' },
+     { key: TAB_TYPE.SCHEDULE, label: '행사일정' },
+   ],
+   [],
+ );

이후 topBarTabs 대신 tabs를 사용하세요.

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

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx` around lines 70 - 86,
The two identical useMemo arrays (tabs and topBarTabs) should be consolidated
into a single constant: remove the duplicate topBarTabs definition and keep one
useMemo (e.g., tabs) that returns [{ key: TAB_TYPE.INTRO, label: '소개내용' }, {
key: TAB_TYPE.PHOTOS, label: '활동사진' }, { key: TAB_TYPE.SCHEDULE, label: '행사일정'
}]; then update all references that use topBarTabs to use tabs instead and
ensure imports/usage around TAB_TYPE and useMemo remain unchanged.
frontend/src/pages/GamePage/GamePage.tsx (1)

10-10: 💤 Low value

세션 스토리지 키를 storageKeys.ts에서 중앙 관리하세요.

코딩 가이드라인에 따르면 localStorage/sessionStorage 키는 src/constants/storageKeys.ts에서 중앙 관리해야 합니다.

♻️ 제안된 수정

src/constants/storageKeys.ts에 추가:

export const STORAGE_KEYS = {
  // ... existing keys
  gameClubName: 'game_club_name',
};

GamePage.tsx에서:

-const STORAGE_KEY = 'game_club_name';
+import { STORAGE_KEYS } from '@/constants/storageKeys';
+
+// 사용 시:
+sessionStorage.getItem(STORAGE_KEYS.gameClubName)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/GamePage/GamePage.tsx` at line 10, Replace the local
STORAGE_KEY constant usage in GamePage.tsx with the centralized key from
src/constants/storageKeys.ts: add a gameClubName entry to the exported
STORAGE_KEYS object (e.g., STORAGE_KEYS.gameClubName = 'game_club_name') and
update GamePage.tsx to import STORAGE_KEYS and use STORAGE_KEYS.gameClubName
wherever STORAGE_KEY was referenced; ensure the import name matches the exported
symbol and remove the now-unused local STORAGE_KEY constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/weekly-reports/2026-W14-club-engagement-analysis.md`:
- Around line 9-31: The title "## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리" conflicts with
the table which contains 11 rows due to tied ranks (duplicate rank 6 and
multiple rank 8 entries); update the heading and/or table to be consistent:
either change the heading to "TOP10 (동점 포함 — 11개)" or "TOP11 동아리", or keep
"TOP10" and add a short footnote below the table explaining that ties produced
extra entries; also ensure the ranking values in the table (the repeated "6" and
repeated "8") and club list (e.g., "백경 유스호스텔", "포시즌", "집현전", "동반", "바구니", "쇳물결",
"백경클래식기타연구회", "SIC", "짚신 유스호스텔", "조나단", "네오쇼크") remain unchanged while you
adjust the heading/footnote for clarity.

In `@frontend/.claude/agents/공통컴포넌트부서.md`:
- Line 248: The markdown reference still points to the deleted component
`src/components/common/Modal/PortalModal.tsx`; update that reference to the new
file name `src/components/common/Modal/Portal.tsx` wherever it appears in
`공통컴포넌트부서.md` so the documentation matches the current codebase.

In `@frontend/.gitignore`:
- Around line 30-31: 현재 frontend/.gitignore에 전체 `.claude/`를 무시하면
`frontend/.claude/agents/API훅부서.md`, `frontend/.claude/agents/공통컴포넌트부서.md` 및
`frontend/.claude/commands/*.md` 같은 공유해야 하는 파일들이 버전 관리에서 빠집니다; 수정 방법은 두 가지 중 하나를
선택해 적용하세요: (1) 공유할 파일을 유지하려면 `.gitignore`에서 `.claude/` 대신 개인 설정만 무시하도록 패턴을 좁혀
`.claude/local/` 또는 `.claude/*.local` 같은 구체적 경로/글로벌 패턴을 사용하고
`.claude/agents/*.md` 및 `.claude/commands/*.md`가 추적되도록 유지하세요; (2) 전체 디렉토리를 영구히
언트랙하려면 먼저 `git rm --cached`로 `frontend/.claude/agents/API훅부서.md`,
`frontend/.claude/agents/공통컴포넌트부서.md`, `frontend/.claude/commands/commit.md`,
`create-e2e-test.md`, `find-e2e-test.md`, `mixpanel.md`, `test.md` 등을 제거한 뒤
`.claude/`를 .gitignore에 추가하세요.

In `@frontend/docs/features/game/game-page-layout.md`:
- Around line 8-16: The fenced ASCII diagram in game-page-layout.md is missing a
language specifier which triggers MD040; update the code fence that contains the
diagram (the triple-backtick block with the ASCII game layout) to include a
language token such as text or plaintext (e.g., ```text) so the markdown linter
recognizes it as plain text and the MD040 warning is resolved.

In `@frontend/docs/features/hooks/useClubSuggestions.md`:
- Around line 13-21: The docs and implementation use setTimeout for debouncing
which violates the guideline—replace the manual setTimeout logic with the shared
debounce utility: refactor ClubNameInput.tsx to stop using the useEffect +
setTimeout pattern (the value, debouncedKeyword, and setDebouncedKeyword logic)
and instead call the debounce utility when wiring the input change handler
(keeping useClubSuggestions(debouncedKeyword) as the consumer), and update
useClubSuggestions.md example to show the debounce utility usage rather than
setTimeout so both code and docs illustrate the same approved pattern.

In `@frontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.md`:
- Line 33: Several fenced code blocks contain ASCII diagrams/plaintext without a
language identifier, triggering markdownlint MD040; update each fenced block so
the opening triple backticks include a language like text or plaintext (e.g.,
change ``` to ```text for the ASCII diagram that begins with "[tokens/]" and
apply the same pattern to the other eight plaintext/diagram blocks), ensuring
all nine code fences include a language identifier.

In `@frontend/src/components/common/Modal/Modal.tsx`:
- Around line 23-25: The Escape key handler always calls onClose regardless of
the backdrop setting; update the handleKeyDown logic in Modal.tsx (the
handleKeyDown function) to respect the closeOnBackdrop prop (or add and respect
a new closeOnEscape prop) so that when closeOnBackdrop is false (or
closeOnEscape is false) pressing Escape does not invoke onClose; locate the
handleKeyDown function and guard the onClose() call with the appropriate prop
check and ensure event listener cleanup still works.

In `@frontend/src/components/map/NaverMap/NaverMap.styles.ts`:
- Around line 9-11: The global CSS rule using the universal selector (*) with
"cursor: default !important" overrides all map children (markers, controls) and
prevents pointer cursors; remove or replace that rule and target only the map
canvas/container element instead. In NaverMap.styles.ts, replace the "*"
selector (and drop !important) with a more specific selector for the map
rendering element (e.g., the NaverMap container or canvas class used by the
component), and add targeted rules for drag behavior (set cursor: grab /
grabbing on the canvas) while leaving interactive children (marker, cluster,
control) free to inherit pointer cursors so SDK-rendered clickable elements
still show cursor: pointer.

In `@frontend/src/constants/clubLocation.ts`:
- Around line 156-161: The entry with clubName '스매싱' has detailLocation set to
'A동 306' missing the '호' suffix; update the detailLocation value for that object
to 'A동 306호' so it matches the other entries (look for the object containing
clubName: '스매싱' and change the detailLocation field).

In `@frontend/src/hooks/Map/useNaverMap.ts`:
- Around line 79-125: The effect can create the map after unmount due to the
async loadNaverMapScript() Promise; to fix, add a mounted/aborted guard inside
the useEffect and check it before creating or assigning mapInstance: set a local
let isMounted = true at start, change the then() handler to immediately return
if !isMounted or !mapRef.current or !window.naver, and set isMounted = false in
the cleanup; ensure mapInstance is only assigned and externalRef.current updated
while isMounted, and still call mapInstance?.destroy() in cleanup if it was
created. Reference: useEffect, loadNaverMapScript, mapInstance, mapRef,
externalRef, buildMarkerContent.

In `@frontend/src/hooks/useNavigator.ts`:
- Around line 21-27: The external URL branch in useNavigator treats itms-apps://
as external (isExternalUrl) but calls requestOpenExternalUrl(trimmedUrl) without
checking its boolean result; since requestOpenExternalUrl only permits
http/https it returns false and the link is silently ignored in webviews. Update
useNavigator (the block using isInAppWebView, requestOpenExternalUrl and
trimmedUrl) to check the return value of requestOpenExternalUrl and, if it
returns false, perform a fallback (e.g., window.location.href = trimmedUrl or
window.open(trimmedUrl)) so non-http(s) schemes like itms-apps:// are not
dropped in the webview environment.

In `@frontend/src/hooks/useWebviewSubscribe.ts`:
- Around line 51-56: In useWebviewSubscribe, the message event listener is only
attached to window which misses iOS WebView messages; add a second listener
using document.addEventListener('message', handleMessage) after
window.addEventListener('message', handleMessage) and ensure the cleanup in the
returned function also calls document.removeEventListener('message',
handleMessage); keep references to the existing handleMessage and
requestSubscribeState calls unchanged so both iOS (document) and Android
(window) receive SUBSCRIBE_STATE / SUBSCRIBE_RESULT events.

In `@frontend/src/index.tsx`:
- Around line 10-14: The current code registers window.navermap_authFailure only
in DEV, causing production Naver Map auth failures to be ignored; change it so
window.navermap_authFailure is registered unconditionally and forwards the error
to Sentry (use the existing initializeSentry context) while still logging to
console for DEV; update the registration around the navermap_authFailure
function (and any related initialization logic) to call Sentry.captureException
or the project's Sentry helper when an auth failure occurs and keep the DEV
console.error behavior.

In `@frontend/src/pages/ClubMapPage/ClubMapPage.tsx`:
- Around line 1-3: ClubMapPage currently returns null causing a blank screen;
replace the null return in the ClubMapPage functional component with a minimal
placeholder UI (e.g., a centered loading spinner and a "준비 중" / "Coming soon"
message) so users see feedback while the page is incomplete; update the JSX in
ClubMapPage (preserving the component name and default export) to render the
placeholder and ensure any styling or existing common Spinner/Loader component
(if available) is reused for consistency.

In `@frontend/src/pages/GamePage/components/DotTextEffect/DotTextEffect.tsx`:
- Around line 285-293: The toCanvasCoords function uses canvasRef.current!
without checking for null; update toCanvasCoords to first get const canvas =
canvasRef.current and if (!canvas) return null before calling
canvas.getBoundingClientRect(); keep the existing checks for r.width/r.height
and the coordinate conversion logic; reference the toCanvasCoords function and
canvasRef.current so the null-guard is added immediately after retrieving canvas
and before any non-null assertions or calls.
- Line 3: The module-level call to window.matchMedia (mobileQuery) can crash
during SSR/test where window is undefined; move the matchMedia setup into the
DotTextEffect component (e.g., inside useEffect or conditional on typeof window
!== 'undefined'), initialize mobileQuery there, and subscribe to its change
events within useEffect (cleanup on unmount). Update any references to
mobileQuery inside the component to use the state or ref populated by this
effect so the component is SSR-safe.

In `@frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx`:
- Around line 12-16: The file uses React.ReactNode in the ClubCardProps type but
does not import the React namespace, causing a TS compile error under "jsx":
"react-jsx"; either import the React namespace (e.g., add an import for React)
so React.ReactNode resolves, or update the prop type to use the named type
(e.g., ReactNode) and add a named import from 'react'; locate ClubCardProps in
ClubCard.tsx to apply one of these fixes and ensure the import is added/adjusted
accordingly.

In
`@frontend/src/pages/WebviewMainPage/components/SubscribeButton/SubscribeButton.tsx`:
- Around line 10-13: The handler uses the React namespace type React.MouseEvent
but React is not imported, causing TS compile errors; fix by adding an import
for React at the top of the file (e.g., import React from 'react') or replace
the type with a non-namespace type (e.g., import type { MouseEvent } from
'react' and use MouseEvent) so the reference for React.MouseEvent in handleClick
is valid; apply the same pattern to other files listed (ClubCard, ClubNameInput,
DotTextEffect, TimelineRow, PrivateRoute, PhotoEditTab, ApplicationEditTab,
ClubCoverEditor, QuestionContainer, InputField, CustomTextArea, ClubTag,
AdminClubContext, ApplicationRowItem) to resolve project-wide TypeScript errors.

In `@frontend/src/utils/calendarSyncUtils.test.ts`:
- Around line 86-91: The test for formatDateText uses a UTC timestamp that can
shift the calendar day in extreme timezones; update the test to fix the
environment or the clock so the date won't roll: either set the test to force
TZ='UTC' for the test case (set process.env.TZ = 'UTC' before the it and restore
after) or use Jest fake timers (jest.useFakeTimers + jest.setSystemTime(new
Date('2026-03-19T12:00:00Z')) before the assertion and restore timers after) and
keep the same input and assertions against formatDateText; reference the
formatDateText call inside the existing "유효한 날짜는 한국어 로케일로 포맷한다" test and ensure
you restore TZ or timers to avoid leaking into other tests.

In `@frontend/src/utils/loadNaverMapScript.ts`:
- Around line 1-28: The loadNaverMapScript Promise currently never rejects and
can hang if the script fails or if an existing script emits load before we
attach a listener; update loadNaverMapScript to accept and call both resolve and
reject, add script.onerror handlers for the newly created script and for
existingScript to call reject on failure, and for existingScript before
attaching listeners check its readyState (=== 'complete' or 'loaded') or whether
window.naver?.maps is already available and resolve immediately if so; also
remove/cleanup event listeners after they fire to avoid leaks and ensure both
success and error paths settle the Promise.

---

Outside diff comments:
In `@frontend/src/experiments/ExperimentRepository.ts`:
- Line 8: Move the localStorage key constant ASSIGNMENT_STORAGE_KEY out of
ExperimentRepository.ts into the centralized src/constants/storageKeys.ts and
update all references (including in resetAssignments() and any other usages
within the ExperimentRepository class/module) to import and use the exported key
from storageKeys.ts; ensure the exported name matches the original (or update
uses accordingly) so the repository reads/writes the same 'moadong_experiments'
key via the central constant.

In
`@frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx`:
- Around line 289-300: The onClick handler currently always calls
e.preventDefault() which disables normal anchor behavior; change the onClick on
Styled.EventLink to only intercept clicks in webviews by checking
isInAppWebView(): inside the onClick, if (isInAppWebView()) {
e.preventDefault(); handleLink(event.url!); } else do nothing so the browser
follows href and target="_blank" normally; reference the onClick on
Styled.EventLink, handleLink, and isInAppWebView() when applying the change.

---

Nitpick comments:
In @.gitignore:
- Line 10: Update the .gitignore entry '/.omc' to explicitly target the
directory by adding a trailing slash: change '/.omc' to '/.omc/' so it
consistently ignores the directory (matching the frontend/.gitignore pattern)
and makes the intent clear; edit the '/.omc' line in the .gitignore file to
'/.omc/'.

In `@frontend/src/apis/game.ts`:
- Line 9: Move the hardcoded endpoints `/api/game/click` and `/api/game/ranking`
into constants and reference them from game API calls: add two named constants
(e.g., GAME_CLICK_ENDPOINT and GAME_RANKING_ENDPOINT) to src/constants/api.ts
and export them, then update frontend/src/apis/game.ts to import these constants
and use them in the fetch calls (replace the inline
`'/api/game/click'`/`'/api/game/ranking'` usages while keeping the existing
API_BASE_URL concatenation). Ensure the constant names match the imports in
game.ts and update both locations (the fetch call that currently uses
API_BASE_URL + '/api/game/click' and the other call using '/api/game/ranking').
- Around line 5-14: postGameClick currently uses plain fetch which bypasses the
app's token-renew logic and may allow unauthenticated abuses; if the backend
requires auth for /api/game/click, replace the fetch call in postGameClick with
secureFetch (which handles 403 -> token refresh) and preserve the same request
shape (method POST, headers/body with clubName, count, ctAt) and error handling
when !response.ok; locate the postGameClick function and swap the fetch
invocation to secureFetch(API_BASE_URL + '/api/game/click', { method: 'POST',
headers: {...}, body: JSON.stringify(...) }) or conditionally call secureFetch
after verifying the endpoint requires authentication.

In `@frontend/src/components/common/Header/Header.tsx`:
- Around line 55-61: The toggleMenu closure uses a redundant condition (prev &&
!next) since next = !prev; simplify it by replacing that check with a single
prev check and call handleMenuClose() when prev is true inside the setIsMenuOpen
updater in the toggleMenu function (referencing toggleMenu, setIsMenuOpen,
handleMenuClose, prev/next).

In `@frontend/src/components/common/Portal/Portal.tsx`:
- Around line 1-15: Add a Storybook story file named Portal.stories.tsx
alongside the Portal component that exports a default meta object and one or two
stories demonstrating the Portal component rendering children into a target root
(use the Portal component symbol) and showing the rootId prop variation; include
setup to create a DOM element with id 'modal-root' in the story's play or
decorator so the portal can mount, import Portal from './Portal', and ensure
story names and args cover default usage and a custom rootId case.

In `@frontend/src/constants/eventName.ts`:
- Around line 67-74: The WEBVIEW_LINK_TARGET constant is not an event name and
should be moved out of eventName.ts to preserve single responsibility; extract
WEBVIEW_LINK_TARGET (and its members like CLUB_FESTIVAL) into a new constants
file such as webviewConfig.ts (or similar), update imports/usages throughout the
codebase to import from the new module, and ensure eventName.ts only contains
Mixpanel event constants like PROMOTION_CLUB_CTA_CLICKED and
WEBVIEW_SUBSCRIBE_TOGGLED.

In `@frontend/src/hooks/Queries/useGame.ts`:
- Around line 5-12: The current useGameRanking hook sets refetchInterval: 2000
causing every connected client to poll getGameRanking every 2s (useQuery in
useGameRanking with queryKey queryKeys.game.ranking), which can overload the
server; change to a lower/default polling strategy: remove or increase
refetchInterval, or make polling conditional (e.g., only poll when the tab is
visible or when a "live" flag is true), and/or implement fallback to a
push-based solution later (WebSocket/SSE) — update useGameRanking to use a
sensible interval or visibility/enable gating around refetchInterval and
document the change.

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts`:
- Around line 59-73: MapCard currently uses a hardcoded background-color
"#f2f2f2"; replace it with the theme color token (e.g. colors.gray[200]) from
your theme module so all colors come from src/styles/theme; update the import if
missing and change the background-color line in the MapCard styled component to
use colors.gray[200] (confirm the exact token value in the theme file before
applying).

In `@frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx`:
- Around line 70-86: The two identical useMemo arrays (tabs and topBarTabs)
should be consolidated into a single constant: remove the duplicate topBarTabs
definition and keep one useMemo (e.g., tabs) that returns [{ key:
TAB_TYPE.INTRO, label: '소개내용' }, { key: TAB_TYPE.PHOTOS, label: '활동사진' }, { key:
TAB_TYPE.SCHEDULE, label: '행사일정' }]; then update all references that use
topBarTabs to use tabs instead and ensure imports/usage around TAB_TYPE and
useMemo remain unchanged.

In
`@frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts`:
- Around line 239-249: MapLink uses a hardcoded color "#3366bb"; replace that
literal with the theme link color token to maintain design-system consistency.
Locate the styled component MapLink in ClubProfileCard.styles.ts and change the
color line to reference the theme (e.g. use theme.colors.link or the project’s
token name) so the component consumes the theme token instead of the hex; keep
the rest of the styles (setTypography call, cursor, hover underline) unchanged.

In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.styles.ts`:
- Around line 26-29: Replace the raw "@media (max-width: 500px)" queries in
ClickButton.styles.ts for the ClubLabel and Button components with the project's
media utility (use media.mobile), import the media symbol at the top of the
file, and move the max-width and font-size overrides into the media.mobile block
so the components use the centralized breakpoint helper (update ClubLabel and
Button styles accordingly).

In `@frontend/src/pages/GamePage/components/ClickButton/ClickButton.tsx`:
- Around line 21-54: The component is using inline hardcoded styles instead of
the styled motion components S.Button and S.Count, so replace the direct
motion.button and motion.span usage with the styled motion-wrapped components
defined in ClickButton.styles.ts (S.Button and S.Count) to restore responsive
sizes and theme colors; if the styles file hasn’t wrapped them for framer-motion
yet, change its exports to use styled(motion.button) and styled(motion.span) (or
motion.custom equivalent) so S.Button and S.Count can be used here, then remove
the hardcoded '#FF5414' and size values and rely on the styles in
S.Button/S.Count and theme.colors.primary[900], while keeping the motion props
(onClick, whileTap, whileHover, transition, AnimatePresence/keys) passed through
to those components.

In
`@frontend/src/pages/GamePage/components/ClubNameInput/ClubNameInput.styles.ts`:
- Around line 1-118: This file uses hardcoded color & font-size values instead
of the theme tokens; import colors, setTypography, and typography from your
theme and replace literal values in the styled components (Title, Input,
StartButton, Dropdown, ErrorMessage, etc.): swap "#fff" and other hex literals
for theme.colors.* tokens and replace direct font-size declarations with
setTypography(typography.xxx) calls for the appropriate text styles; ensure
imports are added at the top and that each component uses ${({ theme }) =>
theme...} or setTypography(typography...) consistently to match other files like
ClubIntroContent.styles.ts.

In `@frontend/src/pages/GamePage/GamePage.tsx`:
- Line 10: Replace the local STORAGE_KEY constant usage in GamePage.tsx with the
centralized key from src/constants/storageKeys.ts: add a gameClubName entry to
the exported STORAGE_KEYS object (e.g., STORAGE_KEYS.gameClubName =
'game_club_name') and update GamePage.tsx to import STORAGE_KEYS and use
STORAGE_KEYS.gameClubName wherever STORAGE_KEY was referenced; ensure the import
name matches the exported symbol and remove the now-unused local STORAGE_KEY
constant.

In `@frontend/src/pages/PromotionPage/PromotionListPage.tsx`:
- Line 8: The import for the Filter component uses a relative path instead of
the required path alias; update the import statement that references Filter
(import Filter from '../../components/common/Filter/Filter') to use the project
alias prefix (i.e., import from '@/components/common/Filter/Filter') so it
follows the coding guideline of using '@/...' mapped to src/*; ensure any other
similar relative imports in PromotionListPage.tsx are also converted to the
'@/...' alias.

In `@frontend/src/pages/WebviewMainPage/WebviewMainPage.styles.ts`:
- Line 12: Several components in WebviewMainPage.styles.ts use hardcoded color
literals (e.g., SubscribeButton's `#FF5414`) causing inconsistency with
RetryButton which correctly uses theme.colors.primary[900]; update
SubscribeButton and all other hardcoded color usages to read from the
styled-components theme (e.g., replace literals with theme.colors.primary[900]
or the appropriate token like theme.colors.primary[500]/[900]), ensure
imports/types for ThemeProps remain intact and adjust any styled-component props
(e.g., in SubscribeButton, RetryButton, and other affected styled components) to
reference theme instead of literal values so the ThemeProvider tokens control
the colors.
🪄 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: c61d1f2c-76f7-4d98-bc13-6544358fe157

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec3a42 and 445489a.

⛔ Files ignored due to path filters (4)
  • frontend/src/assets/images/icons/close_button_icon.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/location_icon.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/marker.svg is excluded by !**/*.svg
  • frontend/src/assets/images/icons/subscribe_button_icon.svg is excluded by !**/*.svg
📒 Files selected for processing (110)
  • .gitignore
  • docs/weekly-reports/2026-01-01-to-03-04-club-engagement-percentile-analysis.md
  • docs/weekly-reports/2026-01-to-03-club-engagement-analysis.md
  • docs/weekly-reports/2026-03-05-to-04-14-always-open-apply-click-rate.md
  • docs/weekly-reports/2026-W14-club-engagement-analysis.md
  • frontend/.claude/agents/공통컴포넌트부서.md
  • frontend/.claude/commands/mixpanel.md
  • frontend/.gitignore
  • frontend/CLAUDE.md
  • frontend/docs/claude/api.md
  • frontend/docs/claude/architecture.md
  • frontend/docs/claude/conventions.md
  • frontend/docs/claude/features.md
  • frontend/docs/claude/testing.md
  • frontend/docs/claude/ui.md
  • frontend/docs/features/components/Button.md
  • frontend/docs/features/components/Portal.md
  • frontend/docs/features/components/header.md
  • frontend/docs/features/experiments/mixpanel-super-property.md
  • frontend/docs/features/game/game-page-layout.md
  • frontend/docs/features/hooks/useClubSuggestions.md
  • frontend/docs/features/hooks/useValidateClubName.md
  • frontend/docs/superpowers/specs/2026-04-14-design-system-toolkit-design.md
  • frontend/eslint.config.mjs
  • frontend/src/App.tsx
  • frontend/src/apis/game.ts
  • frontend/src/components/application/modals/ApplicationSelectModal.tsx
  • frontend/src/components/common/Button/Button.tsx
  • frontend/src/components/common/Filter/Filter.styles.ts
  • frontend/src/components/common/Filter/Filter.tsx
  • frontend/src/components/common/Header/Header.tsx
  • frontend/src/components/common/Modal/Modal.stories.tsx
  • frontend/src/components/common/Modal/Modal.tsx
  • frontend/src/components/common/Modal/PortalModal.tsx
  • frontend/src/components/common/Portal/Portal.tsx
  • frontend/src/components/common/SearchField/SearchField.styles.ts
  • frontend/src/components/common/UnderlineTabs/UnderlineTabs.styles.ts
  • frontend/src/components/common/UnderlineTabs/UnderlineTabs.tsx
  • frontend/src/components/map/InteractiveMapView/InteractiveMapView.styles.ts
  • frontend/src/components/map/InteractiveMapView/InteractiveMapView.tsx
  • frontend/src/components/map/MapClubInfoCard/MapClubInfoCard.styles.ts
  • frontend/src/components/map/MapClubInfoCard/MapClubInfoCard.tsx
  • frontend/src/components/map/MapModal/MapModal.styles.ts
  • frontend/src/components/map/MapModal/MapModal.tsx
  • frontend/src/components/map/MapZoomControls/MapZoomControls.styles.ts
  • frontend/src/components/map/MapZoomControls/MapZoomControls.tsx
  • frontend/src/components/map/NaverMap/NaverMap.styles.ts
  • frontend/src/components/map/NaverMap/NaverMap.tsx
  • frontend/src/constants/clubLocation.ts
  • frontend/src/constants/eventName.ts
  • frontend/src/constants/queryKeys.ts
  • frontend/src/experiments/ExperimentRepository.ts
  • frontend/src/hooks/Header/useHeaderNavigation.ts
  • frontend/src/hooks/Header/useHeaderVisibility.test.ts
  • frontend/src/hooks/Header/useHeaderVisibility.ts
  • frontend/src/hooks/Map/useMapZoom.ts
  • frontend/src/hooks/Map/useNaverMap.ts
  • frontend/src/hooks/Queries/useClub.ts
  • frontend/src/hooks/Queries/useGame.ts
  • frontend/src/hooks/Queries/usePromotionNotification.ts
  • frontend/src/hooks/useNavigator.ts
  • frontend/src/hooks/useWebviewSubscribe.ts
  • frontend/src/index.tsx
  • frontend/src/mocks/handlers/index.ts
  • frontend/src/pages/AdminPage/components/PersonalInfoConsentModal/PersonalInfoConsentModal.tsx
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.styles.ts
  • frontend/src/pages/ClubDetailPage/ClubDetailPage.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubApplyButton/ClubApplyButton.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubFeed/ClubFeed.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubIntroContent/ClubIntroContent.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubProfileCard/ClubProfileCard.tsx
  • frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.styles.ts
  • frontend/src/pages/ClubDetailPage/components/ClubScheduleCalendar/ClubScheduleCalendar.tsx
  • frontend/src/pages/ClubDetailPage/components/PhotoModal/PhotoModal.tsx
  • frontend/src/pages/ClubMapPage/ClubMapPage.tsx
  • frontend/src/pages/FestivalPage/IntroductionPage/IntroductionPage.styles.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/pages/MainPage/components/Banner/Banner.tsx
  • frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx
  • frontend/src/pages/MainPage/components/SearchBox/SearchBox.tsx
  • frontend/src/pages/PromotionPage/PromotionDetailPage.tsx
  • frontend/src/pages/PromotionPage/PromotionListPage.tsx
  • frontend/src/pages/PromotionPage/components/detail/PromotionClubCTA/PromotionClubCTA.tsx
  • frontend/src/pages/PromotionPage/components/detail/PromotionDetailTopBar/PromotionDetailTopBar.tsx
  • frontend/src/pages/PromotionPage/components/list/PromotionGrid/PromotionGrid.styles.ts
  • frontend/src/pages/WebviewLayout/WebviewLayout.tsx
  • frontend/src/pages/WebviewMainPage/WebviewMainPage.styles.ts
  • frontend/src/pages/WebviewMainPage/WebviewMainPage.tsx
  • frontend/src/pages/WebviewMainPage/components/SubscribeButton/SubscribeButton.tsx
  • frontend/src/routes/AppRoutes.tsx
  • frontend/src/routes/webviewRoutes.tsx
  • frontend/src/styles/WebviewGlobal.styles.ts
  • frontend/src/styles/theme/typography.ts
  • frontend/src/types/device.ts
  • frontend/src/types/game.ts
  • frontend/src/types/window.d.ts
  • frontend/src/utils/calendarSyncUtils.test.ts
  • frontend/src/utils/loadNaverMapScript.ts
  • frontend/src/utils/webviewBridge.ts
💤 Files with no reviewable changes (1)
  • frontend/src/components/common/Modal/PortalModal.tsx

Comment on lines +9 to +31
## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리

### 주요 지표

- **측정 이벤트**: `ClubDetailPage Duration`
- **측정 지표**: 체류시간 중간값 (`duration_seconds`)
- **분석 방법**: 동아리별 체류시간 중간값 집계

### TOP10 동아리

| 순위 | 동아리명 | 중간값 (초) |
| ---- | -------------------- | ----------- |
| 1 | 백경 유스호스텔 | 22 |
| 2 | 포시즌 | 18 |
| 3 | 집현전 | 16 |
| 4 | 동반 | 15 |
| 5 | 바구니 | 14 |
| 6 | 쇳물결 | 13 |
| 6 | 백경클래식기타연구회 | 13 |
| 8 | SIC | 12 |
| 8 | 짚신 유스호스텔 | 12 |
| 8 | 조나단 | 12 |
| 8 | 네오쇼크 | 12 |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

"TOP10" 제목과 실제 11개 데이터 행 간 불일치

8위에 4개 동아리(SIC, 짚신 유스호스텔, 조나단, 네오쇼크)가 동점으로 묶여 실제 테이블은 11행입니다. 제목과 불일치합니다.

📝 수정 제안
-## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리
+## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리 (동점 포함 11개)

또는 테이블 하단에 각주 추가:

| 8    | 네오쇼크             | 12          |
+
+> 8위 동점으로 인해 실제 집계 동아리는 11개입니다.
📝 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.

Suggested change
## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리
### 주요 지표
- **측정 이벤트**: `ClubDetailPage Duration`
- **측정 지표**: 체류시간 중간값 (`duration_seconds`)
- **분석 방법**: 동아리별 체류시간 중간값 집계
### TOP10 동아리
| 순위 | 동아리명 | 중간값 (초) |
| ---- | -------------------- | ----------- |
| 1 | 백경 유스호스텔 | 22 |
| 2 | 포시즌 | 18 |
| 3 | 집현전 | 16 |
| 4 | 동반 | 15 |
| 5 | 바구니 | 14 |
| 6 | 쇳물결 | 13 |
| 6 | 백경클래식기타연구회 | 13 |
| 8 | SIC | 12 |
| 8 | 짚신 유스호스텔 | 12 |
| 8 | 조나단 | 12 |
| 8 | 네오쇼크 | 12 |
## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리 (동점 포함 11개)
### 주요 지표
- **측정 이벤트**: `ClubDetailPage Duration`
- **측정 지표**: 체류시간 중간값 (`duration_seconds`)
- **분석 방법**: 동아리별 체류시간 중간값 집계
### TOP10 동아리
| 순위 | 동아리명 | 중간값 (초) |
| ---- | -------------------- | ----------- |
| 1 | 백경 유스호스텔 | 22 |
| 2 | 포시즌 | 18 |
| 3 | 집현전 | 16 |
| 4 | 동반 | 15 |
| 5 | 바구니 | 14 |
| 6 | 쇳물결 | 13 |
| 6 | 백경클래식기타연구회 | 13 |
| 8 | SIC | 12 |
| 8 | 짚신 유스호스텔 | 12 |
| 8 | 조나단 | 12 |
| 8 | 네오쇼크 | 12 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/weekly-reports/2026-W14-club-engagement-analysis.md` around lines 9 -
31, The title "## 📊 분석 1: 상세페이지 체류시간 TOP10 동아리" conflicts with the table which
contains 11 rows due to tied ranks (duplicate rank 6 and multiple rank 8
entries); update the heading and/or table to be consistent: either change the
heading to "TOP10 (동점 포함 — 11개)" or "TOP11 동아리", or keep "TOP10" and add a short
footnote below the table explaining that ties produced extra entries; also
ensure the ranking values in the table (the repeated "6" and repeated "8") and
club list (e.g., "백경 유스호스텔", "포시즌", "집현전", "동반", "바구니", "쇳물결", "백경클래식기타연구회",
"SIC", "짚신 유스호스텔", "조나단", "네오쇼크") remain unchanged while you adjust the
heading/footnote for clarity.


- `src/components/common/Button/Button.tsx` - 기본 컴포넌트 패턴 (명시적 커스텀 props 인터페이스)
- `src/components/common/InputField/InputField.tsx` - 복잡한 props 인터페이스 예시
- `src/components/common/Modal/PortalModal.tsx` - 포털 컴포넌트 패턴
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

PortalModal.tsx 참조가 이미 제거된 파일을 가리키고 있습니다.

이 PR에서 PortalModal.tsx가 삭제되고 Portal.tsx로 대체되었으므로, 참조 파일 경로를 업데이트해야 합니다.

📝 수정 제안
-- `src/components/common/Modal/PortalModal.tsx` - 포털 컴포넌트 패턴
+- `src/components/common/Portal/Portal.tsx` - 포털 컴포넌트 패턴
📝 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.

Suggested change
- `src/components/common/Modal/PortalModal.tsx` - 포털 컴포넌트 패턴
- `src/components/common/Portal/Portal.tsx` - 포털 컴포넌트 패턴
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/.claude/agents/공통컴포넌트부서.md` at line 248, The markdown reference
still points to the deleted component
`src/components/common/Modal/PortalModal.tsx`; update that reference to the new
file name `src/components/common/Modal/Portal.tsx` wherever it appears in
`공통컴포넌트부서.md` so the documentation matches the current codebase.

Comment thread frontend/.gitignore
Comment on lines +30 to +31
# Claude Code (personal settings)
.claude/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# .claude/ 디렉토리 내에 현재 Git으로 추적 중인 파일이 있는지 확인

# 추적 중인 .claude/ 파일 확인
git ls-files 'frontend/.claude/**' '.claude/**' 2>/dev/null

# .claude/agents/ 내의 추적 파일 확인
git ls-files 'frontend/.claude/agents/**' '.claude/agents/**' 2>/dev/null

echo "---"
echo "위 결과가 비어있으면 현재 추적 중인 파일이 없는 것이며, 무시 규칙 추가가 안전합니다."
echo "파일이 표시되면 이 변경으로 인해 해당 파일들이 추적에서 제외될 수 있습니다."

Repository: Moadong/moadong

Length of output: 743


.claude/ 디렉토리 전체 무시로 인해 현재 추적 중인 에이전트 및 커맨드 정의 파일이 제외됩니다.

.claude/ 디렉토리 내에 다음의 추적 중인 파일들이 있습니다:

  • frontend/.claude/agents/API훅부서.md
  • frontend/.claude/agents/공통컴포넌트부서.md
  • frontend/.claude/commands/commit.md, create-e2e-test.md, find-e2e-test.md, mixpanel.md, test.md

전체 .claude/ 디렉토리를 .gitignore에 추가하면 이 파일들이 버전 관리에서 제외됩니다. 다음 중 하나를 선택하세요:

  1. 공유 필요 파일 유지: 팀에서 공유해야 할 에이전트/커맨드 파일은 .gitignore에서 제외하고, 개인 설정 파일만 무시하기
  2. 전체 언트래킹: 이 파일들이 더 이상 필요 없다면 먼저 git rm --cached 또는 유사 방식으로 버전 관리에서 제거한 후 .gitignore 추가
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/.gitignore` around lines 30 - 31, 현재 frontend/.gitignore에 전체
`.claude/`를 무시하면 `frontend/.claude/agents/API훅부서.md`,
`frontend/.claude/agents/공통컴포넌트부서.md` 및 `frontend/.claude/commands/*.md` 같은 공유해야
하는 파일들이 버전 관리에서 빠집니다; 수정 방법은 두 가지 중 하나를 선택해 적용하세요: (1) 공유할 파일을 유지하려면
`.gitignore`에서 `.claude/` 대신 개인 설정만 무시하도록 패턴을 좁혀 `.claude/local/` 또는
`.claude/*.local` 같은 구체적 경로/글로벌 패턴을 사용하고 `.claude/agents/*.md` 및
`.claude/commands/*.md`가 추적되도록 유지하세요; (2) 전체 디렉토리를 영구히 언트랙하려면 먼저 `git rm
--cached`로 `frontend/.claude/agents/API훅부서.md`,
`frontend/.claude/agents/공통컴포넌트부서.md`, `frontend/.claude/commands/commit.md`,
`create-e2e-test.md`, `find-e2e-test.md`, `mixpanel.md`, `test.md` 등을 제거한 뒤
`.claude/`를 .gitignore에 추가하세요.

Comment on lines +8 to +16
```
┌─────────────────────────────────────────────┐
│ [빈 공간] 동아리 클릭 배틀 [실시간 순위] │
│ │
│ [ 개 발 팀 (DotText) ] │
│ │
│ [클릭! 버튼] │
└─────────────────────────────────────────────┘
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

코드 펜스에 언어 지정자 추가 필요 (MD040)

ASCII 다이어그램 코드 블록에 언어 지정자가 없어 markdownlint 경고가 발생합니다. textplaintext를 지정해 주세요.

📝 수정 제안
-```
+```text
 ┌─────────────────────────────────────────────┐
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 8-8: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

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

In `@frontend/docs/features/game/game-page-layout.md` around lines 8 - 16, The
fenced ASCII diagram in game-page-layout.md is missing a language specifier
which triggers MD040; update the code fence that contains the diagram (the
triple-backtick block with the ASCII game layout) to include a language token
such as text or plaintext (e.g., ```text) so the markdown linter recognizes it
as plain text and the MD040 warning is resolved.

Comment on lines +13 to +21
```typescript
// useEffect로 300ms debounce
useEffect(() => {
const timer = setTimeout(() => setDebouncedKeyword(value.trim()), 300);
return () => clearTimeout(timer);
}, [value]);

const { data: suggestions = [] } = useClubSuggestions(debouncedKeyword);
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# ClubNameInput.tsx에서 debounce 유틸리티 또는 setTimeout 사용 여부 확인
fd -i "ClubNameInput.tsx" --exec rg -n "setTimeout|debounce" {}

Repository: Moadong/moadong

Length of output: 278


debounce 유틸리티 대신 setTimeout 직접 사용 — 구현과 문서 모두 코딩 가이드라인 위반

검증 결과, 실제 ClubNameInput.tsx 구현(line 29)도 documentation 예시와 동일하게 setTimeout을 직접 사용합니다. 코딩 가이드라인 "입력/검색 디바운싱에는 debounce 유틸리티를 사용"을 따르도록 다음을 수정해야 합니다:

  • ClubNameInput.tsx의 실제 구현을 debounce 유틸리티로 리팩토링
  • useClubSuggestions.md 문서의 코드 예시를 올바른 패턴으로 업데이트
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/docs/features/hooks/useClubSuggestions.md` around lines 13 - 21, The
docs and implementation use setTimeout for debouncing which violates the
guideline—replace the manual setTimeout logic with the shared debounce utility:
refactor ClubNameInput.tsx to stop using the useEffect + setTimeout pattern (the
value, debouncedKeyword, and setDebouncedKeyword logic) and instead call the
debounce utility when wiring the input change handler (keeping
useClubSuggestions(debouncedKeyword) as the consumer), and update
useClubSuggestions.md example to show the debounce utility usage rather than
setTimeout so both code and docs illustrate the same approved pattern.

Comment on lines +285 to +293
const toCanvasCoords = (clientX: number, clientY: number) => {
const canvas = canvasRef.current!;
const r = canvas.getBoundingClientRect();
if (r.width === 0 || r.height === 0) return null;
return {
x: (clientX - r.left) * (canvas.width / r.width),
y: (clientY - r.top) * (canvas.height / r.height),
};
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

canvasRef.current에 대한 non-null assertion 전에 null 체크가 필요합니다.

Line 286에서 canvasRef.current!를 사용하고 있지만, Line 288에서 r.width === 0 || r.height === 0 체크 전에 canvas가 null일 경우 런타임 오류가 발생할 수 있습니다.

🛡️ 안전한 null 체크 추가
 const toCanvasCoords = (clientX: number, clientY: number) => {
-  const canvas = canvasRef.current!;
+  const canvas = canvasRef.current;
+  if (!canvas) return null;
   const r = canvas.getBoundingClientRect();
   if (r.width === 0 || r.height === 0) return null;
🤖 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 285 - 293, The toCanvasCoords function uses canvasRef.current!
without checking for null; update toCanvasCoords to first get const canvas =
canvasRef.current and if (!canvas) return null before calling
canvas.getBoundingClientRect(); keep the existing checks for r.width/r.height
and the coordinate conversion logic; reference the toCanvasCoords function and
canvasRef.current so the null-guard is added immediately after retrieving canvas
and before any non-null assertions or calls.

Comment on lines +12 to +16
interface ClubCardProps {
club: Club;
children?: React.ReactNode;
onCardClick?: (club: Club) => void;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# tsconfig에 allowUmdGlobalAccess 또는 global.d.ts에 React 네임스페이스 선언 여부 확인

echo "=== tsconfig 검색 ==="
fd tsconfig.json --exec cat {}

echo ""
echo "=== 글로벌 타입 선언에서 React 네임스페이스 확인 ==="
rg -n "declare.*React|import.*React" --glob "*.d.ts" -C 2

echo ""
echo "=== 다른 파일에서 React.ReactNode 사용 패턴 확인 ==="
rg -n "React\.ReactNode" --type tsx --type ts -C 1 | head -40

Repository: Moadong/moadong

Length of output: 765


🏁 Script executed:

#!/bin/bash

echo "=== ClubCard.tsx - First 30 lines (imports and interface) ==="
head -30 frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx

echo ""
echo "=== Search for React import patterns in frontend/src/pages ==="
rg "^import.*React" frontend/src/pages --max-count 20

echo ""
echo "=== How other files use ReactNode type ==="
rg "ReactNode|React\.ReactNode" frontend/src --max-count 20

Repository: Moadong/moadong

Length of output: 5708


🏁 Script executed:

# Verify PrivateRoute.tsx and other files using React.ReactNode to confirm import patterns
echo "=== PrivateRoute.tsx imports ==="
head -5 frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx

echo ""
echo "=== TimelineRow.tsx imports ==="
head -5 frontend/src/pages/FestivalPage/components/TimelineRow/TimelineRow.tsx

echo ""
echo "=== ClubTag.tsx imports ==="
head -5 frontend/src/components/ClubTag/ClubTag.tsx

Repository: Moadong/moadong

Length of output: 666


🏁 Script executed:

# Get full imports section and interface for these files
echo "=== PrivateRoute.tsx - full context with React.ReactNode usage ==="
head -20 frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx

echo ""
echo "=== TimelineRow.tsx - full context with React.ReactNode usage ==="
head -25 frontend/src/pages/FestivalPage/components/TimelineRow/TimelineRow.tsx

echo ""
echo "=== ClubTag.tsx - full context with React.ReactNode usage ==="
head -25 frontend/src/components/ClubTag/ClubTag.tsx

Repository: Moadong/moadong

Length of output: 2141


🏁 Script executed:

# Final verification: Check if React is imported in TimelineRow.tsx and verify TypeScript compilation would catch this
echo "=== TimelineRow.tsx - check for React import ==="
head -1 frontend/src/pages/FestivalPage/components/TimelineRow/TimelineRow.tsx
grep "import.*React" frontend/src/pages/FestivalPage/components/TimelineRow/TimelineRow.tsx || echo "No React import found"

echo ""
echo "=== Summary: Files using React.ReactNode without React namespace ==="
rg "React\.ReactNode" frontend/src --max-count 50 -B 5 | grep -E "^[^-]*\.tsx:|React\.ReactNode" | paste - - | head -20

Repository: Moadong/moadong

Length of output: 310


🏁 Script executed:

# Final confirmation: Check if these other files have same issue
echo "=== PrivateRoute.tsx - React import check ==="
grep "^import.*React" frontend/src/pages/AdminPage/auth/PrivateRoute/PrivateRoute.tsx || echo "No React namespace import"

echo ""
echo "=== ClubTag.tsx - React import check ==="
grep "^import.*React" frontend/src/components/ClubTag/ClubTag.tsx || echo "No React namespace import"

echo ""
echo "=== Files with React.ReactNode usage (without namespace import) ==="
rg "React\.ReactNode" frontend/src --type tsx -l

Repository: Moadong/moadong

Length of output: 314


🏁 Script executed:

# Alternative search without type flag
echo "=== Files with React.ReactNode usage ==="
rg "React\.ReactNode" frontend/src -l | head -20

echo ""
echo "=== Verify tsconfig has no allowUmdGlobalAccess ==="
grep "allowUmdGlobalAccess" frontend/tsconfig.json || echo "allowUmdGlobalAccess NOT set (confirms issue)"

Repository: Moadong/moadong

Length of output: 650


React.ReactNode 사용 시 React 네임스페이스 import 누락

현재 파일에서 React.ReactNode를 사용하지만 React 네임스페이스를 import하지 않았습니다. "jsx": "react-jsx" 설정에서는 TypeScript가 'React' refers to a UMD global, but the current file is a module 컴파일 오류를 발생시킵니다.

다음 중 하나로 수정하세요:

-import { useState } from 'react';
+import { useState } from 'react';
+import type { ReactNode } from 'react';

 interface ClubCardProps {
   club: Club;
-  children?: React.ReactNode;
+  children?: ReactNode;
   onCardClick?: (club: Club) => void;
 }

또는:

-import { useState } from 'react';
+import React, { useState } from 'react';

 interface ClubCardProps {
   club: Club;
   children?: React.ReactNode;
   onCardClick?: (club: Club) => void;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/pages/MainPage/components/ClubCard/ClubCard.tsx` around lines 12
- 16, The file uses React.ReactNode in the ClubCardProps type but does not
import the React namespace, causing a TS compile error under "jsx": "react-jsx";
either import the React namespace (e.g., add an import for React) so
React.ReactNode resolves, or update the prop type to use the named type (e.g.,
ReactNode) and add a named import from 'react'; locate ClubCardProps in
ClubCard.tsx to apply one of these fixes and ensure the import is added/adjusted
accordingly.

Comment on lines +10 to +13
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onToggle();
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check for global React type declarations and files using React.* without import

echo "=== 1. Global type declaration files that might declare React globally ==="
fd -e d.ts -e ts --glob "*.d.ts" | xargs grep -l "declare.*React\|namespace React" 2>/dev/null | head -20

echo ""
echo "=== 2. TSX files using React.MouseEvent without importing React ==="
rg -n "React\." --type=ts -g "*.tsx" -l | while read f; do
  if ! grep -q "import.*React\|import \* as React" "$f"; then
    echo "MISSING IMPORT: $f"
    grep -n "React\." "$f" | head -3
  fi
done

echo ""
echo "=== 3. SubscribeButton.tsx imports ==="
rg -n "^import" frontend/src/pages/WebviewMainPage/components/SubscribeButton/SubscribeButton.tsx

Repository: Moadong/moadong

Length of output: 3215


React 임포트 없이 React.MouseEvent 타입 사용 — 컴파일 오류 발생

이 파일에는 React 임포트가 없습니다. 새로운 JSX 변환(react-jsx)은 JSX 작성 시 React를 자동으로 임포트하지만, React.MouseEvent 같은 네임스페이스 타입 참조에서는 여전히 React가 스코프에 있어야 합니다. 전역 타입 선언이 없으므로 TypeScript 컴파일 오류가 발생합니다.

수정 방법
+import { type MouseEvent } from 'react';
 import SubscribeIcon from '@/assets/images/icons/subscribe_button_icon.svg?react';
 import * as Styled from '../../WebviewMainPage.styles';

 ...

-  const handleClick = (e: React.MouseEvent) => {
+  const handleClick = (e: MouseEvent<HTMLButtonElement>) => {

참고: 이 패턴은 같은 문제를 가진 다른 여러 파일(ClubCard, ClubNameInput, DotTextEffect, TimelineRow, PrivateRoute, PhotoEditTab, ApplicationEditTab, ClubCoverEditor, QuestionContainer, InputField, CustomTextArea, ClubTag, AdminClubContext, ApplicationRowItem 등)에서도 발견됩니다. 프로젝트 전반에 걸쳐 일괄 수정이 필요합니다.

📝 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.

Suggested change
const handleClick = (e: React.MouseEvent) => {
e.stopPropagation();
onToggle();
};
const handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
onToggle();
};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@frontend/src/pages/WebviewMainPage/components/SubscribeButton/SubscribeButton.tsx`
around lines 10 - 13, The handler uses the React namespace type React.MouseEvent
but React is not imported, causing TS compile errors; fix by adding an import
for React at the top of the file (e.g., import React from 'react') or replace
the type with a non-namespace type (e.g., import type { MouseEvent } from
'react' and use MouseEvent) so the reference for React.MouseEvent in handleClick
is valid; apply the same pattern to other files listed (ClubCard, ClubNameInput,
DotTextEffect, TimelineRow, PrivateRoute, PhotoEditTab, ApplicationEditTab,
ClubCoverEditor, QuestionContainer, InputField, CustomTextArea, ClubTag,
AdminClubContext, ApplicationRowItem) to resolve project-wide TypeScript errors.

Comment on lines +86 to +91
it('유효한 날짜는 한국어 로케일로 포맷한다', () => {
const result = formatDateText('2026-03-19T10:30:00Z');
expect(result).toContain('2026');
expect(result).toContain('3');
expect(result).toContain('19');
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

formatDateText 테스트의 UTC 입력값이 극단적 타임존에서 날짜가 바뀔 수 있습니다

'2026-03-19T10:30:00Z'은 UTC-11 환경에서 3월 18일(23:30), UTC+14 환경에서 3월 20일(00:30)이 됩니다. toContain('19') 단언이 이런 타임존에서 실패할 수 있습니다.

CI가 주로 UTC에서 실행되므로 실용적 위험은 낮지만, 시간대를 T00:00:00Z(자정 UTC)으로 변경하면 UTC-X 계열 타임존에서 날짜가 바뀔 수 있고, 사실상 UTC±12 범위 모두를 안전하게 커버하는 단일 UTC 시각은 존재하지 않습니다. 가장 간단한 해결책은 jest.useFakeTimersDate 모킹, 또는 테스트 환경에 TZ=UTC를 강제하는 것입니다.

🛡️ 제안 수정: 환경 타임존 고정 또는 느슨한 단언 사용
-    it('유효한 날짜는 한국어 로케일로 포맷한다', () => {
-      const result = formatDateText('2026-03-19T10:30:00Z');
-      expect(result).toContain('2026');
-      expect(result).toContain('3');
-      expect(result).toContain('19');
-    });
+    it('유효한 날짜는 한국어 로케일로 포맷한다', () => {
+      // 타임존 의존성을 피하기 위해 날짜만 있는 ISO 문자열 사용
+      const result = formatDateText('2026-03-19');
+      expect(result).toContain('2026');
+      expect(result).toContain('3');
+      expect(result).toContain('19');
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/calendarSyncUtils.test.ts` around lines 86 - 91, The test
for formatDateText uses a UTC timestamp that can shift the calendar day in
extreme timezones; update the test to fix the environment or the clock so the
date won't roll: either set the test to force TZ='UTC' for the test case (set
process.env.TZ = 'UTC' before the it and restore after) or use Jest fake timers
(jest.useFakeTimers + jest.setSystemTime(new Date('2026-03-19T12:00:00Z'))
before the assertion and restore timers after) and keep the same input and
assertions against formatDateText; reference the formatDateText call inside the
existing "유효한 날짜는 한국어 로케일로 포맷한다" test and ensure you restore TZ or timers to
avoid leaking into other tests.

Comment on lines +1 to +28
export const loadNaverMapScript = () => {
return new Promise<void>((resolve) => {
if (window.naver?.maps) {
resolve();
return;
}

const existingScript = document.querySelector(
'script[src*="oapi.map.naver.com"]',
);
if (existingScript) {
if (window.naver?.maps) {
resolve();
} else {
existingScript.addEventListener('load', () => resolve());
}
return;
}

const script = document.createElement('script');
script.src = `https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=${import.meta.env.VITE_NAVER_MAP_CLIENT_ID}`;
script.async = true;

script.onload = () => resolve();

document.head.appendChild(script);
});
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

스크립트 로드 실패 시 Promise가 영구 대기 상태에 빠집니다

Promise 생성자에 reject가 없고 script.onerror 핸들러도 없어, 스크립트 로드가 실패하면(네트워크 오류, 잘못된 VITE_NAVER_MAP_CLIENT_ID 등) Promise는 영원히 settled 되지 않습니다. useNaverMap이 이 Promise를 await할 경우 지도 초기화가 무한히 대기하게 됩니다.

추가로, existingScript 분기에서 load 이벤트 리스너를 등록하기 전에 스크립트가 이미 로드 완료된 경우(리스너 등록 직전 load 이벤트 발생) Promise가 마찬가지로 영구 대기 상태가 됩니다.

🔧 수정 제안
 export const loadNaverMapScript = () => {
-  return new Promise<void>((resolve) => {
+  return new Promise<void>((resolve, reject) => {
     if (window.naver?.maps) {
       resolve();
       return;
     }

     const existingScript = document.querySelector(
       'script[src*="oapi.map.naver.com"]',
     );
     if (existingScript) {
       if (window.naver?.maps) {
         resolve();
       } else {
         existingScript.addEventListener('load', () => resolve());
+        existingScript.addEventListener('error', () =>
+          reject(new Error('Naver Map 스크립트 로드 실패')),
+        );
+        // 리스너 등록 전 이미 로드된 경우를 대비한 재확인
+        if (window.naver?.maps) resolve();
       }
       return;
     }

     const script = document.createElement('script');
     script.src = `https://oapi.map.naver.com/openapi/v3/maps.js?ncpKeyId=${import.meta.env.VITE_NAVER_MAP_CLIENT_ID}`;
     script.async = true;

     script.onload = () => resolve();
+    script.onerror = () => reject(new Error('Naver Map 스크립트 로드 실패'));

     document.head.appendChild(script);
   });
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/utils/loadNaverMapScript.ts` around lines 1 - 28, The
loadNaverMapScript Promise currently never rejects and can hang if the script
fails or if an existing script emits load before we attach a listener; update
loadNaverMapScript to accept and call both resolve and reject, add
script.onerror handlers for the newly created script and for existingScript to
call reject on failure, and for existingScript before attaching listeners check
its readyState (=== 'complete' or 'loaded') or whether window.naver?.maps is
already available and resolve immediately if so; also remove/cleanup event
listeners after they fire to avoid leaks and ensure both success and error paths
settle the Promise.

requestOpenExternalUrl은 http/https 외 스킴에 false를 반환하므로,
반환값이 false일 때 window.open()으로 폴백해 OS가 처리하도록 위임.
oesnuj and others added 6 commits May 3, 2026 18:13
React Router 내부 이동 대신 requestNavigateWebview로 앱에 위임해
스와이프 뒤로가기가 동작하지 않던 문제 수정.
isFestival 카드도 동일하게 처리.
useNavigator에서 내부 경로(/로 시작)도 웹뷰 여부에 따라
requestNavigateWebview로 자동 위임하도록 확장.
Banner, PromotionCard에서 isInAppWebView + requestNavigateWebview
직접 호출 제거.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
isInAppWebView 중복 호출 제거, early return으로 분기 평탄화

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 웹뷰에서 외부 링크 → requestOpenExternalUrl 위임
- 웹뷰에서 itms-apps:// → window.open 폴백
- 웹뷰에서 내부 경로 → requestNavigateWebview slug 변환

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[fix] 웹뷰 네비게이션 뒤로가기 버그 수정 및 useNavigator 개선
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

💻 FE Frontend ➕ MINOR Minor 릴리즈 📈 release 릴리즈 배포

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants