Files
document-haness/scripts/preview-figure.py
T
DongHyeonkaandClaude Opus 5 e00c1a2b76 검사: 볼 것이 없어서 통과한 것을 「문제 없음」이라고 쓰지 않는다
R4 · R13 · R6 · R3. 네 가지가 같은 자리를 본다 — 검사기가 대상을 못 찾았을 때 무엇을
내는가.

세 상태를 가른다 (CLAUDE.md 「검사」 절의 표).

  봤고 괜찮다              exit 0   문제 없음 · error 0
  대상이 성립하지 않는다   exit 2   대상이 성립하지 않는다 — <이유>
  볼 것이 아직 없다        exit 0   기록 0건 — 아직 쓴 기록이 없다

R4 — 없는 프로젝트를 주면 여섯 중 넷이 초록을 냈다. `verify-tech-log-tree.py` 는 그것을
「프로젝트 1 · error 0 · PASS」로 셌다 — 오타 한 번이면 검사를 다 돈 것처럼 보인다.
판정은 `techlog.check_targets()` 하나로 모은다. 여섯 곳에 같은 규칙을 따로 쓰면 다음에
하나만 어긋난다. `check_evidence.mjs` 는 이미 exit 2 라 문구만 맞춘다.

R13 — 프로젝트 이름 쪽만 고치면 `--file` 로 오타를 내는 순간 다시 조용히 0건이 된다.
`techlog.check_files()` 로 같은 자리에 둔다. `preview-figure.py` 는 인자가 아예 없을 때
exit 1 을 냈는데 그것도 「대상이 성립하지 않는다」다.

R6 — 두 자리를 함께 고쳐야 했다.
  (a) `verify_projects()` 가 `docs/*/tech-log-studio` 만 훑어 계약 없는 프로젝트가
      목록에서 사라졌다. 기준을 `final/document.md` 로 바꾼다 — SSOT 가 있으면 대상이다.
  (b) `verify-tech-log-tree.py:194` 가 「분해 계약 없음」을 warn 으로 냈다. CLAUDE.md 는
      「계약 미채택도 error 다 — 경고로 두면 옛 스키마로 남아 있는 한 검사를 피한다」고
      적어 두었는데, 경고로 두었더니 실제로 그렇게 됐다.

R3 — `verify-pipeline.py` 가 `check-figure-text.py` 와 `check_evidence.mjs --repo` 를
프로젝트마다 돌린다(`OUTPUT CHECKS`). 게시 전에 돌리라고 적어 둔 검사인데 전체 훑기가
부르지 않아 결함이 있는 채로 PASS 로 보고됐다. `check-required-content.py` 자리는 주석으로
남겨 둔다 — 그 파일이 들어온 뒤에 더한다.

회귀 `scripts/tests/test_no_target.py` 7건 (92 → 99). 「실재하는 경로는 통과한다」 대조를
함께 넣는다 — 무조건 거절로 성공률을 올리는 것이 R-003 이 든 실패다. 판정을 일부러
되돌려 FAILED 가 나는 것을 확인한 뒤 복구했다.

알려진 부채는 그대로 둔다. `verify-pipeline.py` 는 exit 1 이다 — 미준수 런 1건과
`OUTPUT CHECKS` 5건, 그리고 `ca-tmpl` 의 계약 없음이 이제 `TECH LOG TREES` 에서도 보인다.
같은 사실이고 두 번 세지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
2026-09-10 11:07:24 +09:00

96 lines
3.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""그림을 PNG 로 떠서 눈으로 볼 수 있게 만든다.
lint 는 엣지가 노드를 지나가는 것은 잡지만 **라벨이 상자를 덮는 것**은 못 잡는다.
라벨 폭을 재지 않고 앵커 점만 보기 때문이다. 그래서 컴파일한 뒤 한 번은 봐야 한다.
python3 scripts/preview-figure.py <프로젝트> # 그 프로젝트의 그림 전부
python3 scripts/preview-figure.py --file 그림.svg
python3 scripts/preview-figure.py <프로젝트> -o /tmp/png
Chrome 을 headless 로 띄워 SVG 를 그대로 찍는다. 경로가 다르면 CHROME 으로 알려 준다.
"""
from __future__ import annotations
import argparse
import glob
import os
import re
import shutil
import subprocess
import sys
import tempfile
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "scripts"))
import techlog # noqa: E402
SIZE = re.compile(r'width="(\d+)"\s+height="(\d+)"')
def chrome() -> str:
found = os.environ.get("CHROME") or shutil.which("google-chrome") \
or shutil.which("chromium") or shutil.which("chromium-browser")
if not found:
sys.exit("Chrome 을 찾지 못했다. CHROME 으로 경로를 알려 준다.")
return found
def shoot(svg: str, out_dir: str, binary: str) -> str:
body = open(svg, encoding="utf-8").read()
m = SIZE.search(body)
w, h = (int(m.group(1)), int(m.group(2))) if m else (1200, 800)
name = os.path.basename(svg)[:-4]
png = os.path.join(out_dir, f"{name}.png")
with tempfile.NamedTemporaryFile("w", suffix=".html", delete=False,
encoding="utf-8") as fh:
fh.write("<style>html,body{margin:0;padding:0;background:#fff}"
"img{display:block}</style>\n"
f'<img src="file://{os.path.abspath(svg)}" width="{w}" height="{h}">')
wrapper = fh.name
try:
subprocess.run([binary, "--headless", "--disable-gpu", "--no-sandbox",
"--hide-scrollbars", f"--screenshot={png}",
f"--window-size={w + 20},{h + 20}", f"file://{wrapper}"],
check=True, capture_output=True)
finally:
os.unlink(wrapper)
return png
def main() -> int:
ap = argparse.ArgumentParser(description="그림을 PNG 로 떠서 눈으로 본다.")
ap.add_argument("projects", nargs="*")
ap.add_argument("--file", action="append", default=[])
ap.add_argument("-o", "--out", default=None, help="PNG 를 둘 곳 (기본: 임시 폴더)")
args = ap.parse_args()
targets = list(args.file)
if targets:
bad = techlog.check_files(targets)
if bad is not None:
return bad
if args.projects:
bad = techlog.check_targets(args.projects, ROOT, "final")
if bad is not None:
return bad
for project in args.projects:
targets.extend(sorted(glob.glob(
os.path.join(ROOT, "docs", project, "final", "assets", "**", "*.svg"),
recursive=True)))
if not targets:
if args.projects:
print(f"그림 0장 — {' · '.join(args.projects)} 에 아직 SVG 가 없다")
return 0
return techlog.missing_target("(인자 없음)", "볼 그림을 주지 않았다")
out_dir = args.out or tempfile.mkdtemp(prefix="figure-preview-")
os.makedirs(out_dir, exist_ok=True)
binary = chrome()
for svg in targets:
print(shoot(svg, out_dir, binary))
return 0
if __name__ == "__main__":
raise SystemExit(main())