수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
204 lines
8.0 KiB
TypeScript
204 lines
8.0 KiB
TypeScript
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {bookingActionLabel, bookingLinks, heroFacts, lowestPrice} from '@site/lib/derive';
|
|
|
|
/**
|
|
* 파스텔의 첫 화면 — **연노랑 판 위에 둥근 사진**.
|
|
*
|
|
* ★ 사진을 쓴다 (2026-09-03, 두 번째 반려)
|
|
* 처음엔 상호를 담은 CSS 배지를 벽지처럼 깔았다. 레퍼런스(아이스크림 브랜드)는 그게 맞다 —
|
|
* **그쪽은 파는 게 브랜드고, 여기는 파는 게 집이다.** 손님이 첫 화면에서 보고 싶은 건
|
|
* 상호를 반복한 무늬가 아니라 방이 어떻게 생겼는가다. 파스텔은 사진을 감싸는 판으로 남기고,
|
|
* 원 무늬는 사진 뒤에 몇 개만 흘려 이 안의 결만 유지한다.
|
|
* ★ 사진 모서리를 크게 굴린다(--tpl-radius 가 24px 인 안이다). 각진 사진 하나만 딱딱해 보인다.
|
|
* ★ 장식 원은 aria-hidden 이고 사진 **뒤**에 둔다. 사진을 가리면 장식이 아니라 방해다.
|
|
*/
|
|
export function Hero() {
|
|
const payload = useSite();
|
|
const {place, narrative} = payload;
|
|
const facts = heroFacts(payload);
|
|
const price = lowestPrice(payload);
|
|
const links = bookingLinks(payload);
|
|
const line = narrative.tagline ?? narrative.summary;
|
|
const locality = place.addressLocality ?? place.addressRegion;
|
|
|
|
// 대표 사진이 먼저. og:image 와 첫 장이 다르면 공유 카드와 화면이 어긋난다.
|
|
const shots = [...payload.media]
|
|
.filter((image) => image.alt?.trim())
|
|
.sort((a, b) => Number(b.isPrimary) - Number(a.isPrimary))
|
|
.slice(0, 3);
|
|
|
|
// id="top" — 상단 워드마크와 하단 바가 여기로 돌아온다.
|
|
return (
|
|
<section id="top" data-pastel="own" className="w-full">
|
|
{/* 판 1 — 연노랑 바탕. 왼쪽에 말, 오른쪽에 사진. */}
|
|
<div className="relative w-full overflow-hidden" style={{backgroundColor: 'var(--pastel-1)'}}>
|
|
{/* 장식 원 — 사진 뒤에서 살짝만 삐져나온다. 화면 밖으로 잘려도 된다. */}
|
|
<div aria-hidden className="pointer-events-none absolute inset-0">
|
|
<span
|
|
className="absolute rounded-full"
|
|
style={{
|
|
width: 'clamp(12rem, 26vw, 22rem)',
|
|
aspectRatio: '1',
|
|
backgroundColor: 'var(--pastel-2)',
|
|
right: '-6%',
|
|
top: '-6%',
|
|
opacity: 0.75,
|
|
}}
|
|
/>
|
|
<span
|
|
className="absolute rounded-full"
|
|
style={{
|
|
width: 'clamp(8rem, 18vw, 15rem)',
|
|
aspectRatio: '1',
|
|
backgroundColor: 'var(--pastel-3)',
|
|
left: '46%',
|
|
bottom: '-10%',
|
|
opacity: 0.7,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
className="shell relative grid items-center gap-8 lg:grid-cols-2 lg:gap-14"
|
|
style={{paddingBlock: 'var(--section-space)'}}
|
|
>
|
|
<div className="order-2 lg:order-1">
|
|
{locality && (
|
|
<p
|
|
className="inline-flex items-center rounded-full px-3 py-1 text-[length:var(--fs-xs)] font-extrabold"
|
|
style={{backgroundColor: 'var(--tpl-bg, #fff)', color: 'var(--pastel-ink)'}}
|
|
>
|
|
{locality}
|
|
</p>
|
|
)}
|
|
|
|
{/* h1 은 페이지에 하나. 가게 이름이 들어가야 "○○ 홈페이지" 질의에 잡힌다. */}
|
|
<h1
|
|
className="mt-4 break-keep"
|
|
style={{
|
|
fontSize: 'var(--fs-display)',
|
|
fontWeight: 800,
|
|
lineHeight: 1.2,
|
|
letterSpacing: 'var(--tpl-heading-tracking, -0.03em)',
|
|
}}
|
|
>
|
|
{place.name}
|
|
</h1>
|
|
|
|
{line && (
|
|
<p className="measure mt-4 text-[length:var(--fs-lead)] font-medium leading-relaxed opacity-80">
|
|
<HeroCatchphrase>{line}</HeroCatchphrase>
|
|
</p>
|
|
)}
|
|
|
|
{links.length > 0 && (
|
|
<a
|
|
href={links[0].url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="tap mt-7 inline-flex items-center justify-center rounded-full px-8 text-[length:var(--fs-sm)] font-extrabold transition-opacity hover:opacity-90"
|
|
style={{backgroundColor: 'var(--pastel-ink)', color: 'var(--tpl-bg, #fff)'}}
|
|
>
|
|
{bookingActionLabel(links[0])}
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
<div className="order-1 lg:order-2">
|
|
{shots[0] && (
|
|
<div
|
|
className="relative w-full overflow-hidden"
|
|
style={{
|
|
aspectRatio: '1 / 1',
|
|
borderRadius: 'calc(var(--tpl-radius, 24px) * 1.6)',
|
|
// 그림자 대신 흰 테두리로 띄운다 — 이 안의 규칙이다.
|
|
boxShadow: '0 0 0 clamp(6px, 1.4vw, 12px) var(--tpl-bg, #fff)',
|
|
}}
|
|
>
|
|
<img
|
|
src={shots[0].url}
|
|
alt={shots[0].alt}
|
|
fetchPriority="high"
|
|
decoding="async"
|
|
className="size-full object-cover"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 나머지 두 장은 아래 작게. 사진 문장(alt)이 문서에서 사라지지 않게 남긴다. */}
|
|
{shots.length > 1 && (
|
|
<ul className="mt-3 grid grid-cols-2 gap-3">
|
|
{shots.slice(1).map((image) => (
|
|
<li
|
|
key={image.mediaId}
|
|
className="relative overflow-hidden"
|
|
style={{
|
|
aspectRatio: '4 / 3',
|
|
borderRadius: 'var(--tpl-radius, 24px)',
|
|
boxShadow: '0 0 0 clamp(4px, 1vw, 8px) var(--tpl-bg, #fff)',
|
|
}}
|
|
>
|
|
<img
|
|
src={image.url}
|
|
alt={image.alt}
|
|
loading="lazy"
|
|
decoding="async"
|
|
className="size-full object-cover"
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/*
|
|
판 2 — 가격 · 핵심 값.
|
|
★ 국내 숙박 사이트에서 첫 화면이 답해야 하는 건 셋이다: 얼마인가 · 언제 드나 ·
|
|
어디서 예약하나. 예약은 위 버튼이 받고 나머지 둘이 이 띠다.
|
|
값이 하나도 없으면(수집 전 가게) 빈 칸 네 개 대신 판을 지운다.
|
|
*/}
|
|
{(price || facts.length > 0) && (
|
|
<div className="w-full" style={{backgroundColor: 'var(--pastel-2)'}}>
|
|
<dl className="shell flex flex-wrap items-center justify-center gap-x-12 gap-y-5 py-8 text-center">
|
|
{price && (
|
|
<div>
|
|
<dt
|
|
className="text-[length:var(--fs-xs)] font-extrabold"
|
|
style={{color: 'var(--tpl-bg, #fff)', opacity: 0.85}}
|
|
>
|
|
최저가
|
|
</dt>
|
|
<dd
|
|
className="mt-1 font-extrabold tabular-nums"
|
|
style={{fontSize: 'var(--fs-h3)', color: 'var(--tpl-bg, #fff)'}}
|
|
>
|
|
{price}
|
|
</dd>
|
|
</div>
|
|
)}
|
|
{facts.map((row) => (
|
|
<div key={row.label}>
|
|
<dt
|
|
className="text-[length:var(--fs-xs)] font-extrabold"
|
|
style={{color: 'var(--tpl-bg, #fff)', opacity: 0.85}}
|
|
>
|
|
{row.label}
|
|
</dt>
|
|
<dd
|
|
className="mt-1 font-extrabold"
|
|
style={{fontSize: 'var(--fs-h3)', color: 'var(--tpl-bg, #fff)'}}
|
|
>
|
|
{row.value}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
)}
|
|
</section>
|
|
);
|
|
}
|