o2o-site-AEO/solution/site/scripts/mockup/dump_text.py
Mina Choi 43ba33dd65 [feat] site/scripts: /s/stay 시연본 조립 도구 일습 — 일정 생성기·주입분·검증
이 목업은 payload 가 없어 프리렌더 재굽기 대상이 아니다 — `index.html` 한 장이 유일본이고
자산이 지워지면 사람이 되돌려 넣어야 한다(CLAUDE.md 의 ★★ 함정). 지금까지 그 한 장을
이 맥에서만 만들고 있었다. 다른 사람이 이어받을 수 있도록 재료를 전부 올린다.
`build/index.html` 은 생성물이라 이그노어 그대로다 — `patch_stay.py` 로 다시 나온다.

- build_itinerary.py: 테마 21개 일정 생성. 좌표에서 이동시간을 계산하고(도보 4km/h,
  1.5km 초과는 차 25km/h + 주차 5분) 입·퇴실·끼니 창을 맞춘다. 규칙 14종 감사가
  **빌드 안**에 있어 하나라도 어기면 payload 를 쓰지 않고 멈춘다 — 검사가 빌드 밖에
  있던 동안 뼈대를 고칠 때마다 안 보는 규칙이 생겼다(2026-09-11 REVIEW)
- build_story.py: 노래 25곡·인물 57명. 사진은 위키백과 문서 pageimages 만 믿는다
  (이름 검색은 동명이인을 끌고 온다 — 이수현→걸그룹, 박성현→골퍼)
- patch_stay.py: 캐치프레이즈 100개·자작곡 5곡·객실 사진(A동 12/B동 10)을 넣고
  payload 를 갈아 끼운 뒤 inject.css/js 를 `</body>` 앞에 주입해 index.html 을 짠다
- inject.js/css: React 가 다시 그려도 살아남아야 하는 다섯 가지(캐치프레이즈 순환·
  헤더 미니 플레이어·지도 링크·사진 저작자 표시·카로셀 제어). `#root` 밖에 둔다
- audit-all.mjs / rails-test.mjs: 실제 브라우저로 34종 검사, 레일 13개 자동 넘김 전수
- README.md: 이어받는 사람이 먼저 읽는 문서. 배포·함정·§7 "내가 틀렸던 6가지"
- PROMPTS.md / TEXT.md / REVIEW-2026-09-11.md: 문구 생성 프롬프트 · 전체 텍스트 · 검수 보고

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

106 lines
6.6 KiB
Python

"""시연본에 들어간 **문구 전부**를 읽기 좋은 문서로 뽑는다.
★ 왜 따로 뽑나 — 문구는 파이썬 리스트 안에 흩어져 있다(캐치프레이즈는 patch_stay.py,
노래·인물은 build_story.py, 정거장 할 일은 build_itinerary.py).
사장님이 **읽고 고칠 것**은 문구뿐이라, 코드를 열지 않고 볼 수 있게 한 장으로 모은다.
고치는 자리는 여전히 스크립트다 — 이 파일은 산출물이고 손으로 고치지 않는다.
"""
import json
from pathlib import Path
SP = Path(__file__).parent
# ★ 최종 산출물(build/index.html)에서 읽는다. 캐치프레이즈·자작곡·객실 사진은
# patch_stay.py 가 HTML 을 조립하면서 넣으므로 중간 파일(stay-payload-new.json)에는 없다.
# "실제로 나간 값" 을 적는 문서라 최종본이 맞다.
import re
_html = (SP / "build" / "index.html").read_text(encoding="utf-8")
payload = json.loads(re.search(r"<script>window\.__SITE_PAYLOAD__=(.*?)</script>", _html, re.S).group(1))
SECT = {s["id"]: s for s in payload["theme"]["sections"]}
env = lambda sid: json.loads(SECT[sid]["data"])
out = ["# /s/stay 시연본 — 들어간 문구 전부",
"",
"`python3 dump_text.py` 산출물. **고치는 자리는 스크립트다** — 이 파일을 고쳐도 반영되지 않는다.",
""]
# ── 캐치프레이즈 ──────────────────────────────────────────────────────────────
box = payload["narrative"]["catchphrases"]["items"]
out += [f"## 1. 히어로 문구 {len(box)}개 — `patch_stay.py`", "",
"상호 아래 한 줄. 7초마다 `일반 → 계절 → 일반 → 월 → 일반 → 날씨` 순으로 바뀐다.", ""]
general = [x for x in box if x["kind"] == "general"]
out += [f"### 일반 {len(general)}개 (`patch_stay.py` `GENERAL`)", ""]
out += [f"{i}. {x['text']}" for i, x in enumerate(general, 1)] + [""]
out += [f"### 계절별 {sum(1 for x in box if x['kind'] == 'season')}개 (`SEASON`)", ""]
for season in ("", "여름", "가을", "겨울"):
rows = [x["text"] for x in box if x.get("season") == season]
out += [f"**{season}**"] + [f"- {t}" for t in rows] + [""]
out += [f"### 월별 {sum(1 for x in box if x['kind'] == 'month')}개 (`MONTH`)", ""]
for month in range(1, 13):
rows = [x["text"] for x in box if x.get("month") == month]
out += [f"**{month}월** — " + " · ".join(rows)]
out += [""]
out += [f"### 날씨별 {sum(1 for x in box if x['kind'] == 'weather')}개 (`WEATHER`)", ""]
for weather in ("맑음", "구름많음", "흐림", "", ""):
rows = [x["text"] for x in box if x.get("weather") == weather]
out += [f"**{weather}**"] + [f"- {t}" for t in rows] + [""]
# ── 가요 다방 ────────────────────────────────────────────────────────────────
songs = env("songs")["items"]
out += [f"## 2. 가요 다방 {len(songs)}곡 — `build_story.py` `SONGS`", "",
"| # | 곡명 | 가수 | 작사 / 작곡 | 연도 | 한 줄 |", "|---|---|---|---|---|---|"]
for i, s in enumerate(songs, 1):
credit = " / ".join(filter(None, [s.get("lyricist"), s.get("composer")])) or ""
out.append(f"| {i} | {s['title']} | {s.get('artist', '')} | {credit} | {s.get('year', '')} | {s.get('story', '')} |")
out += ["", f"출처: {songs[0]['source']['name']}", ""]
# ── 인물 열전 ────────────────────────────────────────────────────────────────
people = env("people")["items"]
shot = sum(1 for p in people if p.get("imageUrl"))
out += [f"## 3. 인물 열전 {len(people)}명 (사진 {shot}장) — `build_story.py` `PEOPLE`", "",
"사진 있는 사람이 앞에 온다.", "",
"| # | 사진 | 이름 | 연도 | 하는 일 | 한 줄 |", "|---|---|---|---|---|---|"]
for i, p in enumerate(people, 1):
out.append(f"| {i} | {'📷' if p.get('imageUrl') else ''} | {p['name']} | {p.get('years', '')} | {p['role']} | {p['oneLine']} |")
out += [""]
# ── 일정 정거장 ──────────────────────────────────────────────────────────────
itin = env("itinerary")["items"]
seen = {}
for item in itin:
for day in item["days"]:
for stop in day["stops"]:
place = stop["name"].split(" · ")[-1]
if stop.get("note"):
seen.setdefault(f"{place}|{stop['note']}", (place, stop["note"], stop["minutes"]))
out += [f"## 4. 일정 — 정거장에서 할 일 {len(seen)}줄 — `build_itinerary.py` `PLACES`·`STAY_ACTION`", "",
"| 장소 | 무엇을 하는지 | 머무는 시간 |", "|---|---|---|"]
for place, note, minutes in sorted(seen.values()):
out.append(f"| {place} | {note} | {minutes}분 |")
out += [""]
# ── 일정 테마 ────────────────────────────────────────────────────────────────
out += [f"## 5. 일정 테마 {len(itin)}개 — `build_itinerary.py` `THEMES`", "",
"| 테마 | 기간 | 누구에게 | 왜 |", "|---|---|---|---|"]
for item in itin:
out.append(f"| {item['name']} | {item['duration']} | {item['audience']} | {item['why']} |")
out += [""]
# ── 오늘의 엽서 ──────────────────────────────────────────────────────────────
cards = env("postcard")["items"]
out += [f"## 6. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""]
# ── 머뭄이 만든 노래 ─────────────────────────────────────────────────────────
own = payload["narrative"]["ownSongs"]["items"]
out += [f"## 7. 머뭄이 만든 노래 {len(own)}곡 — `patch_stay.py` `OWN_SONGS`", ""]
out += [f"- {s['title']} — `{s['audioUrl']}`" for s in own] + [""]
path = SP / "TEXT.md"
path.write_text("\n".join(out), encoding="utf-8")
print(f"{path.name} 캐치프레이즈 {len(box)} · 노래 {len(songs)} · 인물 {len(people)} · "
f"정거장 문구 {len(seen)} · 테마 {len(itin)} · 엽서 {len(cards)} · 자작곡 {len(own)}")