o2o-site-AEO/docs/TEMPLATES.md
Mina Choi c6908629f3 [feat] solution: 템플릿 정의를 templates.json 한 파일로 — 업종 없는 id · 레이아웃 basic/paper 정리
템플릿 정보가 빌더·렌더러·백엔드에 따로 적혀 어긋나 있었다 — 백엔드 기본값이 없는 id 를
가리켰고, 음식점 강조색 오타, 섹션 간격이 빌더와 서버에서 달랐다.

- shared/src/data/templates.json: 템플릿 4개(simple·magazine·retro·paper)와 업종별 허용·기본
- shared/lib/catalog.ts · backend/common/template_catalog.py: 같은 JSON 을 읽는다, 모르는 id 는 에러
- 저장·미리보기·발행에서 모르는 id 를 거절한다(site_service · site.py 422 · build_service 실패)
- site: 레이아웃 등록표(basic·paper) + LayoutProvider, Shell→Frame, HomePage→SectionList
- 연결 안 된 레이아웃 5개, 배치 고르기(variant), 서체 선택, 빌더 canvas/DevShowcase 삭제
- 빌더: 템플릿을 바꾸면 이전 템플릿이 켠 섹션을 끄고 안내 문구를 띄운다
- postgres-init/migrations/0023: stay-retro → retro, 병원 허용 밖은 NULL(운영 미적용)
- Dockerfile·worker: 백엔드 이미지에 shared/src/data 복사
- docs: TEMPLATES.md 신설(세 폴더 역할·템플릿 추가·렌더링 순서), DATA_MODEL·ARCHITECTURE 등 갱신

shared·site·frontend tsc 통과, site vitest 통과, 백엔드 DB 없는 테스트 41 passed(DB 테스트 미실행)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 15:57:35 +09:00

9.0 KiB

템플릿과 렌더링

사장님이 고르는 "템플릿"이 어디에 정의돼 있고, 화면에 어떤 순서로 그려지는지 적은 문서다. 2026-09-28에 구조를 한 번 갈아엎었고, 이 문서는 그 뒤의 모습이다.

1. 세 폴더가 하는 일

solution/ 밑의 세 폴더는 하는 일이 다르다. 한 줄로 말하면 이렇다.

폴더 누가 보나 하는 일
shared/ 아무도 직접 안 본다 나머지 둘과 백엔드가 같이 쓰는 약속을 둔다. 템플릿 목록, payload 모양, 슬러그 규칙
frontend/ 사장님 빌더. 템플릿·색·섹션을 고르고 내용을 고친다. 고른 값은 서버에 저장만 한다
site/ 손님, 검색엔진, AI 발행된 사이트를 그리는 쪽. 서버가 만든 payload를 받아 HTML로 굽는다

조금 더 풀면:

  • shared 는 코드라기보다 계약서다. 템플릿이 몇 개인지, 이름이 뭔지, 어느 업종이 뭘 쓸 수 있는지는 전부 shared/src/data/templates.json 한 파일에 있다. TS 쪽은 shared/src/lib/catalog.ts가, 파이썬 쪽은 backend/common/template_catalog.py가 이 파일을 그대로 읽는다. 그래서 템플릿 정보가 두 군데로 갈라질 수 없다.
  • frontend 는 사이트를 직접 그리지 않는다. 미리보기도 site가 그린 화면을 iframe으로 띄울 뿐이다. 빌더 안에 따로 그리는 코드를 두면 미리보기와 발행본이 조금씩 달라지는데, 예전에 실제로 그랬다.
  • site 는 DB도 API도 모른다. payload JSON 하나만 받으면 사이트 한 장을 그린다. 발행 때는 워커가 부르는 Node 스크립트로 HTML을 굽고, 미리보기 때는 브라우저에서 같은 코드로 그린다.

2. 템플릿은 무엇으로 이뤄지나

templates.json에 템플릿 하나는 이렇게 생겼다.

칸 뜻
name · tag · description 빌더에서 사장님이 보는 이름과 설명
layout 어떤 뼈대로 그릴지. 지금은 basic과 paper 두 개
colors 기본 색. 사장님이 팔레트를 고르면 그 색이 위에 덮인다
look 서체, 모서리, 그림자, 섹션 간격. 사장님이 못 바꾼다
addSections 이 템플릿을 고르면 새로 켜지는 섹션. 레트로의 일력·영상 같은 것

지금 템플릿은 네 개다.

id 이름 뼈대
simple 심플 basic
magazine 매거진 basic
retro 레트로 basic
paper 고택 paper

심플·매거진·레트로는 뼈대가 같고 색과 서체만 다르다. 고택만 머리글, 첫 화면, 객실 목록, 섹션 제목 모양이 따로 있다.

업종마다 쓸 수 있는 템플릿과 기본 템플릿은 같은 파일의 industries에 적는다. 숙박은 레트로가 기본이고, 병원은 심플과 매거진만 쓸 수 있다.

뼈대(레이아웃)는 필요한 것만 바꾼다

레이아웃은 site/src/layouts/에 폴더 하나씩이다. 한 레이아웃이 가질 수 있는 건 셋이다.

  • Frame: 머리글, 본문 자리, 바닥글. 이건 꼭 있어야 한다.
  • SectionHead: 섹션 제목 모양. 없으면 공용 제목을 쓴다.
  • sections: 섹션별로 바꿔 그릴 컴포넌트. 여기 안 적은 섹션은 공용 컴포넌트를 그대로 쓴다.

고택은 첫 화면·객실·제목만 바꿨고, FAQ나 오시는 길은 심플과 같은 컴포넌트를 쓴다. 새 레이아웃을 만들 때도 전부 새로 그릴 필요 없이 다르게 보여야 하는 섹션만 만들면 된다.

이상한 값이 들어오면 멈춘다

DB에 모르는 템플릿 id가 들어 있으면, 조용히 기본값으로 굽지 않고 멈춘다.

  • 저장할 때: 그 업종이 못 쓰는 템플릿이면 저장을 거절한다.
  • 미리보기: 서버가 422를 주고, 화면에 에러가 뜬다.
  • 발행: 잡이 실패로 끝난다.

예전에는 모르는 값이 오면 기본 템플릿으로 슬쩍 구웠다. 그러면 사장님이 고른 디자인과 다른 사이트가 나가도 아무도 모른다.

3. 새 템플릿을 추가할 때

기존 뼈대를 쓰는 경우 (색·서체만 다른 템플릿)

templates.json 한 파일만 고치면 된다.

  1. templates에 새 항목을 넣는다. id는 영어 소문자로 짓고, 업종 이름은 붙이지 않는다.
  2. 쓸 수 있게 할 업종의 industries.<업종>.templates 목록에 그 id를 넣는다.
  3. 기본 템플릿으로 삼을 거면 defaultTemplate도 바꾼다.

타입(TemplateId)은 JSON 키에서 자동으로 뽑히므로 손댈 곳이 없다. 빌더 목록, 미리보기, 백엔드 검증에 자동으로 들어간다. 레이아웃 이름을 틀리게 적거나 업종 목록에 없는 id를 적으면, 앱이 뜰 때 바로 에러가 난다.

새 뼈대가 필요한 경우

위 세 단계에 더해서 다음을 한다.

  1. site/src/layouts/<새이름>/Frame.tsx를 만든다. 바꿔 그릴 섹션이 있으면 같은 폴더에 둔다.
  2. site/src/layouts/index.ts의 LAYOUTS에 한 줄 넣는다.
  3. shared/src/types/builder.ts의 LayoutId에 이름을 넣는다.
  4. shared/src/lib/catalog.ts의 LAYOUT_IDS에도 넣는다.

2~4를 하나라도 빠뜨리면 타입체크나 앱 시작 단계에서 걸린다.

새 섹션이 딸려 오는 경우

addSections에 적는 섹션은 이미 있는 섹션이어야 한다. 섹션 자체를 새로 만드는 건 템플릿과 별개의 일이다. site의 섹션 컴포넌트, 빌더의 섹션 목록, payload 모양을 다 만져야 한다.

올릴 때

JSON은 빌드할 때 번들과 이미지에 들어간다. 그래서 템플릿을 추가하면 backend·worker·site· frontend를 전부 다시 빌드해야 한다. 하나만 올리면 빌더에는 보이는데 저장이 거절되는 식으로 어긋난다.

DB는 건드릴 필요가 없다. 이미 발행된 사이트는 사장님이 다시 발행하기 전까지 그대로다.

4. 렌더링 순서

빌더에서 고칠 때 (미리보기)

사장님이 빌더에서 템플릿·색·섹션을 바꾼다
  │  frontend  stores/builder.ts  (템플릿을 바꾸면 이전 템플릿이 켠 섹션은 꺼진다)
  ▼
서버에 저장한다
  │  템플릿   POST /v1/place/{id}/site/template   → sites.template_id
  │  색·섹션  POST /v1/place/{id}/site/theme      → sites.theme
  ▼
저장이 끝나면 미리보기 iframe을 다시 연다
  │  frontend  features/builder/SitePreview.tsx  (features/publish/siteTheme.ts 의 저장 완료 신호를 듣는다)
  │  iframe 주소  /preview?placeId=…   (site가 미리 구워 둔 빈 껍데기 페이지)
  ▼
껍데기 안의 site 코드가 서버에 payload를 달라고 한다
  │  site  entry-client.tsx  renderPreview
  │  GET /v1/place/{id}/site/preview
  │  backend  services/site_payload.py  — 발행 때와 같은 함수로 payload를 만든다
  ▼
템플릿 id를 확인하고 그린다
  │  모르는 id면 여기서 에러 문구를 띄우고 멈춘다
  │  App.tsx → templates.json의 layout을 보고 LAYOUTS에서 뼈대를 고른다
  │  Frame 안에 SectionList가 섹션을 순서대로 그린다
  ▼
다 그렸다고 빌더에 알린다 (postMessage) → 빌더가 로딩 표시를 걷는다

발행할 때

사장님이 "발행하기"를 누른다
  ▼
jobs 표에 BUILD 잡이 들어간다
  ▼
워커가 잡을 집는다              backend  services/build_service.py  run_build
  │  1. 상호명·업종이 있는지, 사실 값이 확인됐는지 본다. 아니면 발행 실패
  │  2. 템플릿 id 확인          common/template_catalog.py  — 모르면 발행 실패
  │  3. payload JSON 만들기     services/site_payload.py
  │     → site/payloads/<slug>.json 에 떨어뜨린다
  ▼
워커가 Node 렌더러를 실행한다     site  scripts/prerender.ts
  │  1. 공개하면 안 되는 값을 한 번 더 걸러 낸다   shared  lib/facts.ts
  │  2. 남의 도메인 사진을 우리 서버로 내려받는다
  │  3. React로 HTML 문자열을 만든다             site  entry-server.tsx → App.tsx
  │  4. 검색용 JSON-LD, llms.txt를 같이 만든다
  │  5. payload를 HTML 안에 심는다 (window.__SITE_PAYLOAD__)
  │  → out/versions/<slug>/<버전>/ 에 쓴다
  ▼
결과를 확인하고 공개 주소를 새 버전으로 바꾼다
  │  out/s/<slug> 링크를 새 버전 폴더로 갈아 끼운다   (PUBLISH_VERSION.md)
  │  DB에 버전과 발행 기록을 남긴다
  ▼
손님이 /s/<slug> 에 들어온다
  │  nginx가 구워 둔 HTML을 그대로 준다. 검색엔진은 여기까지만 읽는다
  ▼
브라우저가 JS를 받아 화면을 이어받는다     site  entry-client.tsx  hydrateRoot
     심어 둔 payload로 같은 화면을 다시 만들어서, 버튼·달력 같은 동작을 붙인다

두 흐름의 차이는 하나다. 미리보기는 브라우저가 처음부터 그리고, 발행은 서버에서 미리 그려 둔 HTML에 브라우저가 동작만 붙인다. 그리는 코드(App.tsx)는 같다.