Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d1ec5d6
refactor(festival): PerformanceList props 기반 공통화
seongwon030 May 9, 2026
7e3e7ef
feat(festival): PerformanceCard 곡 미공개 상태 처리
seongwon030 May 9, 2026
3ab456d
feat(festival): 2026 대동제 버스킹 시간표 페이지 구현
seongwon030 May 9, 2026
3e5fe79
feat(festival): 아티스트 공연 섹션 추가 및 Performance 타입 개선
seongwon030 May 10, 2026
436b5da
feat: 공연시간표 top,bottom 마진 늘림
seongwon030 May 10, 2026
a3b2adc
feat(festival): 대동제 필터칩 추가 및 BuskingPage Filter 통합
seongwon030 May 10, 2026
c75153e
feat: YB 시작시간 추가
seongwon030 May 10, 2026
7eb465f
refactor(festival): date-fns format으로 날짜 문자열 생성 개선
seongwon030 May 10, 2026
9c6978f
fix(festival): DayArrowsNav findIndex -1 및 빈 days 가드 추가
seongwon030 May 10, 2026
554525c
fix: 대동제로 이벤트명 변경
seongwon030 May 10, 2026
1dce9eb
refactor(main): Popup을 PopupConfig 기반 다중 팝업 구조로 변경
seongwon030 May 10, 2026
abd86c4
test(main): Popup 테스트를 popupUtils 기반으로 업데이트
seongwon030 May 10, 2026
594636e
docs: Popup 다중 팝업 구조 문서 추가
seongwon030 May 10, 2026
67b586b
refactor(main): PopupConfig에 내부 라우팅 to 필드 추가
seongwon030 May 10, 2026
acfdbfb
feat(main): 대동제 팝업 추가 및 메인 페이지 교체
seongwon030 May 10, 2026
6c0ff67
fix(main): Popup 미표시 시 MAIN_POPUP_NOT_SHOWN 트래킹 복구
seongwon030 May 11, 2026
5b2f644
Merge pull request #1519 from Moadong/feature/festival-popup
seongwon030 May 11, 2026
b084e86
feat(main): 대동제 필터칩 NotificationDot 세션 기반 표시 추가
seongwon030 May 11, 2026
200a4ea
feat(main): 웹뷰 대동제 팝업 추가 및 이미지 여백 CSS 보정
seongwon030 May 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions frontend/docs/features/festival/busking-timetable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 2026 대동제 버스킹 시간표 페이지

4일간 진행되는 버스킹 공연 시간표를 제공하는 페이지. 날짜 네비게이션 방식을 A/B 테스트로 검증한다.

## 라우트

`/festival-busking` → `BuskingPage`

## 페이지 구조

```
BuskingPage
├── DayTabsNav (variant: 'tabs') — UnderlineTabs로 날짜 탭
├── DayArrowsNav (variant: 'arrows') — ‹ 날짜 › 화살표 네비게이션
├── SectionLabel "동아리 공연" — 두 섹션 모두 있을 때만 표시
├── PerformanceList (performances) — 동아리 공연 목록
├── SectionLabel "🎤 아티스트 공연"
└── PerformanceList (mainStagePerformances, hideSongs) — 아티스트 공연 목록
└── TimelineRow + PerformanceCard
```
Comment thread
seongwon030 marked this conversation as resolved.

## A/B 실험

| 항목 | 내용 |
| --------- | --------------------------- |
| 실험 키 | `festival_timetable_nav_v1` |
| Variant A | `tabs` — 날짜 4개 탭 |
| Variant B | `arrows` — 화살표 순차 이동 |
| 비율 | 50 : 50 |

실험 정의: `src/experiments/definitions.ts` → `festivalTimetableNavExperiment`

## Mixpanel 이벤트

| 이벤트명 | 트리거 | 주요 속성 |
| ----------------------------------- | ----------------------- | -------------------------------------------------- |
| `2026-daedong 버스킹 시간표 페이지` | 페이지 진입 | — |
| `2026-daedong Day Changed` | 날짜 전환 | `from_day`, `to_day`, `nav_variant`, `interaction` |
| `2026-daedong Day Duration` | 날짜 이탈 / 페이지 이탈 | `day`, `nav_variant`, `duration_seconds` |

`interaction` 값: `'click'` (버튼) / `'swipe'` (터치 제스처)

## 데이터

`src/pages/FestivalPage/data/buskingDays.ts` — `BUSKING_DAYS: FestivalDay[]`

- `performances: []`이고 `mainStagePerformances`도 없는 날짜는 탭/화살표에서 제외
- 오늘 날짜가 축제 기간이면 해당 날짜로 자동 진입
- `mainStagePerformances`: 동아리 공연 종료 후 진행되는 아티스트 공연 (Day1~4 모두 포함)

### 아티스트 공연 라인업

| 날짜 | 아티스트 |
| ---- | ------------------- |
| Day1 | YB |
| Day2 | 최예나, 이창섭 |
| Day3 | FIFTY FIFTY, 비와이 |
| Day4 | V.O.S, 청하 |

## 관련 코드

- `src/pages/FestivalPage/BuskingPage/BuskingPage.tsx` — 메인 페이지, 실험 분기 및 이벤트 트래킹
- `src/pages/FestivalPage/components/DayTabsNav/DayTabsNav.tsx` — tabs variant
- `src/pages/FestivalPage/components/DayArrowsNav/DayArrowsNav.tsx` — arrows variant
- `src/pages/FestivalPage/data/buskingDays.ts` — 4일치 공연 데이터 (`performances` + `mainStagePerformances`)
- `src/pages/FestivalPage/components/PerformanceList/PerformanceList.tsx` — `performances`, `festivalDate`, `hideSongs` props 지원
- `src/pages/FestivalPage/components/PerformanceCard/PerformanceCard.tsx` — `hideSongs`이면 곡목 영역 숨김, `songs: []`이면 "🎵 추후 공개 예정" 표시
12 changes: 12 additions & 0 deletions frontend/docs/features/main/filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Filter 칩 NotificationDot — 대동제 세션 기반 표시

`Filter` 컴포넌트의 `NotificationDot`은 두 가지 조건으로 표시된다.

- `홍보`: `usePromotionNotification` 훅이 반환하는 서버 데이터 기반
- `대동제`: 세션 기반 — 첫 방문 시 항상 표시, 칩 클릭 시 사라짐 (탭 종료 후 재표시)

`대동제` dot 상태는 `Filter` 내부에서 `sessionStorage('daedong_filter_seen')`으로 자체 관리하며, `MainPage.tsx`나 Filter props 변경 없이 동작한다.

## 관련 코드

- `src/components/common/Filter/Filter.tsx` — `daedongDotSeen` state, `handleFilterOptionClick` 내 sessionStorage 처리
34 changes: 34 additions & 0 deletions frontend/docs/features/main/popup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Popup — 다중 팝업 지원 구조

메인 페이지에서 노출되는 팝업 시스템. `PopupConfig` 배열로 여러 팝업을 관리하며, 첫 번째 eligible 팝업을 자동으로 표시한다.

## 구조

- `configs: PopupConfig[]`를 prop으로 받아, 조건을 통과하는 첫 번째 팝업을 표시
- 각 팝업은 독립적인 `storageKey` / `sessionKey`를 가짐
- `daysToHide: 0`으로 설정하면 "다시 보지 않기" 클릭 후에도 항상 재표시

## PopupConfig 필드

| 필드 | 타입 | 설명 |
| -------------- | ---------------------- | ---------------------------------------- |
| `id` | `string` | Mixpanel `popupType` 값으로 사용 |
| `storageKey` | `string` | "다시 보지 않기" localStorage 키 |
| `sessionKey` | `string` | "닫기" sessionStorage 키 |
| `daysToHide` | `number?` | 숨김 유지 일수 (기본 7, 0이면 항상 표시) |
| `image` | `string` | 팝업 이미지 경로 |
| `mobileOnly` | `boolean?` | 모바일 전용 여부 |
| `to` | `string?` | 이미지 클릭 시 내부 라우팅 경로 |
| `onImageClick` | `(trackEvent) => void` | 이미지 클릭 핸들러 (트래킹 포함) |

## 새 팝업 추가 방법

1. `popupConfigs.ts`에 `PopupConfig` 객체 추가
2. `MainPage.tsx`의 `configs` 배열에 삽입 (앞에 넣을수록 우선 표시)

## 관련 코드

- `src/utils/popupUtils.ts` — `PopupConfig` 인터페이스, `isPopupHidden`, `DAYS_TO_HIDE`
- `src/pages/MainPage/components/Popup/Popup.tsx` — 팝업 렌더링 컴포넌트
- `src/pages/MainPage/components/Popup/popupConfigs.ts` — 팝업 config 정의 (`APP_DOWNLOAD_POPUP`)
- `src/pages/MainPage/components/Popup/Popup.test.tsx` — `isPopupHidden` 유틸 테스트
Binary file added frontend/src/assets/images/popup/daedong.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion frontend/src/components/common/Filter/Filter.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { USER_EVENT } from '@/constants/eventName';
import useMixpanelTrack from '@/hooks/Mixpanel/useMixpanelTrack';
Expand All @@ -7,11 +8,13 @@ import * as Styled from './Filter.styles';
const WEB_FILTER_OPTIONS = [
{ label: '동아리', path: '/' },
{ label: '홍보', path: '/promotions' },
{ label: '대동제', path: '/festival-busking' },
Comment thread
seongwon030 marked this conversation as resolved.
] as const;

const WEBVIEW_FILTER_OPTIONS = [
{ label: '동아리', path: '/webview/main' },
{ label: '홍보', path: '/webview/promotions' },
{ label: '대동제', path: '/webview/festival-busking' },
Comment thread
suhyun113 marked this conversation as resolved.
] as const;

interface FilterProps {
Expand All @@ -25,12 +28,20 @@ const Filter = ({ alwaysVisible = false, hasNotification }: FilterProps) => {
const { pathname } = useLocation();
const trackEvent = useMixpanelTrack();

const [daedongDotSeen, setDaedongDotSeen] = useState(
() => sessionStorage.getItem('daedong_filter_seen') === 'true',
);

const isWebview = pathname.startsWith('/webview');
const filterOptions = isWebview ? WEBVIEW_FILTER_OPTIONS : WEB_FILTER_OPTIONS;
const shouldShow = alwaysVisible || isMobile || isWebview;

const handleFilterOptionClick = (path: string) => {
trackEvent(USER_EVENT.FILTER_OPTION_CLICKED, { path });
if (path.includes('festival-busking')) {
sessionStorage.setItem('daedong_filter_seen', 'true');
setDaedongDotSeen(true);
}
navigate(path);
};

Expand All @@ -41,7 +52,10 @@ const Filter = ({ alwaysVisible = false, hasNotification }: FilterProps) => {
{filterOptions.map((filter) => (
<Styled.FilterButtonWrapper key={filter.path}>
<Styled.NotificationDot
$isVisible={hasNotification && filter.label === '홍보'}
$isVisible={
(hasNotification && filter.label === '홍보') ||
(filter.label === '대동제' && !daedongDotSeen)
}
/>
<Styled.FilterButton
$isActive={pathname === filter.path}
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/constants/eventName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ export const USER_EVENT = {
FESTIVAL_PERFORMANCE_CARD_CLICKED: 'Festival PerformanceCard Clicked',
FESTIVAL_TAB_DURATION: 'Festival Tab Duration',

// 버스킹 시간표
DAEDONG2026_DAY_CHANGED: '2026-daedong Day Changed',
DAEDONG2026_DAY_DURATION: '2026-daedong Day Duration',

// 홍보
PROMOTION_BUTTON_CLICKED: 'Promotion Button Clicked',
PROMOTION_CARD_CLICKED: 'Promotion Card Clicked',
Expand Down Expand Up @@ -125,6 +129,7 @@ export const PAGE_VIEW = {
INTRODUCE_PAGE: 'IntroducePage',
CLUB_UNION_PAGE: 'ClubUnionPage',
FESTIVAL_INTRODUCTION_PAGE: '동소한 페이지',
DAEDONG2026_BUSKING_PAGE: '2026 대동제 버스킹 시간표 페이지',
PROMOTION_LIST_PAGE: '홍보 목록 페이지',
PROMOTION_DETAIL_PAGE: '홍보 상세 페이지',

Expand Down
29 changes: 8 additions & 21 deletions frontend/src/experiments/definitions.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,13 @@
import type { ExperimentDefinition } from './types';

export const mainBannerExperiment = {
key: 'main_banner_v1',
variants: ['A', 'B'] as const,
defaultVariant: 'A',
export const festivalTimetableNavExperiment = {
key: 'festival_timetable_nav_v1',
variants: ['tabs', 'arrows'] as const,
defaultVariant: 'tabs',
weights: {
A: 50,
B: 50,
tabs: 50,
arrows: 50,
},
} satisfies ExperimentDefinition<'A' | 'B'>;
} satisfies ExperimentDefinition<'tabs' | 'arrows'>;

export const applyButtonCopyExperiment = {
key: 'apply_button_copy_v1',
variants: ['A', 'B'] as const,
defaultVariant: 'A',
weights: {
A: 50,
B: 50,
},
} satisfies ExperimentDefinition<'A' | 'B'>;

export const ALL_EXPERIMENTS = [
mainBannerExperiment,
applyButtonCopyExperiment,
] as const;
export const ALL_EXPERIMENTS = [festivalTimetableNavExperiment] as const;
60 changes: 60 additions & 0 deletions frontend/src/pages/FestivalPage/BuskingPage/BuskingPage.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import styled from 'styled-components';

export const Container = styled.div`
width: 100%;
max-width: 550px;
margin: 0 auto;
`;

export const NavWrapper = styled.div`
margin-bottom: 16px;
`;

export const TimetableSection = styled.section`
width: 100%;
max-width: 500px;
margin: 0 auto 20px;
`;

export const TimetableHeader = styled.div`
margin: 10px 20px 14px;
padding: 14px 16px;
border-radius: 14px;
background: #fff7f3;
border: 1px solid #ffe0d4;
display: flex;
flex-direction: column;
gap: 4px;
`;

export const TimetableDate = styled.p`
margin: 0;
color: #3a3a3a;
font-size: 14px;
font-weight: 700;
`;

export const TimetableLocation = styled.p`
margin: 0;
color: #7a7a7a;
font-size: 12px;
font-weight: 600;
`;

export const SectionLabel = styled.div`
display: flex;
align-items: center;
gap: 10px;
margin: 20px 20px 12px;
color: #7a7a7a;
font-size: 12px;
font-weight: 600;

&::before,
&::after {
content: '';
flex: 1;
height: 1px;
background: #e8e8e8;
}
`;
Loading
Loading