# -*- coding: utf-8 -*- """법령 XML ↔ md 구조 회귀검사 (네트워크 미사용). 과거 3결함(부칙 [:20] 절단·목(가나다) 누락·별표 80건 상한)과 CHANGELOG [:40] 절단의 재발을 잡는다. 수집·보정 후 반드시 실행 (README W5). 검사 항목 (원본 XML 이 있는 모든 판본 md 대상): 1. 부칙 — XML 부칙단위 각 건의 앞부분이 md에 존재하는가 2. 목 — XML 목내용 각 건의 앞부분이 md에 존재하는가 3. 별표 — XML 다운로드 대상(삭제 제외·PDF 링크 보유) 수 ≤ 별표/ PDF 파일 수 4. 변경이력 — CHANGELOG.md 헤더 "개정 N회" == 표 행 수 사용: python verify_structure.py # 전수 검사, 결함 있으면 exit 1 python verify_structure.py -v # 통과 항목도 출력 """ import re import sys import glob import os import xml.etree.ElementTree as ET from pathlib import Path ROOT = Path(__file__).resolve().parent.parent # ...\original # md 대조는 공백·인용부호 제거 후 앞 30자 포함 여부로 판정. # '>' 도 제거 — 부칙은 인용블록이라 빈 줄이 '>' 로 남아 공백만 제거하면 키가 어긋난다. KEY_LEN = 30 def norm(s): return re.sub(r"[\s>*`]+", "", s) def check_pair(xml_path, errors, verbose): md_path = xml_path.with_suffix(".md") if not md_path.exists(): errors.append(f"{md_path}: md 판본 없음 (xml만 존재)") return md_norm = norm(md_path.read_text(encoding="utf-8")) try: root = ET.parse(xml_path).getroot() except ET.ParseError as e: errors.append(f"{xml_path}: XML 파싱 실패 — {e}") return for tag, label in (("부칙내용", "부칙"), ("목내용", "목")): total = miss = 0 for e in root.iter(tag): body = "".join(e.itertext()).strip() if not body: continue total += 1 if norm(body)[:KEY_LEN] not in md_norm: miss += 1 if miss: errors.append(f"{md_path}: {label} {total}건 중 {miss}건 md 누락") elif verbose and total: print(f" OK {md_path.name}: {label} {total}건 전건 존재") def check_별표(folder, root, errors, verbose): 별표 = root.find("별표") if 별표 is None or not len(별표): return targets = 0 for b in 별표.findall("별표단위"): title = (b.findtext("별표제목") or "").strip() link = (b.findtext("별표서식PDF파일링크") or "").strip() if title.startswith("삭제") or not link: continue targets += 1 saved = len(list((folder / "별표").glob("*.pdf"))) if (folder / "별표").is_dir() else 0 if saved < targets: errors.append( f"{folder}: 별표 PDF {targets}건 대상 중 {saved}건만 저장 (미수집 {targets - saved})" ) elif verbose and targets: print(f" OK {folder.name}: 별표 PDF {saved}/{targets}") def check_changelog(folder, errors, verbose): ch = folder / "CHANGELOG.md" if not ch.exists(): return text = ch.read_text(encoding="utf-8") m = re.search(r"교본 이후 개정 (\d+)회", text) if not m: return declared = int(m.group(1)) # 날짜는 점 구분(2026.02.01) — collect_law.py ymd() 출력 형식 rows = len(re.findall(r"^\| \d{4}[.\-]\d{2}[.\-]\d{2} \|", text, re.M)) if declared and rows != declared: errors.append(f"{ch}: 헤더 개정 {declared}회 vs 표 {rows}행 — 이력 절단") elif verbose and declared: print(f" OK {ch.parent.name}: 변경이력 {rows}/{declared}") def main(): verbose = "-v" in sys.argv errors = [] folders = set() for pat in ("**/현행_*.xml", "**/교본시점_*.xml"): for x in glob.glob(str(ROOT / pat), recursive=True): xp = Path(x) check_pair(xp, errors, verbose) folders.add(xp.parent) for folder in sorted(folders): # 별표 대상 수는 현행본 XML 기준 (최신 별표 구성) curs = sorted(folder.glob("현행_*.xml")) if curs: try: check_별표(folder, ET.parse(curs[-1]).getroot(), errors, verbose) except ET.ParseError: pass # check_pair 에서 이미 보고 check_changelog(folder, errors, verbose) print(f"검사 폴더 {len(folders)}곳") if errors: print(f"결함 {len(errors)}건:") for e in errors: print(" ✗", e) sys.exit(1) print("결함 0건 — 구조 보존 확인") if __name__ == "__main__": main()