Files
Aislo/resources/knowledge/original/_pipeline/qc_lint.py
T
eomsangdonandClaude Opus 5 4cb9b15939 style: 저장소 전체 포맷터 일괄 적용 (prettier·biome·ruff)
파일마다 포맷 폭이 달라(≈80 대 100) 한 줄만 고쳐도 포맷터가 무관한 줄을 대량
재포맷했음. 사용자 지시로 전체를 한 번에 맞춤. 코드 동작 변경 없음 — 포맷만.

- 프론트엔드 `.ts/.css/.html` → 저장소 prettier (`.prettierrc`, printWidth 100)
- `B07_DesignDetail/openwebcad/**` → 자체 biome (tab 들여쓰기·single quote·lineWidth 100).
  `biome format` 만 사용 — `biome lint --write` 는 포맷 아닌 코드 수정까지 하므로 제외
- 파이썬 → `ruff format` (엔진 코드는 이미 정합, resources·scratch 스크립트 24개만 변경)

두 포맷터가 서로 되돌리지 않도록 `.prettierignore` 신규 — openwebcad 와 빌드·산출물
폴더를 prettier 대상에서 뺌. `.prettierrc` 에 `endOfLine: "auto"` 추가 — 기본값 `lf` 가
`core.autocrlf=true` 로 받은 CRLF 파일을 매번 전부 다시 써서 `--list-different` 가
실제 포맷 차이를 가리고 있었음.

검증: `tsc --noEmit` 통과(루트·openwebcad 둘 다), pytest 349 passed / 17 skipped /
0 failed, CAD vitest 87건 중 81 passed / 6 failed(laptop-sub 기준선과 동일, 회귀 없음).
포맷터 재실행 시 prettier·biome 모두 변경 0건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 07:08:24 +09:00

172 lines
5.5 KiB
Python

# -*- coding: utf-8 -*-
"""법령·기준 md 전수 품질 점검.
검출: 테이블깨짐 / 사진누락 / 내용누락 / 띄어쓰기소실 / 줄바꿈
소스 대조: 별표·첨부 md ↔ 같은 이름 PDF, 현행 본문 md ↔ 같은 이름 XML.
결과를 qc_report.json 으로 저장하고 카테고리별 요약 출력.
"""
import json, re
from pathlib import Path
import pymupdf
ROOT = Path(__file__).resolve().parent.parent
OUT = Path(__file__).resolve().parent / "data"
def norm(s):
return re.sub(r"[^가-힣0-9A-Za-z%㎞㎡㎥℃]", "", s)
def strip_fenced(text):
"""``` 코드펜스 안을 빈 줄로 치환(위치 보존)."""
out, infence = [], False
for l in text.split(chr(10)):
if l.lstrip().startswith("```"):
infence = not infence
out.append("")
continue
out.append("" if infence else l)
return chr(10).join(out)
def ncols(line):
s = line.strip()
if s.startswith("|"):
s = s[1:]
if s.endswith("|"):
s = s[:-1]
return len(s.split("|"))
def check_tables(text):
"""마크다운 표 유효성. (문제 리스트) 반환."""
issues = []
lines = strip_fenced(text).split("\n")
i = 0
while i < len(lines):
if not lines[i].lstrip().startswith("|"):
i += 1
continue
start = i
block = []
while i < len(lines) and lines[i].lstrip().startswith("|"):
block.append(lines[i])
i += 1
head = ncols(block[0])
if len(block) < 2 or not set(
block[1].replace("|", "").replace(" ", "").replace(":", "")
) <= set("-"):
issues.append(f"L{start + 1} 구분선 없음/이상")
continue
for j, b in enumerate(block):
if j == 1:
continue
if ncols(b) != head:
issues.append(f"L{start + j + 1} 열수 {ncols(b)}{head}")
return issues
def check_space(text):
"""프로즈(표·펜스 제외)의 한글 12자 이상 연속 비율."""
prose = [l for l in strip_fenced(text).split(chr(10)) if not l.lstrip().startswith("|")]
t = chr(10).join(prose)
kor = len(re.findall(r"[가-힣]", t))
if kor < 400:
return 0.0
runs = re.findall(r"[가-힣]{12,}", t)
return round(sum(len(x) for x in runs) / kor, 3)
def check_linebreak(text):
"""줄바꿈 결함: 표 앞 빈 줄 없음, 헤딩 직후 표 붙음."""
issues = []
lines = strip_fenced(text).split("\n")
for i in range(1, len(lines)):
s = lines[i].strip()
prev = lines[i - 1].strip()
# 표 시작인데 앞 줄이 텍스트(표/빈줄/헤딩 아님)
if (
s.startswith("|")
and prev
and not prev.startswith("|")
and not prev.startswith("#")
and not prev.startswith(">")
):
issues.append(f"L{i + 1} 표 앞 빈 줄 없음")
return issues[:5]
def pdf_stats(pdf):
d = pymupdf.open(pdf)
txt = "\n".join(p.get_text() for p in d)
imgs = sum(len(p.get_images()) for p in d)
return txt, imgs
def run():
report = []
targets = []
for pat in ("법률", "행정규칙", "표준시방서"):
base = ROOT / pat
targets += [p for p in base.rglob("*.md")]
targets += [p for p in (ROOT / "KS").glob("*.md")]
targets = [p for p in targets if p.name not in ("_목록.md", "_meta.md")]
for md in sorted(targets):
rel = str(md.relative_to(ROOT)).replace("\\", "/")
text = md.read_text(encoding="utf-8")
rec = {"file": rel, "issues": {}}
t = check_tables(text)
if t:
rec["issues"]["테이블"] = t[:6]
sp = check_space(text)
if sp > 0.15:
rec["issues"]["띄어쓰기"] = sp
lb = check_linebreak(text)
if lb:
rec["issues"]["줄바꿈"] = lb
# 소스 대조 (별표/첨부 → PDF). 단, HWP/HWPX 재추출본은 PDF가 소스가 아니므로 제외.
pdf = md.with_suffix(".pdf")
head = text[:200]
reextracted = ("재추출" in head) or (".hwp" in head) or (".hwpx" in head)
if pdf.exists() and not reextracted:
try:
ptxt, pimgs = pdf_stats(pdf)
pn, mn = norm(ptxt), norm(text)
if pn and len(mn) / len(pn) < 0.98:
rec["issues"]["내용누락"] = f"{len(pn)}{len(mn)} ({len(mn) / len(pn):.2f})"
mimg = text.count("〔그림〕") + text.count("![")
if pimgs > 0 and mimg == 0:
rec["issues"]["사진누락"] = f"PDF 이미지 {pimgs}개 / md 0"
except Exception as e:
rec["issues"]["PDF오류"] = str(e)[:50]
if rec["issues"]:
report.append(rec)
json.dump(
report, open(OUT / "qc_report.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1
)
# 요약
cat = {}
for r in report:
for k in r["issues"]:
cat[k] = cat.get(k, 0) + 1
print(f"점검 {len(targets)}개 / 문제 파일 {len(report)}개")
print("카테고리별:", cat)
print("\n=== 심각(내용누락·사진누락·테이블) 상위 ===")
sev = [r for r in report if set(r["issues"]) & {"내용누락", "사진누락", "테이블"}]
for r in sev[:30]:
ks = ", ".join(
f"{k}={v if not isinstance(v, list) else len(v)}" for k, v in r["issues"].items()
)
print(f" {r['file'][-64:]} [{ks}]")
if __name__ == "__main__":
run()