Skip to content

[release] FE v1.7.0#1524

Merged
seongwon030 merged 30 commits into
mainfrom
develop-fe
May 11, 2026
Merged

[release] FE v1.7.0#1524
seongwon030 merged 30 commits into
mainfrom
develop-fe

Conversation

@seongwon030
Copy link
Copy Markdown
Member

@seongwon030 seongwon030 commented May 11, 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

  • New Features

    • 2026 대동제 버스킹 시간표 페이지 추가(일별 공연 일정, 탭/화살표/스와이프 네비게이션).
    • 팝업 시스템 전면 개선: 다중 팝업 구성 및 축제 안내 팝업 추가.
    • 메인 필터에 ‘대동제’ 알림 도트 추가(세션 기반 표시).
  • Documentation

    • 버스킹 시간표, 필터 알림, 팝업 구성 문서 추가/보강.
  • Chores

    • 축제 관련 페이지뷰·날짜 전환·체류 시간 이벤트 트래킹 및 공연 시간 데이터 업데이트.

Review Change Stack

seongwon030 and others added 22 commits May 10, 2026 01:57
performances, festivalDate를 optional props로 추가해 다른 축제 페이지에서도 재사용 가능하도록 개선.
기존 IntroductionPage는 기본값으로 동작이 유지된다.
songs가 비어있을 때 '🎵 추후 공개 예정' 텍스트를 표시하고,
chevron 아이콘 숨김 및 카드 클릭 비활성화 처리.
- /festival-busking 라우트 추가
- 날짜 네비게이션 A/B 실험 구현 (tabs vs arrows, 50:50)
- framer-motion onPanEnd 기반 swipe 제스처 지원
- interaction: 'click' | 'swipe' 속성으로 Mixpanel 이벤트 구분
- performances가 없는 날짜는 탭에서 자동 제외
- 미사용 실험(mainBanner, applyButtonCopy) 제거
- FestivalDay에 mainStagePerformances 추가 (Day1~4 아티스트 라인업)
- 동아리/아티스트 섹션 구분선(SectionLabel) 렌더링
- PerformanceCard/List에 hideSongs prop 추가 (아티스트 카드 곡목 숨김)
- Performance.clubName → name 리팩토링
- Filter에 대동제 칩 추가 (/festival-busking, /webview/festival-busking)
- BuskingPage에 Filter 렌더링 (홍보 알림 dot 동적 처리)
- Filter 추가로 인한 상단 여백 조정
- PopupConfig 인터페이스 도입으로 팝업별 storageKey/sessionKey/daysToHide 분리
- configs: PopupConfig[] prop으로 첫 번째 eligible 팝업 자동 표시
- onImageClick에 trackEvent 주입하여 팝업별 트래킹 로직 자체 정의 가능
- isPopupHidden/DAYS_TO_HIDE를 src/utils/popupUtils.ts로 추출
- popupConfigs.ts에 APP_DOWNLOAD_POPUP config 분리
- daysToHide: 0 설정 시 항상 표시되는 버그 수정
- isPopupHidden(config) 시그니처에 맞게 mock config 도입
- sessionStorage 초기화 및 sessionKey 관련 테스트 케이스 추가
- daysToHide: 0 엣지 케이스 테스트 추가
- PopupConfig에 to?: string 필드 추가
- Popup 컴포넌트에 useNavigate 연결, 이미지 클릭 시 React Router로 이동
- DAEDONG_POPUP 추가 (이미지 클릭 시 /festival-busking 이동)
- MainPage를 DAEDONG_POPUP으로 교체 (축제 기간 임시)
PopupConfig 기반 리팩터 과정에서 누락된 MAIN_POPUP_NOT_SHOWN
이벤트 트래킹을 복구. eligible한 팝업이 없을 때 해당 이벤트를 전송.
[feature] 대동제 팝업 추가 및 팝업 로직 개선
sessionStorage를 활용해 대동제 필터 칩의 dot을 세션 내 최초 방문 시 표시하고,
클릭 시 사라지도록 Filter 컴포넌트 내부에서 자체 관리
- WebviewMainPage에 DAEDONG_POPUP 추가 (대동제 종료 후 제거 예정)
- PopupImage에 scale(1.1) 적용으로 이미지 내 흰 여백 크롭 처리
…ule-page-MOA-835

[feature] 대동제 공연시간표 페이지 및 탭/슬라이드 ab테스트추가
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

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

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4bbe406-10af-485a-95f0-0fd92012caaa

📥 Commits

Reviewing files that changed from the base of the PR and between dc0d2f3 and a92cd1d.

📒 Files selected for processing (1)
  • frontend/src/hooks/Map/useNaverMap.ts

Walkthrough

2026 대동제 버스킹 시간표 페이지를 추가하고, 팝업 시스템을 단일 고정 팝업에서 설정 기반의 다중 팝업 지원으로 리팩토링했다. 필터에 "대동제" 옵션과 세션 기반 알림 도트 상태를 추가했으며, 날짜 네비게이션 UI를 A/B 테스트(탭 vs 화살표)로 구분하는 실험을 정의했다.

Changes

Festival Busking Timetable & Popup System

Layer / File(s) Summary
Shared Types, Utilities & Events
frontend/src/utils/popupUtils.ts, frontend/src/constants/eventName.ts
PopupConfig 인터페이스, TrackEventFn 타입, isPopupHidden()DAYS_TO_HIDE 추가. DAEDONG2026_DAY_CHANGED, DAEDONG2026_DAY_DURATION, DAEDONG2026_BUSKING_PAGE 이벤트 상수 추가.
Navigation Experiment Definition
frontend/src/experiments/definitions.ts
festivalTimetableNavExperiment 정의 (tabs / arrows, 50:50). 기존 mainBannerExperiment, applyButtonCopyExperiment 제거 및 ALL_EXPERIMENTS 업데이트.
Festival Data Models & Performance Timings
frontend/src/pages/FestivalPage/data/buskingDays.ts, frontend/src/pages/FestivalPage/data/performances.ts
FestivalDay 타입 및 BUSKING_DAYS 상수(4일 데이터) 추가. 클럽 공연 performances의 startTime/endTime을 30분 단위로 조정(13:00–18:30 범위).
Popup Configuration Definitions
frontend/src/pages/MainPage/components/Popup/popupConfigs.ts
APP_DOWNLOAD_POPUP (앱스토어 링크 + 추적) 및 DAEDONG_POPUP (/festival-busking 네비게이션) 설정 추가.
Popup Component Refactoring
frontend/src/pages/MainPage/components/Popup/Popup.tsx, frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
configs: PopupConfig[] 기반으로 활성 팝업 선택, 이미지 프리로딩 후 오픈, viewed/closed 이벤트 추적, sessionKey/storageKey로 상태 유지. 이미지 스타일에 scale(1.1) 적용.
Day Navigation Components & Styles
frontend/src/pages/FestivalPage/components/DayTabsNav/DayTabsNav.tsx, frontend/src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.tsx, .../DayArrowsNav.styles.ts
DayTabsNav(UnderlineTabs 래퍼) 및 DayArrowsNav(이전/다음 화살표) 컴포넌트와 스타일 추가. 실험 변형에 따라 조건부 렌더링.
BuskingPage Implementation
frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx, .../BuskingPage.styles.ts
BuskingPage 추가: 사용 가능한 일자 필터링, 초기 일자 결정(오늘 기준), Mixpanel 페이지뷰/일자 변경/체류시간 추적, 탭/화살표/스와이프 네비게이션 지원, 메인/클럽 공연 섹션 조건부 렌더링.
Performance Card & List Updates
frontend/src/pages/FestivalPage/components/PerformanceCard/PerformanceCard.tsx, .../PerformanceList.tsx
PerformanceCardhideSongs?: boolean 추가(곡목 영역 숨김/클릭 비활성화). 곡목 비어있을 때 "추후 공개 예정" 표시. PerformanceList를 props 기반으로 일반화(performances, festivalDate, hideSongs).
Filter Component Enhancement
frontend/src/components/common/Filter/Filter.tsx
웹/웹뷰 필터에 대동제 옵션 추가(/festival-busking, /webview/festival-busking). daedongDotSeensessionStorage로 관리하여 알림 도트 가시성 제어.
Page Routing & Integration
frontend/src/routes/AppRoutes.tsx, frontend/src/pages/MainPage/MainPage.tsx, frontend/src/pages/WebviewMainPage/WebviewMainPage.tsx
/festival-busking 라우트 추가(에러 바운더리로 래핑). MainPageWebviewMainPage에서 DAEDONG_POPUPPopup에 전달하여 표시.
Map Hook Cleanup
frontend/src/hooks/Map/useNaverMap.ts
비동기 로드 콜백이 정리 후 실행되지 않도록 isCleaned 플래그 추가, cleanup 시 안전한 mapInstance.destroy() 호출 및 외부 ref 초기화.
Tests, Storybook & Documentation
frontend/src/pages/MainPage/components/Popup/Popup.test.tsx, .../Popup.stories.tsx, frontend/docs/features/*
Popup.test.tsxPopupConfig 기반으로 리팩토링, Storybook 스토리의 args/스토리데코레이터 갱신, 버스킹/팝업/필터 관련 문서 추가.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Moadong/moadong#1518: 동일한 festival busking timetable 기능 추가(BuskingPage, DayTabsNav/DayArrowsNav, BUSKING_DAYS, experiment, events).
  • Moadong/moadong#1519: Popup 리팩터링 및 DAEDONG_POPUP → /festival-busking 라우팅 변경과 직접적으로 겹칩니다.
  • Moadong/moadong#1317: frontend/src/pages/FestivalPage/data/performances.ts의 startTime/endTime 조정(30분 이동) 관련 변경이 일치합니다.

Suggested labels

✨ Feature, AB TEST

Suggested reviewers

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

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive PR 제목 'Develop fe'는 너무 모호하고 구체적이지 않아 실제 변경 사항의 핵심을 전달하지 못합니다. 제목을 변경하여 주요 기능(예: '2026 대동제 버스킹 시간표 페이지 및 팝업 시스템 리팩토링')을 명확하게 설명해주세요.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop-fe

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@seongwon030 seongwon030 added 💻 FE Frontend 📈 release 릴리즈 배포 ➕ MINOR Minor 릴리즈 labels May 11, 2026
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

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

Project Deployment Actions Updated (UTC)
moadong Ready Ready Preview, Comment May 11, 2026 6:12am

performances.ts 타입 변경에 맞춰 buskingDays, PerformanceCard,
관련 스토리 파일의 name 필드를 clubName으로 통일
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 11, 2026

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

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

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

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

…sh-MOA-836

지도 모달 닫을 때 오류 페이지로 이동하는 현상 수정
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: 11

🧹 Nitpick comments (2)
frontend/src/components/common/Filter/Filter.tsx (1)

41-41: 💤 Low value

경로 매칭 정확도 개선을 고려하세요.

현재 path.includes('festival-busking')는 부분 문자열 매칭을 사용합니다. 현재 필터 옵션들에서는 문제가 없지만, 미래에 'festival-busking'을 포함하는 다른 경로가 추가될 경우 의도치 않게 매칭될 수 있습니다.

♻️ 더 정확한 매칭 방식

옵션 1: 정확한 경로 매칭

-    if (path.includes('festival-busking')) {
+    if (path === '/festival-busking' || path === '/webview/festival-busking') {

옵션 2: 경로 끝부분 매칭

-    if (path.includes('festival-busking')) {
+    if (path.endsWith('/festival-busking')) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/common/Filter/Filter.tsx` at line 41, The current
conditional if (path.includes('festival-busking')) in Filter.tsx is using a
substring match which can produce false positives; change it to a stricter match
(for example replace the includes check with an exact match or boundary-aware
test). Edit the condition in the component where path is inspected (the if in
Filter.tsx) to use one of: path === '/festival-busking',
path.startsWith('/festival-busking/') if you expect nested routes, or a regex
like /^\/festival-busking(\/|$)/. Also ensure you strip query/search/hash (e.g.,
use location.pathname) before matching so query strings won’t affect the
comparison.
frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx (1)

14-18: ⚡ Quick win

TS import 경로를 @/ 별칭으로 통일해 주세요.

현재 상대 경로 import가 섞여 있어 파일 이동/리팩터링 시 경로 안정성이 떨어집니다.

As per coding guidelines, "Use path alias @/* to import from src/* in all TypeScript files".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx` around lines 14
- 18, Imports in BuskingPage use relative paths which break path-alias
consistency; update all imports (DayArrowsNav, DayTabsNav, PerformanceList,
BUSKING_DAYS and Styled) to use the project TypeScript alias form (start with
"@/") pointing to the same modules under src so imports become alias-based and
consistent across moves/refactors; keep the same exported symbols and update
only the import paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/docs/features/festival/busking-timetable.md`:
- Around line 11-20: The fenced code block in busking-timetable.md (the diagram
showing BuskingPage, DayTabsNav, DayArrowsNav, SectionLabel, PerformanceList,
TimelineRow, PerformanceCard) is missing a language tag which triggers
markdownlint MD040; fix it by adding a language identifier (e.g., ```text) to
the opening fence so the block becomes fenced with a language, ensuring the
diagram is recognized as a code block by linters and avoids the MD040 warning.

In `@frontend/src/components/common/Filter/Filter.tsx`:
- Around line 31-33: Wrap the sessionStorage access used in the initial state
for daedongDotSeen in a try-catch to avoid exceptions in environments where
storage is disabled: change the useState initializer that calls
sessionStorage.getItem('daedong_filter_seen') to a safe initializer that catches
any error and returns a sensible default (false) on failure; keep using the same
state variables (daedongDotSeen, setDaedongDotSeen) and ensure subsequent
reads/writes to sessionStorage also guard against exceptions.
- Around line 41-44: Wrap the sessionStorage.setItem call in a try/catch so
storage errors (quota/full or disabled) don’t throw; specifically, around the
block that checks path.includes('festival-busking') call
sessionStorage.setItem('daedong_filter_seen','true') inside try, catch any error
and handle it (e.g., console.warn or silent noop), and still call
setDaedongDotSeen(true) so the UI state updates even if storage fails.

In `@frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx`:
- Around line 63-82: The handler handleDayChange sends DAY_DURATION and
DAY_CHANGED even when the newly selected dayId equals the current activeDayId;
add a guard at the top of handleDayChange to return early if dayId ===
activeDayId to prevent duplicate tracking, ensuring you do this before using
dayStartTime.current or calling trackEvent; keep dayStartTime.current and
setActiveDayId untouched when it's the same day.
- Line 45: The code uses a non-null assertion on activeDay (const activeDay =
BUSKING_DAYS.find((d) => d.id === activeDayId)!) which can crash at runtime if
activeDayId doesn't match; change this to safely handle undefined by assigning
const activeDay = BUSKING_DAYS.find((d) => d.id === activeDayId) and then either
early-return a fallback UI (e.g., return null or a "not found" message) or
render a safe default, and optionally log a warning; update BuskingPage.tsx to
guard any usage of activeDay (title, props, etc.) to avoid dereferencing when
it's undefined.

In `@frontend/src/pages/FestivalPage/components/DayTabsNav/DayTabsNav.tsx`:
- Line 2: 현재 DayTabsNav.tsx의 FestivalDay 타입을 가져오는 절대 상대경로 import (import ...
from '../../data/buskingDays')를 프로젝트 규칙에 맞게 경로 별칭으로 바꿔 주세요; 즉 파일 내 FestivalDay
타입 import 문을 '../../data/buskingDays' 대신 프로젝트 alias 규칙에 맞는 '@/...' 형식으로 변경하여
상대경로 취약성을 제거하고 일관된 alias 사용을 보장하세요.

In `@frontend/src/pages/FestivalPage/data/buskingDays.ts`:
- Around line 23-31: The array literal mainStagePerformances (and other similar
arrays) uses an empty string ('') as a sentinel for an unknown time which breaks
the HH:mm contract; change those sentinel values to null and update the
Performance/endTime type to allow null (e.g., endTime: string | null) so
parsing/formatting code can handle absent times safely; update all occurrences
referenced (the other performance arrays around the file) and ensure any
consumers check for null rather than empty string.
- Line 1: The import in buskingDays.ts currently uses a relative path to import
the Performance type; change the import to use the project path alias `@/*`
(e.g. import type { Performance } from '@/pages/FestivalPage/data/performances')
so all TypeScript imports follow the alias convention; update the import
statement that references Performance accordingly.

In `@frontend/src/pages/MainPage/components/Popup/Popup.tsx`:
- Around line 22-30: When the eligible popup is not found the code only calls
setActiveConfig(eligible ?? null) and tracks USER_EVENT.MAIN_POPUP_NOT_SHOWN but
fails to reset isOpen/scroll lock; update the effect so that when eligible is
falsy you also setIsOpen(false) (or explicitly reset
document.body.style.overflow = ''), ensuring any prior scroll lock is released;
adjust the effect that computes eligible (using configs, isMobile,
isPopupHidden) to call setIsOpen(false) right after setActiveConfig(null) and
before/after trackEvent as appropriate.

In `@frontend/src/pages/MainPage/components/Popup/popupConfigs.ts`:
- Line 20: window.open(getAppStoreLink(), '_blank') in popupConfigs.ts allows
the new tab to access window.opener; update the window.open call to prevent this
by passing the feature string 'noopener,noreferrer' as the third argument (e.g.
window.open(getAppStoreLink(), '_blank', 'noopener,noreferrer')) or, if you
prefer, capture the returned window and set newWindow.opener = null after
opening; modify the invocation that references getAppStoreLink() accordingly.

In `@frontend/src/utils/popupUtils.ts`:
- Around line 20-31: The isPopupHidden function currently calls
sessionStorage.getItem and localStorage.getItem without guarding against storage
access exceptions; wrap both storage accesses in try/catch blocks inside
isPopupHidden (referencing the function name isPopupHidden, config.sessionKey
and config.storageKey) and on any exception return false as the safe default;
preserve existing logic for hiddenDate parsing (parseInt) and days calculation
(using DAYS_TO_HIDE and config.daysToHide) but ensure any thrown error from
sessionStorage/localStorage.getItem is caught and logged or ignored and causes
the function to return false.

---

Nitpick comments:
In `@frontend/src/components/common/Filter/Filter.tsx`:
- Line 41: The current conditional if (path.includes('festival-busking')) in
Filter.tsx is using a substring match which can produce false positives; change
it to a stricter match (for example replace the includes check with an exact
match or boundary-aware test). Edit the condition in the component where path is
inspected (the if in Filter.tsx) to use one of: path === '/festival-busking',
path.startsWith('/festival-busking/') if you expect nested routes, or a regex
like /^\/festival-busking(\/|$)/. Also ensure you strip query/search/hash (e.g.,
use location.pathname) before matching so query strings won’t affect the
comparison.

In `@frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx`:
- Around line 14-18: Imports in BuskingPage use relative paths which break
path-alias consistency; update all imports (DayArrowsNav, DayTabsNav,
PerformanceList, BUSKING_DAYS and Styled) to use the project TypeScript alias
form (start with "@/") pointing to the same modules under src so imports become
alias-based and consistent across moves/refactors; keep the same exported
symbols and update only the import paths.
🪄 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: e80fa662-a437-4e65-89f4-a2127a15edf2

📥 Commits

Reviewing files that changed from the base of the PR and between b06a7a4 and dc0d2f3.

⛔ Files ignored due to path filters (1)
  • frontend/src/assets/images/popup/daedong.png is excluded by !**/*.png
📒 Files selected for processing (24)
  • frontend/docs/features/festival/busking-timetable.md
  • frontend/docs/features/main/filter.md
  • frontend/docs/features/main/popup.md
  • frontend/src/components/common/Filter/Filter.tsx
  • frontend/src/constants/eventName.ts
  • frontend/src/experiments/definitions.ts
  • frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.styles.ts
  • frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx
  • frontend/src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.styles.ts
  • frontend/src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.tsx
  • frontend/src/pages/FestivalPage/components/DayTabsNav/DayTabsNav.tsx
  • frontend/src/pages/FestivalPage/components/PerformanceCard/PerformanceCard.tsx
  • frontend/src/pages/FestivalPage/components/PerformanceList/PerformanceList.tsx
  • frontend/src/pages/FestivalPage/data/buskingDays.ts
  • frontend/src/pages/FestivalPage/data/performances.ts
  • frontend/src/pages/MainPage/MainPage.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.stories.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.styles.ts
  • frontend/src/pages/MainPage/components/Popup/Popup.test.tsx
  • frontend/src/pages/MainPage/components/Popup/Popup.tsx
  • frontend/src/pages/MainPage/components/Popup/popupConfigs.ts
  • frontend/src/pages/WebviewMainPage/WebviewMainPage.tsx
  • frontend/src/routes/AppRoutes.tsx
  • frontend/src/utils/popupUtils.ts

Comment thread frontend/docs/features/festival/busking-timetable.md
Comment thread frontend/src/components/common/Filter/Filter.tsx
Comment on lines +41 to +44
if (path.includes('festival-busking')) {
sessionStorage.setItem('daedong_filter_seen', 'true');
setDaedongDotSeen(true);
}
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

sessionStorage 쓰기 작업에도 에러 처리가 필요합니다.

sessionStorage.setItem()도 저장소 용량 초과나 비활성화 상태에서 예외를 발생시킬 수 있습니다. 사용자 경험을 보호하기 위해 에러 처리를 추가하세요.

🛡️ 제안하는 안전한 구현
  const handleFilterOptionClick = (path: string) => {
    trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { path });
    if (path.includes('festival-busking')) {
-      sessionStorage.setItem('daedong_filter_seen', 'true');
-      setDaedongDotSeen(true);
+      try {
+        sessionStorage.setItem('daedong_filter_seen', 'true');
+        setDaedongDotSeen(true);
+      } catch {
+        // sessionStorage 실패 시에도 state는 업데이트 (현재 세션 동안만 유효)
+        setDaedongDotSeen(true);
+      }
    }
    navigate(path);
  };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/components/common/Filter/Filter.tsx` around lines 41 - 44, Wrap
the sessionStorage.setItem call in a try/catch so storage errors (quota/full or
disabled) don’t throw; specifically, around the block that checks
path.includes('festival-busking') call
sessionStorage.setItem('daedong_filter_seen','true') inside try, catch any error
and handle it (e.g., console.warn or silent noop), and still call
setDaedongDotSeen(true) so the UI state updates even if storage fails.

const dayStartTime = useRef(Date.now());
const activeDayIdRef = useRef(activeDayId);

const activeDay = BUSKING_DAYS.find((d) => d.id === activeDayId)!;
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

activeDay non-null assertion은 런타임 크래시 위험이 있습니다.

activeDayId가 데이터와 불일치하면 페이지가 바로 깨집니다. 안전한 fallback 또는 early return이 필요합니다.

💡 제안 수정안
-  const activeDay = BUSKING_DAYS.find((d) => d.id === activeDayId)!;
+  const activeDay =
+    BUSKING_DAYS.find((d) => d.id === activeDayId) ??
+    availableDays[0] ??
+    BUSKING_DAYS[0];
+
+  if (!activeDay) return null;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx` at line 45, The
code uses a non-null assertion on activeDay (const activeDay =
BUSKING_DAYS.find((d) => d.id === activeDayId)!) which can crash at runtime if
activeDayId doesn't match; change this to safely handle undefined by assigning
const activeDay = BUSKING_DAYS.find((d) => d.id === activeDayId) and then either
early-return a fallback UI (e.g., return null or a "not found" message) or
render a safe default, and optionally log a warning; update BuskingPage.tsx to
guard any usage of activeDay (title, props, etc.) to avoid dereferencing when
it's undefined.

Comment on lines +63 to +82
const handleDayChange = (
dayId: string,
interaction: 'click' | 'swipe' = 'click',
) => {
const duration = Date.now() - dayStartTime.current;
trackEvent(USER_EVENT.DAEDONG2026_DAY_DURATION, {
day: activeDayId,
nav_variant: navVariant,
duration,
duration_seconds: Math.round(duration / 1000),
});
trackEvent(USER_EVENT.DAEDONG2026_DAY_CHANGED, {
from_day: activeDayId,
to_day: dayId,
nav_variant: navVariant,
interaction,
});
dayStartTime.current = Date.now();
setActiveDayId(dayId);
};
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

동일 날짜 재선택 시 이벤트 중복 전송을 막아주세요.

현재 같은 날짜를 다시 누를 때도 DAY_DURATION/DAY_CHANGED가 전송되어 A/B 실험 지표가 왜곡될 수 있습니다.

💡 제안 수정안
   const handleDayChange = (
     dayId: string,
     interaction: 'click' | 'swipe' = 'click',
   ) => {
+    if (dayId === activeDayId) return;
+
     const duration = Date.now() - dayStartTime.current;
     trackEvent(USER_EVENT.DAEDONG2026_DAY_DURATION, {
       day: activeDayId,
📝 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 handleDayChange = (
dayId: string,
interaction: 'click' | 'swipe' = 'click',
) => {
const duration = Date.now() - dayStartTime.current;
trackEvent(USER_EVENT.DAEDONG2026_DAY_DURATION, {
day: activeDayId,
nav_variant: navVariant,
duration,
duration_seconds: Math.round(duration / 1000),
});
trackEvent(USER_EVENT.DAEDONG2026_DAY_CHANGED, {
from_day: activeDayId,
to_day: dayId,
nav_variant: navVariant,
interaction,
});
dayStartTime.current = Date.now();
setActiveDayId(dayId);
};
const handleDayChange = (
dayId: string,
interaction: 'click' | 'swipe' = 'click',
) => {
if (dayId === activeDayId) return;
const duration = Date.now() - dayStartTime.current;
trackEvent(USER_EVENT.DAEDONG2026_DAY_DURATION, {
day: activeDayId,
nav_variant: navVariant,
duration,
duration_seconds: Math.round(duration / 1000),
});
trackEvent(USER_EVENT.DAEDONG2026_DAY_CHANGED, {
from_day: activeDayId,
to_day: dayId,
nav_variant: navVariant,
interaction,
});
dayStartTime.current = Date.now();
setActiveDayId(dayId);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsx` around lines 63
- 82, The handler handleDayChange sends DAY_DURATION and DAY_CHANGED even when
the newly selected dayId equals the current activeDayId; add a guard at the top
of handleDayChange to return early if dayId === activeDayId to prevent duplicate
tracking, ensuring you do this before using dayStartTime.current or calling
trackEvent; keep dayStartTime.current and setActiveDayId untouched when it's the
same day.

@@ -0,0 +1,304 @@
import type { Performance } from './performances';
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.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

import 경로를 alias(@/*)로 통일해 주세요.

상대경로 대신 alias를 사용하면 경로 안정성과 일관성이 좋아집니다.

변경 예시
-import type { Performance } from './performances';
+import type { Performance } from '@/pages/FestivalPage/data/performances';

As per coding guidelines, "Use path alias @/* to import from src/* in all TypeScript files".

📝 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
import type { Performance } from './performances';
import type { Performance } from '@/pages/FestivalPage/data/performances';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/FestivalPage/data/buskingDays.ts` at line 1, The import in
buskingDays.ts currently uses a relative path to import the Performance type;
change the import to use the project path alias `@/*` (e.g. import type {
Performance } from '@/pages/FestivalPage/data/performances') so all TypeScript
imports follow the alias convention; update the import statement that references
Performance accordingly.

Comment on lines +23 to +31
mainStagePerformances: [
{
id: 101,
clubName: 'YB',
startTime: '19:30',
endTime: '',
songs: [],
},
],
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.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

메인 스테이지 시간에 빈 문자열('') 센티넬 사용은 피해주세요.

Performance 시간 필드가 HH:mm 전제를 갖는데 빈 문자열이 섞이면 포맷 계약이 깨집니다. 이후 시간 정렬/파싱/표시 분기에서 런타임 리스크가 커지므로, “미정”은 null 가능 타입으로 명시해 처리하는 편이 안전합니다.

수정 방향 예시
+interface MainStagePerformance extends Omit<Performance, 'startTime' | 'endTime'> {
+  startTime: string | null;
+  endTime: string | null;
+}
...
-  mainStagePerformances?: Performance[];
+  mainStagePerformances?: MainStagePerformance[];
...
-        endTime: '',
+        endTime: null,

Also applies to: 99-113, 203-217, 287-301

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/FestivalPage/data/buskingDays.ts` around lines 23 - 31,
The array literal mainStagePerformances (and other similar arrays) uses an empty
string ('') as a sentinel for an unknown time which breaks the HH:mm contract;
change those sentinel values to null and update the Performance/endTime type to
allow null (e.g., endTime: string | null) so parsing/formatting code can handle
absent times safely; update all occurrences referenced (the other performance
arrays around the file) and ensure any consumers check for null rather than
empty string.

Comment on lines +22 to +30
const eligible = configs.find((config) => {
if (config.mobileOnly && !isMobile) return false;
return !isPopupHidden(config);
});
setActiveConfig(eligible ?? null);
if (!eligible) {
trackEvent(USER_EVENT.MAIN_POPUP_NOT_SHOWN);
}
}, [configs, isMobile, trackEvent]);
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

eligible 팝업이 없어질 때 isOpen 정리가 누락되어 스크롤 잠금이 남을 수 있습니다.

Line 27에서 eligible이 없을 때 activeConfignull로 바뀌고 isOpen은 유지되어, document.body.style.overflow='hidden' 상태가 잔류할 수 있습니다.

수정 예시
   useEffect(() => {
     const eligible = configs.find((config) => {
       if (config.mobileOnly && !isMobile) return false;
       return !isPopupHidden(config);
     });
     setActiveConfig(eligible ?? null);
     if (!eligible) {
+      setIsOpen(false);
+      setImageLoaded(false);
       trackEvent(USER_EVENT.MAIN_POPUP_NOT_SHOWN);
     }
   }, [configs, isMobile, trackEvent]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/MainPage/components/Popup/Popup.tsx` around lines 22 - 30,
When the eligible popup is not found the code only calls
setActiveConfig(eligible ?? null) and tracks USER_EVENT.MAIN_POPUP_NOT_SHOWN but
fails to reset isOpen/scroll lock; update the effect so that when eligible is
falsy you also setIsOpen(false) (or explicitly reset
document.body.style.overflow = ''), ensuring any prior scroll lock is released;
adjust the effect that computes eligible (using configs, isMobile,
isPopupHidden) to call setIsOpen(false) right after setActiveConfig(null) and
before/after trackEvent as appropriate.

popupType: 'app_download',
platform: detectPlatform(),
});
window.open(getAppStoreLink(), '_blank');
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

🧩 Analysis chain

🏁 Script executed:

# Check if the file exists and examine its content
if [ -f "frontend/src/pages/MainPage/components/Popup/popupConfigs.ts" ]; then
  echo "=== File found. Reading content ==="
  wc -l "frontend/src/pages/MainPage/components/Popup/popupConfigs.ts"
  echo "=== First 50 lines ==="
  head -50 "frontend/src/pages/MainPage/components/Popup/popupConfigs.ts" | cat -n
else
  echo "File not found at specified path"
  # Try to find the file
  echo "=== Searching for popupConfigs files ==="
  find . -name "popupConfigs.ts" 2>/dev/null
fi

Repository: Moadong/moadong

Length of output: 1433


🏁 Script executed:

# Search for all window.open usages in the frontend directory
echo "=== All window.open usages in frontend ==="
rg "window\.open\(" frontend/src --type ts --type tsx -n -B2 -A2

Repository: Moadong/moadong

Length of output: 148


🏁 Script executed:

# Check the specific line 20 context more carefully
if [ -f "frontend/src/pages/MainPage/components/Popup/popupConfigs.ts" ]; then
  echo "=== Context around line 20 ==="
  sed -n '15,25p' "frontend/src/pages/MainPage/components/Popup/popupConfigs.ts" | cat -n
fi

Repository: Moadong/moadong

Length of output: 453


🏁 Script executed:

# Search for window.open usages more broadly without type specifiers
echo "=== All window.open usages in frontend ==="
rg "window\.open\(" frontend/src -n -B2 -A2 --no-type

Repository: Moadong/moadong

Length of output: 556


🏁 Script executed:

# Check if there are any other similar popup configs or callbacks
echo "=== Search for callbacks that might open windows ==="
rg "onImageClick|onClick.*window" frontend/src -n --no-type -B2 -A2

Repository: Moadong/moadong

Length of output: 566


🏁 Script executed:

# Search for window.open usages without type filter
echo "=== All window.open usages in frontend ==="
rg "window\.open\(" frontend/src -n -B2 -A2

Repository: Moadong/moadong

Length of output: 1371


🏁 Script executed:

# Check if there are any security-related utilities or helpers for opening external links
echo "=== Search for link opening utilities ==="
rg "window\.open|href.*_blank|openExternal|openLink" frontend/src -n | head -30

Repository: Moadong/moadong

Length of output: 1121


🏁 Script executed:

# Look for best practices or security patterns in the codebase
echo "=== Search for noopener or noreferrer usage ==="
rg "noopener|noreferrer" frontend/src -n

Repository: Moadong/moadong

Length of output: 964


외부 탭 오픈 시 noopener,noreferrer를 추가해 주세요.

Line 20의 window.open(..., '_blank')는 새 탭이 window.opener에 접근할 수 있어 보안상 안전하지 않습니다.

수정 예시
-    window.open(getAppStoreLink(), '_blank');
+    window.open(getAppStoreLink(), '_blank', 'noopener,noreferrer');
📝 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
window.open(getAppStoreLink(), '_blank');
window.open(getAppStoreLink(), '_blank', 'noopener,noreferrer');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/pages/MainPage/components/Popup/popupConfigs.ts` at line 20,
window.open(getAppStoreLink(), '_blank') in popupConfigs.ts allows the new tab
to access window.opener; update the window.open call to prevent this by passing
the feature string 'noopener,noreferrer' as the third argument (e.g.
window.open(getAppStoreLink(), '_blank', 'noopener,noreferrer')) or, if you
prefer, capture the returned window and set newWindow.opener = null after
opening; modify the invocation that references getAppStoreLink() accordingly.

Comment on lines +20 to +31
export const isPopupHidden = (config: PopupConfig): boolean => {
if (sessionStorage.getItem(config.sessionKey)) return true;

const hiddenDate = localStorage.getItem(config.storageKey);
if (!hiddenDate) return false;

const daysSinceHidden =
(Date.now() - parseInt(hiddenDate)) / (1000 * 60 * 60 * 24);
const daysToHide =
config.daysToHide !== undefined ? config.daysToHide : DAYS_TO_HIDE;
return daysSinceHidden < daysToHide;
};
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

스토리지 접근 예외를 처리하지 않으면 화면 렌더링이 깨질 수 있습니다.

일부 환경에서는 sessionStorage/localStorage.getItem이 예외를 던질 수 있습니다. 여기서 예외를 흡수하고 기본값(false)로 복구하는 방어 로직이 필요합니다.

💡 제안 수정안
 export const isPopupHidden = (config: PopupConfig): boolean => {
-  if (sessionStorage.getItem(config.sessionKey)) return true;
+  try {
+    if (sessionStorage.getItem(config.sessionKey)) return true;
 
-  const hiddenDate = localStorage.getItem(config.storageKey);
-  if (!hiddenDate) return false;
+    const hiddenDate = localStorage.getItem(config.storageKey);
+    if (!hiddenDate) return false;
 
-  const daysSinceHidden =
-    (Date.now() - parseInt(hiddenDate)) / (1000 * 60 * 60 * 24);
-  const daysToHide =
-    config.daysToHide !== undefined ? config.daysToHide : DAYS_TO_HIDE;
-  return daysSinceHidden < daysToHide;
+    const hiddenTs = Number(hiddenDate);
+    if (!Number.isFinite(hiddenTs)) return false;
+
+    const daysSinceHidden = (Date.now() - hiddenTs) / (1000 * 60 * 60 * 24);
+    const daysToHide =
+      config.daysToHide !== undefined ? config.daysToHide : DAYS_TO_HIDE;
+    return daysSinceHidden < daysToHide;
+  } catch {
+    return false;
+  }
 };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/src/utils/popupUtils.ts` around lines 20 - 31, The isPopupHidden
function currently calls sessionStorage.getItem and localStorage.getItem without
guarding against storage access exceptions; wrap both storage accesses in
try/catch blocks inside isPopupHidden (referencing the function name
isPopupHidden, config.sessionKey and config.storageKey) and on any exception
return false as the safe default; preserve existing logic for hiddenDate parsing
(parseInt) and days calculation (using DAYS_TO_HIDE and config.daysToHide) but
ensure any thrown error from sessionStorage/localStorage.getItem is caught and
logged or ignored and causes the function to return false.

@seongwon030 seongwon030 merged commit 4316969 into main May 11, 2026
5 checks passed
@seongwon030 seongwon030 changed the title Develop fe [release] FE v1.7.0 May 12, 2026
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.

3 participants