"""목업(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("", "<\\/")
updated = html[:start] + encoded + html[end:]
(path.parent / BACKUP_NAME).write_text(html, encoding="utf-8")
tmp = path.with_name(f".{path.name}.tmp")
tmp.write_text(updated, encoding="utf-8")
os.replace(tmp, path)
LOG.i(f"[mockup-blog] {slug} 미니블로그 {len(payload['posts'])}건으로 갱신")
return len(payload["posts"])