# 렌더링 한눈에 보기 사이트가 화면에 그려지는 경우는 세 가지다. 셋 다 그리는 코드는 `solution/site/src/App.tsx` 하나이고, **누가 언제 그리느냐**만 다르다. | 경우 | 누가 그리나 | 입력 | |---|---|---| | 정적 사이트 | 워커가 미리 구운 HTML → 브라우저가 이어받음 | HTML 안에 심어 둔 payload | | 미리보기 | 브라우저가 처음부터 | API 가 그때그때 만든 payload | | 발행 | 워커가 Node 렌더러를 돌려 HTML 을 굽는다 | DB → payload 파일 | 경로는 레포 루트 기준이다. `site/` 는 `solution/site/`, `backend/` 는 `solution/backend/` 를 줄인 것이다. --- ## 1. 정적 사이트 — 손님·크롤러가 `/s/` 를 받을 때 ```mermaid flowchart TD A["손님 · 크롤러
GET /s/<slug>"] --> B["nginx
location ^~ /s/"] B --> C["out/s/<slug>
(심볼릭 링크)"] C --> D["out/versions/<slug>/<ver>/index.html"] D -->|크롤러는 여기까지| E["HTML · JSON-LD · meta"] D --> F["브라우저가 /assets/index-해시.js · .css 를 받음"] F --> G["entry-client.tsx
window.__SITE_PAYLOAD__ 있음"] G --> H["hydrateRoot(App)
버튼·달력 등 동작이 붙는다"] D --> I["사진 /s/<slug>/img/*
노래 /s/<slug>/*.mp3"] ``` | 단계 | 하는 일 | 파일 | |---|---|---| | 요청 받기 | `/s/` 는 구운 파일을 그대로 준다. `/s` 는 목록, `/s/` 는 `/s` 로 301 | `nginx/site.conf.example` (`location = /s`, `location ^~ /s/`) | | 공개 버전 찾기 | `out/s/` 는 지금 공개 중인 버전 폴더를 가리키는 링크다 | `site/scripts/prerender.ts` `publishVersion` | | HTML | 본문·``(title·canonical·JSON-LD)·심어 둔 payload 가 한 파일에 있다 | `out/versions///index.html` | | 번들 | 해시 이름의 JS·CSS. 1년 캐시 | `nginx/site.conf.example` `location ^~ /assets/` → `out/assets/` | | 이어받기 | 심어 둔 payload 로 같은 화면을 다시 만들어 마크업에 동작을 붙인다 | `site/src/entry-client.tsx` (`hydrateRoot`) | | 그리기 | 템플릿의 레이아웃을 고르고 섹션을 순서대로 그린다 | `site/src/App.tsx` → `site/src/pages/SectionList.tsx` | 검색엔진이 읽는 건 구운 HTML 이다. 렌더러를 고쳐도 이미 구운 HTML 은 사장님이 다시 발행하기 전까지 그대로다. --- ## 2. 미리보기 — 빌더 iframe `/preview?placeId=…` ```mermaid flowchart TD A["빌더에서 템플릿·색·섹션 저장
POST …/site/template · …/site/theme"] --> B["onSiteThemeSaved 신호"] B --> C["SitePreview.tsx
iframe 다시 로드"] C --> D["GET /preview?placeId=…
nginx location = /preview"] D --> E["out/preview/index.html
빈 껍데기 + 번들"] E --> F["entry-client.tsx renderPreview"] F --> G["GET /v1/place/{id}/site/preview"] G --> H["SiteService.preview_payload
build_snapshot → prepare_site_payload"] H --> F F --> I["themeVars · 폰트 로드"] I --> J["createRoot(App)"] J --> K["postMessage o2o:preview-painted"] K --> L["빌더가 스피너를 걷는다
(12초 상한)"] ``` | 단계 | 하는 일 | 파일 | |---|---|---| | 다시 그릴 때를 안다 | 저장이 끝나면 iframe 을 새로 고친다. 보던 스크롤 위치는 지킨다 | `solution/frontend/src/features/builder/SitePreview.tsx`, `solution/frontend/src/features/publish/siteTheme.ts` `onSiteThemeSaved` | | 껍데기 받기 | 본문이 빈 HTML. `noindex` 가 붙어 있다 | `nginx/site.conf.example` `location = /preview` → `out/preview/index.html` (`prerender.ts` `writePreviewShell`) | | payload 받기 | 로그인 토큰을 붙여 API 를 부른다 | `site/src/entry-client.tsx` `renderPreview` | | payload 만들기 | 발행과 같은 함수로 만든다. 버전도 파일도 만들지 않는다 | `backend/router/v1/site/site.py` `site_preview` → `backend/services/site_service.py` `preview_payload` → `services/snapshot.py` `build_snapshot` → `services/site_payload.py` `prepare_site_payload` | | 템플릿 확인 | 모르는 id 면 API 가 422, 화면은 에러 문구로 멈춘다 | `backend/common/template_catalog.py`, `solution/shared/src/lib/catalog.ts` `templateOf` | | 그리기 | 색 변수·폰트를 먼저 넣고 처음부터 그린다 | `entry-client.tsx` (`themeVars`, `fontHref` ← `site/src/seo/head.ts`), `App.tsx` | | 완료 알림 | 두 프레임 뒤 부모 창에 알린다. 빌더는 출처와 iframe 을 확인한다 | `entry-client.tsx` `signalPreviewPainted`, `SitePreview.tsx` `PAINT_TIMEOUT_MS` | 미리보기는 사진을 내려받지 않는다. 원래 주소를 그대로 쓴다. --- ## 3. 발행 — 무엇을 읽고 무엇을 쓰나 ```mermaid flowchart TD A["사장님 '발행하기'
POST /v1/place/{id}/site/build"] --> B["SiteService.start_build
jobs 에 BUILD"] B --> C["워커 worker/handlers.py
build_service.run_build"] C --> D["build_snapshot
DB 값 모으기 · site_versions 행 추가"] D --> E{"1차 게이트
상호·업종·사실 확인 · 템플릿 id"} E -->|실패| X["버전 FAILED · 발행 로그"] E --> F["emit_payload
payloads/<slug>.json"] F --> G["render_service.render_site
node prerender.js --stage-only"] G --> H["mirrorMedia → prerenderSite
out/versions/<slug>/<ver>/"] H --> I["보고서
payloads/.status/<slug>.json"] I --> J{"2차 게이트
publish_gate.evaluate"} J -->|실패| X J --> K["render_service.activate_site
node prerender.js --activate=slug:ver"] K --> L["out/s/<slug> 링크 전환
루트 sitemap · robots · llms 갱신"] L --> M["Azure 업로드 · 썸네일 · IndexNow"] M --> N["DB 기록
버전 BUILT · sites PUBLISHED · 발행 로그"] ``` | 단계 | 하는 일 | 파일 | |---|---|---| | 잡 넣기 | 검증 안 된 사업장은 막는다. 같은 사업장 BUILD 는 겹치지 않는다 | `backend/router/v1/site/site.py` `start_build` → `services/site_service.py` `start_build` | | 잡 집기 | BUILD 잡을 `run_build` 로 넘긴다 | `backend/worker/handlers.py` | | 스냅샷 | DB 값을 한 벌로 모아 `site_versions.snapshot` 에 박제한다 | `services/snapshot.py` `build_snapshot`, `services/build_service.py` `run_build` | | 1차 게이트 | 상호명·업종·사실 확인 여부, 템플릿 id | `services/publish_gate.py` `check_facts_verified`, `common/template_catalog.py` `resolve_template_id` | | payload | JSON 으로 쓴다. 임시 파일에 쓰고 이름을 바꾼다 | `services/site_payload.py` `emit_payload` → `write_payload` | | 굽기 | Node 를 직접 실행한다. 파일 잠금으로 한 번에 하나만 돈다 | `services/render_service.py` `render_site` (`.render.lock`) | | 렌더 | 공개 금지 값 걸러내기 → 사진 내려받기 → HTML·JSON-LD·llms.txt → 대조 | `site/scripts/prerender.ts` `sanitizePayloadForPublish` · `mirrorMedia` · `prerenderSite` · `verifyJsonLd` | | 2차 게이트 | 보고서의 대조 결과·고유 콘텐츠 건수로 판정 | `services/publish_gate.py` `evaluate`, `services/render_report.py` | | 공개 전환 | 검증된 버전인지 보고서로 다시 보고 링크를 바꾼다 | `render_service.activate_site` → `prerender.ts` `publishVersion` · `writeRootMachineFiles` | | 바깥 알리기 | 설정된 경우만 돈다 | `services/azure_static.py` `publish`, `services/site_thumbnail.py` `store`, `services/indexnow.py` `submit` | | DB 기록 | 버전·사이트·사업장 상태와 발행 로그를 남긴다 | `services/build_service.py` `run_build` · `_log` | ### 입력 | 무엇 | 어디서 | 읽는 쪽 | |---|---|---| | DB 값 | `place_facts` `place_units` `place_faqs` `place_photos` `place_songs` `place_posts` `place_reviews` `place_social_posts` `area_contents` `site_sections` `sites` | `services/snapshot.py` `build_snapshot` | | 템플릿 목록 | `solution/shared/src/data/templates.json` | 백엔드 `common/template_catalog.py`, 렌더러 `shared/src/lib/catalog.ts` | | payload | `site/payloads/.json` (`SITE_PAYLOAD_DIR`) | `prerender.ts` `loadOne` — `schemaVersion` 1 · 슬러그 · 버전을 본다 | | 번들 목록 | `site/dist/client/.vite/manifest.json` | `prerender.ts` `readAssets` — 엔트리 JS·CSS 파일명 | | 번들 파일 | `site/dist/client/assets/`, `site/public/fonts/` | `prerender.ts` `writeSharedAssets` | | 사진 | `payload.media[].url` 이 가리키는 바깥 주소 | `prerender.ts` `mirrorMedia` (15초 · 8MB) | | 노래 | `site/songs/*.mp3` | `prerender.ts` `copySongs` | ### 출력 | 무엇 | 어디에 | 누가 쓰나 | |---|---|---| | payload | `site/payloads/.json` | `site_payload.py` `write_payload` | | 렌더 보고서 | `site/payloads/.status/.json` | `prerender.ts` `writeReport` (백엔드가 `render_report.py` 로 읽는다) | | HTML | `out/versions///index.html` — JSON-LD 는 따로 파일이 없고 `` 안 `