import {useEffect, useState, type ReactNode} from 'react';
import {useSite} from '@site/lib/site-context';
import {bookingActionLabel, bookingLinks, channelLabel, primaryChannelLink} from '@site/lib/derive';
import {formatKoreanDate, isoDate} from '@site/lib/format';
// 배럴(@/sections)이 아니라 파일을 직접 가리킨다 — 배럴을 타면 섹션 전부가 딸려 온다.
import {MobileTabBar} from '@site/sections/MobileTabBar';
import {GRAIN, useSectionIndex, type IndexEntry} from './SectionHead';
/**
* 편집(잡지)형 껍데기.
*
* ★ 왼쪽 세로 차례가 이 안의 얼굴이다. 잡지의 등(spine)처럼 화면 왼쪽에 붙어 따라오고,
* 스크롤에 따라 지금 읽는 항목만 진해진다. **데스크톱에서만** 세운다 — 좁은 화면에서
* 목차는 자리만 먹고, 그 일은 이미 상단 제호 줄과 하단 고정 바가 한다.
* ★ 차례를 fixed 로 띄우지 않고 flex 한 칸으로 세웠다. fixed 로 얹으면 어두운 판이
* 밑으로 지나갈 때 목차 글자가 통째로 사라진다 — 칸으로 두면 항상 제 종이 위에 있다.
* ★ 제호 줄에 전화·예약을 둔다. 국내 숙박 사이트에서 상단이 답해야 하는 건 그 둘뿐이다.
*/
export function Shell({children}: {children: ReactNode}) {
const payload = useSite();
const {place, site} = payload;
const index = useSectionIndex();
const active = useActiveSection(index);
const booking = bookingLinks(payload)[0];
// ★ 확정 채널을 전부 늘어놓지 않는다 — 예약 우선으로 딱 하나만 낸다(seo/jsonld.ts 주석 참고).
const link = primaryChannelLink(payload);
const address = place.roadAddress ?? place.address;
return (
{/* 제호(masthead) — 얇은 한 줄, 아래는 굵은 실선. 잡지의 표제부다. */}
{/* 판권장(colophon) — 잡지 맨 뒷장. 상호·주소·연락처·법정 표기가 한자리에 남는다. */}
);
}
/**
* 지금 읽고 있는 섹션.
*
* ★ 화면 한가운데 얇은 띠(위 45% · 아래 50% 를 잘라낸 나머지)를 지나는 판만
* '읽는 자리'로 본다. 띠를 안 좁히면 긴 섹션 둘이 동시에 걸려 목차가 깜빡인다.
* ★ 서버 렌더에서는 아무것도 진해지지 않는다(useEffect 는 브라우저에서만 돈다) —
* 초기 상태가 양쪽 다 같아서 하이드레이션이 어긋나지 않는다.
*/
function useActiveSection(entries: IndexEntry[]): string {
// 배열은 렌더마다 새로 만들어진다 — 문자열로 굳혀야 effect 가 매 렌더 다시 돌지 않는다.
const anchors = entries.map((entry) => entry.anchor).join(',');
const [active, setActive] = useState('');
useEffect(() => {
if (typeof IntersectionObserver === 'undefined') return;
const targets = anchors
.split(',')
.map((id) => document.getElementById(id))
.filter((el): el is HTMLElement => el !== null);
if (targets.length === 0) return;
const observer = new IntersectionObserver(
(records) => {
const hit = records.find((record) => record.isIntersecting);
if (hit) setActive(hit.target.id);
},
{rootMargin: '-45% 0px -50% 0px'},
);
targets.forEach((el) => observer.observe(el));
return () => observer.disconnect();
}, [anchors]);
return active;
}