o2o-site-AEO/solution/site/src/layouts/round/Rooms.tsx
Mina Choi fad20e8958 [feat] solution/site,shared: 새 템플릿 8종 업종 확장 — 앞 커밋에서 빠진 본체
앞 커밋(18a2874)에 템플릿 등록과 레이아웃 변경이 빠지고 다른 작업의 새 파일
(services/activity_feed.py, 어디서도 아직 import 하지 않음)이 대신 들어갔다.
같은 작업 폴더를 다른 세션이 함께 쓰고 있었다.

- templates.json: cafe · restaurant · clinic 에 라운드~솔숲 8종
- layouts/*/Rooms.tsx: 객실별 '예약 요청' 버튼은 숙박일 때만, 영문 라벨은 unitWord
- layouts/*/Frame.tsx · Intro.tsx: 이용안내 머리말 · 미니 블로그 설명을 업종 중립으로

eslint·tsc 통과, vitest 123 passed, 33개 사이트 굽기 성공

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:58:29 +09:00

136 lines
5.0 KiB
TypeScript

import {useState} from 'react';
import {factText, sanitizeUnits, PlaceCategory} from '@o2o/shared';
import {useSite} from '@site/lib/site-context';
import {sectionName, unitSpec, unitViews, type UnitView} from '@site/lib/derive';
import {Car} from '../paper/Car';
import {BOOK_EVENT} from '../paper/Rooms';
import {Sheet} from '../paper/Sheet';
function splitName(name: string): [string, string | undefined] {
const m = /^(.+?)\s*\((.+)\)\s*$/.exec(name);
return m ? [m[1], m[2]] : [name, undefined];
}
function specRows(unit: UnitView, facts: Parameters<typeof factText>[0]) {
const bed = [factText(facts, 'bed_type'), factText(facts, 'room_size')].filter(Boolean).join(' · ');
const std = factText(facts, 'standard_capacity');
const max = factText(facts, 'max_capacity');
const people = [std && `기준 ${std.replace(/명$/, '인')}`, max && `최대 ${max.replace(/명$/, '인')}`]
.filter(Boolean)
.join(' · ');
const price = unit.priceText ?? unit.prices.map((p) => `${p.label} ${p.value}`).join(' · ');
return [
['침대', bed],
['정원', people],
['요금', price],
].filter(([, value]) => value) as [string, string][];
}
const HOME_MAX = 4;
export function Rooms() {
const payload = useSite();
const lodging = payload.place.category === PlaceCategory.LODGING;
const units = unitViews(payload);
const spec = unitSpec(payload);
const raw = new Map(sanitizeUnits(payload.units).map((unit) => [unit.unitId, unit]));
const [open, setOpen] = useState<UnitView | null>(null);
if (units.length === 0) return null;
const rowsOf = (unit: UnitView) => specRows(unit, raw.get(unit.unitId)?.facts ?? []);
const people = (unit: UnitView) => rowsOf(unit).find(([label]) => label === '정원')?.[1];
return (
<>
<div data-pg="home">
<section className="sec" id="units" aria-labelledby="units-heading">
<p className="kicker">{spec.label}</p>
<h2 id="units-heading">{sectionName(payload, spec.path, `${spec.label} 안내`)}</h2>
<div className="rooms">
{units.slice(0, HOME_MAX).map((unit) => (
<a key={unit.unitId} href="#/stay" className="room room--mini">
{unit.images[0] && (
<div className="ph">
<img src={unit.images[0].url} alt={unit.images[0].alt} loading="lazy" />
</div>
)}
<div className="bd">
<h3>{unit.name}</h3>
{people(unit) && <p>{people(unit)}</p>}
</div>
</a>
))}
</div>
<a href="#/stay" className="more">
이용안내에서 전체 보기
</a>
</section>
</div>
<div data-pg="stay">
{units.map((unit, i) => {
const [main, sub] = splitName(unit.name);
return (
<section key={unit.unitId} className="sec" id={`unit-${unit.slug}`}>
<p className="kicker">
{spec.label} {String.fromCharCode(65 + i)}
</p>
<h2>
{main} {sub && <span className="sub">{sub}</span>}
</h2>
<div className="room room--full">
{unit.images.length > 0 && (
<Car
label={`${main} 사진`}
arrows
count
dots
slides={unit.images.map((image) => (
<img key={image.mediaId} src={image.url} alt={image.alt} loading="lazy" />
))}
/>
)}
<div className="bd">
{unit.intro && <p className="intro">{unit.intro}</p>}
<dl className="spec">
{rowsOf(unit).map(([label, value]) => (
<div key={label} style={{display: 'contents'}}>
<dt>{label}</dt>
<dd>{value}</dd>
</div>
))}
</dl>
<div className="btnrow">
<button type="button" className="btn ghost" onClick={() => setOpen(unit)}>
자세히
</button>
{lodging && (
<button type="button" className="btn" onClick={() => window.dispatchEvent(new Event(BOOK_EVENT))}>
예약 요청
</button>
)}
</div>
</div>
</div>
</section>
);
})}
</div>
<Sheet open={open != null} onClose={() => setOpen(null)} label={open?.name ?? ''}>
{open && (
<>
{open.images[0] && <img src={open.images[0].url} alt={open.images[0].alt} />}
<h3>{open.name}</h3>
{open.intro && <p>{open.intro}</p>}
{open.rows.map((row) => (
<p key={row.label}>
{row.label} — {row.value}
</p>
))}
</>
)}
</Sheet>
</>
);
}