모달 onClose 가 inline 함수라 부모 리렌더마다 useEffect 가 다시 걸려 scrollY 를 0으로 덮어써 X 버튼으로 닫으면 페이지가 맨 위로 튀었다. - lib/ui/Modal.tsx: onClose 를 ref 로 들고 [open] 에만 의존하도록 수정 (BlogSection·ReviewSection 등 Modal 쓰는 7곳 전부 적용) - sections/EssentialInfoSection.tsx: 체크인·체크아웃 행을 한 줄로 병합 - sections/GallerySection.tsx, UnitsSection.tsx, MobileTabBar.tsx, SiteFooter.tsx, items/*: 표시 폭·간격·라벨 정리 - lib/ui/Carousel.tsx: loop 이음매 간격 재점검 - scripts/prerender.ts: countUniqueContent 가 socialPosts(자체 출력)를 세지 않도록 — 콘텐츠 0건 사이트가 게이트를 우회하던 경로 tsc 통과, vitest 100/102 passed (use-live-weather 실패 2건은 기존·무관) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
131 lines
5.8 KiB
TypeScript
131 lines
5.8 KiB
TypeScript
import {Menu, Phone} from 'lucide-react';
|
|
import {useSite} from '@site/lib/site-context';
|
|
import {SongPlayer} from '@site/sections/SongPlayer';
|
|
import {isSectionEnabled, stayBookingView, unitSpec} from '@site/lib/derive';
|
|
|
|
/**
|
|
* 상단 내비.
|
|
*
|
|
* ★ 메뉴를 <nav> + <a> 로 그대로 둔다. 자바스크립트로 만드는 메뉴는
|
|
* 크롤러가 링크 그래프를 못 읽는다.
|
|
* ★ 모바일 메뉴도 <details> 다. 상태를 리액트로 들면 하이드레이션 전에는 안 열리고,
|
|
* 그 사이에 누른 손님에게는 그냥 고장난 버튼이다.
|
|
* ★ 사이트가 한 장이라 전부 앵커다.
|
|
*/
|
|
export function SiteHeader() {
|
|
const payload = useSite();
|
|
const spec = unitSpec(payload);
|
|
|
|
/* ★ 실제 페이지 순서와 같은 순서로 둔다(HomePage.tsx 가 theme.sections 순서로 그린다).
|
|
실측: 여기 목록은 '축제 → 주변 정보 → 오시는 길' 인데 화면은 '오시는 길(map) →
|
|
축제 → 주변 정보(local)' 순으로 나갔다 — 메뉴를 누르면 아래로 가야 할 항목이 위로 갔다. */
|
|
const items = [
|
|
{label: '소개', href: '#about', show: isSectionEnabled(payload, 'intro')},
|
|
{label: '이용 정보', href: '#info', show: true},
|
|
{label: spec.label, href: '#units', show: payload.units.length > 0},
|
|
{label: '오시는 길', href: '#location', show: true},
|
|
{label: '축제', href: '#festival', show: (payload.local.festivals?.length ?? 0) > 0},
|
|
{label: '주변 정보', href: '#guide', show: isSectionEnabled(payload, 'local')},
|
|
{label: 'FAQ', href: '#faq', show: payload.faqs.length > 0},
|
|
].filter((item) => item.show);
|
|
|
|
return (
|
|
<header
|
|
className="border-line safe-t sticky top-0 z-40 w-full border-b backdrop-blur-md"
|
|
style={{backgroundColor: 'color-mix(in srgb, var(--tpl-surface, #fff) 88%, transparent)'}}
|
|
>
|
|
<div className="shell flex h-16 items-center justify-between gap-3">
|
|
<a href="#top" className="flex min-w-0 items-center gap-2.5">
|
|
<span
|
|
className="serif flex size-8 shrink-0 items-center justify-center rounded-md text-[length:var(--fs-sm)]"
|
|
style={{
|
|
backgroundColor: 'var(--color-brand)',
|
|
color: 'var(--tpl-bg, #fff)',
|
|
fontWeight: 'var(--tpl-heading-weight, 700)',
|
|
}}
|
|
aria-hidden
|
|
>
|
|
{payload.place.name.slice(0, 1)}
|
|
</span>
|
|
<span className="flex min-w-0 flex-col">
|
|
{/*
|
|
★ font-bold 를 뺐다 (2026-09-04, 사장님: "폰트가 헤더랑 맞지 않는다")
|
|
워드마크와 섹션 제목은 같은 --tpl-font-heading 을 쓴다. 그런데 1안의 간판체(Gugi)는
|
|
**굵기가 400 하나뿐**이라, 700 을 요구하면 브라우저가 가짜 굵게로 획을 뭉개 그린다.
|
|
같은 서체인데 헤더만 번져 보였다. 굵기는 템플릿이 정한 값을 따른다.
|
|
*/}
|
|
<span
|
|
className="serif truncate text-[length:var(--fs-lead)] leading-tight"
|
|
style={{fontWeight: 'var(--tpl-heading-weight, 700)'}}
|
|
>
|
|
{payload.place.name}
|
|
</span>
|
|
{payload.place.englishName && (
|
|
<span className="text-muted truncate text-[length:var(--fs-xs)] uppercase leading-tight tracking-[0.14em]">
|
|
{payload.place.englishName}
|
|
</span>
|
|
)}
|
|
</span>
|
|
</a>
|
|
|
|
<nav aria-label="주요 메뉴" className="hidden items-center gap-7 lg:flex">
|
|
{items.map((item) => (
|
|
<a
|
|
key={item.label}
|
|
href={item.href}
|
|
className="text-muted py-1 text-[length:var(--fs-sm)] font-medium transition-colors hover:text-current"
|
|
>
|
|
{item.label}
|
|
</a>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
{/* 이 숙소의 노래. 곡이 없으면 아무것도 그리지 않는다(SongPlayer 머리주석). */}
|
|
<SongPlayer />
|
|
|
|
{payload.place.phone && (
|
|
<a
|
|
href={`tel:${payload.place.phone}`}
|
|
className="tap inline-flex items-center gap-1.5 rounded-lg px-3 text-[length:var(--fs-xs)] font-bold transition-opacity hover:opacity-100"
|
|
style={{backgroundColor: 'var(--color-brand)', color: 'var(--tpl-bg, #fff)'}}
|
|
>
|
|
<Phone className="size-4" />
|
|
<span className="hidden sm:inline">{payload.place.phone}</span>
|
|
<span className="sr-only sm:hidden">전화</span>
|
|
</a>
|
|
)}
|
|
|
|
{/* 좁은 화면의 전체 메뉴. 하단 탭은 네 자리뿐이라 소개·FAQ 로 갈 길이 여기밖에 없다. */}
|
|
<details className="relative lg:hidden">
|
|
<summary
|
|
className="tap flex cursor-pointer items-center justify-center rounded-lg marker:content-none [&::-webkit-details-marker]:hidden"
|
|
aria-label="메뉴 열기"
|
|
>
|
|
<Menu className="size-5" />
|
|
</summary>
|
|
<nav
|
|
aria-label="전체 메뉴"
|
|
className="border-line tpl-border absolute right-0 top-[calc(100%+0.5rem)] z-50 w-52 overflow-hidden rounded-xl border shadow-lg"
|
|
style={{backgroundColor: 'var(--tpl-surface, #fff)'}}
|
|
>
|
|
<ul className="divide-line divide-y">
|
|
{items.map((item) => (
|
|
<li key={item.label}>
|
|
<a
|
|
href={item.href}
|
|
className="tap flex items-center px-4 text-[length:var(--fs-sm)] font-medium"
|
|
>
|
|
{item.label}
|
|
</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</nav>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
}
|