빌더·백엔드에서 이 시연본을 실제 생성 파이프라인으로 옮기려는 사람이, 화면을 보고 역산해야 하는 상태였다. 시연본은 payload 가 없는 목업이라 "화면에 있는 것"과 "제품이 만들 수 있는 것"이 갈라져 있는데 그 간격이 어디에 얼마나 있는지 적힌 곳이 없었다. 섹션마다 값이 오는 payload 자리 · 계약 유무 · 프롬프트 위치 · 목업이 손으로 한 것을 적고, 계약에 아예 없는 셋(히어로 캐치프레이즈 · 자작곡 플레이어 · 일정 규칙)은 추가할 타입과 프롬프트 초안까지 넣었다. - SECTIONS.md 신설: 22개 섹션표 · 새로 만들어야 할 6가지 · 프롬프트 작성 규칙 · 검증 - README.md: 제품으로 옮기는 사람을 SECTIONS.md 로 보낸다. 감사 항목 수 33 → 34 정정 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
17 KiB
/s/stay 섹션별 — 목업은 무엇으로 만들었고, 제품은 무엇으로 만들어야 하나
작성 2026-09-11 · 대상 https://web4ai.o2osolution.ai/s/stay (스테이,머뭄 · 전북 군산)
읽는 사람: 빌더·백엔드에서 이 시연본을 실제 생성 파이프라인으로 옮기는 사람.
시연본은 payload 가 없는 목업이다 — 프리렌더가 굽지 않고, index.html 한 장을
손으로 조립했다(patch_stay.py). 그래서 "화면에 있는 것" 과 "제품이 만들 수 있는 것"이
지금 갈라져 있다. 이 문서는 섹션마다 그 간격을 적는다.
0. 한 줄 결론
22개 섹션 중 16개는 이미 계약과 생성 경로가 있다. 나머지 6개가 할 일이다:
| 섹션 | 왜 | |
|---|---|---|
| ① | 히어로 캐치프레이즈 | 계약에 없다. narrative.tagline 은 문자열 하나인데 목업은 100개를 돌린다 |
| ② | 음악 플레이어(자작곡) | 계약에 없다. media 에 오디오 종류가 없다 |
| ③ | 추천 일정 | 계약·생성기 둘 다 있는데 깊이가 다르다. 서버판은 하루 4칸(관광 2·끼니 2)이고 체류·이동·입퇴실 개념이 없다 |
| ④ | 인물·노래 사진 | 프롬프트가 imageQuery 만 받는다. 실제 사진을 찾아 붙이고 라이선스를 표시하는 경로가 없다 |
| ⑤ | 상한(maxItems) | songs 8·people 10 인데 목업은 25·57 이다 |
| ⑥ | 객실 사진 | 목업은 야놀자 등록본에서 가져왔다. 수집 어댑터 정책(DECISIONS 1-1)에 걸리는지 결론이 필요하다 |
1. 값이 화면까지 가는 길
수집·생성(백엔드) → area_contents / places / media → site_payload.py → payload JSON
↓
프리렌더(Node) → out/s/<slug>/index.html
↓
window.__SITE_PAYLOAD__ → React 하이드레이션
- 읽는 쪽 계약은
solution/shared/src/types/site-payload.ts·shared/src/lib/section-data.ts한 곳이다. - 프롬프트 계약은
shared/src/lib/section-prompts.ts한 곳이고, 백엔드는npm run export:prompts산출물(backend/services/prompts/section_prompts.json)을 읽는다. 파이썬 쪽을 손으로 고치지 않는다. - 섹션 목록·기본 on/off 는 업종별로
services/site_payload.py:155부근에 있다.
payload 안에서 섹션이 값을 받는 자리는 셋뿐이다.
| 자리 | 무엇이 들어가나 | 예 |
|---|---|---|
theme.sections[].data |
JSON 봉투 문자열({kind, version, title, items[]}) |
songs · people · itinerary · event · video · planner |
| 최상단 필드 | 업소가 적은 사실·사진·문답 | place facts units media faqs links narrative |
local.* |
지역에서 수집한 것 | attractions(12) restaurants(8) festivals(10) weather |
2. 섹션 22개 — 한눈에
on 은 이 목업에서 켜져 있는지다. story 안의 6개는 탭이라 최상단은 꺼져 있다.
| # | id | 이름 | on | 값이 오는 자리 | 계약 | 프롬프트 | 목업에서 한 일 |
|---|---|---|---|---|---|---|---|
| 1 | hero |
히어로 | ● | narrative + 대표 사진 |
있음 | 없음 | 캐치프레이즈 100개를 주입분이 돌림 |
| 2 | intro |
소개 | ● | narrative.about[] |
있음 | 있음(빌더) | 원본 그대로 |
| 3 | rooms |
객실 안내 | ● | units[] + media[] |
있음 | — | 야놀자 등록본으로 전면 교체(A동 12·B동 10) |
| 4 | event |
소식 | ● | data(kind=event) 3건 |
있음 | — | 원본 그대로 |
| 5 | info |
기본 정보 | ● | facts[] 12건 |
있음 | — | 맨 아래 가로선만 CSS 로 제거 |
| 6 | booking |
실시간 예약 | ○ | — | 있음 | — | 껐다(시연에 결제 흐름 없음) |
| 7 | video |
영상 | ● | data(kind=video) 3건 |
있음 | — | 원본 그대로 |
| 8 | photos |
사진 갤러리 | ● | media[] 37장 |
있음 | — | 원본 그대로 |
| 9 | map |
오시는 길 | ● | place + routes[] |
있음 | — | routes 비어 있음 |
| 10 | festival |
계절별 축제 | ● | local.festivals 10건 |
있음 | — | 원본 그대로 |
| 11 | local |
지역 정보 | ● | local.attractions/restaurants |
있음 | — | 원본 그대로 |
| 12 | itinerary |
추천 일정 | ● | data(kind=itinerary) 21개 |
있음 | 없음 | 전부 다시 생성(§4) |
| 13 | story |
군산 이야기 | ● | 아래 6탭의 껍데기 | 있음 | — | — |
| 14 | songs |
가요 다방 | 탭 | data(kind=songs) 25곡 |
있음 | 있음 section-prompts.ts:67 |
8 → 25곡으로 늘림 |
| 15 | people |
인물 열전 | 탭 | data(kind=people) 57명 |
있음 | 있음 :113 |
10 → 57명 + 사진 18장 |
| 16 | chronicle |
시간의 골목 | 탭 | data(kind=chronicle) 7건 |
있음 | 있음 :135 |
원본 그대로 |
| 17 | postcard |
오늘의 엽서 | 탭 | data(kind=postcard) 4건 |
있음 | 있음 :160 |
지도 링크만 추가 |
| 18 | quiz |
뒤집어 보는 질문 | 탭 | data(kind=quiz) 4건 |
있음 | 있음 :188 |
원본 그대로 |
| 19 | faq |
자주 묻는 질문 | ● | faqs[] 32건 |
있음 | 있음 | 원본 그대로 |
| 20 | rules |
이용 규정 | ● | facts[] |
있음 | — | 원본 그대로 |
| 21 | weather |
날씨 | ● | local.weather |
있음 | — | 애니메이션만 정지 |
| 22 | planner |
계절별 추천 하루 | ○ | data(kind=planner) 4건 |
있음 | 없음 | 껐다 |
daily(오늘의 한 장)는 프롬프트에는 있는데 이 목업 섹션 목록에 없다.
새 업장에서 켜려면 섹션 목록에 자리를 먼저 만들어야 한다.
3. 계약에 없는 것 — 새로 만들어야 하는 셋
① 히어로 캐치프레이즈 — 계약을 늘려야 한다
지금 계약: Narrative(site-payload.ts:316)는 heroHeadline heroSubline tagline
about[] summary 뿐이고 전부 문자열 하나다. 히어로는
narrative.tagline ?? heroSubline ?? summary 를 그대로 찍는다(HeroPension.tsx:125).
목업이 한 것: narrative.catchphrases = {version:1, items:[…100]} 를 payload 에
끼워 넣고, 주입분이 7초마다 단어 단위로 갈아 끼운다. 렌더러는 이 필드를 모른다 —
주입분(inject.js)이 읽는다. 100개 구성은 patch_stay.py:20(일반 40) :62(계절 24)
:72(월 24) :87(날씨 12).
제품에서 해야 할 일
// site-payload.ts — Narrative 에 추가
catchphrases?: {
version: 1;
items: {
text: string; // 18자 안쪽
type: 'general' | 'season' | 'month' | 'weather';
season?: '봄' | '여름' | '가을' | '겨울'; // type=season
month?: number; // 1–12, type=month
weather?: '맑음' | '흐림' | '비' | '눈'; // type=weather
}[];
};
고르는 규칙(목업이 쓰는 것 그대로): 지금 계절·이번 달·현재 날씨에 맞는 것만 후보로
두고 일반과 섞어 돌린다. 계절은 3개월 단위(가을 9–11, 겨울 12–2). 날씨는 local.weather.condition.
새로 쓸 프롬프트 — SECTION_PROMPTS 에 catchphrase 종류를 더한다.
[해야 할 일]
[업소]의 히어로에 돌려 쓸 짧은 문구를 100개 쓴다.
일반 40 · 계절 24(계절마다 6) · 월 24(달마다 2) · 날씨 12(맑음/흐림/비/눈 각 3).
[스키마]
{ "kind":"catchphrase", "version":1, "items":[
{ "text":"문구", "type":"general|season|month|weather",
"season":"봄|여름|가을|겨울", "month":9, "weather":"맑음|흐림|비|눈" } ] }
[이 아이템만의 규칙]
· 18자 안쪽. 두 문장으로 쓰지 않는다.
· 느낌표·이모지를 쓰지 않는다. 광고 문구로 들리면 실패다.
· **업소에서 실제로 일어나는 일**을 쓴다. 지역 자랑이 아니다.
("새소리로 눈이 떠지는 집" ○ / "천년의 역사가 살아 숨쉬는 군산" ×)
· 계절·월·날씨 문구는 그 조건에서만 말이 되어야 한다.
9월 문구가 1월에 나와도 어색하지 않으면 그건 일반 문구다.
· 시설명·가격·전화번호를 넣지 않는다.
· source 는 필요 없다. 사실 주장이 아니라 업소가 하는 말이다.
② 자작곡 플레이어 — media 에 오디오가 없다
목업: narrative.ownSongs = {version:1, items:[…5]}(patch_stay.py:109),
파일은 /s/stay/audio/*.mp3 로 직접 넣었다. nginx ^~ /s/ 가 Range 206 으로 준다.
플레이어는 주입분이 헤더에 그린다. 프로토타입이라 파일이고, 제품에서는 미디어 파이프라인에
태워야 한다.
해야 할 일: MediaItem 에 kind: 'audio' 를 넣고(url title artist durationSec),
발행 시 다른 미디어와 같이 미러/업로드한다. 렌더러에는 플레이어 컴포넌트가 없다 —
헤더에 붙는 작은 플레이어(아이콘 · 곡명 · 재생/멈춤 · 목록)를 새로 만든다.
자동재생은 넣지 않는다(사장님 지시 2026-09-11). 재생 순서는 랜덤 시작 후 순차.
③ 추천 일정 — 생성기가 있는데 깊이가 다르다
서버판(backend/services/itinerary.py)은 payload 빌드 때 즉석 계산한다.
하루 뼈대가 관광지 2 + 맛집 2, 반경 30km, 최근접 이웃 순서다.
체류시간·이동시간·입퇴실·끼니 시각 개념이 없다.
목업판(build_itinerary.py)이 더한 것:
| 목업이 넣은 것 | 어디 | |
|---|---|---|
| 장소별 체류 | (최소, 기본, 최대) 분 — '오후' 칸이면 뭐든 90분이던 것을 장소마다 | :40 |
| 이동시간 | 좌표에서 계산. 도보 4km/h(≤1.5km), 그 위는 차 25km/h + 주차 5분 | move_minutes() |
| 숙소 고정 | 모든 일정이 숙소에서 출발해 숙소로 돌아온다. 첫 칸·마지막 칸이 숙소 | :108 :182 |
| 시각 창 | 입실 15:00+ · 퇴실 11:00- · 점심 11:30–13:30 · 늦은 점심 13:00–15:00 · 저녁 17:30–20:00 · 밤 산책 18:30–20:00 | :156 :166 |
| 테마 | 21개. 테마마다 시작 시각과 체류 배율이 다르다(끝나는 시각이 13:35~21:00 로 퍼진다) | :368 :470 |
| 제약 | 비 와도 되는 테마는 실내만 · 차 없는 테마는 도보권만 · 아이 동반에 술집 금지 | :519 |
| 감사 | 규칙 14종. 하나라도 어기면 payload 를 쓰지 않고 빌드가 멈춘다 | :526 |
해야 할 일: 이 규칙을 services/itinerary.py 로 옮긴다. 재료가 모자라면 지어내지 않는
원칙은 그대로다 — 감사에 걸리면 그 테마를 빼고, 하루도 못 채우면 섹션을 안 낸다.
장소별 체류시간이 관건이다. 지금 local_contents 에는 그 값이 없다.
LLM 에 물어 채우는 게 현실적이다 — 새 프롬프트:
[해야 할 일]
아래 장소들에 손님이 실제로 머무는 시간을 분으로 적고,
거기서 "무엇을 하는지" 한 문장으로 쓴다.
[스키마]
{ "kind":"stopmeta", "version":1, "items":[
{ "name":"장소명", "minMinutes":25, "baseMinutes":35, "maxMinutes":50,
"indoor":true, "needsCar":false, "kidFriendly":true,
"todo":"대웅전 처마와 대나무 숲을 봅니다. 종각까지 돌면 35분입니다." } ] }
[이 아이템만의 규칙]
· todo 는 **거기서 하는 일**이다. "무엇인지" 설명하지 않는다.
("안에 들어가 사진관 세트에서 한 장 찍습니다" ○ / "1950년대 사진관입니다" ×)
· 한 바퀴 도는 데 15분이면 15분이라고 적는다. 30분으로 올리지 않는다.
· indoor 는 비가 와도 갈 수 있느냐다. 지붕이 있어도 가는 길이 야외면 false.
· needsCar 는 숙소에서 걸어서 20분을 넘느냐다.
④ 인물·노래 사진 — imageQuery 다음이 없다
프롬프트는 사진 URL 을 금지하고 imageQuery 만 받는다(section-prompts.ts:128).
초상권·저작권 때문이고 그 판단은 맞다. 그런데 그 다음 단계가 없어서 인물 열전이
활자만으로 선다.
목업이 한 것(build_story.py): 위키백과 문서의 pageimages 만 믿는다.
이름으로 커먼즈를 검색하면 동명이인이 온다(실측: 이수현→걸그룹, 박성현→골퍼, 이길여→건물).
57명 중 18명만 사진을 얻었고, 얻은 것은 imageinfo.extmetadata 에서 저작자·라이선스를
받아 화면에 표시한다 — CC BY-SA 의 조건이라 안 쓰면 위반이다.
해야 할 일: services/external/wikimedia.py 에 "문서 → pageimages → 라이선스" 경로를 붙이고,
imageCredit(문자열)을 payload 에 넣는다. ★ 문자열이다 — 객체로 넣으면 렌더러가
조용히 아무것도 안 그린다(site/src/sections/items/common.tsx:206).
사진이 붙은 사람을 앞으로 정렬한다(빈 카드가 먼저 오면 목록이 비어 보인다).
⑤ 상한 — maxItems 를 올려야 한다
| 종류 | 지금 상한 | 목업 | 왜 |
|---|---|---|---|
| songs | 8 | 25 | 도시 하나에 그만큼 있다. 8곡이면 '다방'이 아니라 목록이다 |
| people | 10 | 57 | 위키백과 '군산시 출신' 분류만으로 57명이 확인된다 |
| chronicle | 14 | 7 | 상한은 맞는데 채우는 쪽이 모자라다 |
| postcard | 12 | 4 | 같다 |
상한을 올릴 때 한 번에 다 받지 않는다. 목업은 곡을 8개씩 세 번 나눠 받았다 — 한 번에 25곡을 시키면 뒤쪽이 급격히 부실해진다. 나눠 받고 합칠 때 제목으로 중복을 지운다. 프롬프트 분할 방법은 PROMPTS.md 에 있다.
⑥ 객실 사진 — 출처 정책 결론이 필요하다
목업은 야놀자 등록본(nol.yanolja.com/stay/domestic/10068088)의 Next.js 플라이트 데이터에서
roomTypes[].photos 를 파싱해 A동 12·B동 10 을 가져왔다(rooms.json).
이건 시연용으로 손으로 한 것이고 제품 경로가 아니다.
수집 어댑터를 만들려면 DECISIONS 1-1 과
DATA_SOURCE_RESEARCH.md 를 먼저 읽는다 —
봇 탐지 우회는 결론과 무관하게 영구 금지다.
현실적인 제품 경로는 사장님이 올리는 것이거나 업소가 준 채널의 공개 API 다.
4. 프롬프트를 새로 쓸 때 — 이 레포의 규칙
SECTION_PROMPT_RULES(section-prompts.ts:55)가 공통 7줄이고, 종류별 규칙을 덧붙인다.
새 종류를 더할 때 지켜야 하는 것:
STORY_KINDS에 등록한다. 빠지면 서버 생성 잡이 그 종류를 아예 모른다 —daily가 그래서 한동안 빈칸이었다(주석에 실측이 적혀 있다).maxItems와 프롬프트 안의 숫자를 같게 둔다. 어긋나면 받아 놓고 잘라 버린다.- 빈 값을 지어내지 않는다(공통 2번). 확실한 게 12개면 12개만 낸다.
- 출처는 실제로 열리는 페이지여야 한다(공통 3번). 검색 결과 주소는 안 된다.
- 가사·시·소설 원문을 한 줄도 옮기지 않는다(공통 5번).
[지역]같은 빈칸을 남기지 않는다 —buildSectionPrompt가 채워서 내보낸다.- 고친 뒤
npm run export:prompts를 돌린다. 백엔드는 그 산출물을 읽는다.
5. 검증 — "됐다"고 말하기 전에
목업에서 쓰는 방식 그대로 제품에도 필요하다.
| 무엇 | 어디 | |
|---|---|---|
| 생성 단계 | 규칙 위반이면 결과물을 쓰지 않고 멈춘다. 검사를 생성기 안에 둔다 | build_itinerary.py:526 |
| 렌더 단계 | 실제 브라우저로 열어 숫자를 센다. 34종 | audit-all.mjs |
| 카로셀 | 레일 13개 × 데스크톱·모바일 | rails-test.mjs |
★ 목업에서 같은 실수가 반복된 원인은 "검사를 안 했다"가 아니라 "검사가 빌드 밖에 있었다" 였다. 그때그때 쓴 일회용 스크립트로 돌리니, 뼈대를 고칠 때마다 이번에 안 본 규칙이 생겼다. 경위는 REVIEW-2026-09-11.md 2장.
6. 같이 읽을 것
| 목업을 고치고 배포하는 절차 · 밟으면 조용히 틀리는 자리 | README.md |
| 실제로 쓴 프롬프트 전문과 나눠 받는 법 | PROMPTS.md |
| 화면에 나가는 텍스트 전문 | TEXT.md |
| 일정이 왜 틀렸고 무엇을 고쳤나 | REVIEW-2026-09-11.md |
| 값이 DB 에서 페이지까지 가는 길 | docs/DATA_MODEL.md |
| 미결 사항 · 수집 어댑터 정책 | docs/DECISIONS.md |