목업(/s/stay)이 참조하는 스타일시트가 서버에서 사라져(404) 그 화면을 기준으로 삼을 수
없었다. 브라우저 캐시에 남아 있던 규칙을 꺼내 레포에 되돌리고, 섹션 마크업을 목업 HTML
에서 그대로 옮겼다. Playwright 대조로 섹션 래퍼·제목 16/17, 안쪽 구조 14/17 일치.
- index.css: --fs-display~--fs-xs(유동 타이포 7) · --section-space · --color-line/muted ·
.h2 .h3 .panel .paper .measure .border-line .text-muted .divide-line .slider-viewport/track
★ 이 토큰이 없던 동안 컴포넌트가 var(--fs-display)를 써도 브라우저가 조용히 무시했다 —
히어로 제목이 본문 크기로 나오던 원인
- HeroSection: 가운데 → 좌하단. 사진 위에 글자를 한가운데 얹으면 피사체를 정확히 가린다
- AnswerBlock·EssentialInfo·Units·Gallery·Location·Faq·LocalGuide·Weather·About:
목업 마크업 그대로. embla 를 scroll-snap 으로 바꿔 스크립트 없이도 레일이 밀린다
- FestivalSection(계절 탭) · StorySection(이야기 탭 래퍼) · items/TripMap(OSM 타일 지도) 신설
★ 지도는 iframe 이 아니라 타일을 직접 깐다 — iframe 은 핀을 하나밖에 못 찍어
"어떤 순서로 도는가" 를 그릴 수 없다
- items: itinerary·event·video kind 추가(파서에 없어 payload 까지 실려 오고도 화면에서
사라지던 것) · Rail 을 slider-viewport/track 으로 · SongsSection 을 턴테이블로
- color.ts: 선 색을 secondary → mix(bg, text, .2). secondary 는 '본문 다음으로 진한
글자색' 이라 선으로 쓰면 표와 카드가 격자무늬처럼 새까맣게 그어진다
- seo/head.ts: --tpl-texture. 없으면 색만 갱지고 면은 매끈해서 인쇄물로 안 보인다
- seo/verify.ts: unitCode·numberOfRooms 를 화면 대조에서 뺀다
★ unitCode 는 ㎡의 ISO 코드로 priceCurrency 와 같은 성격인데 예외에 없었다. 그래서
room_size 가 있는 사업장은 발행 게이트가 전부 막았다(실측: 가은채 객실 12개).
stay 는 객실 2개 + room_size 없음이라 우연히 통과했다
- shared: LocalPlace.imageUrl/distanceMeters · FestivalEntry.imageUrl ·
ItineraryStop 좌표 · TemplateLook.texture · people/chronicle/postcard imageUrl
★ 좌표·사진은 모델이 만드는 칸이 아니다. 공식 API 로 조회해 채운다
검증: tsc 통과 · Playwright 대조(목업 CSS 주입) 섹션 래퍼·제목 16/17, 안쪽 14/17.
남은 차이는 데이터 한계다 — people 사진은 위키에 원본이 없고(original:false),
weather.note 는 정적 페이지에 날씨 문장을 박는 문제라 보류.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP1ykMnWZFtLFpm2mow1Sw
108 lines
4.3 KiB
TypeScript
108 lines
4.3 KiB
TypeScript
import {
|
|
AboutSection,
|
|
AnswerBlock,
|
|
BookingSection,
|
|
EssentialInfoSection,
|
|
ExhibitionSection,
|
|
FaqSection,
|
|
FestivalSection,
|
|
GallerySection,
|
|
HeroSection,
|
|
InquirySection,
|
|
ITEM_SECTIONS,
|
|
LocalGuideSection,
|
|
WeatherSection,
|
|
LocationSection,
|
|
RulesSection,
|
|
SpaceSection,
|
|
StorySection,
|
|
UnitsSection,
|
|
} from '@/sections';
|
|
import {useSite} from '@/lib/site-context';
|
|
import {isSectionEnabled} from '@/lib/derive';
|
|
|
|
/**
|
|
* StorySection 이 탭으로 묶어 그리는 아이템.
|
|
*
|
|
* ★ `sections/StorySection.tsx` 의 STORY_KINDS 와 같은 목록이어야 한다.
|
|
* 어긋나면 어떤 아이템은 탭 밖에 따로 서고, 어떤 아이템은 어디에도 안 나온다.
|
|
*/
|
|
const STORY_KINDS = ['songs', 'people', 'chronicle', 'literature', 'postcard', 'quiz', 'daily'];
|
|
|
|
/**
|
|
* 홈.
|
|
*
|
|
* 섹션 순서는 관리자 에디터가 정한 것(theme.sections)을 따른다.
|
|
* ★ 히어로 바로 다음에 AnswerBlock 이 온다. 이 자리는 순서 설정과 무관하게 고정이다 —
|
|
* AI 검색이 답을 뽑아 가는 자리라, 사장님이 아래로 내리면 인용률이 떨어진다.
|
|
*
|
|
* ★ variantId 는 아직 해석하지 않는다.
|
|
* `SectionSetting.variantId` 에 사장님이 [디자인] 탭에서 고른 배리에이션 키
|
|
* (예: 'rules.cards')가 실려 오지만, 발행본은 그 값을 보지 않고 섹션마다 하나뿐인
|
|
* 기본 레이아웃으로 그린다. 배리에이션 40종을 발행본에 옮기는 건 별도 작업이다.
|
|
* 렌더러가 모르는 키가 와도 화면이 깨지지 않아야 한다는 규칙(site-payload.ts)은
|
|
* "값을 통째로 무시한다"로 이미 지켜진다.
|
|
* ★ 붙여넣기 아이템(songs·daily·…)은 배리에이션이 타입당 하나뿐이라 지금은 어긋날 것이 없다.
|
|
*/
|
|
export function HomePage() {
|
|
const payload = useSite();
|
|
|
|
/**
|
|
* 섹션 id → 발행본 컴포넌트.
|
|
*
|
|
* ★ 에디터의 섹션 타입(admin `builder/canvas/registry.ts`)과 이 표의 키가 어긋나면,
|
|
* 사장님이 켜 둔 섹션이 아래 `if (!Component) return null` 에서 말없이 사라진다.
|
|
* 실제로 rules(이용 규정)·booking·space·inquiry·exhibition 다섯이 그렇게 빠져 있었고,
|
|
* 숙박 사장님이 써 넣은 환불 규정이 payload 까지 실려 오고도 발행본에 없었다.
|
|
* 에디터에 섹션 타입을 늘릴 때는 여기도 같이 늘린다.
|
|
*/
|
|
const SECTION_COMPONENTS: Record<string, () => React.ReactElement | null> = {
|
|
intro: AboutSection,
|
|
info: EssentialInfoSection,
|
|
rooms: UnitsSection,
|
|
menu: UnitsSection,
|
|
programs: UnitsSection,
|
|
rules: RulesSection,
|
|
booking: BookingSection,
|
|
space: SpaceSection,
|
|
inquiry: InquirySection,
|
|
exhibition: ExhibitionSection,
|
|
photos: GallerySection,
|
|
local: LocalGuideSection,
|
|
festival: FestivalSection,
|
|
weather: WeatherSection,
|
|
map: LocationSection,
|
|
faq: FaqSection,
|
|
// 붙여넣기 아이템. 데이터가 fact 가 아니라 theme.sections[].data 의 JSON 에서 온다.
|
|
// ★ 같은 컴포넌트를 두 번 그리지 않는 아래 규칙과 상관없다 — 아이템마다 컴포넌트가 다르다.
|
|
...ITEM_SECTIONS,
|
|
// ★ 지역 이야기(가요·인물·연표·엽서·퀴즈·문학·일력)는 StorySection 이 탭으로 묶어 그린다.
|
|
// 그래서 그 일곱은 위 표에서 **가려낸다** — 여기 남겨 두면 탭 안과 밖에 두 번 나온다.
|
|
...Object.fromEntries(STORY_KINDS.map((kind) => [kind, StorySection])),
|
|
};
|
|
|
|
const rendered = new Set<string>();
|
|
|
|
return (
|
|
<main className="w-full flex-1">
|
|
<HeroSection />
|
|
<AnswerBlock />
|
|
|
|
{payload.theme.sections
|
|
.filter((section) => section.enabled && section.id !== 'hero')
|
|
.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 <Component key={section.id} />;
|
|
})}
|
|
|
|
{/* 섹션 설정에 없더라도 오시는 길은 항상 나간다 — 위치 질의의 근거다. */}
|
|
{!isSectionEnabled(payload, 'map') && <LocationSection />}
|
|
</main>
|
|
);
|
|
}
|