[release] FE v1.7.0#1524
Conversation
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테스트추가
|
Warning
|
| 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 |
PerformanceCard에 hideSongs?: boolean 추가(곡목 영역 숨김/클릭 비활성화). 곡목 비어있을 때 "추후 공개 예정" 표시. PerformanceList를 props 기반으로 일반화(performances, festivalDate, hideSongs). |
Filter Component Enhancement frontend/src/components/common/Filter/Filter.tsx |
웹/웹뷰 필터에 대동제 옵션 추가(/festival-busking, /webview/festival-busking). daedongDotSeen을 sessionStorage로 관리하여 알림 도트 가시성 제어. |
Page Routing & Integration frontend/src/routes/AppRoutes.tsx, frontend/src/pages/MainPage/MainPage.tsx, frontend/src/pages/WebviewMainPage/WebviewMainPage.tsx |
/festival-busking 라우트 추가(에러 바운더리로 래핑). MainPage 및 WebviewMainPage에서 DAEDONG_POPUP을 Popup에 전달하여 표시. |
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.tsx를 PopupConfig 기반으로 리팩토링, 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.
Comment @coderabbitai help to get the list of available commands and usage tips.
[fix] 동소한 공연시간표 main과 동일시
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
performances.ts 타입 변경에 맞춰 buskingDays, PerformanceCard, 관련 스토리 파일의 name 필드를 clubName으로 통일
fix: name -> clubName으로 복구
🎨 UI 변경사항을 확인해주세요
2개 스토리 변경 · 전체 56개 스토리 · 22개 컴포넌트 |
…sh-MOA-836 지도 모달 닫을 때 오류 페이지로 이동하는 현상 수정
There was a problem hiding this comment.
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 winTS import 경로를
@/별칭으로 통일해 주세요.현재 상대 경로 import가 섞여 있어 파일 이동/리팩터링 시 경로 안정성이 떨어집니다.
As per coding guidelines, "Use path alias
@/*to import fromsrc/*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
⛔ Files ignored due to path filters (1)
frontend/src/assets/images/popup/daedong.pngis excluded by!**/*.png
📒 Files selected for processing (24)
frontend/docs/features/festival/busking-timetable.mdfrontend/docs/features/main/filter.mdfrontend/docs/features/main/popup.mdfrontend/src/components/common/Filter/Filter.tsxfrontend/src/constants/eventName.tsfrontend/src/experiments/definitions.tsfrontend/src/pages/FestivalPage/BuskingPage/BuskingPage.styles.tsfrontend/src/pages/FestivalPage/BuskingPage/BuskingPage.tsxfrontend/src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.styles.tsfrontend/src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.tsxfrontend/src/pages/FestivalPage/components/DayTabsNav/DayTabsNav.tsxfrontend/src/pages/FestivalPage/components/PerformanceCard/PerformanceCard.tsxfrontend/src/pages/FestivalPage/components/PerformanceList/PerformanceList.tsxfrontend/src/pages/FestivalPage/data/buskingDays.tsfrontend/src/pages/FestivalPage/data/performances.tsfrontend/src/pages/MainPage/MainPage.tsxfrontend/src/pages/MainPage/components/Popup/Popup.stories.tsxfrontend/src/pages/MainPage/components/Popup/Popup.styles.tsfrontend/src/pages/MainPage/components/Popup/Popup.test.tsxfrontend/src/pages/MainPage/components/Popup/Popup.tsxfrontend/src/pages/MainPage/components/Popup/popupConfigs.tsfrontend/src/pages/WebviewMainPage/WebviewMainPage.tsxfrontend/src/routes/AppRoutes.tsxfrontend/src/utils/popupUtils.ts
| if (path.includes('festival-busking')) { | ||
| sessionStorage.setItem('daedong_filter_seen', 'true'); | ||
| setDaedongDotSeen(true); | ||
| } |
There was a problem hiding this comment.
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)!; |
There was a problem hiding this comment.
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.
| 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); | ||
| }; |
There was a problem hiding this comment.
동일 날짜 재선택 시 이벤트 중복 전송을 막아주세요.
현재 같은 날짜를 다시 누를 때도 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.
| 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'; | |||
There was a problem hiding this comment.
🛠️ 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.
| 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.
| mainStagePerformances: [ | ||
| { | ||
| id: 101, | ||
| clubName: 'YB', | ||
| startTime: '19:30', | ||
| endTime: '', | ||
| songs: [], | ||
| }, | ||
| ], |
There was a problem hiding this comment.
🛠️ 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.
| 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]); |
There was a problem hiding this comment.
eligible 팝업이 없어질 때 isOpen 정리가 누락되어 스크롤 잠금이 남을 수 있습니다.
Line 27에서 eligible이 없을 때 activeConfig만 null로 바뀌고 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'); |
There was a problem hiding this comment.
🧩 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
fiRepository: 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 -A2Repository: 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
fiRepository: 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-typeRepository: 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 -A2Repository: 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 -A2Repository: 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 -30Repository: 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 -nRepository: 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.
| 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.
| 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; | ||
| }; |
There was a problem hiding this comment.
스토리지 접근 예외를 처리하지 않으면 화면 렌더링이 깨질 수 있습니다.
일부 환경에서는 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.
🚀 릴리즈 PR
📦 버전 정보
💾 BE/💻 FE🚨 MAJOR/➕ MINOR/🔧 PATCHvX.Y.Z📖 버전 라벨 선택 가이드 (Semantic Versioning)
🚨 MAJORv1.0.0→v2.0.0➕ MINORv1.0.0→v1.1.0🔧 PATCHv1.0.0→v1.0.1📋 포함된 변경사항
Summary by CodeRabbit
New Features
Documentation
Chores