o2o-site-AEO/solution/site/src/layouts/cinema/Festival.tsx
Mina Choi 4831e2d25d [fix] solution/site: 사진 카드를 PhotoCard 하나로 통일 — 템플릿마다 제각각이던 축제·명소 카드
같은 템플릿 안에서도 축제 카드(흰 바탕·글 안쪽)와 명소 카드(바탕 없음·글 아래)가 따로 놀았다.
실측(9종 전수): 제목 굵기 400/500/700/900, 사진 모서리 0/12/18/22px 가 한 화면에 섞여 있었다.

- kit/Card.tsx·card.css: 사진 3:2 + 제목 1줄 + 거리 1줄 + 설명 2줄. 템플릿 차이는 --kc-* 변수만
- 9종 Around·Festival, 고택 인물: 사진 카드를 PhotoCard 로 교체. 맛집·홈 목록 행은 그대로
- 템플릿 CSS: 모서리·글꼴·강조색 변수와 그리드 간격. 일러스트는 상자형, 빅타이포는 선 격자
- 모바일 캐러셀 화살표가 카드를 가려 1024px 미만에서 숨김
- docs/TEMPLATE_DESIGN.md: 사진 카드 규칙

tsc·eslint 통과 · vitest 123 passed · 12개 사이트 굽기 · 카드 모양 템플릿당 1종 확인

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:33:17 +09:00

75 lines
2.6 KiB
TypeScript

import {useState} from 'react';
import type {FestivalEntry} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {naverSearchUrl} from '@site/lib/format';
import {PhotoCard} from '../kit/Card';
import {Sheet} from '../paper/Sheet';
const SEASONS = ['봄', '여름', '가을', '겨울'];
export function Festival() {
const payload = useSite();
const festivals = payload.local.festivals;
const [season, setSeason] = useState('');
const [open, setOpen] = useState<FestivalEntry | null>(null);
if (festivals.length === 0) return null;
const tabs = SEASONS.filter((s) => festivals.some((f) => f.season === s));
const shown = festivals.filter((f) => !season || f.season === season);
return (
<>
<section className="wrap" id="festival" aria-labelledby="festival-heading">
<p className="label">SEASON</p>
<h2 id="festival-heading">
계절별 축제 <span className="cnt">{festivals.length}</span>
</h2>
<p className="lead">{payload.place.addressLocality ?? '이 지역'}의 축제와 행사를 계절로 묶었습니다.</p>
{tabs.length > 1 && (
<div className="chips" role="tablist">
{['', ...tabs].map((s) => (
<button
key={s || 'all'}
type="button"
role="tab"
aria-current={s === season}
onClick={() => setSeason(s)}
>
{s || '전체'} {festivals.filter((f) => !s || f.season === s).length}
</button>
))}
</div>
)}
<div className="rail">
{shown.map((f) => (
<PhotoCard
key={f.name}
image={f.imageUrl}
title={f.name}
meta={[f.month, f.location].filter(Boolean).join(' · ')}
onClick={() => setOpen(f)}
/>
))}
</div>
</section>
<Sheet open={open != null} onClose={() => setOpen(null)} label={open?.name ?? ''}>
{open && (
<>
{open.imageUrl && <img src={open.imageUrl} alt={open.name} />}
<h3>{open.name}</h3>
<p className="meta">{[open.period ?? open.month, open.location].filter(Boolean).join(' · ')}</p>
{open.description && <p>{open.description}</p>}
<a
className="out"
href={open.officialUrl ?? naverSearchUrl(open.searchQuery)}
target="_blank"
rel="noopener noreferrer"
>
자세히 보기
</a>
</>
)}
</Sheet>
</>
);
}