o2o-site-AEO/solution/site/scripts/mockup/SECTIONS.md
Mina Choi a134805b3f [docs] site/scripts: 섹션 22개별 "목업은 무엇으로 만들었고 제품은 무엇으로 만드나"
빌더·백엔드에서 이 시연본을 실제 생성 파이프라인으로 옮기려는 사람이,
화면을 보고 역산해야 하는 상태였다. 시연본은 payload 가 없는 목업이라
"화면에 있는 것"과 "제품이 만들 수 있는 것"이 갈라져 있는데 그 간격이 어디에
얼마나 있는지 적힌 곳이 없었다.

섹션마다 값이 오는 payload 자리 · 계약 유무 · 프롬프트 위치 · 목업이 손으로 한 것을
적고, 계약에 아예 없는 셋(히어로 캐치프레이즈 · 자작곡 플레이어 · 일정 규칙)은
추가할 타입과 프롬프트 초안까지 넣었다.

- SECTIONS.md 신설: 22개 섹션표 · 새로 만들어야 할 6가지 · 프롬프트 작성 규칙 · 검증
- README.md: 제품으로 옮기는 사람을 SECTIONS.md 로 보낸다. 감사 항목 수 33 → 34 정정

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 10:42:33 +09:00

17 KiB
Raw Blame History

/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;                             // 112, type=month
    weather?: '맑음' | '흐림' | '비' | '눈';    // type=weather
  }[];
};

고르는 규칙(목업이 쓰는 것 그대로): 지금 계절·이번 달·현재 날씨에 맞는 것만 후보로 두고 일반과 섞어 돌린다. 계절은 3개월 단위(가을 911, 겨울 122). 날씨는 local.weather.condition.

새로 쓸 프롬프트SECTION_PROMPTScatchphrase 종류를 더한다.

[해야 할 일]
[업소]의 히어로에 돌려 쓸 짧은 문구를 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 으로 준다. 플레이어는 주입분이 헤더에 그린다. 프로토타입이라 파일이고, 제품에서는 미디어 파이프라인에 태워야 한다.

해야 할 일: MediaItemkind: '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:3013:30 · 늦은 점심 13:0015:00 · 저녁 17:3020:00 · 밤 산책 18:3020: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-1DATA_SOURCE_RESEARCH.md 를 먼저 읽는다 — 봇 탐지 우회는 결론과 무관하게 영구 금지다. 현실적인 제품 경로는 사장님이 올리는 것이거나 업소가 준 채널의 공개 API 다.


4. 프롬프트를 새로 쓸 때 — 이 레포의 규칙

SECTION_PROMPT_RULES(section-prompts.ts:55)가 공통 7줄이고, 종류별 규칙을 덧붙인다. 새 종류를 더할 때 지켜야 하는 것:

  1. STORY_KINDS 에 등록한다. 빠지면 서버 생성 잡이 그 종류를 아예 모른다 — daily 가 그래서 한동안 빈칸이었다(주석에 실측이 적혀 있다).
  2. maxItems 와 프롬프트 안의 숫자를 같게 둔다. 어긋나면 받아 놓고 잘라 버린다.
  3. 빈 값을 지어내지 않는다(공통 2번). 확실한 게 12개면 12개만 낸다.
  4. 출처는 실제로 열리는 페이지여야 한다(공통 3번). 검색 결과 주소는 안 된다.
  5. 가사·시·소설 원문을 한 줄도 옮기지 않는다(공통 5번).
  6. [지역] 같은 빈칸을 남기지 않는다 — buildSectionPrompt 가 채워서 내보낸다.
  7. 고친 뒤 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