Files
Aislo/resources/knowledge/original/_pipeline/gen_md.py
T

785 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""참조 법령/기준 목록 md — §5 '원문 md 색인' 절만 갱신 (AUTO:INDEX 마커 스플라이스).
⚠ 잔여 코드 안내 (2026-08-15): 아래에는 구 버전(문서 전체 생성)의 §1~§4 등록표·KS 표
조립 코드가 그대로 남아 실행되지만, 저장 단계에서 §5 블록만 추출하므로 산출물에는
쓰이지 않는다 (등록표는 04 문서에서 수동 관리로 전환됨). 기능 문제 없음 — 정리는 선택.
"""
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)
# API 키: 커밋 금지 파일(law/.secrets.local.md) 또는 환경변수에서 읽는다.
def _load_key(name):
v = _os.environ.get(name)
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)
return ""
from collections import defaultdict, OrderedDict
BASE = Path(str(ROOT_DIR / "임도기술교본"))
# 운영본 위치 = 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"건설공사표준시방서"]),
]
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 = (
"규정",
"적합",
"따른",
"따라",
"사용",
"한다",
"표준에",
"부속서",
"규격",
"품질",
"또는",
"이상",
"맞는",
"준하",
)
ks_hits = defaultdict(lambda: OrderedDict())
ks_title = {}
for p in files:
rel = str(p.relative_to(BASE)).replace("\\", "/")
for i, ln in enumerate(p.read_text(encoding="utf-8").split("\n"), 1):
for m in re.finditer(r"KS\s*([A-Z])\s*(\d{4})", ln):
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 = re.split(r"\s*[|,]", rest)[0].strip()
if rest.startswith("(") and ")" in rest[:60]:
cand = rest[1 : rest.find(")")].strip()
else:
t = re.match(r"^([가-힣][^|\n]{2,58})$", rest)
if not t:
continue
cand = t.group(1).rstrip(" .").rstrip("(")
if not cand or cand[0] in "에의를을은는과와도만":
continue
if any(b in cand for b in BADTOK):
continue
if cand.count("(") > cand.count(")"):
cand += ")"
if len(cand) > len(ks_title.get(code, "")):
ks_title[code] = cand
hits = defaultdict(lambda: OrderedDict())
for p in files:
rel = str(p.relative_to(BASE)).replace("\\", "/")
lines = p.read_text(encoding="utf-8").split("\n")
for i, ln in enumerate(lines, 1):
n = norm(re.sub(r"^#{1,6}\s+", "", ln))
if not n:
continue
for cat, org, name, note, pats in ENTRIES:
if any(re.search(pt, n) for pt in pats):
hits[name].setdefault(rel, []).append(i)
NUM = re.compile(r"^([0-9][0-9\-]*)\.?\s")
def fmtloc(d):
out = []
for rel, ii in d.items():
chap, fn = rel.split("/", 1)
cno = NUM.match(chap).group(1)
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})")
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. 표준시방서"),
]
)
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
if (d / "_index.md").exists():
return f"{folder_rel}/_index.md"
cur = sorted(d.glob("현행_*.md"))
if cur:
return f"{folder_rel}/{cur[-1].name}"
return f"{folder_rel}/"
def linked(name):
g = GOT.get(name)
if g:
return f"[{name}](<{_entry_target(g['path'])}>)"
if name in KCSC:
return f"[{name}](<{_entry_target('표준시방서/' + KCSC[name][0])}>)"
if name in HWP_ONLY:
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):
src = "국가법령정보센터" + ("·국가건설기준센터" if name in KCSC else "")
return f"{org}<br>{src}"
if name in KCSC:
return f"{org}<br>국가건설기준센터"
return org
def got_note(name):
g = GOT.get(name)
if not g:
if name in KCSC:
return f"KCS본문 {KCSC[name][1]}건 (국가건설기준센터)"
if name in HWP_ONLY:
return "원본 HWP(2016.12) 확보 · 본문은 교본 15.4와 동일"
return ""
if g.get("별표참조"):
return f"시행규칙 {g['별표참조']} PDF로 확보"
v = [x.split(":")[0] for x in g.get("판본", [])]
bits = []
if "교본시점" in v:
bits.append("현행+교본시점")
else:
bits.append("현행만")
if g.get("별표"):
bits.append(f"별표{g['별표']}")
if g.get("개정"):
bits.append(f"교본후 개정{g['개정']}회")
if name in KCSC:
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개 코드 수집. 임도시설공사(산림청) 미수집.",
}
def dates(name):
d = SRC.get(name)
if not d:
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":
return " " * n + t
if align == "c":
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"),
]
def row(vals):
out = []
for (h, w, a), v in zip(COLS, vals):
v = str(v)
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
)
+ "|"
)
L = []
L.append("# 0. 참조 법령·기준 목록")
L.append("")
L.append(f"> 출처: `임도기술교본/` 전 문서({len(files)}개 md, `원문/` 제외) 전수 스캔")
L.append(
"> 위치 표기: `폴더no/파일no:줄번호` 링크 — 클릭 시 교본 해당 줄로 이동 (파일당 5개까지, 초과분은 `…(+n)`)"
)
L.append("> `인용` 열은 교본 내 언급 횟수(줄 단위 집계)")
L.append("> `소관` 열 둘째 줄은 실제로 내려받은 출처 (미수집 건은 출처 미표기 — 확인되지 않음)")
L.append(
"> `확보` ■ = 원문 수집 완료 (명칭 클릭 시 폴더로 이동) / ◐ = 메타데이터만(원문 DRM) / □ = 미수집"
)
L.append(
"> `시행일`/`개정일`은 **현행본** 기준 (국가법령정보센터 조회, 2026-08 확인). 교본이 인용한 2019년 판본과 다르면 `비고`에 표기"
)
L.append("> `확보` 대상은 현행본. 교본 대조가 필요하면 연혁본을 함께 받을 것")
L.append(
"> 4개 표 모두 동일한 9열 구조 · 동일 열 폭 (`No. / 명칭 / 시행일 / 개정일 / 소관 / 비고 / 인용 / 확보 / 교본 내 위치`)"
)
L.append("")
total = 0
for cat, title in CAT_TITLE.items():
rows = [e for e in ENTRIES if e[0] == cat and e[2] in hits]
L.append(f"## {title}")
L.append("")
L.append(DL[cat])
L.append("")
L.append(HDR)
L.append(SEP)
for k, (c, org, name, note, pats) in enumerate(rows, 1):
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("")
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("**원문**: MarkAny DRM 뷰어 열람 전용이라 파일 다운로드 경로 없음. **전건 원문 미수집.**")
L.append(
f"**상태**: 현행 {n_ks_active} · 폐지 {n_ks_repeal} (교본 인용 규격 중 폐지분은 교본 갱신 시 반영 필요)."
)
L.append("")
L.append(
"> `확보` ◐ = 메타데이터만 수집(원문 DRM). `시행일`=제정일 / `개정일`=최종개정확인일. 규격명은 e나라 공식 표준명."
)
L.append("")
L.append(HDR)
L.append(SEP)
for k, code in enumerate(sorted(ks_hits, key=lambda c: (c.split()[1], c.split()[2])), 1):
total += 1
km = KSMAP.get(code, {})
t = km.get("표준명") or ks_title.get(code, "")
nm = f"{code} {t}".strip()
linked_nm = f"[{nm}](<KS/{code}.md>)" if km else nm
ef = re.sub(r"-", ".", km.get("제정일", "")) or "—"
am = re.sub(r"-", ".", km.get("최종개정확인일", "")) or "—"
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("")
miss = [e[2] for e in ENTRIES if e[2] not in hits]
if miss:
L.append("## 미확인 (스캔 무매칭)")
L.append("")
for m in miss:
L.append(f"- {m}")
L.append("")
# ── 5. 원문 md 색인 (법령 본문 + 별표 전체) ──
# ORIG = 원문 코퍼스 루트(…/original). ⚠ OUTMD.parent 사용 금지 — OUTMD가 knowledge
# 루트로 이동(2026-08-15)한 뒤 OUTMD.parent는 원문 폴더가 아니다 (빈 색인 생성 버그 원인).
ORIG = ROOT_DIR
CATDIR = [("법률", "법률"), ("행정규칙", "행정규칙"), ("표준시방서", "표준시방서")]
L.append("## 5. 원문 md 색인")
L.append("")
L.append("> 수집한 원문 전체의 마크다운 변환본. 법령 본문(현행·교본시점) + 별표/서식 전건.")
L.append("> 별표 md는 같은 폴더의 PDF를 변환한 것으로, 표는 마크다운 표로 복원됨.")
L.append("")
n_law = n_byl = n_att = n_kcs = 0
n_zip = [0]
for cat, d in CATDIR:
base = ORIG / d
if not base.is_dir():
continue
L.append(f"### {cat}")
L.append("")
for folder in sorted([x for x in base.iterdir() if x.is_dir()], key=lambda x: x.name):
rel = f"original/{d}/{folder.name}" # 링크 기준 = knowledge 루트(OUTMD 위치)
docs = []
for f in sorted(folder.glob("*.md")):
label = {"_meta": "메타", "CHANGELOG": "변경이력"}.get(f.stem, f.stem.replace("_", " "))
docs.append(f"[{label}](<{rel}/{f.name}>)")
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 "_(원문 폴더 없음, 하위 자료만)_")
)
byl = sorted((folder / "별표").glob("*.md")) if (folder / "별표").is_dir() else []
if byl:
n_byl += len(byl)
links = []
for f in byl:
nm = f.stem
num, _, rest = nm.partition("_")
links.append(f"[{num}](<{rel}/별표/{f.name}>)")
L.append(f" - 별표·서식 {len(byl)}건: " + " · ".join(links))
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)
)
# 첨부 zip 해제 폴더([압축] …)
zdirs = sorted((folder / "첨부").glob("[[]압축[]]*")) if (folder / "첨부").is_dir() else []
for zd in zdirs:
zmds = [p for p in zd.rglob("*.md") if p.name != "_목록.md"]
n_zip[0] += len(zmds)
idx = zd / "_목록.md"
link = f"[_목록](<{rel}/첨부/{zd.name}/_목록.md>)" if idx.exists() else zd.name
L.append(f" - 압축해제 「{zd.name[4:].strip()}{len(zmds)}건: {link}")
kcs = sorted((folder / "KCS").glob("*.md")) if (folder / "KCS").is_dir() else []
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("")
L.append("### KS (메타데이터)")
L.append("")
ksdir = sorted((ORIG / "KS").glob("*.md")) if (ORIG / "KS").is_dir() else []
if ksdir:
L.append("- " + " · ".join(f"[{f.stem}](<original/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("")
L.append("---")
L.append("")
L.append(f"- 총 {total}건 (법률 36 / 행정규칙 21 / 표준시방서 6 / KS 45)")
L.append(f"- 스캔 대상: `임도기술교본/` 하위 md {len(files)}개 (`원문/` 제외)")
L.append(
"- 스캔 기준: 정식명칭 및 약칭(산림자원법, 국가계약법, 지방계약법, 임도규정 등) 정규식 매칭, 공백·`ㆍ`·`<br>` 정규화 후 대조"
)
L.append("- 교본 기준 시점: 2019년 (고시·요율은 당시 개정본 기준. 원문 수집 시 현행본 병기 필요)")
L.append("- 시행일/개정일 조회: 국가법령정보센터 Open API (`law.go.kr/DRF`), 2026-08 기준 현행본")
# ── 쓰기: 전체 덮어쓰기 금지 — '원문 md 색인' 절만 마커 사이에 스플라이스 ──
# 배경(2026-08-15): 04 목록의 등록표(§1~§4)는 수기 추가 행(사방 고시·조달수수료 등)이
# 축적된 수동 관리 문서가 됐다. 전체 재생성하면 수기 행이 소실되므로, 이 스크립트는
# 자동화 가치가 큰 '원문 md 색인' 절만 AUTO 마커 블록 안에서 재생성한다.
MARK_BEGIN = "<!-- AUTO:INDEX:BEGIN — gen_md.py가 재생성하는 블록. 수동 편집 금지 -->"
MARK_END = "<!-- AUTO:INDEX:END -->"
idx_start = L.index("## 5. 원문 md 색인")
idx_end = next(i for i, ln in enumerate(L) if ln.startswith("> 법령 본문 md "))
auto_block = "\n".join([MARK_BEGIN] + L[idx_start : idx_end + 1] + [MARK_END])
if not OUTMD.exists():
raise SystemExit(f"오류: {OUTMD} 없음 — 이 스크립트는 색인 절만 갱신하며 신규 생성하지 않는다.")
cur = OUTMD.read_text(encoding="utf-8")
if MARK_BEGIN not in cur or MARK_END not in cur:
raise SystemExit(
"오류: AUTO:INDEX 마커 없음 — 04 문서에 마커 블록을 먼저 넣어야 한다 "
"('## 5. 원문 md 색인' 절을 MARK_BEGIN/END로 감싼다)."
)
pre = cur.split(MARK_BEGIN)[0]
post = cur.split(MARK_END, 1)[1]
OUTMD.write_text(pre + auto_block + post, encoding="utf-8")
print(f"색인 절 갱신 완료: {OUTMD.name} (등록표 §1~§4·큐레이션 절은 수동 관리 — 보존됨)")