o2o-site-AEO/solution/site/scripts/mockup/build_daily.py
Mina Choi 0500ab3a3f [feat] site/mockup: /s/stay 에 엽서 쓰기 · 새 군산 읽기를 넣는다 — 탭 전환 버그 수정
'오늘의 엽서'는 미리 써 둔 문구만 보여줬다 — 손님이 사진 고르고 직접 한마디를 써서
카드를 만들고 저장·공유까지 하게 바꿨다. 군산 읽기는 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
2026-09-14 17:01:40 +09:00

153 lines
8.9 KiB
Python

"""오늘의 한 장 — 군산의 노래와 문학을 하루에 한 장씩 넘기는 일력.
★ 왜 이 자리인가 (2026-09-14 대표 의견: "엽서 형식보다 대중가요·시·소설이 더 풍부했으면")
렌더러에는 `daily`(오늘의 한 장) 탭이 이미 있는데 **payload 에 그 섹션 자체가 없었다** —
자리는 있고 값이 없어 탭이 서지 않던 자리다(README 1.2). 엽서를 갈아 끼우는 대신
빈 자리를 채운다. 엽서는 그대로 둔다.
★ **이 자리는 도시를 소개하는 자리가 아니다** (같은 날 대표 의견: "오늘의 한 장으로 군산을
소개하기는 무리다 — 하루에 하나씩밖에 안 알려 주니까"). 일력은 오늘·어제·내일 세 장만
화면에 선다. 군산을 펼쳐 보이는 자리는 `build_reading.py`(군산 읽기)가 맡는다.
여기는 **날마다 바뀌는 한 장**이고, 그래서 곡·시인·문학을 한 장씩 돌린다.
★ 받은 자료(`gunsan_365_story_db.xlsx`)를 그대로 싣지 않는다.
365행이지만 **실제 주제는 52개**고, 한 주제를 '작품 배경 / 역사 맥락 / 계절 연결 /
SNS 한 문장 / 교육용 질문 …' 일곱 가지 운영 슬롯으로 복제해 둔 시드다. 그대로 실으면
같은 문장이 일주일에 일곱 번 돈다. 게다가 231행이 `검증필요`, 14행이 `후보`이고
시·수필 카테고리는 전부 빈 슬롯이다("…를 찾아 소개하기 위한 **운영용 슬롯**이다") —
손님 화면에 나갈 문장이 아니라 우리끼리 쓰는 메모다.
→ **소재 목록으로만 쓴다.** 화면에 나가는 문장은 여기서 다시 쓰고, 출처는 그 주제를
실제로 설명하는 기관 페이지를 단다.
★ 365장을 억지로 채우지 않는다. 확인되는 소재가 39개면 39장이다 —
렌더러는 오늘 자리가 비면 **다음 장**을 편다(`DailySection.tsx`). 채우려면 같은 글을
여러 날짜에 복사해야 하는데, 그건 인용되려고 만든 사이트에서 할 일이 아니다.
입력: stay-payload-new.json (build_story.py · build_reading.py 까지 끝난 것 —
가요 25곡 · 인물 57명 · 문학 8가지를 **전부 거기서 읽는다.** 여기서 손으로 적는 것은 없다)
출력: 같은 파일의 theme.sections 에 `daily` 섹션을 넣는다.
"""
import json
from pathlib import Path
SP = Path(__file__).parent
SRC = SP / "stay-payload-new.json"
payload = json.loads(SRC.read_text(encoding="utf-8"))
SECTIONS = payload["theme"]["sections"]
SECT = {s["id"]: s for s in SECTIONS}
# ── 문학은 '군산 읽기' 표에서 가져온다 ─────────────────────────────────────
# ★ 한 벌만 둔다 (2026-09-14). 문학 카드를 여기에도 적어 두면 한쪽만 고치는 날이 온다 —
# 이 레포에서 프롬프트가 두 곳으로 갈려 서버가 세 종류를 못 만들던 것과 같은 종류의 사고다.
# 손으로 적는 자리는 `build_reading.py` `ITEMS` 하나다.
LIT_GROUP = "문학"
# ── 시 — 군산이 낳은 시인 ────────────────────────────────────────────────────
# ★ 시 작품을 싣지 않는다. 확인되는 것은 '이 도시에서 난 시인' 까지다 —
# 받은 자료의 시 카테고리도 전부 빈 슬롯이었고(위 머리말), 군산을 노래한 시를
# 출처와 함께 확인한 것이 아직 없다. 없는 것을 지어내느니 시인을 세운다.
POETS = ["고은", "이병훈", "심호택", "문효치", "이연주", "주하림"]
def _items(section_id: str) -> list[dict]:
section = SECT.get(section_id)
if not section or not section.get("data"):
raise SystemExit(f"{section_id} 섹션이 비었다 — build_story.py · build_reading.py 를 먼저 돌린다")
return json.loads(section["data"])["items"]
# ── 가요 25곡 → 25장 ─────────────────────────────────────────────────────────
# 가요 다방(`songs`)에 이미 선 곡을 그대로 읽는다. 목록을 두 벌로 두면 한쪽만 고치게 된다.
song_pages = []
for song in _items("songs"):
facts = []
if song.get("year"):
facts.append(f"{song['year']}년 곡")
credit = " · ".join(
part for part in (
f"작사 {song['lyricist']}" if song.get("lyricist") else "",
f"작곡 {song['composer']}" if song.get("composer") else "",
) if part)
if credit:
facts.append(credit)
body = ". ".join(facts)
body = f"{body}. {song['story']}" if body else song["story"]
song_pages.append({"category": "대중가요",
"title": f"{song['title']}」 · {song['artist']}",
"body": body,
"verified": song.get("verified", "확인필요"),
"source": song["source"]})
# ── 시인 6명 → 6장 ───────────────────────────────────────────────────────────
people = {p["name"]: p for p in _items("people")}
poet_pages = []
for name in POETS:
person = people.get(name)
if not person:
raise SystemExit(f"인물 열전에 {name} 이 없다 — build_story.py 의 PEOPLE 을 본다")
years = f"{person['years']} · " if person.get("years") else ""
poet_pages.append({"category": "", "title": f"시인 {name}",
"body": f"{years}{person['oneLine']}",
"verified": person.get("verified", "확인필요"),
"source": person["source"]})
lit_pages = [{"category": LIT_GROUP, "title": x["title"], "body": x["body"],
# 2026-09-14 부로 build_reading.py 의 ITEMS 에서 verified 필드를 뺐다
# (대표: "확인필요 없애고 링크는 네이버 링크로") — 여기 기본값은 그 결정을 따른다.
"verified": x.get("verified", "확인"), "source": x["source"]}
for x in _items("reading") if x["group"] == LIT_GROUP]
# ── 섞고, 날짜를 준다 ────────────────────────────────────────────────────────
# ★ 같은 분류를 연달아 세우지 않는다 — 일력은 어제·오늘·내일 세 장이 한 화면에 걸린다
# (`DailySection.tsx`). 가요만 석 장 나란히 서면 일력이 아니라 음반 목록으로 보인다.
others = lit_pages + poet_pages
pages, si, oi = [], 0, 0
total = len(song_pages) + len(others)
for step in range(total):
take_other = oi < len(others) and (si >= len(song_pages)
or (step + 1) * len(others) / total > oi)
if take_other:
pages.append(others[oi])
oi += 1
else:
pages.append(song_pages[si])
si += 1
# ★ 날짜는 **고르게 편다**. 곡이 나온 날·작가의 생일에 맞추고 싶지만 그 날짜를 출처로
# 확인한 것이 없다 — 지어낸 날짜를 일력에 박으면 틀린 줄 모르고 해마다 틀린다.
for index, page in enumerate(pages):
day = round(index * 365 / len(pages)) + 1
month, rest = 1, day
for length in (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31):
if rest <= length:
break
rest -= length
month += 1
page["monthDay"] = f"{month:02d}-{rest:02d}"
env = {"kind": "daily", "version": 1, "title": "오늘의 한 장",
"subtitle": f"군산의 노래와 문학 {len(pages)}장 — 날마다 한 장씩 넘어갑니다",
"items": [{"monthDay": p["monthDay"], "category": p["category"], "title": p["title"],
"body": p["body"], "verified": p["verified"], "source": p["source"]}
for p in pages]}
# 없으면 만든다 — 원본 payload 에는 `daily` 섹션 자체가 없다(그래서 탭이 안 섰다).
if "daily" not in SECT:
# 형제(가요 다방) 옆에 둔다. 탭 순서는 렌더러가 정하므로 배열 순서는 보기 좋으라고 맞추는 것이다.
SECTIONS.insert(next(i for i, s in enumerate(SECTIONS) if s["id"] == "songs") + 1,
{"id": "daily", "name": "오늘의 한 장", "enabled": False, "locked": False})
SECT = {s["id"]: s for s in SECTIONS}
SECT["daily"]["data"] = json.dumps(env, ensure_ascii=False)
SRC.write_text(json.dumps(payload, ensure_ascii=False), encoding="utf-8")
counts: dict[str, int] = {}
for page in pages:
counts[page["category"]] = counts.get(page["category"], 0) + 1
unverified = sum(1 for p in pages if p["verified"] != "확인")
print(f"오늘의 한 장 {len(pages)}장 — "
+ " · ".join(f"{k} {v}" for k, v in counts.items())
+ f" (확인필요 {unverified}장)")
print(f" 날짜 {pages[0]['monthDay']}{pages[-1]['monthDay']}")