/** * 계절별 축제 — 지역 축제·행사만 따로 세운 섹션. * * ★ 왜 '주변 안내'에서 떼어냈나 * 맛집·명소는 아무 때나 가는 곳이고 축제는 **날짜가 지나면 못 가는 것**이다. 성격이 다른데 * 같은 목록에 섞여 있으면 손님은 기간을 안 읽고, 우리는 "이번 계절에 뭐 있어요?"라는 * 질문에 답할 자리를 잃는다. 계절을 축으로 세우면 그 질문이 곧 화면 구조가 된다. * * ★ 지금 계절만 열고 나머지 계절은 **HTML 에 남겨 둔 채 접는다**(`hidden`). * 지우면 검색·AI 가 나머지 세 계절을 못 읽는다 — 이 레포의 규칙이다. * ★ 첫 렌더에서는 계절이 비어 있다(`useLiveSeasons`). 그때는 아무것도 접지 않는다 — * SSR 과 어긋나면 하이드레이션이 깨지고, 구운 HTML 에는 사계절이 다 남아야 한다. * ★ 링크는 검색으로만 보낸다. 공공데이터가 준 공식 홈페이지가 있어도 우리가 붙이지 않는다 — * 축제 사이트는 해마다 주소가 바뀌고, 죽은 링크의 책임은 이 홈페이지가 진다. */ import {useState} from 'react'; import {ArrowUpRight, MapPin} from 'lucide-react'; import type {FestivalEntry} from '@o2o/shared'; import {useSite} from '@/lib/site-context'; import {naverSearchUrl} from '@/lib/format'; import {useLiveSeasons} from '@/lib/use-live-seasons'; import {Section} from '@/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 (
지금은 {active} 축제입니다

) : undefined } footnote={ <>한국관광공사 TourAPI 기준. 일정은 주최 측 사정으로 바뀔 수 있습니다. } > {tabs.length > 1 && (
{[...tabs, ALL].map((season) => { const on = active === season; return ( ); })}
)}
{tabs.map((season) => { const items = festivals.filter((f) => f.season === season); return ( ); })} {evergreen.length > 0 && (

계절 없이 열리는 행사

)}
); } /** * ★ 좁은 화면에서는 **가로줄**, 넓어지면 세로 카드 (2026-09-04, 사장님: "모바일에서 너무 길다") * 실측 390px 에서 이 섹션 하나가 1,274px — 화면 1.7개였다. 큰 사진을 세로로 쌓은 탓이다. * 축제 카드에서 손님이 찾는 건 **언제·어디서** 셋 줄이지 사진이 아니다. 사진은 썸네일로 줄이고 * 글을 옆에 붙이면 한 장이 400px 에서 120px 이 된다. * ★ 폭으로 컴포넌트를 갈아 끼우지 않고 CSS 로만 가른다 — 서버 렌더는 화면 폭을 모른다. */ function FestivalGrid({items}: {items: FestivalEntry[]}) { return ( ); }