o2o-site-AEO/solution/site/src/layouts/coral/Hero.tsx
Mina Choi 013cdcfc9f [feat] solution/site,shared: 숙박 템플릿 8종 추가 — 라운드·시네마·빅타이포·부티크·일러스트·코랄·미니멀·솔숲
사장님이 고를 수 있는 숙박 템플릿이 고택 하나뿐이었다. 모두 stay2 콘텐츠(날씨·미니 블로그·
지역 이야기·객실·축제·일정·후기·엽서·노래)를 그대로 싣고 모양만 다르다.

- layouts/<id>/: 템플릿별 Frame·섹션·CSS. 데스크톱은 넓은 배치(1024px 이상), 모바일 우선
- layouts/kit/kit.css: 탭 전환·시트·예약 폼·접기 등 공통 구조
- shared templates.json · builder.ts · catalog.ts: 템플릿·LayoutId 등록
- seo/head.ts: 템플릿별 한글 웹폰트 — 서로 겹치지 않게 배정
- paper Frame·Intro: 같은 탭을 다시 눌러도 맨 위로, 미니 블로그를 글 여러 개로
- docs/TEMPLATES.md · TEMPLATE_DESIGN.md: 목록과 화면 규칙(간격·접기·✓ 표시·글꼴)

eslint·tsc 통과, vitest 123 passed, 템플릿 9개 굽기 성공 · 간격 검사 0건
2026-09-30 13:45:10 +09:00

96 lines
3.2 KiB
TypeScript

import {PlaceCategory} from '@o2o/shared';
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, heroFacts, isSectionEnabled, stayBookingView} from '@site/lib/derive';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Weather} from './Weather';
const MAX_SLIDES = 5;
export function Hero() {
const payload = useSite();
const {place, narrative} = payload;
const slides = [...payload.media]
.filter((image) => image.alt?.trim())
.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary))
.slice(0, MAX_SLIDES);
const fixed = narrative.tagline ?? narrative.heroSubline;
const lodging = place.category === PlaceCategory.LODGING;
const region = place.addressLocality ?? place.addressRegion;
const booking = bookingLinks(payload)[0];
const stay = lodging ? stayBookingView(payload) : null;
const chips = heroFacts(payload).map((row) => `${row.label} ${row.value}`);
return (
<>
<div className="hero" id="top" data-pg="home">
<div className="hero-card">
{slides.length > 0 && (
<Car
label={`${place.name} 사진`}
autoplay={5000}
dots
style={{height: '100%'}}
slides={slides.map((image, i) => (
<img
key={image.mediaId}
src={image.url}
alt={image.alt}
fetchPriority={i === 0 ? 'high' : 'low'}
loading={i === 0 ? undefined : 'lazy'}
decoding="async"
/>
))}
/>
)}
<div className="hero-copy">
<h1 className="eyebrow">
{place.name}
{region && ` · ${region}`}
</h1>
{lodging ? (
<p className="hl">
<HeroCatchphrase />
</p>
) : (
fixed && <p className="hl in">{fixed}</p>
)}
{lodging && fixed && <p className="tagline">{fixed}</p>}
{chips.length > 0 && (
<div className="chips">
{chips.map((chip) => (
<span key={chip} className="chip">
{chip}
</span>
))}
</div>
)}
<div className="hero-cta">
{stay ? (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>
예약하기
</button>
) : (
booking && (
<a className="btn" href={booking.url} target="_blank" rel="noopener noreferrer">
{bookingActionLabel(booking)}
</a>
)
)}
<a className="btn soft" href="#/stay">
이용안내 보기
</a>
</div>
</div>
</div>
</div>
{isSectionEnabled(payload, 'weather') && (
<div data-editor-id="weather" style={{display: 'contents'}}>
<Weather />
</div>
)}
</>
);
}