"""렌더 보고서 — SSG 가 실제로 페이지를 구웠는지 확인하는 창구. ★ 왜 필요한가 BUILD 잡은 payload JSON 을 쓰고 렌더러를 직접 돌린다(services/render_service). 렌더러는 굽는 동안 있었던 일(성공/실패, 구조화 데이터 대조, 고유 콘텐츠 수)을 사이트마다 `/.status/.json` 에 남긴다 — 백엔드에 마운트된 디렉토리가 payload 디렉토리뿐이라 보고서도 그 안에 둔다. ★ 여기서 하는 일 — 그 보고서를 읽는다. render_service 가 렌더러 subprocess 를 기다린 **직후** 한 번 읽어 발행 여부를 정하고, 사이트 조회 API(render_status)는 아무 때나 다시 읽어 "지금 이 버전이 구워져 있나" 를 보여준다. ★ 읽기 전용이다. 보고서가 없거나 깨져 있어도 예외를 밖으로 내보내지 않는다 — 사이트 조회가 부수 산출물 때문에 실패하면 안 된다. """ import json from pathlib import Path from common.logger import LOG from services.site_payload import payload_dir # 프리렌더가 쓰는 보고서 스키마 버전. 모양이 바뀌면 prerender.ts 와 같이 올린다. REPORT_SCHEMA_VERSION = 1 # 보고서 디렉토리 이름. prerender.ts 의 writeReport 와 맞춰야 한다. STATUS_DIR = ".status" def report_path(slug: str) -> Path: """`/.status/.json`.""" return payload_dir() / STATUS_DIR / f"{slug}.json" def read_report(slug: str) -> dict | None: """렌더 보고서를 읽는다. 없거나 깨졌으면 None. ★ 없는 것과 깨진 것을 구분하지 않는다. 호출측이 할 일은 같다 — '아직 못 구웠다'로 본다.""" if not slug: return None path = report_path(slug) try: if not path.is_file(): return None report = json.loads(path.read_text(encoding="utf-8")) except Exception as ex: # noqa: BLE001 — 부수 산출물이 사이트 조회를 깨뜨리면 안 된다 LOG.w(f"[render] 보고서 읽기 실패 slug={slug}: {type(ex).__name__}: {ex}") return None if not isinstance(report, dict): return None if report.get("schemaVersion") != REPORT_SCHEMA_VERSION: # 모르는 버전을 반쯤 해석해 "정상"이라고 답하는 게 최악이다. LOG.w(f"[render] 모르는 보고서 버전 slug={slug}: {report.get('schemaVersion')}") return None return report def render_status(slug: str, site_version: int | None = None) -> dict: """사이트 조회 API 가 그대로 내보내는 렌더 상태. state PENDING 아직 안 구웠다(보고서 없음) STALE 구웠지만 지금 버전이 아니다(발행 후 프리렌더 대기 중) OK 현재 버전이 구워져 있다 FAILED 프리렌더가 실패했다 — 페이지가 없거나 낡았다 """ report = read_report(slug) if report is None: return {"state": "PENDING", "rendered_at": None, "error": None, "rendered_version": None} rendered_version = report.get("siteVersion") base = { "rendered_at": report.get("renderedAt"), "error": report.get("error"), "rendered_version": rendered_version, } if not report.get("ok"): return {"state": "FAILED", **base} if site_version is not None and rendered_version != site_version: return {"state": "STALE", **base} return {"state": "OK", **base}