'오늘의 엽서'는 미리 써 둔 문구만 보여줬다 — 손님이 사진 고르고 직접 한마디를 써서 카드를 만들고 저장·공유까지 하게 바꿨다. 군산 읽기는 xlsx 원본 대비 2줄짜리 항목이 많아 부실했고(gunsan_365_story_db.xlsx 365행은 52개 주제를 10가지 각도로 복제한 것 — 그대로 실으면 같은 말이 반복된다), 3~5줄로 다시 쓰고 확인필요 배지 대신 네이버 검색 링크로 바꿨다. 그 과정에서 군산 읽기 탭에서 다른 탭(시간의 골목)을 누르면 화면이 사라지는 버그를 잡았다 — React 가 no-op setState 를 건너뛰면서 직접 건드린 DOM 속성(hidden·aria-selected)이 안 돌아오던 것. - inject.js: 엽서 쓰기 섹션(사진 선택 → 캔버스 카드 → 저장/공유), 4줄 캡+글자 단위 줄바꿈, 공유는 이미지+링크만(캡션 중복 안 보냄) · 군산 읽기 탭 5~6개 무작위 노출 · 탭 전환 시 React 미관리 속성(style.display)으로 전환하고 클릭된 탭에 상태를 직접 강제해 no-op setState 버그 제거 - inject.css: 엽서 쓰기 버튼·썸네일 카로셀 스타일, 군산 읽기 확인필요 배지 제거, 768~1023px 구간에서 하단 탭바가 푸터 마지막 줄을 덮는 것 막는 보정 (SiteFooter.tsx 는 md: 768px 부터, MobileTabBar.tsx 는 lg: 1024px 부터 기준을 잡아 그 사이가 비었다 — 렌더러 쪽 근본 수정은 별도) - build_reading.py: 34꼭지 재작성(2줄짜리 21개를 3~5줄로), 출처를 네이버 검색 링크로 통일, verified 필드 제거 - build_daily.py · dump_text.py: 위에서 뺀 verified 필드를 읽던 자리 방어 처리 - README.md: 1부(T7 군산 읽기 · T8 엽서 쓰기)를 실제 구현 가이드로 다시 씀 — 히스토리·인용 없이 무엇을 만들어야 하는지만, 엽서는 AI/스키마 작업이 아니라 UI 그대로 옮기면 되는 것임을 명시 검증: Playwright 로 탭 재전환·엽서 줄바꿈 재현 확인. 킹서버 반영 후 `docker exec o2o-web4ai-solution-site md5sum` 로컬-킹 일치 확인. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XYUb8ZfNiDd15tZRYrusCX
147 lines
9.3 KiB
Python
147 lines
9.3 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 += [""]
|
|
|
|
# ── 군산 읽기 ───────────────────────────────────────────────────────────────
|
|
reading = env("reading")
|
|
out += [f"## 6. 군산 읽기 {len(reading['items'])}가지 — `build_reading.py` `ITEMS`", "",
|
|
"한 화면에 다 펴 두는 카드. 일력(7번)과 달리 손님이 훑어 내려가며 읽는 자리다.", ""]
|
|
for group in reading["groups"]:
|
|
rows = [x for x in reading["items"] if x["group"] == group["name"]]
|
|
out += [f"### {group['name']} {len(rows)}가지 — {group['note']}", "",
|
|
"| 제목 | 글 | 연도 | 출처 |", "|---|---|---|---|"]
|
|
for x in rows:
|
|
out.append(f"| {x['title']} | {x['body']} | {x.get('year', '')} | "
|
|
f"{x['source']['name']} |")
|
|
out += [""]
|
|
|
|
# ── 오늘의 한 장 ────────────────────────────────────────────────────────────
|
|
daily = env("daily")["items"]
|
|
out += [f"## 7. 오늘의 한 장 {len(daily)}장 — `build_daily.py`", "",
|
|
"날마다 한 장씩 넘어가는 일력. 가요는 가요 다방을 그대로 읽고, 시·소설은 손으로 적는다.", "",
|
|
"| 날짜 | 분류 | 제목 | 글 | 확인 | 출처 |", "|---|---|---|---|---|---|"]
|
|
for x in daily:
|
|
out.append(f"| {x['monthDay']} | {x['category']} | {x['title']} | {x['body']} | "
|
|
f"{x['verified']} | {x['source']['name']} |")
|
|
out += [""]
|
|
|
|
# ── 오늘의 엽서 ──────────────────────────────────────────────────────────────
|
|
cards = env("postcard")["items"]
|
|
out += [f"## 8. 오늘의 엽서 {len(cards)}장 — 원본 payload (아직 다시 안 뽑음)", ""]
|
|
out += [f"- {c['line']} `{' '.join(c.get('hashtags', []))}`" for c in cards] + [""]
|
|
|
|
# ── 머뭄이 만든 노래 ─────────────────────────────────────────────────────────
|
|
own = payload["narrative"]["ownSongs"]["items"]
|
|
out += [f"## 9. 머뭄이 만든 노래 {len(own)}곡 — `patch_stay.py` `OWN_SONGS`", ""]
|
|
out += [f"- {s['title']} — `{s['audioUrl']}`" for s in own] + [""]
|
|
|
|
# ── 오늘의 날씨 ──────────────────────────────────────────────────────────────
|
|
# 하늘 다섯 · 기온대 다섯. 화면에는 케이스마다 한 줄만 서고 20초마다 자루에서 다음 것이 나온다.
|
|
wx = payload["local"]["weather"]
|
|
sky, temp = wx.get("noteSets", {}), wx.get("tempNoteSets", {})
|
|
nsky, ntemp = sum(map(len, sky.values())), sum(map(len, temp.values()))
|
|
out += [f"## 10. 오늘의 날씨 문구 {nsky + ntemp}개 — `patch_stay.py` `SKY_NOTES`·`TEMP_NOTES`", "",
|
|
"하늘 줄은 **집 안**에서 뭘 할지, 기온 줄은 **밖에서** 어디를 갈지다. 두 줄이 나란히 선다.",
|
|
f"### 하늘 {nsky}개 (`SKY_NOTES`)", ""]
|
|
for case, rows in sky.items():
|
|
out += [f"**{case}**"] + [f"- {t}" for t in rows] + [""]
|
|
out += [f"### 기온대 {ntemp}개 (`TEMP_NOTES`)", ""]
|
|
for band, rows in temp.items():
|
|
out += [f"**{band}**"] + [f"- {t}" for t in rows] + [""]
|
|
|
|
|
|
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(reading['items'])} · "
|
|
f"일력 {len(daily)} · 엽서 {len(cards)} · "
|
|
f"자작곡 {len(own)} · "
|
|
f"날씨 {nsky + ntemp}")
|