템플릿 정보가 빌더·렌더러·백엔드에 따로 적혀 어긋나 있었다 — 백엔드 기본값이 없는 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>
178 lines
9.0 KiB
Markdown
178 lines
9.0 KiB
Markdown
# 템플릿과 렌더링
|
|
|
|
사장님이 고르는 "템플릿"이 어디에 정의돼 있고, 화면에 어떤 순서로 그려지는지 적은 문서다.
|
|
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`)는 같다.
|