수집한 객실·이용 정보가 발행 화면에 연결되지 않던 경로를 보완하고, 숙소 소개와 지역 맛집 표시를 개선한다. - NOL 브라우저 수집 어댑터와 수집·반영 스크립트 추가 - 크롤링 fact 즉시 노출 및 직접 입력·정정값 보호 - 이용안내 항목별 구조화와 기존 표 연결, 원문 UI 비표시 - 군산 한일옥 고정 등록과 지역 맛집 탐색·보강 경로 추가 - 숙소 소개 요약, 히어로 문구, 지역 콘텐츠·목업 표시 개선 검증: 작업 트리 기준 site 타입·린트·빌드 및 안내 렌더링 테스트 통과, PC·모바일 화면 확인. 스테이징 diff 공백 검사 통과. 사용자 요청에 따라 현재 스테이징된 55개 파일만 포함하며 미스테이징 문서·테스트 등은 제외.
164 lines
6.7 KiB
TypeScript
164 lines
6.7 KiB
TypeScript
import {HeroCatchphrase} from '@site/sections/HeroCatchphrase';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {primaryImage} from '@site/seo/meta';
|
|
import {
|
|
bookingActionLabel,
|
|
bookingLinks,
|
|
contactLinks,
|
|
channelLabel,
|
|
heroFacts,
|
|
lowestPrice,
|
|
unitSpec,
|
|
unitViews,
|
|
} from '@site/lib/derive';
|
|
import {GRAIN} from './SectionHead';
|
|
|
|
/**
|
|
* 편집(잡지)형 첫 화면 — 표지.
|
|
*
|
|
* ★ 사진은 오른쪽에 크게 걸고, 상호는 그 경계를 넘어 왼쪽 여백까지 흘러나온다.
|
|
* 겹침은 12칸 격자 위에서 만든다 — 사진과 상호 띠를 **같은 행 같은 칸**에 놓고
|
|
* 띠만 아래로 붙였다(absolute 를 쓰지 않아 모바일에서 자동으로 위아래로 풀린다).
|
|
* ★ 상호를 사진에 맨몸으로 얹지 않는다. 어느 사진의 어느 쪽이 밝은지 우리는 모른다 —
|
|
* 어두운 띠를 한 겹 깔고 그 위에 얹으면 어떤 사진에서도 대비가 남는다(88% 기준
|
|
* 흰 사진 위 최악값이 약 3.9:1, 큰 글자 기준 3:1 을 넘긴다).
|
|
* ★ 태그라인·요금·이용 정보는 사진 위가 아니라 **왼쪽 종이 면**에 둔다. 작은 글자는
|
|
* 3:1 로 부족하고(4.5:1 이 기준이다), 무엇보다 인용되는 문장을 사진에 태우면 안 된다.
|
|
*/
|
|
export function Hero() {
|
|
const payload = useSite();
|
|
const {place, narrative} = payload;
|
|
const image = primaryImage(payload);
|
|
const spec = unitSpec(payload);
|
|
const units = unitViews(payload);
|
|
const facts = heroFacts(payload);
|
|
const price = lowestPrice(payload);
|
|
const booking = bookingLinks(payload)[0];
|
|
const contact = contactLinks(payload)[0];
|
|
const tagline = narrative.tagline ?? narrative.heroSubline;
|
|
// 검색창에 치는 이름을 먼저 쓴다 — "제주시" 보다 "애월" 이다.
|
|
const locality = place.addressSubLocality ?? place.addressLocality ?? place.addressRegion;
|
|
|
|
// 메타 한 줄 — 지역 · 객실 수 · 최저가. 확인된 값만 · 로 잇는다.
|
|
const meta = [
|
|
locality,
|
|
units.length > 0 ? `${spec.label} ${units.length}개` : undefined,
|
|
price,
|
|
].filter((part): part is string => Boolean(part));
|
|
|
|
return (
|
|
<section id="top" className="relative w-full" style={{backgroundImage: GRAIN}}>
|
|
<div className="grid grid-cols-12">
|
|
{image && (
|
|
<div
|
|
className="relative col-span-12 xl:col-span-8 xl:col-start-5 xl:row-start-1"
|
|
style={{minHeight: 'clamp(15rem, 52vh, 34rem)', backgroundColor: 'var(--tpl-inverse)'}}
|
|
>
|
|
<img
|
|
src={image.url}
|
|
alt={image.alt}
|
|
// 첫 화면 이미지는 지연 로딩하지 않는다 — LCP 가 그만큼 늦어진다.
|
|
fetchPriority="high"
|
|
decoding="async"
|
|
width={image.width}
|
|
height={image.height}
|
|
className="absolute inset-0 size-full object-cover"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/*
|
|
상호 띠 — 데스크톱에서는 격자 1~12칸 같은 행에 놓고 아래에 붙인다(self-end).
|
|
사진이 5~12칸이므로 왼쪽 4칸은 종이 위, 나머지는 사진 위를 지난다.
|
|
★ z-10: 격자에서 뒤에 오는 형제(왼쪽 칸)가 이 띠를 덮지 않게 한다.
|
|
*/}
|
|
<div
|
|
className="z-10 col-span-12 xl:col-start-1 xl:row-start-1 xl:mb-12 xl:mr-16 xl:self-end"
|
|
style={{
|
|
backgroundColor: 'color-mix(in srgb, var(--tpl-inverse) 88%, transparent)',
|
|
color: 'var(--tpl-bg)',
|
|
}}
|
|
>
|
|
{/* h1 은 페이지에 하나. 가게 이름이 들어가야 "○○ 홈페이지" 질의에 잡힌다. */}
|
|
<h1
|
|
className="serif px-4 py-5 sm:px-6 xl:py-8 xl:pl-10 xl:pr-16"
|
|
style={{
|
|
fontSize: 'clamp(2.25rem, 7.5vw, 5.5rem)',
|
|
fontWeight: 800,
|
|
lineHeight: 0.98,
|
|
letterSpacing: '-0.035em',
|
|
}}
|
|
>
|
|
{place.name}
|
|
</h1>
|
|
</div>
|
|
|
|
{/* 왼쪽 종이 면 — 읽는 값이 전부 여기 있다. 아래는 상호 띠가 지나가는 자리다. */}
|
|
{/* pb-56 은 상호 띠 높이(제목 5.5rem + 여백)에서 나온 값이다 — 줄이면 글자가 겹친다. */}
|
|
<div className="col-span-12 flex flex-col gap-5 px-4 py-8 sm:px-6 xl:col-span-4 xl:col-start-1 xl:row-start-1 xl:py-12 xl:pb-56 xl:pl-10 xl:pr-8">
|
|
{tagline && (
|
|
<p className="measure font-serif text-[length:var(--fs-lead)] leading-relaxed">
|
|
<HeroCatchphrase>{tagline}</HeroCatchphrase>
|
|
</p>
|
|
)}
|
|
|
|
{meta.length > 0 && (
|
|
<p className="text-[length:var(--fs-sm)] font-semibold tabular-nums">
|
|
{meta.join(' · ')}
|
|
</p>
|
|
)}
|
|
|
|
{/* 버튼을 세우지 않는다 — 잡지의 규칙은 밑줄 링크다. 대신 .tap 으로 높이를 지킨다. */}
|
|
{(booking || place.phone || contact) && (
|
|
<div className="flex flex-wrap items-center gap-x-6 gap-y-1 text-[length:var(--fs-sm)] font-bold">
|
|
{booking && (
|
|
<a
|
|
href={booking.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="tap inline-flex items-center underline underline-offset-4"
|
|
>
|
|
{bookingActionLabel(booking)}
|
|
</a>
|
|
)}
|
|
{place.phone && (
|
|
<a
|
|
href={`tel:${place.phone}`}
|
|
className="tap inline-flex items-center underline underline-offset-4"
|
|
>
|
|
{place.phone}
|
|
</a>
|
|
)}
|
|
{contact && (
|
|
<a
|
|
href={contact.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="tap inline-flex items-center underline underline-offset-4"
|
|
>
|
|
{channelLabel(contact)} 문의
|
|
</a>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* 확인된 값만. 없는 업종에서는 칸 자체가 안 생긴다. */}
|
|
{facts.length > 0 && (
|
|
<dl className="mt-1">
|
|
{facts.map((row) => (
|
|
<div
|
|
key={row.label}
|
|
className="border-line flex items-baseline justify-between gap-4 border-t 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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|