import {PlaceCategory} from '@o2o/shared'; import { AboutSection, SocialPostsSection, AnswerBlock, BookingSection, EssentialInfoSection, ExhibitionSection, FaqSection, FestivalSection, GallerySection, HeroSection, InquirySection, ITEM_SECTIONS, LocalGuideSection, WeatherSection, LocationSection, SpaceSection, UnitsSection, StayBookingSection, } from '@site/sections'; import {useSite} from '@site/lib/site-context'; import {hasSection, isSectionEnabled} from '@site/lib/derive'; /** * 홈. * * 섹션 순서는 관리자 에디터가 정한 것(theme.sections)을 따른다. * ★ AnswerBlock('예약 전 확인')은 **소개 바로 다음**이다 (2026-09-04, 사장님 지시). * 전에는 히어로 바로 밑이었다. 첫 화면에서 상호를 본 손님이 곧바로 체크인 시각·환불 규정 * 표를 만나면 "여기가 어떤 집인가"를 못 읽고 값부터 본다 — 소개 한 문단을 먼저 세운다. * 순서 설정과 무관하게 이 자리에 고정인 것은 그대로다. AI 검색이 답을 뽑아 가는 자리라 * 더 아래로 내려가면 인용률이 떨어진다. * ★ 소개가 꺼져 있으면 히어로 바로 다음으로 올라온다 — 빈자리를 기다리지 않는다. * * ★ variantId 는 아직 해석하지 않는다. * `SectionSetting.variantId` 에 사장님이 [디자인] 탭에서 고른 배리에이션 키 * (예: 'rules.cards')가 실려 오지만, 발행본은 그 값을 보지 않고 섹션마다 하나뿐인 * 기본 레이아웃으로 그린다. 배리에이션 40종을 발행본에 옮기는 건 별도 작업이다. * 렌더러가 모르는 키가 와도 화면이 깨지지 않아야 한다는 규칙(site-payload.ts)은 * "값을 통째로 무시한다"로 이미 지켜진다. * ★ 붙여넣기 아이템(songs·daily·…)은 배리에이션이 타입당 하나뿐이라 지금은 어긋날 것이 없다. */ export function HomePage() { const payload = useSite(); const isLodging = payload.place.category === PlaceCategory.LODGING; /** * 섹션 id → 발행본 컴포넌트. * * ★ 에디터의 섹션 타입(admin `builder/canvas/registry.ts`)과 이 표의 키가 어긋나면, * 사장님이 켜 둔 섹션이 아래 `if (!Component) return null` 에서 말없이 사라진다. * 실제로 rules(이용 규정)·booking·space·inquiry·exhibition 다섯이 그렇게 빠져 있었고, * 숙박 사장님이 써 넣은 환불 규정이 payload 까지 실려 오고도 발행본에 없었다. * 에디터에 섹션 타입을 늘릴 때는 여기도 같이 늘린다. */ const SECTION_COMPONENTS: Record React.ReactElement | null> = { intro: AboutSection, info: EssentialInfoSection, rooms: UnitsSection, menu: UnitsSection, programs: UnitsSection, // ★ 숙박은 예약 섹션이 다른 컴포넌트다(요금·인원·창구 + 날짜/시간 목업). // 같은 섹션 id 가 업종에 따라 다른 것을 그리는 자리는 여기가 유일하다 — // 이유는 StayBookingSection 머리주석. // ★ 이 줄이 64ce467 에서 조용히 사라졌다. 그러자 숙박 사이트의 예약 섹션이 전화번호 // 한 줄짜리 BookingSection 으로 돌아갔고, 화면에서 요금이 사라져 **발행이 통째로 // 막혔다** — JSON-LD 의 makesOffer.price 를 화면이 뒷받침하지 못해 절대규칙 3 대조에 // 걸린다(실측 2026-09-10: 발행본 2개 중 2개 실패). booking: isLodging ? StayBookingSection : BookingSection, space: SpaceSection, inquiry: InquirySection, exhibition: ExhibitionSection, photos: GallerySection, festival: FestivalSection, local: LocalGuideSection, weather: WeatherSection, map: LocationSection, faq: FaqSection, // 붙여넣기 아이템 아홉. 데이터가 fact 가 아니라 theme.sections[].data 의 JSON 에서 온다. // ★ 같은 컴포넌트를 두 번 그리지 않는 아래 규칙과 상관없다 — 아이템마다 컴포넌트가 다르다. ...ITEM_SECTIONS, }; const rendered = new Set(); return (
{!isSectionEnabled(payload, 'intro') && } {payload.theme.sections .filter((section) => section.enabled && section.id !== 'hero' && section.id !== 'social') .map((section) => { const Component = SECTION_COMPONENTS[section.id]; if (!Component) return null; // 같은 컴포넌트를 두 번 그리지 않는다(rooms/menu/programs 가 모두 UnitsSection). const key = Component.name; if (rendered.has(key)) return null; rendered.add(key); return ( /* * ★ `display: contents` 라 상자를 만들지 않는다 — 레이아웃에 손대지 않고 * **어느 섹션 설정에서 나온 것인지**만 표시한다. 빌더 편집 모드가 이 값으로 * 클릭한 자리를 왼쪽 목록의 섹션과 잇는다(`builder/SitePreview`). * 화면 id(`gallery`)와 설정 id(`photos`)가 달라서 필요하다. */
{section.id === 'intro' && }
); })} {/* 섹션 설정에 없더라도 오시는 길은 항상 나간다 — 위치 질의의 근거다. */} {!isSectionEnabled(payload, 'map') && } {/* ★ 숙박에서 예약 안내는 **항목이 없을 때만** 기본으로 낸다. "사장님이 껐다" 와 "payload 에 항목이 아예 없다" 는 다른 상태다(`hasSection`) — 옛 payload·손으로 만든 fixture 에는 booking 항목이 없는데, 숙박에서 예약 창구가 없는 페이지는 이 업종 질의의 대부분("어떻게 예약해요")에 답을 못 한다. 끈 것을 되살리지는 않는다 — 그건 사장님 결정이다. */} {isLodging && !hasSection(payload, 'booking') && } {/* 모든 Shell에서 푸터 바로 위. 에디터 순서를 바꿔도 마지막 위치를 유지한다. */} {isSectionEnabled(payload, 'social') &&
}
); }