import {StrictMode, useEffect} from 'react'; import {createRoot, hydrateRoot} from 'react-dom/client'; import type {SitePayload} from '@o2o/shared'; import {App} from './App'; import {fontHref, themeVars} from '@site/seo/head'; import './index.css'; declare global { interface Window { /** 프리렌더가 HTML 안에 심어 둔 payload. 이게 있으면 하이드레이션, 없으면 개발 모드. */ __SITE_PAYLOAD__?: SitePayload; } } const container = document.getElementById('root')!; const injected = window.__SITE_PAYLOAD__; /** * 미리보기를 띄운 빌더에게 "이제 그림이 나왔다" 고 알린다. * * ★ 빌더는 iframe 의 `load` 만으로는 이 시점을 알 수 없다. `load` 는 **셸이 뜬** 순간이고, * 그 뒤에 payload fetch + 웹폰트 대기(최대 2.5s) + `document.fonts.ready` 가 남아 있다. * 그 사이 iframe 은 흰 화면인데, load 를 완료로 읽으면 로딩 표시가 바로 사라져 * 사장님은 "다 됐다는데 아무것도 없는" 화면을 본다. * ★ 두 번의 rAF 를 기다린다 — 첫 프레임은 React 가 DOM 을 붙인 직후라 아직 그려지기 전이다. * ★ 실패해도 보낸다(ok=false). 안 보내면 빌더의 로딩 표시가 영영 안 걷힌다. */ function signalPreviewPainted(ok: boolean) { if (window.parent === window) return; const post = () => window.parent.postMessage({type: 'o2o:preview-painted', ok}, window.location.origin); requestAnimationFrame(() => requestAnimationFrame(post)); } /** * 빌더 미리보기(iframe)가 여는 셸에서만 쓰는 경로 — `/preview?placeId=…`. * * ★ 왜 여기서 payload 를 가져오나 * 미리보기는 **발행 전 최신 값**을 봐야 한다. 굽는 시점에는 어느 사업장을 미리 볼지 * 모르므로 셸에 payload 를 심을 수 없다(prerender `writePreviewShell`). * ★ 토큰은 부모(빌더)와 **같은 오리진의 localStorage** 에서 읽는다. 미리보기는 자기 서버가 * 아니라 빌더가 쓰는 것과 같은 API 를 부르므로, 키도 그쪽과 같아야 한다 * (`frontend/src/api/mutator/custom-fetch.ts`). * ★ 색·서체 토큰과 **웹폰트 링크**를 발행본이 `
` 에 굽는 것과 같은 함수로 만든다 * (themeVars · fontHref). 셸은 어느 템플릿인지 모른 채 구워지므로 둘 다 여기서 얹는다. * ★ 폰트를 빠뜨리면 조용히 틀린다 — 레이아웃·색은 그대로인데 글자만 기본 산세리프로 * 떨어진다. 실측(2026-09-09): 간판체(Gugi)가 안 실려 픽셀 차이가 92% 였는데 * 지오메트리(header·hero·h1 위치)는 발행본과 완전히 같았다. */ function PreviewReady() { useEffect(() => signalPreviewPainted(true), []); return null; } async function renderPreview(placeId: string) { const token = (() => { try { return localStorage.getItem('o2o-web4ai.accessToken'); } catch { return null; } })(); const res = await fetch(`/v1/place/${placeId}/site/preview`, { headers: token ? {Authorization: `Bearer ${token}`} : {}, }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const payload = (await res.json()) as SitePayload; for (const [name, value] of Object.entries(themeVars(payload))) { document.documentElement.style.setProperty(name, value); } const font = document.createElement('link'); font.rel = 'stylesheet'; font.href = fontHref(payload); document.head.appendChild(font); // 글자가 기본 서체로 한 번 그려졌다 바뀌는 것을 줄인다. 못 받아도 렌더는 계속한다. await new Promise