"""목업(stay · stay2)은 굽지 않고 index.html 의 미니블로그 글만 갈아 끼운다.""" import json import os from pathlib import Path from common.logger import LOG from services import render_service MOCKUP_SLUGS = frozenset({"stay", "stay2"}) PAYLOAD_MARKER = "window.__SITE_PAYLOAD__" BACKUP_NAME = "index.html.bak-blog" class MockupBlogError(Exception): pass def is_mockup(slug: str) -> bool: return slug in MOCKUP_SLUGS def index_path(slug: str) -> Path: return Path(render_service.output_dir()) / "s" / slug / "index.html" def _payload_span(html: str) -> tuple[int, int, dict]: marker = html.find(PAYLOAD_MARKER) if marker < 0: raise MockupBlogError("payload 가 없다") start = html.index("{", marker) payload, end = json.JSONDecoder().raw_decode(html, start) return start, end, payload def merge_posts(existing: list[dict], snapshot_posts: list[dict]) -> list[dict]: merged = {post["postId"]: post for post in existing if post.get("postId")} for row in snapshot_posts: body = (row.get("body") or "").strip() if not body: continue merged[row["post_id"]] = {"postId": row["post_id"], "body": row["body"], "publishedAt": row["published_at"]} return sorted(merged.values(), key=lambda post: post.get("publishedAt") or "", reverse=True) def refresh_posts(slug: str, snapshot_posts: list[dict]) -> int: path = index_path(slug) if not path.is_file(): raise MockupBlogError(f"목업 파일이 없다: {path}") html = path.read_text(encoding="utf-8") start, end, payload = _payload_span(html) payload["posts"] = merge_posts(payload.get("posts") or [], snapshot_posts) encoded = json.dumps(payload, ensure_ascii=False).replace("