# -*- coding: utf-8 -*- """품셈 PDF → 절별 md 도구 (규칙: `원가계산/_품셈md_작성규칙.md` · 2026-09-19). 판정 기준은 PDF 뿐 — 원가계산(B09)·공종 마스터·옛 md 는 보지 않음. 사용: python pum_md_tool.py plan <산림|건설> 차례에서 장·절 → 본문/ 빈 파일(머리만 · 상태 대기) python pum_md_tool.py pages <산림|건설> [쪽범위] 쪽 그림 150dpi PNG → tmp/pum_pages/ (git 밖) python pum_md_tool.py check ① 수 대조 ② 글자 대조 → 머리 「기계검사」 칸 python pum_md_tool.py status 장별 대기/작성중/검증대기/확정 표 절 경계 = 시작표지·끝표지 글자가 쪽 안에서 놓인 줄(좌표로 묶은 줄의 맨 앞). 좌표로 줄 묶기는 `resources/tester/test_const_pdf_coverage.py` 의 것을 그대로 씀(그 시험은 안 고침). """ from __future__ import annotations import collections import sys from pathlib import Path from pum_md_base import ( # noqa: E402,F401 — 바탕(책 설정·줄 읽기·머리 찾기)은 pum_md_base.py _NUM, BOOKS, COST, HEAD_KEYS, ROOT, STATES, _page_rows, find_heading, heading_pattern, join_spaced, key, locate, read_rows, visual_text, ) from pum_md_plan import ( # noqa: E402,F401 — 차례 읽기 · 새 md 틀은 pum_md_plan.py Entry, _BAD_NAME, _DIVISION, _EMPTY_PAGE_CHARS, _INLINE, _LEADER, _MAX_PAGES, _REVISION, _SLOT, _TOC_ID, _TOC_SKIP, _blank, _body_rows, _is_footer, _side_number, _span, _span_of, claimed_elsewhere, file_label, head_text, name_of, parse_toc, parse_toc_inline, plan, read_head, safe, toc_entries, ) from pum_md_check import ( # noqa: E402,F401 — 기계검사는 pum_md_check.py _COMMENT, _ESCAPED, _EXEMPT, _GLUED, _HOLD, _IMAGE, _ITEM_HEAD, _NOT_REPEAT, _OFF_CLOSE, _OFF_MARK, _OFF_OPEN, _PUA, _SPACED, _SPACED_PAIR, _TABLE_RULE, _chars, _numbers, _repeat_budget, _repeat_run, _rewrite, _same_han, _split_off, _tight, book_of, check, check_file, compare, fingerprint, join_spaced_numbers, md_lines, passed, pdf_lines, set_head, split_glued_numbers, split_head, verdict, ) # ── pages · status ───────────────────────────────────────────────────── PAGES_DIR = ROOT / "tmp/pum_pages" def pages(book_name: str, span: str = "") -> list[Path]: """쪽 그림 150dpi PNG — `tmp/pum_pages/<책>/p0135.png`(git 밖).""" import pymupdf out = PAGES_DIR / book_name out.mkdir(parents=True, exist_ok=True) made = [] with pymupdf.open(BOOKS[book_name]["pdf"]) as doc: first, _, last = (span or f"1~{doc.page_count}").replace("-", "~").partition("~") for number in range(int(first), int(last or first) + 1): path = out / f"p{number:04d}.png" doc[number - 1].get_pixmap(dpi=150).save(path) made.append(path) return made def status() -> str: """장별 상태 표(md) — 머리 「상태」 칸을 모음.""" rows = [ "| 책 | 장 | " + " | ".join(STATES) + " | 계 |", "|---|---|" + "---|" * (len(STATES) + 1), ] for name, book in BOOKS.items(): by_chapter: dict[str, collections.Counter] = {} for path in sorted(book["out"].rglob("*.md")): chapter = path.parent.relative_to(book["out"]).as_posix() by_chapter.setdefault(chapter, collections.Counter())[ read_head(path).get("상태", "") ] += 1 for chapter, counts in by_chapter.items(): cells = " | ".join(str(counts[s]) for s in STATES) rows.append(f"| {name} | {chapter} | {cells} | {sum(counts.values())} |") return "\n".join(rows) def main(argv: list[str]) -> int: command, args = (argv[0], argv[1:]) if argv else ("", []) if command == "plan" and args: print(f"새 파일 {len(plan(args[0]))}") return 0 if command == "pages" and args: made = pages(args[0], args[1] if len(args) > 1 else "") print(f"쪽 그림 {len(made)} → {PAGES_DIR / args[0]}") return 0 if command == "check" and args: return max(check(Path(a)) for a in args) if command == "status": print(status()) return 0 print(__doc__) return 2 if __name__ == "__main__": sys.exit(main(sys.argv[1:]))