fix(knowledge): 검토내역 8건 반영 — 상한 제거·행정규칙 부칙 복원·회귀검사 신설
타 AI 검토내역(tmp/기술문서DB_검토내역_260814.md) 전수 검증 후 동의분 적용. 파이프라인: - fix_fences.py units[:20] / collect_law.py 별표 limit=80·변경이력 [:40] 상한 제거 - gen_md.py 기준목록 출력을 운영본(04_참조_법령기준_목록.md) 경로로 정정 - README 구경로(docs/raw/law) 3곳 현행화, ruff format 일괄 적용 - verify_structure.py 신설: XML↔md 부칙·목·별표·CHANGELOG 행수 회귀검사 신규 발견 결함 2건 정정 (검토내역에 없던 것): - 행정규칙 XML은 부칙내용이 부칙 직속(부칙단위 래퍼 없음) — 변환기가 미처리해 행정규칙 md 22건에 부칙 전무. patch_law_md 확장 후 부칙 138건 복원 - patch_law_md 부칙 dedup 키가 괄호형 타법개정 헤더 미매칭 — 재실행 시 기존 부칙 1,724건 중복 추가되는 비멱등 결함. 정규식 보강 재수집 (law.go.kr): - 대기환경보전법 시행규칙 별표 PDF 78건 추가 확보 (80→158, 전수) - CHANGELOG 절단 3건 재생성 (고용보험법 시행령 42·국가계약법 시행령 41· 대기환경 45회 — API 연혁과 전건 일치 확인) 기술문서·데이터: - _최종검증로그: 55문서 스냅샷 범위 명시 (현재 65문서, 신설 10건 미검증 주지) - 제비율.md frontmatter에 조달청 제비율(2026.04.13)·국민연금법 출처 보강 - 원가_입력변수_사전: 장비대금 지급보증을 공사종류별 표로 정정, csv→json 표기 - labor_const JSON: 원문 노임 118건 전건 일치 확인 후 해시만 재동기화 (원문 변경분 = 신뢰도 플래그 컬럼뿐, 단가 무변) 검증: verify_structure 결함 0건, patch_law_md 재실행 +0 (멱등), 원가 JSON 해시 56건 중 잔여 불일치 1건(coef_2026 — 사용자 협의 대상) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
- Python 3.12 + `pymupdf`, `olefile`
|
||||
- 인증키(커밋 금지):
|
||||
- `docs/raw/law/.secrets.local.md` 에 KCSC 키 (스크립트가 자동 파싱) — 또는 환경변수 `KCSC_KEY`
|
||||
- `resources/knowledge/.secrets.local.md` 에 KCSC 키 (스크립트가 자동 파싱) — 또는 환경변수 `KCSC_KEY`
|
||||
- law.go.kr OC키는 기본 `umsangdon` (환경변수 `LAW_OC` 로 교체 가능)
|
||||
|
||||
## 스크립트 → 워크플로(README W1~W6) 대응
|
||||
@@ -27,7 +27,8 @@
|
||||
| `qc_lint.py` | md 전수 품질 점검(테이블·사진·내용누락·띄어쓰기·줄바꿈) | W5 |
|
||||
| `verify_pdf2md.py` | 변환 손실·줄누락 검증 | W5 |
|
||||
| `build_srcmap.py` | law API 결과 → 시행일/개정일 맵(srcmap.json) | W6 |
|
||||
| `gen_md.py` | `0. 참조 법령·기준 목록.md` 재생성(9열·색인) | W6 |
|
||||
| `gen_md.py` | `../04_참조_법령기준_목록.md` 재생성(9열·색인) | W6 |
|
||||
| `verify_structure.py` | 법령 XML ↔ md 구조 회귀검사(부칙·목·별표 수량 대조). 네트워크 미사용 | W5 |
|
||||
|
||||
## data/ (생성 데이터 = gen_md 입력)
|
||||
|
||||
@@ -46,7 +47,7 @@
|
||||
## 표준 실행 순서
|
||||
|
||||
```bash
|
||||
cd docs/raw/law/original/_pipeline
|
||||
cd resources/knowledge/original/_pipeline
|
||||
python collect_law.py data/items_all.json # 법령·행정규칙 + 별표
|
||||
python get_attach.py # 첨부·HWP
|
||||
python get_kcsc.py # KCS 본문
|
||||
@@ -56,7 +57,7 @@ python verify_pdf2md.py # 검증
|
||||
python gen_md.py # 색인 재생성
|
||||
```
|
||||
|
||||
수집은 새로 받은 현행본을 `현행_새시행일.*` 로 **추가**한다(기존 판본 삭제 금지). 상세 규칙은 `docs/raw/law/README.md` W1~W6 참조.
|
||||
수집은 새로 받은 현행본을 `현행_새시행일.*` 로 **추가**한다(기존 판본 삭제 금지). 상세 규칙은 `resources/knowledge/README.md` W1~W6 참조.
|
||||
|
||||
## 변환 규칙이 바뀐 경우 (2026-08-14 감사 반영)
|
||||
|
||||
|
||||
@@ -4,32 +4,41 @@
|
||||
항목별로 현행본 + 교본시점(2019-12-31 기준) 연혁본을 받아
|
||||
<분류>/<명칭>/ 에 XML · Markdown · 별표 PDF · _meta.md · CHANGELOG.md 로 저장.
|
||||
"""
|
||||
|
||||
import json, re, sys, time, urllib.parse, urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
import os as _os
|
||||
from pathlib import Path as _P
|
||||
|
||||
# 이 스크립트는 original/_pipeline/ 에 위치. 코퍼스 루트 = 상위 폴더.
|
||||
ROOT_DIR = _P(__file__).resolve().parent.parent # ...\original
|
||||
DATA_DIR = _P(__file__).resolve().parent / "data" # 생성 데이터(JSON)
|
||||
ROOT_DIR = _P(__file__).resolve().parent.parent # ...\original
|
||||
DATA_DIR = _P(__file__).resolve().parent / "data" # 생성 데이터(JSON)
|
||||
|
||||
|
||||
# API 키: 커밋 금지 파일(law/.secrets.local.md) 또는 환경변수에서 읽는다.
|
||||
def _load_key(name):
|
||||
v = _os.environ.get(name)
|
||||
if v: return v.strip()
|
||||
if v:
|
||||
return v.strip()
|
||||
sec = ROOT_DIR.parent / ".secrets.local.md"
|
||||
if sec.exists():
|
||||
import re as _re
|
||||
|
||||
for pat in (r"KCSC[\s\S]*?`([A-Za-z0-9]{20,})`", r"인증키[:\s]*`?([A-Za-z0-9]{30,})`?"):
|
||||
m = _re.search(pat, sec.read_text(encoding="utf-8"))
|
||||
if m: return m.group(1)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
ROOT = Path(str(ROOT_DIR))
|
||||
SC = Path(str(DATA_DIR))
|
||||
BASEURL = "https://www.law.go.kr"
|
||||
OC = _os.environ.get("LAW_OC", "umsangdon") # law.go.kr 인증 ID
|
||||
OC = _os.environ.get("LAW_OC", "umsangdon") # law.go.kr 인증 ID
|
||||
교본기준일 = "20191231"
|
||||
|
||||
|
||||
# ───────────────────────── 통신 ─────────────────────────
|
||||
def fetch(url, binary=False, tries=3):
|
||||
for k in range(tries):
|
||||
@@ -43,9 +52,12 @@ def fetch(url, binary=False, tries=3):
|
||||
return None
|
||||
time.sleep(1.5)
|
||||
|
||||
|
||||
def search(target, query, nw=None, display=100):
|
||||
url = (f"{BASEURL}/DRF/lawSearch.do?OC={OC}&type=XML&target={target}"
|
||||
f"&display={display}&query={urllib.parse.quote(query)}")
|
||||
url = (
|
||||
f"{BASEURL}/DRF/lawSearch.do?OC={OC}&type=XML&target={target}"
|
||||
f"&display={display}&query={urllib.parse.quote(query)}"
|
||||
)
|
||||
if nw:
|
||||
url += f"&nw={nw}"
|
||||
b = fetch(url)
|
||||
@@ -54,6 +66,7 @@ def search(target, query, nw=None, display=100):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def service(target, key, kind="MST"):
|
||||
url = f"{BASEURL}/DRF/lawService.do?OC={OC}&type=XML&target={target}&{kind}={key}"
|
||||
b = fetch(url)
|
||||
@@ -62,6 +75,7 @@ def service(target, key, kind="MST"):
|
||||
except Exception:
|
||||
return None, None
|
||||
|
||||
|
||||
def T(node, *names):
|
||||
for n in names:
|
||||
el = node.find(n)
|
||||
@@ -69,16 +83,20 @@ def T(node, *names):
|
||||
return el.text.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def ymd(s):
|
||||
s = re.sub(r"\D", "", s or "")
|
||||
return f"{s[:4]}.{s[4:6]}.{s[6:8]}" if len(s) == 8 else ""
|
||||
|
||||
|
||||
def safe(s):
|
||||
return re.sub(r'[\\/:*?"<>|]', "_", s).strip().rstrip(".")
|
||||
|
||||
|
||||
# ─────────────────── XML → Markdown 변환 ───────────────────
|
||||
JOMUN = re.compile(r"^(제\d+조(?:의\d+)?)\s*(?:\(([^)]*)\))?\s*(.*)$", re.S)
|
||||
|
||||
|
||||
def split_jo(text):
|
||||
"""조문 텍스트를 (헤딩, 본문)으로 분리."""
|
||||
m = JOMUN.match(text.strip())
|
||||
@@ -88,6 +106,7 @@ def split_jo(text):
|
||||
head = f"{num}({title})" if title else num
|
||||
return head, rest.strip()
|
||||
|
||||
|
||||
def clean(t):
|
||||
t = (t or "").replace("\u00a0", " ")
|
||||
t = re.sub(r"[ \t]+", " ", t)
|
||||
@@ -103,34 +122,43 @@ def fence_ascii_tables(text):
|
||||
out = []
|
||||
i = 0
|
||||
infence = False
|
||||
|
||||
def is_tbl(l):
|
||||
s = l.strip()
|
||||
return bool(re.match(r"^\+[-=+]{3,}", s)) or (s.startswith("|") and ("+" in s or s.count("|") >= 2))
|
||||
return bool(re.match(r"^\+[-=+]{3,}", s)) or (
|
||||
s.startswith("|") and ("+" in s or s.count("|") >= 2)
|
||||
)
|
||||
|
||||
while i < len(lines):
|
||||
if lines[i].lstrip().startswith("```"):
|
||||
infence = not infence
|
||||
out.append(lines[i]); i += 1
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
continue
|
||||
if not infence and is_tbl(lines[i]):
|
||||
j = i
|
||||
block = []
|
||||
while j < len(lines):
|
||||
if is_tbl(lines[j]) or lines[j].strip() == "":
|
||||
block.append(lines[j]); j += 1
|
||||
block.append(lines[j])
|
||||
j += 1
|
||||
else:
|
||||
break
|
||||
# 뒤쪽 빈 줄 되돌림
|
||||
while block and block[-1].strip() == "":
|
||||
block.pop(); j -= 1
|
||||
block.pop()
|
||||
j -= 1
|
||||
if any(re.match(r"^\s*\+[-=+]{3,}", b) for b in block):
|
||||
out += ["", "```text"] + [b.rstrip() for b in block] + ["```", ""]
|
||||
else:
|
||||
out += block
|
||||
i = j
|
||||
else:
|
||||
out.append(lines[i]); i += 1
|
||||
out.append(lines[i])
|
||||
i += 1
|
||||
return chr(10).join(out)
|
||||
|
||||
|
||||
def law_to_md(root, 분류, 출처url, 실명=""):
|
||||
info = root.find("기본정보") or root.find("행정규칙기본정보")
|
||||
name = T(info, "법령명_한글", "행정규칙명") or 실명
|
||||
@@ -197,11 +225,15 @@ def law_to_md(root, 분류, 출처url, 실명=""):
|
||||
if ov:
|
||||
out.append(f" {ov}")
|
||||
for mo in kid.findall("목"):
|
||||
mv = clean(T(mo, "목내용")) or clean(" ".join(x for x in mo.itertext() if x))
|
||||
mv = clean(T(mo, "목내용")) or clean(
|
||||
" ".join(x for x in mo.itertext() if x)
|
||||
)
|
||||
if mv:
|
||||
out.append(f" {mv}")
|
||||
elif kid.tag == "목":
|
||||
mv = clean(T(kid, "목내용")) or clean(" ".join(x for x in kid.itertext() if x))
|
||||
mv = clean(T(kid, "목내용")) or clean(
|
||||
" ".join(x for x in kid.itertext() if x)
|
||||
)
|
||||
if mv:
|
||||
out.append(f" {mv}")
|
||||
out.append("")
|
||||
@@ -219,13 +251,17 @@ def law_to_md(root, 분류, 출처url, 실명=""):
|
||||
|
||||
부칙 = root.find("부칙")
|
||||
if 부칙 is not None:
|
||||
units = 부칙.findall("부칙단위")
|
||||
# 법령(law)은 부칙단위 래퍼, 행정규칙(admrul)은 부칙내용이 부칙 직속 —
|
||||
# 부칙단위만 처리하면 행정규칙 md 에 부칙이 통째로 빠진다 (2026-08-15 결함 정정).
|
||||
units = 부칙.findall("부칙단위") or 부칙.findall("부칙내용")
|
||||
if units:
|
||||
# 법제처 XML은 부칙을 오래된 순으로 담는다 — 상한을 두면 최신 부칙
|
||||
# (연도별 요율 특례 등)이 잘려나가므로 전건 출력한다.
|
||||
out += ["", "## 부칙", ""]
|
||||
for b in units:
|
||||
body = clean(T(b, "부칙내용"))
|
||||
body = (
|
||||
clean(T(b, "부칙내용")) if b.tag == "부칙단위" else clean("".join(b.itertext()))
|
||||
)
|
||||
# 인용블록: 각 줄 앞에 '> ', 원문 줄바꿈 보존(빈 줄은 '>')
|
||||
for ln in body.split("\n"):
|
||||
out.append(f"> {ln}" if ln.strip() else ">")
|
||||
@@ -243,13 +279,18 @@ def law_to_md(root, 분류, 출처url, 실명=""):
|
||||
for b in 별표.findall("별표단위"):
|
||||
n = T(b, "별표번호").lstrip("0") or "-"
|
||||
g = T(b, "별표가지번호").lstrip("0")
|
||||
title = re.sub(r"\s+", " ", clean(T(b, "별표제목")).replace("\n", " ")).replace("|", "/")
|
||||
out.append(f"| {T(b,'별표구분')} | {n}{('의'+g) if g else ''} | {title} | `별표/` |")
|
||||
title = re.sub(r"\s+", " ", clean(T(b, "별표제목")).replace("\n", " ")).replace(
|
||||
"|", "/"
|
||||
)
|
||||
out.append(f"| {T(b, '별표구분')} | {n}{('의' + g) if g else ''} | {title} | `별표/` |")
|
||||
out.append("")
|
||||
return fence_ascii_tables("\n".join(out)) + "\n"
|
||||
|
||||
|
||||
# ─────────────────── 별표 PDF 다운로드 ───────────────────
|
||||
def download_별표(root, folder, limit=80):
|
||||
# limit=None: 상한 없음 — 80건 상한으로 대기환경보전법 시행규칙 별표 158건 중
|
||||
# 78건이 누락된 결함 재발 방지 (2026-08-15). 수동 축소 실행 시에만 limit 지정.
|
||||
def download_별표(root, folder, limit=None):
|
||||
별표 = root.find("별표")
|
||||
if 별표 is None or not len(별표):
|
||||
return 0
|
||||
@@ -264,7 +305,7 @@ def download_별표(root, folder, limit=80):
|
||||
continue
|
||||
num = T(b, "별표번호").lstrip("0") or "0"
|
||||
g = T(b, "별표가지번호").lstrip("0")
|
||||
fn = safe(f"{T(b,'별표구분')}{num}{('의'+g) if g else ''}_{title[:48]}") + ".pdf"
|
||||
fn = safe(f"{T(b, '별표구분')}{num}{('의' + g) if g else ''}_{title[:48]}") + ".pdf"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
p = d / fn
|
||||
if p.exists():
|
||||
@@ -275,10 +316,11 @@ def download_별표(root, folder, limit=80):
|
||||
p.write_bytes(blob)
|
||||
n += 1
|
||||
time.sleep(0.25)
|
||||
if n >= limit:
|
||||
if limit is not None and n >= limit:
|
||||
break
|
||||
return n
|
||||
|
||||
|
||||
# ─────────────────── 항목 1건 수집 ───────────────────
|
||||
def collect(item):
|
||||
분류폴더, target, 목록명, 조회명 = item["dir"], item["target"], item["name"], item["query"]
|
||||
@@ -291,10 +333,15 @@ def collect(item):
|
||||
return {"명칭": 목록명, "상태": "검색실패"}
|
||||
nodes = r.findall("law") + r.findall("admrul")
|
||||
want = item.get("exact", 조회명)
|
||||
cur = next((n for n in nodes if T(n, "법령명한글", "행정규칙명") == want), None) \
|
||||
or next((n for n in nodes if T(n, "법령명한글", "행정규칙명").startswith(want)), None)
|
||||
cur = next((n for n in nodes if T(n, "법령명한글", "행정규칙명") == want), None) or next(
|
||||
(n for n in nodes if T(n, "법령명한글", "행정규칙명").startswith(want)), None
|
||||
)
|
||||
if cur is None:
|
||||
return {"명칭": 목록명, "상태": "현행본 없음", "후보": [T(n, "법령명한글", "행정규칙명") for n in nodes[:3]]}
|
||||
return {
|
||||
"명칭": 목록명,
|
||||
"상태": "현행본 없음",
|
||||
"후보": [T(n, "법령명한글", "행정규칙명") for n in nodes[:3]],
|
||||
}
|
||||
실명 = T(cur, "법령명한글", "행정규칙명")
|
||||
cur_key = T(cur, "법령일련번호", "행정규칙일련번호")
|
||||
kind = "MST" if target == "law" else "ID"
|
||||
@@ -308,16 +355,28 @@ def collect(item):
|
||||
for n in h.findall("law"):
|
||||
if T(n, "법령명한글") != 실명:
|
||||
continue
|
||||
hist.append({"일련": T(n, "법령일련번호"), "시행": T(n, "시행일자"),
|
||||
"구분": T(n, "현행연혁코드")})
|
||||
hist.append(
|
||||
{
|
||||
"일련": T(n, "법령일련번호"),
|
||||
"시행": T(n, "시행일자"),
|
||||
"구분": T(n, "현행연혁코드"),
|
||||
}
|
||||
)
|
||||
else:
|
||||
h = search(target, 조회명, nw=2, display=100)
|
||||
if h is not None:
|
||||
for n in h.findall("admrul"):
|
||||
hist.append({"일련": T(n, "행정규칙일련번호"), "시행": T(n, "시행일자"),
|
||||
"발령": T(n, "발령일자"), "구분": T(n, "현행연혁구분")})
|
||||
past = sorted([x for x in hist if x["시행"] and x["시행"] <= 교본기준일],
|
||||
key=lambda x: x["시행"])
|
||||
hist.append(
|
||||
{
|
||||
"일련": T(n, "행정규칙일련번호"),
|
||||
"시행": T(n, "시행일자"),
|
||||
"발령": T(n, "발령일자"),
|
||||
"구분": T(n, "현행연혁구분"),
|
||||
}
|
||||
)
|
||||
past = sorted(
|
||||
[x for x in hist if x["시행"] and x["시행"] <= 교본기준일], key=lambda x: x["시행"]
|
||||
)
|
||||
past = past[-1] if past else None
|
||||
|
||||
saved = []
|
||||
@@ -338,7 +397,7 @@ def collect(item):
|
||||
continue
|
||||
stem = f"{tag}_{eff}"
|
||||
(folder / f"{stem}.xml").write_bytes(blob)
|
||||
url = f"{BASEURL}/DRF/lawService.do?OC={OC}&type=HTML&target={target}&{'MST' if target=='law' else 'ID'}={key}"
|
||||
url = f"{BASEURL}/DRF/lawService.do?OC={OC}&type=HTML&target={target}&{'MST' if target == 'law' else 'ID'}={key}"
|
||||
(folder / f"{stem}.md").write_text(law_to_md(root, 분류폴더, url, 실명), encoding="utf-8")
|
||||
saved.append(f"{tag}:{eff}")
|
||||
if tag == "현행":
|
||||
@@ -351,15 +410,16 @@ def collect(item):
|
||||
between = [x for x in hist if x["시행"] and 교본기준일 < x["시행"] <= T(cur, "시행일자")]
|
||||
between.sort(key=lambda x: x["시행"], reverse=True)
|
||||
meta = [
|
||||
f"# {목록명}", "",
|
||||
f"# {목록명}",
|
||||
"",
|
||||
f"- 정식명칭: {실명}",
|
||||
f"- 목록상 명칭: {목록명}" + (" ← **명칭 상이**" if 실명 != 목록명 else ""),
|
||||
f"- 식별자: `{'법령ID' if target=='law' else '행정규칙ID'}={cur_id}` (불변)",
|
||||
f"- 소관: {T(cur,'소관부처명')}",
|
||||
f"- 종류: {T(cur,'법령구분명','행정규칙종류')}",
|
||||
f"- 현행 시행일: {ymd(T(cur,'시행일자'))} / 공포 {ymd(T(cur,'공포일자','발령일자'))} 제{T(cur,'공포번호','발령번호')}호",
|
||||
f"- 식별자: `{'법령ID' if target == 'law' else '행정규칙ID'}={cur_id}` (불변)",
|
||||
f"- 소관: {T(cur, '소관부처명')}",
|
||||
f"- 종류: {T(cur, '법령구분명', '행정규칙종류')}",
|
||||
f"- 현행 시행일: {ymd(T(cur, '시행일자'))} / 공포 {ymd(T(cur, '공포일자', '발령일자'))} 제{T(cur, '공포번호', '발령번호')}호",
|
||||
f"- 교본시점 판본: {ymd(past['시행']) if past else '없음(교본 이후 제정)'}",
|
||||
f"- 출처: {BASEURL}/DRF/lawService.do?target={target}&{'MST' if target=='law' else 'ID'}={cur_key}",
|
||||
f"- 출처: {BASEURL}/DRF/lawService.do?target={target}&{'MST' if target == 'law' else 'ID'}={cur_key}",
|
||||
# 수집일이 없으면 갱신 지연 여부를 파일만으로 판정할 수 없다 (2026-08-14 감사).
|
||||
f"- 수집일: {time.strftime('%Y-%m-%d')}",
|
||||
f"- 수집 파일: {', '.join(saved)}" + (f" / 별표 PDF {n별표}건" if n별표 else ""),
|
||||
@@ -367,13 +427,20 @@ def collect(item):
|
||||
]
|
||||
(folder / "_meta.md").write_text("\n".join(meta), encoding="utf-8")
|
||||
|
||||
ch = [f"# {목록명} — 변경이력", "",
|
||||
f"기준: 교본시점({ymd(교본기준일)}) → 현행({ymd(T(cur,'시행일자'))})", "",
|
||||
f"**교본 이후 개정 {len(between)}회**", ""]
|
||||
ch = [
|
||||
f"# {목록명} — 변경이력",
|
||||
"",
|
||||
f"기준: 교본시점({ymd(교본기준일)}) → 현행({ymd(T(cur, '시행일자'))})",
|
||||
"",
|
||||
f"**교본 이후 개정 {len(between)}회**",
|
||||
"",
|
||||
]
|
||||
if between:
|
||||
ch += ["| 시행일 | 구분 |", "|---|---|"]
|
||||
for x in between[:40]:
|
||||
ch.append(f"| {ymd(x['시행'])} | {x.get('구분','')} |")
|
||||
# 상한 없음 — [:40] 절단으로 개정 41회 이상 문서(고용보험법 시행령 42회 등)
|
||||
# 이력이 잘리던 결함 재발 방지 (2026-08-15). 헤더 개정횟수와 표 행수 일치 보장.
|
||||
for x in between:
|
||||
ch.append(f"| {ymd(x['시행'])} | {x.get('구분', '')} |")
|
||||
ch.append("")
|
||||
if root_cur is not None:
|
||||
for tag in ("제개정이유", "개정문"):
|
||||
@@ -384,8 +451,15 @@ def collect(item):
|
||||
ch += [f"## 최근 개정 — {tag}", "", f"> {s}", ""]
|
||||
(folder / "CHANGELOG.md").write_text("\n".join(ch), encoding="utf-8")
|
||||
|
||||
return {"명칭": 목록명, "실명": 실명, "상태": "OK", "판본": saved,
|
||||
"별표": n별표, "개정횟수": len(between)}
|
||||
return {
|
||||
"명칭": 목록명,
|
||||
"실명": 실명,
|
||||
"상태": "OK",
|
||||
"판본": saved,
|
||||
"별표": n별표,
|
||||
"개정횟수": len(between),
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
items = json.load(open(SC / sys.argv[1], encoding="utf-8"))
|
||||
@@ -393,6 +467,11 @@ if __name__ == "__main__":
|
||||
for i, it in enumerate(items, 1):
|
||||
r = collect(it)
|
||||
res.append(r)
|
||||
print(f"[{i}/{len(items)}] {r['명칭']} :: {r['상태']} {r.get('판본','')} 별표{r.get('별표',0)} 개정{r.get('개정횟수','')}", flush=True)
|
||||
print(
|
||||
f"[{i}/{len(items)}] {r['명칭']} :: {r['상태']} {r.get('판본', '')} 별표{r.get('별표', 0)} 개정{r.get('개정횟수', '')}",
|
||||
flush=True,
|
||||
)
|
||||
time.sleep(0.3)
|
||||
json.dump(res, open(SC / "collect_result.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1)
|
||||
json.dump(
|
||||
res, open(SC / "collect_result.json", "w", encoding="utf-8"), ensure_ascii=False, indent=1
|
||||
)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
- 개정문·제개정이유(CHANGELOG): 코드펜스 → 인용블록
|
||||
본문·이미지 로컬화 결과는 건드리지 않는다(부칙/개정문 섹션만 치환).
|
||||
"""
|
||||
|
||||
import re, sys
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
@@ -12,13 +13,16 @@ from pathlib import Path
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FENCE = "```"
|
||||
|
||||
|
||||
def clean(t):
|
||||
t = (t or "").replace(" ", " ")
|
||||
return re.sub(r"[ \t]+", " ", t).strip()
|
||||
|
||||
|
||||
def to_quote(text):
|
||||
return "\n".join(f"> {ln}" if ln.strip() else ">" for ln in text.split("\n"))
|
||||
|
||||
|
||||
def buchik_from_xml(xml_path):
|
||||
"""XML에서 부칙 인용블록 md(절단 없음) 생성. '## 부칙' 섹션 문자열 반환."""
|
||||
root = ET.parse(xml_path).getroot()
|
||||
@@ -29,12 +33,14 @@ def buchik_from_xml(xml_path):
|
||||
if not units:
|
||||
return None
|
||||
out = ["## 부칙", ""]
|
||||
for b in units[:20]:
|
||||
# 상한 없음 — 부칙 전건 출력 (상한을 두면 부칙 절단 결함 재발, 2026-08-15)
|
||||
for b in units:
|
||||
body = clean(b.findtext("부칙내용") or "")
|
||||
out.append(to_quote(body))
|
||||
out.append("")
|
||||
return "\n".join(out).rstrip() + "\n"
|
||||
|
||||
|
||||
def fix_law_md(md_path, xml_path):
|
||||
"""현행/교본시점 md: '## 부칙' ~ 다음 '## ' 사이를 XML 부칙으로 교체."""
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
@@ -48,7 +54,7 @@ def fix_law_md(md_path, xml_path):
|
||||
if not m:
|
||||
return 0
|
||||
start = m.start()
|
||||
nxt = re.search(r"^## (?!부칙)", text[m.end():], re.M)
|
||||
nxt = re.search(r"^## (?!부칙)", text[m.end() :], re.M)
|
||||
end = m.end() + nxt.start() if nxt else len(text)
|
||||
new = text[:start] + new_buchik + ("\n" + text[end:].lstrip("\n") if text[end:].strip() else "")
|
||||
if new != text:
|
||||
@@ -56,20 +62,24 @@ def fix_law_md(md_path, xml_path):
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
def fix_changelog(md_path):
|
||||
"""CHANGELOG: 개정문/제개정이유 코드펜스 → 인용블록."""
|
||||
text = md_path.read_text(encoding="utf-8")
|
||||
if FENCE not in text:
|
||||
return 0
|
||||
|
||||
def repl(m):
|
||||
inner = m.group(1).strip()
|
||||
return to_quote(inner)
|
||||
|
||||
new = re.sub(FENCE + r"\w*\n(.*?)\n?" + FENCE, repl, text, flags=re.S)
|
||||
if new != text:
|
||||
md_path.write_text(new, encoding="utf-8")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
n_law = n_cl = 0
|
||||
for md in ROOT.rglob("*.md"):
|
||||
|
||||
@@ -1,107 +1,341 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""참조 법령/기준 목록 md 생성."""
|
||||
|
||||
import re, json
|
||||
from pathlib import Path
|
||||
import os as _os
|
||||
from pathlib import Path as _P
|
||||
|
||||
# 이 스크립트는 original/_pipeline/ 에 위치. 코퍼스 루트 = 상위 폴더.
|
||||
ROOT_DIR = _P(__file__).resolve().parent.parent # ...\original
|
||||
DATA_DIR = _P(__file__).resolve().parent / "data" # 생성 데이터(JSON)
|
||||
ROOT_DIR = _P(__file__).resolve().parent.parent # ...\original
|
||||
DATA_DIR = _P(__file__).resolve().parent / "data" # 생성 데이터(JSON)
|
||||
|
||||
|
||||
# API 키: 커밋 금지 파일(law/.secrets.local.md) 또는 환경변수에서 읽는다.
|
||||
def _load_key(name):
|
||||
v = _os.environ.get(name)
|
||||
if v: return v.strip()
|
||||
if v:
|
||||
return v.strip()
|
||||
sec = ROOT_DIR.parent / ".secrets.local.md"
|
||||
if sec.exists():
|
||||
import re as _re
|
||||
|
||||
for pat in (r"KCSC[\s\S]*?`([A-Za-z0-9]{20,})`", r"인증키[:\s]*`?([A-Za-z0-9]{30,})`?"):
|
||||
m = _re.search(pat, sec.read_text(encoding="utf-8"))
|
||||
if m: return m.group(1)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return ""
|
||||
|
||||
|
||||
from collections import defaultdict, OrderedDict
|
||||
|
||||
BASE = Path(str(ROOT_DIR / "임도기술교본"))
|
||||
OUTMD = Path(str(ROOT_DIR / "0. 참조 법령·기준 목록.md"))
|
||||
# 운영본 위치 = knowledge 루트 (구경로 original/0. 참조 법령·기준 목록.md — 2026-08-15 정정)
|
||||
OUTMD = Path(str(ROOT_DIR.parent / "04_참조_법령기준_목록.md"))
|
||||
SC = Path(str(DATA_DIR))
|
||||
|
||||
|
||||
def norm(s):
|
||||
s = s.replace("<br>", "").replace("ㆍ", "·")
|
||||
return re.sub(r"[\s\u00b7\u2027]", "", s)
|
||||
|
||||
|
||||
ENTRIES = [
|
||||
("법률","산림청","산림자원의 조성 및 관리에 관한 법률","약칭 산림자원법",[r"산림자원의조성및관리에관한법률(?!시행)", r"산림자원법(?!시행)"]),
|
||||
("법률","산림청","산림자원의 조성 및 관리에 관한 법률 시행령","",[r"산림자원의조성및관리에관한법률시행령", r"산림자원법시행령"]),
|
||||
("법률","산림청","산림자원의 조성 및 관리에 관한 법률 시행규칙","별표1 타당성평가·별표2 시설기준 포함",[r"산림자원의조성및관리에관한법률시행규칙", r"산림자원법시행규칙"]),
|
||||
("법률","산림청","산림기술 진흥 및 관리에 관한 법률","약칭 산림기술법",[r"산림기술진흥및관리에관한법률(?!시행)", r"산림기술법(?!시행)"]),
|
||||
("법률","산림청","산림기술 진흥 및 관리에 관한 법률 시행령","",[r"산림기술진흥및관리에관한법률시행령", r"산림기술법시행령"]),
|
||||
("법률","산림청","산림보호법","",[r"산림보호법"]),
|
||||
("법률","산림청","산지관리법","",[r"산지관리법"]),
|
||||
("법률","환경부","자연환경보전법","시행령·시행규칙 포함",[r"자연환경보전법"]),
|
||||
("법률","행정안전부","자연재해대책법","시행령·시행규칙 포함",[r"자연재해대책법"]),
|
||||
("법률","환경부","환경영향평가법","시행령·시행규칙 포함",[r"환경영향평가법"]),
|
||||
("법률","산업통상자원부","국가균형발전특별법","시행령·시행규칙 포함",[r"국가균형발전특별법"]),
|
||||
("법률","기획재정부","보조금 관리에 관한 법률","시행령·시행규칙 포함",[r"보조금관리에관한법률", r"보조금에관한법률"]),
|
||||
("법률","기획재정부","국가를 당사자로 하는 계약에 관한 법률","약칭 국가계약법",[r"국가를당사자로하는계약에관한법률(?!시행)", r"국가계약법(?!시행|률)", r"국가계약법률"]),
|
||||
("법률","기획재정부","국가를 당사자로 하는 계약에 관한 법률 시행령","",[r"국가를당사자로하는계약에관한법률시행령", r"국가계약법시행령"]),
|
||||
("법률","행정안전부","지방자치단체를 당사자로 하는 계약에 관한 법률","약칭 지방계약법",[r"지방자치단체를당사자로하는계약에관한법률(?!시행)", r"지방계약법(?!시행)"]),
|
||||
("법률","행정안전부","지방자치단체를 당사자로 하는 계약에 관한 법률 시행령","",[r"지방자치단체를당사자로하는계약에관한법률시행령", r"지방계약법시행령"]),
|
||||
("법률","국토교통부","도로법","",[r"(?<![가-힣])도로법"]),
|
||||
("법률","행정안전부","농어촌도로정비법","",[r"농어촌도로정비법"]),
|
||||
("법률","국토교통부","국토의 계획 및 이용에 관한 법률","약칭 국토계획법",[r"국토의계획및이용에관한법률"]),
|
||||
("법률","행정안전부","도로명주소법","",[r"도로명주소법(?!시행)"]),
|
||||
("법률","행정안전부","도로명주소법 시행령","",[r"도로명주소법시행령"]),
|
||||
("법률","국토교통부","측량ㆍ수로조사 및 지적에 관한 법률","현 「공간정보의 구축 및 관리 등에 관한 법률」로 개정",[r"측량·?수로조사및지적에관한법률"]),
|
||||
("법률","고용노동부","산업안전보건법","",[r"산업안전보건법"]),
|
||||
("법률","고용노동부","산업재해보상보험법","",[r"산업재해보상보험법(?!의)"]),
|
||||
("법률","고용노동부","고용보험 및 산업재해보상보험의 보험료징수 등에 관한 법률","약칭 보험료징수법",[r"고용보험및산업재해보상보험의보험료징수등에관한법률(?!시행)"]),
|
||||
("법률","고용노동부","고용보험 및 산업재해보상보험의 보험료징수 등에 관한 법률 시행령","제12조·제13조",[r"고용보험및산업재해보상보험의보험료징수등에관한법률시행령"]),
|
||||
("법률","고용노동부","고용보험법 시행령","",[r"고용보험법시행령"]),
|
||||
("법률","보건복지부","국민건강보험법","제73조 및 시행령 제44조",[r"국민건강보험법"]),
|
||||
("법률","보건복지부","국민연금법","제88조제3항",[r"국민연금법"]),
|
||||
("법률","보건복지부","노인장기요양보험법","제9조제1항",[r"노인장기요양보험법(?!시행)"]),
|
||||
("법률","보건복지부","노인장기요양보험법 시행령","제4조 장기요양보험료율",[r"노인장기요양보험법시행령"]),
|
||||
("법률","기획재정부","부가가치세법","제30조 세율",[r"부가가치세법"]),
|
||||
("법률","고용노동부","근로기준법","",[r"근로기준법"]),
|
||||
("법률","산업통상자원부","산업표준화법","KS 표준 근거",[r"산업표준화법"]),
|
||||
("법률","과학기술정보통신부","전자서명법","",[r"전자서명법"]),
|
||||
("법률","국토교통부","공동주택관리법","",[r"공동주택관리법"]),
|
||||
|
||||
("행정규칙","산림청","임도설치 및 관리 등에 관한 규정","약칭 임도규정 / 교본 인용: 산림청훈령 1426호(2019.10.10.)",[r"임도설치및관리등에관한규정", r"임도설치및관리에관한규정", r"임도규정", r"임도시설규정"]),
|
||||
("행정규칙","산림청","산림관리기반시설의 설계 및 시설기준","산림자원법 시행규칙 별표2",[r"산림관리기반시설의설계및시설기준"]),
|
||||
("행정규칙","산림청","산림관리기반시설의 범위 및 기준","산림자원법 시행규칙 별표1",[r"산림관리기반시설의범위및기준"]),
|
||||
("행정규칙","산림청","산림관리기반시설의 타당성평가 항목별 기준 및 방법","산림자원법 시행규칙 별표1의2",[r"산림관리기반시설의타당성평가"]),
|
||||
("행정규칙","산림청","임도 품셈 적용기준 / 임도표준품셈","",[r"임도품셈적용기준", r"임도표준품셈"]),
|
||||
("행정규칙","산림청","지방산림청과 자연휴양림관리소와의 자연휴양림업무 처리지침","",[r"자연휴양림업무처리지침"]),
|
||||
("행정규칙","산림청","중기운용관리예규 / 차량관리예규","",[r"중기운용관리예규", r"차량관리예규"]),
|
||||
("행정규칙","법제처","훈령ㆍ예규 등의 발령 및 관리에 관한 규정","고시 재검토기한 근거",[r"훈령·?예규등의발령및관리에관한규정"]),
|
||||
("행정규칙","행정안전부","국가지점번호판 규격 등 고시","",[r"국가지점번호판규격등"]),
|
||||
("행정규칙","행정안전부","국가지점번호 부여기준 및 방법","",[r"국가지점번호부여기준"]),
|
||||
("행정규칙","행정안전부","재난구호 및 재난복구 비용 부담기준 등에 관한 규정","",[r"재난구호및재난복구비용부담기준등에관한규정"]),
|
||||
("행정규칙","고용노동부","사업종류별 산재보험료율 고시","교본 인용: 고용노동부고시 2018-90호(2019년도분)",[r"사업종류별산재보험료율"]),
|
||||
("행정규칙","고용노동부","건설업 산업안전보건관리비 계상 및 사용기준","교본 인용: 고용노동부고시 2018-72호",[r"건설업산업안전(보건)?관리비계상및사용기준", r"산업안전보건관리비계상및사용기준"]),
|
||||
("행정규칙","국토교통부","(국토교통부) 사회보험의 보험료 적용기준","교본 인용: 국토교통부고시 2018-462호",[r"사회보험의보험료적용기준"]),
|
||||
("행정규칙","산업통상자원부","엔지니어링사업대가의 기준","관계 중앙행정기관장 고시 / 설계·감리비 산정",[r"엔지니어링사업대가의기준"]),
|
||||
("행정규칙","기획재정부","예산편성기준","건설부문 설계비 요율",[r"예산편성기준"]),
|
||||
("행정규칙","조달청","토목공사원가계산 제비율 적용기준","간접노무비·일반관리비·이윤 / 교본 인용: 2019.06.28. 발표분",[r"토목공사원가계산제비율"]),
|
||||
("행정규칙","기획재정부","입찰유의서(계약예규)","",[r"입찰에참가하고자하는자가유의"]),
|
||||
("행정규칙","국토교통부","수치지도 작성 작업규칙","",[r"수치지도작성작업규칙"]),
|
||||
("행정규칙","환경부","공사장의 비산분진 발생원 시설관리기준","",[r"공사장의비산분진발생원시설관리기준"]),
|
||||
("행정규칙","국토교통부","건설공사 감독자 업무 지침","임도규정 미규정 사항의 준용 근거",[r"건설공사감독자업무지침"]),
|
||||
|
||||
("표준시방서","산림청","임도시설공사 표준시방서","교본 15.4 표준시방서의 예시의 모체",[r"임도시설공사표준시방서"]),
|
||||
("표준시방서","국토교통부","콘크리트 표준시방서","",[r"콘크(과)?리트표준시방서"]),
|
||||
("표준시방서","국토교통부","토목공사 표준시방서","토목공사 일반 표준시방서 포함",[r"토목공사(일반)?표준시방서"]),
|
||||
("표준시방서","국토교통부","도로공사 표준시방서","도로공사 일반 표준시방서 포함",[r"도로공사(일반)?표준시방서"]),
|
||||
("표준시방서","국토교통부","건설공사 비탈면 표준시방서","",[r"건설공사비탈면표준시방서"]),
|
||||
("표준시방서","국토교통부","건설공사 표준시방서","",[r"건설공사표준시방서"]),
|
||||
|
||||
(
|
||||
"법률",
|
||||
"산림청",
|
||||
"산림자원의 조성 및 관리에 관한 법률",
|
||||
"약칭 산림자원법",
|
||||
[r"산림자원의조성및관리에관한법률(?!시행)", r"산림자원법(?!시행)"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"산림청",
|
||||
"산림자원의 조성 및 관리에 관한 법률 시행령",
|
||||
"",
|
||||
[r"산림자원의조성및관리에관한법률시행령", r"산림자원법시행령"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"산림청",
|
||||
"산림자원의 조성 및 관리에 관한 법률 시행규칙",
|
||||
"별표1 타당성평가·별표2 시설기준 포함",
|
||||
[r"산림자원의조성및관리에관한법률시행규칙", r"산림자원법시행규칙"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"산림청",
|
||||
"산림기술 진흥 및 관리에 관한 법률",
|
||||
"약칭 산림기술법",
|
||||
[r"산림기술진흥및관리에관한법률(?!시행)", r"산림기술법(?!시행)"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"산림청",
|
||||
"산림기술 진흥 및 관리에 관한 법률 시행령",
|
||||
"",
|
||||
[r"산림기술진흥및관리에관한법률시행령", r"산림기술법시행령"],
|
||||
),
|
||||
("법률", "산림청", "산림보호법", "", [r"산림보호법"]),
|
||||
("법률", "산림청", "산지관리법", "", [r"산지관리법"]),
|
||||
("법률", "환경부", "자연환경보전법", "시행령·시행규칙 포함", [r"자연환경보전법"]),
|
||||
("법률", "행정안전부", "자연재해대책법", "시행령·시행규칙 포함", [r"자연재해대책법"]),
|
||||
("법률", "환경부", "환경영향평가법", "시행령·시행규칙 포함", [r"환경영향평가법"]),
|
||||
(
|
||||
"법률",
|
||||
"산업통상자원부",
|
||||
"국가균형발전특별법",
|
||||
"시행령·시행규칙 포함",
|
||||
[r"국가균형발전특별법"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"기획재정부",
|
||||
"보조금 관리에 관한 법률",
|
||||
"시행령·시행규칙 포함",
|
||||
[r"보조금관리에관한법률", r"보조금에관한법률"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"기획재정부",
|
||||
"국가를 당사자로 하는 계약에 관한 법률",
|
||||
"약칭 국가계약법",
|
||||
[r"국가를당사자로하는계약에관한법률(?!시행)", r"국가계약법(?!시행|률)", r"국가계약법률"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"기획재정부",
|
||||
"국가를 당사자로 하는 계약에 관한 법률 시행령",
|
||||
"",
|
||||
[r"국가를당사자로하는계약에관한법률시행령", r"국가계약법시행령"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"행정안전부",
|
||||
"지방자치단체를 당사자로 하는 계약에 관한 법률",
|
||||
"약칭 지방계약법",
|
||||
[r"지방자치단체를당사자로하는계약에관한법률(?!시행)", r"지방계약법(?!시행)"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"행정안전부",
|
||||
"지방자치단체를 당사자로 하는 계약에 관한 법률 시행령",
|
||||
"",
|
||||
[r"지방자치단체를당사자로하는계약에관한법률시행령", r"지방계약법시행령"],
|
||||
),
|
||||
("법률", "국토교통부", "도로법", "", [r"(?<![가-힣])도로법"]),
|
||||
("법률", "행정안전부", "농어촌도로정비법", "", [r"농어촌도로정비법"]),
|
||||
(
|
||||
"법률",
|
||||
"국토교통부",
|
||||
"국토의 계획 및 이용에 관한 법률",
|
||||
"약칭 국토계획법",
|
||||
[r"국토의계획및이용에관한법률"],
|
||||
),
|
||||
("법률", "행정안전부", "도로명주소법", "", [r"도로명주소법(?!시행)"]),
|
||||
("법률", "행정안전부", "도로명주소법 시행령", "", [r"도로명주소법시행령"]),
|
||||
(
|
||||
"법률",
|
||||
"국토교통부",
|
||||
"측량ㆍ수로조사 및 지적에 관한 법률",
|
||||
"현 「공간정보의 구축 및 관리 등에 관한 법률」로 개정",
|
||||
[r"측량·?수로조사및지적에관한법률"],
|
||||
),
|
||||
("법률", "고용노동부", "산업안전보건법", "", [r"산업안전보건법"]),
|
||||
("법률", "고용노동부", "산업재해보상보험법", "", [r"산업재해보상보험법(?!의)"]),
|
||||
(
|
||||
"법률",
|
||||
"고용노동부",
|
||||
"고용보험 및 산업재해보상보험의 보험료징수 등에 관한 법률",
|
||||
"약칭 보험료징수법",
|
||||
[r"고용보험및산업재해보상보험의보험료징수등에관한법률(?!시행)"],
|
||||
),
|
||||
(
|
||||
"법률",
|
||||
"고용노동부",
|
||||
"고용보험 및 산업재해보상보험의 보험료징수 등에 관한 법률 시행령",
|
||||
"제12조·제13조",
|
||||
[r"고용보험및산업재해보상보험의보험료징수등에관한법률시행령"],
|
||||
),
|
||||
("법률", "고용노동부", "고용보험법 시행령", "", [r"고용보험법시행령"]),
|
||||
("법률", "보건복지부", "국민건강보험법", "제73조 및 시행령 제44조", [r"국민건강보험법"]),
|
||||
("법률", "보건복지부", "국민연금법", "제88조제3항", [r"국민연금법"]),
|
||||
("법률", "보건복지부", "노인장기요양보험법", "제9조제1항", [r"노인장기요양보험법(?!시행)"]),
|
||||
(
|
||||
"법률",
|
||||
"보건복지부",
|
||||
"노인장기요양보험법 시행령",
|
||||
"제4조 장기요양보험료율",
|
||||
[r"노인장기요양보험법시행령"],
|
||||
),
|
||||
("법률", "기획재정부", "부가가치세법", "제30조 세율", [r"부가가치세법"]),
|
||||
("법률", "고용노동부", "근로기준법", "", [r"근로기준법"]),
|
||||
("법률", "산업통상자원부", "산업표준화법", "KS 표준 근거", [r"산업표준화법"]),
|
||||
("법률", "과학기술정보통신부", "전자서명법", "", [r"전자서명법"]),
|
||||
("법률", "국토교통부", "공동주택관리법", "", [r"공동주택관리법"]),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"임도설치 및 관리 등에 관한 규정",
|
||||
"약칭 임도규정 / 교본 인용: 산림청훈령 1426호(2019.10.10.)",
|
||||
[r"임도설치및관리등에관한규정", r"임도설치및관리에관한규정", r"임도규정", r"임도시설규정"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"산림관리기반시설의 설계 및 시설기준",
|
||||
"산림자원법 시행규칙 별표2",
|
||||
[r"산림관리기반시설의설계및시설기준"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"산림관리기반시설의 범위 및 기준",
|
||||
"산림자원법 시행규칙 별표1",
|
||||
[r"산림관리기반시설의범위및기준"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"산림관리기반시설의 타당성평가 항목별 기준 및 방법",
|
||||
"산림자원법 시행규칙 별표1의2",
|
||||
[r"산림관리기반시설의타당성평가"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"임도 품셈 적용기준 / 임도표준품셈",
|
||||
"",
|
||||
[r"임도품셈적용기준", r"임도표준품셈"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"지방산림청과 자연휴양림관리소와의 자연휴양림업무 처리지침",
|
||||
"",
|
||||
[r"자연휴양림업무처리지침"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산림청",
|
||||
"중기운용관리예규 / 차량관리예규",
|
||||
"",
|
||||
[r"중기운용관리예규", r"차량관리예규"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"법제처",
|
||||
"훈령ㆍ예규 등의 발령 및 관리에 관한 규정",
|
||||
"고시 재검토기한 근거",
|
||||
[r"훈령·?예규등의발령및관리에관한규정"],
|
||||
),
|
||||
("행정규칙", "행정안전부", "국가지점번호판 규격 등 고시", "", [r"국가지점번호판규격등"]),
|
||||
("행정규칙", "행정안전부", "국가지점번호 부여기준 및 방법", "", [r"국가지점번호부여기준"]),
|
||||
(
|
||||
"행정규칙",
|
||||
"행정안전부",
|
||||
"재난구호 및 재난복구 비용 부담기준 등에 관한 규정",
|
||||
"",
|
||||
[r"재난구호및재난복구비용부담기준등에관한규정"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"고용노동부",
|
||||
"사업종류별 산재보험료율 고시",
|
||||
"교본 인용: 고용노동부고시 2018-90호(2019년도분)",
|
||||
[r"사업종류별산재보험료율"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"고용노동부",
|
||||
"건설업 산업안전보건관리비 계상 및 사용기준",
|
||||
"교본 인용: 고용노동부고시 2018-72호",
|
||||
[r"건설업산업안전(보건)?관리비계상및사용기준", r"산업안전보건관리비계상및사용기준"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"국토교통부",
|
||||
"(국토교통부) 사회보험의 보험료 적용기준",
|
||||
"교본 인용: 국토교통부고시 2018-462호",
|
||||
[r"사회보험의보험료적용기준"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"산업통상자원부",
|
||||
"엔지니어링사업대가의 기준",
|
||||
"관계 중앙행정기관장 고시 / 설계·감리비 산정",
|
||||
[r"엔지니어링사업대가의기준"],
|
||||
),
|
||||
("행정규칙", "기획재정부", "예산편성기준", "건설부문 설계비 요율", [r"예산편성기준"]),
|
||||
(
|
||||
"행정규칙",
|
||||
"조달청",
|
||||
"토목공사원가계산 제비율 적용기준",
|
||||
"간접노무비·일반관리비·이윤 / 교본 인용: 2019.06.28. 발표분",
|
||||
[r"토목공사원가계산제비율"],
|
||||
),
|
||||
("행정규칙", "기획재정부", "입찰유의서(계약예규)", "", [r"입찰에참가하고자하는자가유의"]),
|
||||
("행정규칙", "국토교통부", "수치지도 작성 작업규칙", "", [r"수치지도작성작업규칙"]),
|
||||
(
|
||||
"행정규칙",
|
||||
"환경부",
|
||||
"공사장의 비산분진 발생원 시설관리기준",
|
||||
"",
|
||||
[r"공사장의비산분진발생원시설관리기준"],
|
||||
),
|
||||
(
|
||||
"행정규칙",
|
||||
"국토교통부",
|
||||
"건설공사 감독자 업무 지침",
|
||||
"임도규정 미규정 사항의 준용 근거",
|
||||
[r"건설공사감독자업무지침"],
|
||||
),
|
||||
(
|
||||
"표준시방서",
|
||||
"산림청",
|
||||
"임도시설공사 표준시방서",
|
||||
"교본 15.4 표준시방서의 예시의 모체",
|
||||
[r"임도시설공사표준시방서"],
|
||||
),
|
||||
("표준시방서", "국토교통부", "콘크리트 표준시방서", "", [r"콘크(과)?리트표준시방서"]),
|
||||
(
|
||||
"표준시방서",
|
||||
"국토교통부",
|
||||
"토목공사 표준시방서",
|
||||
"토목공사 일반 표준시방서 포함",
|
||||
[r"토목공사(일반)?표준시방서"],
|
||||
),
|
||||
(
|
||||
"표준시방서",
|
||||
"국토교통부",
|
||||
"도로공사 표준시방서",
|
||||
"도로공사 일반 표준시방서 포함",
|
||||
[r"도로공사(일반)?표준시방서"],
|
||||
),
|
||||
("표준시방서", "국토교통부", "건설공사 비탈면 표준시방서", "", [r"건설공사비탈면표준시방서"]),
|
||||
("표준시방서", "국토교통부", "건설공사 표준시방서", "", [r"건설공사표준시방서"]),
|
||||
]
|
||||
|
||||
files = [p for p in sorted(BASE.rglob("*.md")) if not str(p.relative_to(BASE)).startswith("원문")]
|
||||
|
||||
# ── KS 규격은 코드 패턴으로 자동 전수 추출 ──
|
||||
KSPAT = re.compile(r"KS\s*([A-Z])\s*(\d{4})(.*)$")
|
||||
BADTOK = ("규정", "적합", "따른", "따라", "사용", "한다", "표준에", "부속서", "규격", "품질", "또는", "이상", "맞는", "준하")
|
||||
BADTOK = (
|
||||
"규정",
|
||||
"적합",
|
||||
"따른",
|
||||
"따라",
|
||||
"사용",
|
||||
"한다",
|
||||
"표준에",
|
||||
"부속서",
|
||||
"규격",
|
||||
"품질",
|
||||
"또는",
|
||||
"이상",
|
||||
"맞는",
|
||||
"준하",
|
||||
)
|
||||
ks_hits = defaultdict(lambda: OrderedDict())
|
||||
ks_title = {}
|
||||
for p in files:
|
||||
@@ -111,10 +345,10 @@ for p in files:
|
||||
code = f"KS {m.group(1)} {m.group(2)}"
|
||||
if i not in ks_hits[code].setdefault(rel, []):
|
||||
ks_hits[code][rel].append(i)
|
||||
rest = ln[m.end():].strip()
|
||||
rest = ln[m.end() :].strip()
|
||||
rest = re.split(r"\s*[|,]", rest)[0].strip()
|
||||
if rest.startswith("(") and ")" in rest[:60]:
|
||||
cand = rest[1:rest.find(")")].strip()
|
||||
cand = rest[1 : rest.find(")")].strip()
|
||||
else:
|
||||
t = re.match(r"^([가-힣][^|\n]{2,58})$", rest)
|
||||
if not t:
|
||||
@@ -142,6 +376,7 @@ for p in files:
|
||||
|
||||
NUM = re.compile(r"^([0-9][0-9\-]*)\.?\s")
|
||||
|
||||
|
||||
def fmtloc(d):
|
||||
out = []
|
||||
for rel, ii in d.items():
|
||||
@@ -150,32 +385,39 @@ def fmtloc(d):
|
||||
fno = NUM.match(fn).group(1)
|
||||
links = [f"[{cno}/{fno}:{n}](<임도기술교본/{rel}#L{n}>)" for n in ii[:5]]
|
||||
if len(ii) > 5:
|
||||
links.append(f"…(+{len(ii)-5})")
|
||||
links.append(f"…(+{len(ii) - 5})")
|
||||
out.append(" ".join(links))
|
||||
return "<br>".join(out)
|
||||
|
||||
|
||||
def loc(name):
|
||||
d = hits.get(name)
|
||||
return fmtloc(d) if d else "—"
|
||||
|
||||
|
||||
def cnt(d):
|
||||
return sum(len(v) for v in d.values())
|
||||
|
||||
CAT_TITLE = OrderedDict([
|
||||
("법률", "1. 법률·시행령·시행규칙"),
|
||||
("행정규칙", "2. 행정규칙 (훈령·예규·고시·지침)"),
|
||||
("표준시방서", "3. 표준시방서"),
|
||||
])
|
||||
|
||||
CAT_TITLE = OrderedDict(
|
||||
[
|
||||
("법률", "1. 법률·시행령·시행규칙"),
|
||||
("행정규칙", "2. 행정규칙 (훈령·예규·고시·지침)"),
|
||||
("표준시방서", "3. 표준시방서"),
|
||||
]
|
||||
)
|
||||
|
||||
SRC = json.load(open(SC / "srcmap.json", encoding="utf-8"))
|
||||
GOT = json.load(open(SC / "collected_map.json", encoding="utf-8"))
|
||||
KSMAP = json.load(open(SC / "ks_map.json", encoding="utf-8"))
|
||||
KCSC = json.load(open(SC / "kcsc_map.json", encoding="utf-8"))
|
||||
|
||||
|
||||
def ks_code_of(nm):
|
||||
m = re.match(r"^(KS [A-Z] \d{4})", nm)
|
||||
return m.group(1) if m else None
|
||||
|
||||
|
||||
def _entry_target(folder_rel):
|
||||
"""폴더의 _index.md(개별 리스트)로 연결. 없으면 현행_*.md, 그것도 없으면 폴더."""
|
||||
d = ROOT_DIR / folder_rel
|
||||
@@ -186,6 +428,7 @@ def _entry_target(folder_rel):
|
||||
return f"{folder_rel}/{cur[-1].name}"
|
||||
return f"{folder_rel}/"
|
||||
|
||||
|
||||
def linked(name):
|
||||
g = GOT.get(name)
|
||||
if g:
|
||||
@@ -196,13 +439,16 @@ def linked(name):
|
||||
return f"[{name}](<{_entry_target('표준시방서/' + HWP_ONLY[name])}>)"
|
||||
return name
|
||||
|
||||
|
||||
HWP_ONLY = {"임도시설공사 표준시방서": "임도시설공사 표준시방서"}
|
||||
|
||||
|
||||
def status(name):
|
||||
if GOT.get(name) or name in KCSC or name in HWP_ONLY:
|
||||
return "■"
|
||||
return "□"
|
||||
|
||||
|
||||
def org_cell(name, org):
|
||||
# 실제로 내려받아 확인된 출처만 표기. 미수집 건은 출처를 적지 않는다.
|
||||
if GOT.get(name):
|
||||
@@ -212,6 +458,7 @@ def org_cell(name, org):
|
||||
return f"{org}<br>국가건설기준센터"
|
||||
return org
|
||||
|
||||
|
||||
def got_note(name):
|
||||
g = GOT.get(name)
|
||||
if not g:
|
||||
@@ -236,7 +483,13 @@ def got_note(name):
|
||||
bits.append(f"KCS본문 {KCSC[name][1]}건")
|
||||
return " · ".join(bits)
|
||||
|
||||
DL = {'법률': '**다운로드**: [국가법령정보센터](https://www.law.go.kr) — 36건 전건 수집 완료.', '행정규칙': '**다운로드**: [국가법령정보센터](https://www.law.go.kr) — 21건 중 15건 수집 완료. 나머지 6건은 법령정보센터 검색 결과 없음(다른 확보처 미확인).', '표준시방서': '**다운로드**: 고시문 [국가법령정보센터](https://www.law.go.kr) + KCS 본문 [국가건설기준센터](https://kcsc.re.kr) OpenApi. KCS 4종(콘크리트·도로·공통·비탈면) 본문 83개 코드 수집. 임도시설공사(산림청) 미수집.'}
|
||||
|
||||
DL = {
|
||||
"법률": "**다운로드**: [국가법령정보센터](https://www.law.go.kr) — 36건 전건 수집 완료.",
|
||||
"행정규칙": "**다운로드**: [국가법령정보센터](https://www.law.go.kr) — 21건 중 15건 수집 완료. 나머지 6건은 법령정보센터 검색 결과 없음(다른 확보처 미확인).",
|
||||
"표준시방서": "**다운로드**: 고시문 [국가법령정보센터](https://www.law.go.kr) + KCS 본문 [국가건설기준센터](https://kcsc.re.kr) OpenApi. KCS 4종(콘크리트·도로·공통·비탈면) 본문 83개 코드 수집. 임도시설공사(산림청) 미수집.",
|
||||
}
|
||||
|
||||
|
||||
def dates(name):
|
||||
d = SRC.get(name)
|
||||
@@ -244,22 +497,27 @@ def dates(name):
|
||||
return "—", "—"
|
||||
return (d.get("시행") or "—"), (d.get("개정") or "—")
|
||||
|
||||
|
||||
def note_of(name, base):
|
||||
d = SRC.get(name) or {}
|
||||
parts = [x for x in (base, d.get("추가", "")) if x]
|
||||
return " / ".join(parts)
|
||||
|
||||
|
||||
import unicodedata
|
||||
|
||||
LINKRE = re.compile(r"\[([^\]]*)\]\(<[^>]*>\)")
|
||||
|
||||
|
||||
def _w(t):
|
||||
return sum(2 if unicodedata.east_asian_width(c) in "WF" else 1 for c in t)
|
||||
|
||||
|
||||
def dw(t):
|
||||
t = LINKRE.sub(lambda m: m.group(1), t)
|
||||
return max(_w(x) for x in t.split("<br>")) if t else 0
|
||||
|
||||
|
||||
def pad(t, w, align="l"):
|
||||
n = max(0, w - dw(t))
|
||||
if align == "r":
|
||||
@@ -268,9 +526,19 @@ def pad(t, w, align="l"):
|
||||
return " " * (n // 2) + t + " " * (n - n // 2)
|
||||
return t + " " * n
|
||||
|
||||
COLS = [("No.", 3, "r"), ("명칭", 56, "l"), ("시행일", 10, "c"), ("개정일", 10, "c"),
|
||||
("소관", 20, "l"), ("비고", 56, "l"), ("인용", 4, "r"), ("확보", 4, "c"),
|
||||
("교본 내 위치", 0, "l")]
|
||||
|
||||
COLS = [
|
||||
("No.", 3, "r"),
|
||||
("명칭", 56, "l"),
|
||||
("시행일", 10, "c"),
|
||||
("개정일", 10, "c"),
|
||||
("소관", 20, "l"),
|
||||
("비고", 56, "l"),
|
||||
("인용", 4, "r"),
|
||||
("확보", 4, "c"),
|
||||
("교본 내 위치", 0, "l"),
|
||||
]
|
||||
|
||||
|
||||
def row(vals):
|
||||
out = []
|
||||
@@ -279,24 +547,42 @@ def row(vals):
|
||||
out.append(pad(v, w, a) if w else v)
|
||||
return "| " + " | ".join(out) + " |"
|
||||
|
||||
|
||||
HDR = row([c[0] for c in COLS])
|
||||
SEP = "|" + "|".join(
|
||||
(":" + "-" * max(1, w) + ":") if a == "c" else
|
||||
("-" * max(1, w + 1) + ":") if a == "r" else
|
||||
("-" * max(1, w + 2)) if w else "---"
|
||||
for h, w, a in COLS) + "|"
|
||||
SEP = (
|
||||
"|"
|
||||
+ "|".join(
|
||||
(":" + "-" * max(1, w) + ":")
|
||||
if a == "c"
|
||||
else ("-" * max(1, w + 1) + ":")
|
||||
if a == "r"
|
||||
else ("-" * max(1, w + 2))
|
||||
if w
|
||||
else "---"
|
||||
for h, w, a in COLS
|
||||
)
|
||||
+ "|"
|
||||
)
|
||||
|
||||
L = []
|
||||
L.append("# 0. 참조 법령·기준 목록")
|
||||
L.append("")
|
||||
L.append(f"> 출처: `임도기술교본/` 전 문서({len(files)}개 md, `원문/` 제외) 전수 스캔")
|
||||
L.append("> 위치 표기: `폴더no/파일no:줄번호` 링크 — 클릭 시 교본 해당 줄로 이동 (파일당 5개까지, 초과분은 `…(+n)`)")
|
||||
L.append(
|
||||
"> 위치 표기: `폴더no/파일no:줄번호` 링크 — 클릭 시 교본 해당 줄로 이동 (파일당 5개까지, 초과분은 `…(+n)`)"
|
||||
)
|
||||
L.append("> `인용` 열은 교본 내 언급 횟수(줄 단위 집계)")
|
||||
L.append("> `소관` 열 둘째 줄은 실제로 내려받은 출처 (미수집 건은 출처 미표기 — 확인되지 않음)")
|
||||
L.append("> `확보` ■ = 원문 수집 완료 (명칭 클릭 시 폴더로 이동) / ◐ = 메타데이터만(원문 DRM) / □ = 미수집")
|
||||
L.append("> `시행일`/`개정일`은 **현행본** 기준 (국가법령정보센터 조회, 2026-08 확인). 교본이 인용한 2019년 판본과 다르면 `비고`에 표기")
|
||||
L.append(
|
||||
"> `확보` ■ = 원문 수집 완료 (명칭 클릭 시 폴더로 이동) / ◐ = 메타데이터만(원문 DRM) / □ = 미수집"
|
||||
)
|
||||
L.append(
|
||||
"> `시행일`/`개정일`은 **현행본** 기준 (국가법령정보센터 조회, 2026-08 확인). 교본이 인용한 2019년 판본과 다르면 `비고`에 표기"
|
||||
)
|
||||
L.append("> `확보` 대상은 현행본. 교본 대조가 필요하면 연혁본을 함께 받을 것")
|
||||
L.append("> 4개 표 모두 동일한 9열 구조 · 동일 열 폭 (`No. / 명칭 / 시행일 / 개정일 / 소관 / 비고 / 인용 / 확보 / 교본 내 위치`)")
|
||||
L.append(
|
||||
"> 4개 표 모두 동일한 9열 구조 · 동일 열 폭 (`No. / 명칭 / 시행일 / 개정일 / 소관 / 비고 / 인용 / 확보 / 교본 내 위치`)"
|
||||
)
|
||||
L.append("")
|
||||
total = 0
|
||||
for cat, title in CAT_TITLE.items():
|
||||
@@ -311,18 +597,38 @@ for cat, title in CAT_TITLE.items():
|
||||
total += 1
|
||||
ef, am = dates(name)
|
||||
nt = " / ".join(x for x in (note_of(name, note), got_note(name)) if x)
|
||||
L.append(row([k, linked(name), ef, am, org_cell(name, org), nt, cnt(hits[name]), status(name), loc(name)]))
|
||||
L.append(
|
||||
row(
|
||||
[
|
||||
k,
|
||||
linked(name),
|
||||
ef,
|
||||
am,
|
||||
org_cell(name, org),
|
||||
nt,
|
||||
cnt(hits[name]),
|
||||
status(name),
|
||||
loc(name),
|
||||
]
|
||||
)
|
||||
)
|
||||
L.append("")
|
||||
|
||||
n_ks_active = sum(1 for c in ks_hits if KSMAP.get(c, {}).get("상태") != "폐지")
|
||||
n_ks_repeal = sum(1 for c in ks_hits if KSMAP.get(c, {}).get("상태") == "폐지")
|
||||
L.append("## 4. 한국산업표준 (KS)")
|
||||
L.append("")
|
||||
L.append(f"**메타데이터**: [e나라 표준인증](https://www.standard.go.kr) — 45건 전건 조회 완료(표준명·제·개정일·적용범위·개정이력). 명칭 클릭 시 `KS/` 폴더로 이동.")
|
||||
L.append(
|
||||
f"**메타데이터**: [e나라 표준인증](https://www.standard.go.kr) — 45건 전건 조회 완료(표준명·제·개정일·적용범위·개정이력). 명칭 클릭 시 `KS/` 폴더로 이동."
|
||||
)
|
||||
L.append("**원문**: MarkAny DRM 뷰어 열람 전용이라 파일 다운로드 경로 없음. **전건 원문 미수집.**")
|
||||
L.append(f"**상태**: 현행 {n_ks_active} · 폐지 {n_ks_repeal} (교본 인용 규격 중 폐지분은 교본 갱신 시 반영 필요).")
|
||||
L.append(
|
||||
f"**상태**: 현행 {n_ks_active} · 폐지 {n_ks_repeal} (교본 인용 규격 중 폐지분은 교본 갱신 시 반영 필요)."
|
||||
)
|
||||
L.append("")
|
||||
L.append("> `확보` ◐ = 메타데이터만 수집(원문 DRM). `시행일`=제정일 / `개정일`=최종개정확인일. 규격명은 e나라 공식 표준명.")
|
||||
L.append(
|
||||
"> `확보` ◐ = 메타데이터만 수집(원문 DRM). `시행일`=제정일 / `개정일`=최종개정확인일. 규격명은 e나라 공식 표준명."
|
||||
)
|
||||
L.append("")
|
||||
L.append(HDR)
|
||||
L.append(SEP)
|
||||
@@ -337,7 +643,21 @@ for k, code in enumerate(sorted(ks_hits, key=lambda c: (c.split()[1], c.split()[
|
||||
st = km.get("상태", "")
|
||||
note = "**폐지**" if st == "폐지" else ("" if t else "교본은 규격번호만 인용")
|
||||
mark = "◐" if km else "□"
|
||||
L.append(row([k, linked_nm, ef, am, "산업통상자원부", note, cnt(ks_hits[code]), mark, fmtloc(ks_hits[code])]))
|
||||
L.append(
|
||||
row(
|
||||
[
|
||||
k,
|
||||
linked_nm,
|
||||
ef,
|
||||
am,
|
||||
"산업통상자원부",
|
||||
note,
|
||||
cnt(ks_hits[code]),
|
||||
mark,
|
||||
fmtloc(ks_hits[code]),
|
||||
]
|
||||
)
|
||||
)
|
||||
L.append("")
|
||||
|
||||
miss = [e[2] for e in ENTRIES if e[2] not in hits]
|
||||
@@ -373,7 +693,10 @@ for cat, d in CATDIR:
|
||||
if f.stem not in ("_meta", "CHANGELOG"):
|
||||
n_law += 1
|
||||
idx_link = f"[📑 개별 목록](<{rel}/_index.md>) " if (folder / "_index.md").exists() else ""
|
||||
L.append(f"- **{folder.name}** — {idx_link}" + (" · ".join(docs) if docs else "_(원문 폴더 없음, 하위 자료만)_"))
|
||||
L.append(
|
||||
f"- **{folder.name}** — {idx_link}"
|
||||
+ (" · ".join(docs) if docs else "_(원문 폴더 없음, 하위 자료만)_")
|
||||
)
|
||||
byl = sorted((folder / "별표").glob("*.md")) if (folder / "별표").is_dir() else []
|
||||
if byl:
|
||||
n_byl += len(byl)
|
||||
@@ -386,8 +709,10 @@ for cat, d in CATDIR:
|
||||
att = sorted((folder / "첨부").glob("*.md")) if (folder / "첨부").is_dir() else []
|
||||
if att:
|
||||
n_att += len(att)
|
||||
L.append(f" - 첨부 본문 {len(att)}건: " + " · ".join(
|
||||
f"[{f.stem[:24]}](<{rel}/첨부/{f.name}>)" for f in att))
|
||||
L.append(
|
||||
f" - 첨부 본문 {len(att)}건: "
|
||||
+ " · ".join(f"[{f.stem[:24]}](<{rel}/첨부/{f.name}>)" for f in att)
|
||||
)
|
||||
# 첨부 zip 해제 폴더([압축] …)
|
||||
zdirs = sorted((folder / "첨부").glob("[[]압축[]]*")) if (folder / "첨부").is_dir() else []
|
||||
for zd in zdirs:
|
||||
@@ -400,9 +725,11 @@ for cat, d in CATDIR:
|
||||
kcs = [f for f in kcs if f.stem != "_목록"]
|
||||
if kcs:
|
||||
n_kcs += len(kcs)
|
||||
L.append(f" - KCS 본문 {len(kcs)}건: [_목록](<{rel}/KCS/_목록.md>) · " + " · ".join(
|
||||
f"[{f.stem.split('_')[0]}](<{rel}/KCS/{f.name}>)" for f in kcs[:8])
|
||||
+ (f" …외 {len(kcs)-8}건" if len(kcs) > 8 else ""))
|
||||
L.append(
|
||||
f" - KCS 본문 {len(kcs)}건: [_목록](<{rel}/KCS/_목록.md>) · "
|
||||
+ " · ".join(f"[{f.stem.split('_')[0]}](<{rel}/KCS/{f.name}>)" for f in kcs[:8])
|
||||
+ (f" …외 {len(kcs) - 8}건" if len(kcs) > 8 else "")
|
||||
)
|
||||
L.append("")
|
||||
L.append("### KS (메타데이터)")
|
||||
L.append("")
|
||||
@@ -410,14 +737,18 @@ ksdir = sorted((ORIG / "KS").glob("*.md")) if (ORIG / "KS").is_dir() else []
|
||||
if ksdir:
|
||||
L.append("- " + " · ".join(f"[{f.stem}](<KS/{f.name}>)" for f in ksdir))
|
||||
L.append("")
|
||||
L.append(f"> 법령 본문 md {n_law} · 별표/서식 md {n_byl} · 첨부 본문 md {n_att} · 압축해제 md {n_zip[0]} · KCS 본문 md {n_kcs} · KS 메타 md {len(ksdir)}")
|
||||
L.append(
|
||||
f"> 법령 본문 md {n_law} · 별표/서식 md {n_byl} · 첨부 본문 md {n_att} · 압축해제 md {n_zip[0]} · KCS 본문 md {n_kcs} · KS 메타 md {len(ksdir)}"
|
||||
)
|
||||
L.append("")
|
||||
|
||||
L.append("---")
|
||||
L.append("")
|
||||
L.append(f"- 총 {total}건 (법률 36 / 행정규칙 21 / 표준시방서 6 / KS 45)")
|
||||
L.append(f"- 스캔 대상: `임도기술교본/` 하위 md {len(files)}개 (`원문/` 제외)")
|
||||
L.append("- 스캔 기준: 정식명칭 및 약칭(산림자원법, 국가계약법, 지방계약법, 임도규정 등) 정규식 매칭, 공백·`ㆍ`·`<br>` 정규화 후 대조")
|
||||
L.append(
|
||||
"- 스캔 기준: 정식명칭 및 약칭(산림자원법, 국가계약법, 지방계약법, 임도규정 등) 정규식 매칭, 공백·`ㆍ`·`<br>` 정규화 후 대조"
|
||||
)
|
||||
L.append("- 교본 기준 시점: 2019년 (고시·요율은 당시 개정본 기준. 원문 수집 시 현행본 병기 필요)")
|
||||
L.append("- 시행일/개정일 조회: 국가법령정보센터 Open API (`law.go.kr/DRF`), 2026-08 기준 현행본")
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
python patch_law_md.py --dry # 변경량만 출력
|
||||
python patch_law_md.py # 저장
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import xml.etree.ElementTree as ET
|
||||
@@ -22,7 +23,9 @@ from collect_law import ROOT, T, clean
|
||||
|
||||
분류목록 = ("법률", "행정규칙")
|
||||
STEM = re.compile(r"^(현행|교본시점)_\d{8}$")
|
||||
부칙헤더 = re.compile(r"부칙\s*<[^>]*>")
|
||||
# 괄호형 타법개정 헤더('부칙(통계법 시행령) <제20331호,…>')도 매칭 — 괄호형을
|
||||
# 놓치면 키가 비어 매 실행마다 같은 부칙을 재추가한다 (2026-08-15 멱등성 결함 정정).
|
||||
부칙헤더 = re.compile(r"부칙\s*(?:\([^)\n]*\))?\s*<[^>]*>")
|
||||
|
||||
|
||||
def 부칙키(text):
|
||||
@@ -64,7 +67,7 @@ def patch(md_path, xml_path):
|
||||
# 앵커별로 묶어 한 번에 삽입
|
||||
묶음 = {}
|
||||
for 앵커, mv in 작업:
|
||||
if not mv or mv in text: # 이미 있으면 건너뜀
|
||||
if not mv or mv in text: # 이미 있으면 건너뜀
|
||||
continue
|
||||
if not 앵커:
|
||||
앵커실패 += 1
|
||||
@@ -78,22 +81,28 @@ def patch(md_path, xml_path):
|
||||
if idx is None:
|
||||
앵커실패 += len(목들)
|
||||
continue
|
||||
lines[idx + 1:idx + 1] = [f" {mv}" for mv in 목들]
|
||||
lines[idx + 1 : idx + 1] = [f" {mv}" for mv in 목들]
|
||||
목추가 += len(목들)
|
||||
|
||||
# ── 2) 부칙 보충 ──
|
||||
부칙 = root.find("부칙")
|
||||
if 부칙 is not None:
|
||||
units = 부칙.findall("부칙단위")
|
||||
# 법령(law)=부칙단위 래퍼 / 행정규칙(admrul)=부칙내용 직속 (2026-08-15 결함 정정
|
||||
# — 직속형 미처리로 행정규칙 md 22건에 부칙이 통째로 빠져 있었다)
|
||||
units = 부칙.findall("부칙단위") or 부칙.findall("부칙내용")
|
||||
본문 = "\n".join(lines)
|
||||
기존키 = {re.sub(r"\s+", "", m) for m in 부칙헤더.findall(본문)}
|
||||
누락 = [b for b in units if (부칙키(T(b, "부칙내용")) or "") not in 기존키]
|
||||
|
||||
def 본문텍스트(b):
|
||||
return T(b, "부칙내용") if b.tag == "부칙단위" else "".join(b.itertext())
|
||||
|
||||
누락 = [b for b in units if (부칙키(본문텍스트(b)) or "") not in 기존키]
|
||||
if 누락:
|
||||
if "## 부칙" not in 본문:
|
||||
lines += ["", "## 부칙", ""]
|
||||
add = []
|
||||
for b in 누락:
|
||||
body = clean(T(b, "부칙내용"))
|
||||
body = clean(본문텍스트(b))
|
||||
if not body.strip():
|
||||
continue
|
||||
for ln in body.split("\n"):
|
||||
@@ -130,14 +139,18 @@ def main(dry=False):
|
||||
md_path.write_text(orig, encoding="utf-8")
|
||||
if b or m or f:
|
||||
파일 += 1
|
||||
print(f" {분류}/{folder.name}/{md_path.name}: 부칙+{b} 목+{m}"
|
||||
+ (f" (앵커실패 {f})" if f else ""))
|
||||
print(
|
||||
f" {분류}/{folder.name}/{md_path.name}: 부칙+{b} 목+{m}"
|
||||
+ (f" (앵커실패 {f})" if f else "")
|
||||
)
|
||||
합계[0] += b
|
||||
합계[1] += m
|
||||
합계[2] += f
|
||||
print(f"\n파일 {파일}건 / 부칙 +{합계[0]} · 목 +{합계[1]}"
|
||||
+ (f" · 앵커실패 {합계[2]}" if 합계[2] else "")
|
||||
+ (" (dry-run — 원복함)" if dry else " 저장 완료"))
|
||||
print(
|
||||
f"\n파일 {파일}건 / 부칙 +{합계[0]} · 목 +{합계[1]}"
|
||||
+ (f" · 앵커실패 {합계[2]}" if 합계[2] else "")
|
||||
+ (" (dry-run — 원복함)" if dry else " 저장 완료")
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
# -*- 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()
|
||||
Reference in New Issue
Block a user