o2o-site-AEO/solution/site/src/layouts/paper/Rooms.tsx
Mina Choi a74f7918c6 [feat] solution/site: "고택" 템플릿 레이아웃 신설 — stay2 DOM 이식 1차
기존 template-look 시스템(색·서체 토큰)만으로는 stay2 실물과 구조 자체가 달랐다.
reservation/oasi/studio/pastel/editorial 다섯 레이아웃과 같은 자리(Shell·Hero·Rooms·
SectionHead)에 paper 를 여섯 번째로 추가 — layoutOf(templateId) 로 갈아 끼운다.

- layouts/paper/{Shell,Hero,Rooms,SectionHead}.tsx 신설
  Shell: 세리프 워드마크+미니플레이어(SongPlayer)+전화, 앵커 탭 헤더, 얇은 텍스트 푸터
  Hero: 풀블리드 회전 캐러셀(HeroPension 뼈대) + 왼쪽 정렬 카피, 가격띠 없음
  Rooms: 유닛마다 전체폭 판 + 캐러셀(카드 격자 아님)
- lib/layout.ts: LayoutId 에 'paper', stay/restaurant/cafe-paper → paper 매핑
- App.tsx·HeroSection.tsx·UnitsSection.tsx·lib/ui/Section.tsx: 기존 5분기에 paper 추가

★ 실물 대조 결과 아직 stay2 와 다르다(사장님 지적) — 탭 개수(4개 vs 지금 섹션 수만큼),
  히어로가 여백 있는 박스가 아니라 풀블리드, 객실 카드가 2열 나란히가 아니라 전체폭
  세로 배치, 영문 눈썹 라벨 없음. 다음 커밋에서 이어서 맞춘다.

★ 배포 함정 실측: prerender.ts 의 "성공한 버전은 불변" 규칙 때문에, 같은 site_version 은
  렌더러 코드를 바꿔도 재빌드 때 옛 HTML 을 그대로 재사용한다 — 버전 캐시를 지워야
  강제로 다시 구워진다(운영 영향은 별도 확인 필요).
2026-09-23 13:18:21 +09:00

140 lines
4.3 KiB
TypeScript

import {Phone} from 'lucide-react';
import type {ChannelLink} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, sectionName, unitSpec, unitViews, type UnitView} from '@site/lib/derive';
import {Carousel, CarouselSlide} from '@site/lib/ui';
import {SectionHead} from './SectionHead';
export function Rooms() {
const payload = useSite();
const units = unitViews(payload);
const spec = unitSpec(payload);
const booking = bookingLinks(payload)[0];
if (units.length === 0) return null;
return (
<section
id="units"
aria-labelledby="units-heading"
className="border-line w-full border-b"
style={{backgroundColor: 'var(--tpl-surface, #fff)', paddingBlock: 'var(--section-space)'}}
>
<div className="shell">
<SectionHead
id="units"
title={sectionName(payload, spec.path, `${spec.label} ${units.length}개 안내`)}
/>
</div>
<div className="flex flex-col">
{units.map((unit, i) => (
<UnitPanel key={unit.unitId} unit={unit} no={i + 1} booking={booking} phone={payload.place.phone} />
))}
</div>
</section>
);
}
function UnitPanel({
unit,
no,
booking,
phone,
}: {
unit: UnitView;
no: number;
booking?: ChannelLink;
phone?: string;
}) {
return (
<article className="border-line border-t">
{unit.images.length > 0 && (
<Carousel label={`${unit.name} 사진`} align="center" gap={0} arrows="overlay">
{unit.images.map((image) => (
<CarouselSlide key={image.mediaId} basis="basis-full">
<div className="relative aspect-3/2 overflow-hidden">
<img
src={image.url}
alt={image.alt}
loading="lazy"
decoding="async"
className="size-full object-cover"
/>
</div>
</CarouselSlide>
))}
</Carousel>
)}
<div className="shell flex flex-col gap-3 py-6">
<p
className="text-[length:var(--fs-xs)] uppercase opacity-70"
style={{letterSpacing: '0.24em'}}
>
{String(no).padStart(2, '0')}
</p>
<h3
className="serif"
style={{
fontSize: 'clamp(1.1rem, 2.4vw, 1.4rem)',
fontWeight: 'var(--tpl-heading-weight, 400)',
letterSpacing: 'var(--tpl-heading-tracking, 0.03em)',
}}
>
{unit.name}
</h3>
{unit.intro && (
<p className="text-[length:var(--fs-sm)] leading-relaxed opacity-100">{unit.intro}</p>
)}
{unit.rows.length > 0 && (
<dl className="border-line divide-line mt-2 divide-y border-t">
{unit.rows.map((row) => (
<div key={row.label} className="flex items-baseline justify-between gap-4 py-2 text-[length:var(--fs-sm)]">
<dt className="text-muted shrink-0">{row.label}</dt>
<dd className="text-right font-semibold">{row.value}</dd>
</div>
))}
</dl>
)}
{unit.chips.length > 0 && (
<ul className="mt-1 flex flex-wrap gap-1.5">
{unit.chips.map((chip) => (
<li
key={chip.label}
className="border-line inline-flex items-center rounded-full border px-3 py-1 text-[length:var(--fs-xs)]"
>
{chip.value}
</li>
))}
</ul>
)}
{(booking || phone) && (
<div className="mt-2 flex flex-wrap items-center gap-x-6 gap-y-1 text-[length:var(--fs-sm)] font-semibold">
{booking && (
<a
href={booking.url}
target="_blank"
rel="noopener noreferrer"
className="tap inline-flex items-center underline underline-offset-4"
>
{bookingActionLabel(booking)}
</a>
)}
{phone && (
<a href={`tel:${phone}`} className="tap inline-flex items-center gap-1.5 underline underline-offset-4">
<Phone className="size-4" />
<span>{phone}</span>
</a>
)}
</div>
)}
</div>
</article>
);
}