785 lines
30 KiB
Python
785 lines
30 KiB
Python
# -*- 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·큐레이션 절은 수동 관리 — 보존됨)")
|