/**
* 계절별 축제 — 지역 축제·행사만 따로 세운 섹션.
*
* ★ 왜 '주변 안내'에서 떼어냈나
* 맛집·명소는 아무 때나 가는 곳이고 축제는 **날짜가 지나면 못 가는 것**이다. 성격이 다른데
* 같은 목록에 섞여 있으면 손님은 기간을 안 읽고, 우리는 "이번 계절에 뭐 있어요?"라는
* 질문에 답할 자리를 잃는다. 계절을 축으로 세우면 그 질문이 곧 화면 구조가 된다.
*
* ★ 지금 계절만 열고 나머지 계절은 **HTML 에 남겨 둔 채 접는다**(`hidden`).
* 지우면 검색·AI 가 나머지 세 계절을 못 읽는다 — 이 레포의 규칙이다.
* ★ 첫 렌더에서는 계절이 비어 있다(`useLiveSeasons`). 그때는 아무것도 접지 않는다 —
* SSR 과 어긋나면 하이드레이션이 깨지고, 구운 HTML 에는 사계절이 다 남아야 한다.
* ★ 링크는 검색으로만 보낸다. 공공데이터가 준 공식 홈페이지가 있어도 우리가 붙이지 않는다 —
* 축제 사이트는 해마다 주소가 바뀌고, 죽은 링크의 책임은 이 홈페이지가 진다.
*/
import {useState} from 'react';
import {ArrowUpRight} from 'lucide-react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {useLiveSeasons} from '@site/lib/use-live-seasons';
import {Carousel, CarouselSlide, Modal, Section} from '@site/lib/ui';
// shared 의 SEASON_ORDER 는 내보내지 않는다. 순서는 달력이 정한 것이라 갈릴 일이 없다.
const SEASONS = ['봄', '여름', '가을', '겨울'];
export function FestivalSection() {
const payload = useSite();
const festivals = payload.local.festivals;
const live = useLiveSeasons();
const [picked, setPicked] = useState(null);
if (festivals.length === 0) return null;
// 데이터에 실제로 있는 계절만 탭이 된다. 없는 계절 탭을 눌러 빈 화면을 보게 하지 않는다.
const tabs = SEASONS.filter((season) => festivals.some((f) => f.season === season));
// 계절을 안 적은 행사(연중·상시)는 어느 탭에서도 접지 않는다.
const evergreen = festivals.filter((f) => !f.season?.trim());
// 간절기에는 `live` 가 둘이고 뒤가 지금 계절이다 — 뒤에서부터 찾는다.
const auto = [...live].reverse().find((season) => tabs.includes(season));
/*
* ★ '전체' 를 넣는다 (2026-09-04, 사장님 지시)
* 지금 계절만 열어 두면 "가을 말고 다른 때 오면 뭐가 있나"를 보려고 탭을 네 번 눌러야 했다.
* 여행은 대개 몇 달 뒤를 잡는 일이라, 사계절을 한 번에 보는 길이 있어야 한다.
* 기본값은 그대로 지금 계절이다 — 처음 온 손님에게 열두 개를 늘어놓지 않는다.
*/
const ALL = '전체';
const active = picked ?? auto ?? null;
const showAll = active === ALL;
return (
지금은 {auto} 축제입니다
) : undefined
}
footnote={
<>한국관광공사 TourAPI 기준. 일정은 주최 측 사정으로 바뀔 수 있습니다.>
}
>
{tabs.length > 1 && (
{[...tabs, ALL].map((season) => {
const on = active === season;
return (
);
})}
{/* 탭이 접혀 있어도 소제목은 남는다 — 펼쳐 놓고 읽는 기계에게는 이게 계절 표지다. */}
{season}
);
})}
{evergreen.length > 0 && (
계절 없이 열리는 행사
)}
);
}
/**
* ★ 격자에서 **레일**로 바꿨다 (2026-09-09, 사장님: "계절별 축제 카로셀로")
* 한 계절에 열 개가 넘으면 격자는 네 줄이 되고, 그 아래 섹션이 화면 밖으로 밀린다.
* 레일은 계절이 몇 개든 세로 길이가 한 장으로 고정된다. 자동 넘김은 Carousel 기본값이다.
* ★ 접힌 계절 안에 있는 레일은 폭이 0 이다 — embla 가 그 상태로 스냅을 재면 어긋난다.
* `remount` 로 활성 탭이 바뀔 때 다시 태운다(LocalGuideSection 의 `key={shown.length}` 와 같은 이유).
* ★ 카드 모양을 **주변 안내와 같게** 맞췄다 (2026-09-09, 사장님: "축제만 왜 ui가 달라 주변맛집이랑?")
* 격자 시절에는 좁은 화면에서 가로줄(썸네일 왼쪽·글 오른쪽)로 눕혔다 — 세로로 열두 장이
* 쌓이니 섹션 하나가 화면 1.7개였기 때문이다(2026-09-04). 레일이 되면서 그 이유가 없어졌다.
* 세로 길이는 이미 카드 한 장으로 고정되는데 모양만 옆 섹션과 달라, 같은 페이지에서
* 같은 성격의 카드(사진·이름·설명·검색 링크)가 두 벌로 보였다.
* 지금은 사진 위 배지 · aspect-4/3 · 슬라이드 폭까지 `LocalGuideSection` 과 같다.
*/
function FestivalRail({
items,
label,
remount,
}: {
items: FestivalEntry[];
label: string;
remount?: string | null;
}) {
const [open, setOpen] = useState(null);
return (
<>
{items.map((festival) => (
))}
setOpen(null)} label={open?.name ?? '축제'} title={open?.name}>
{open && (