기존 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 을 그대로 재사용한다 — 버전 캐시를 지워야
강제로 다시 구워진다(운영 영향은 별도 확인 필요).
140 lines
4.3 KiB
TypeScript
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>
|
|
);
|
|
}
|