"""오늘의 한 장 — 군산의 노래와 문학을 하루에 한 장씩 넘기는 일력. ★ 왜 이 자리인가 (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']}")