knowledge (구 Aislo-law 독립 저장소 → resources/knowledge 이관, 저장소 폐지): - 법령·행정규칙·표준시방서·교본 원문 + 기술문서 55건 + 실무 분석·종합비교 - 루트 지침 체계: README(지도)·00_운영지침·01_수집지침·02_분석지침· 03_미결_및_확인사항(교본 충돌 리스트 포함)·04_참조_법령기준_목록 - 기술문서 55건 원문 전수 검증 완료 (사방 설계홍수량 법정 기준 등 반영) - 정리: CAD·오피스 잔재 142건, 중복 zip 7건(413MB), 빈 폴더 30개 제거 resources 그룹 재편 (이름순 그룹핑): - app_branding(구 prog_icon.jpg)·app_policies(구 legal)· data_global_contours(구 grobal_contours)·data_rainfall_idf_cache(구 wamis_contours)· template_2dDrawing(구 dwg_analysis/templete — 오타 교정, 상수·경로 동기화) - dwg_analysis(분석 완료 1.8GB)·templates(빈 폴더)·templete_calc_cost.xlsx 삭제 - 참조 코드 5파일 경로 수정 + 프론트 재빌드 (구 경로 잔존 0) - .gitignore: resources 추적 전환, national_contours.gpkg(22GB) 영구 제외 - .env: knowledge 수집용 API 정보 주석 통합 (KCSC·법령센터·조달청 제비율) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
426 lines
25 KiB
Python
426 lines
25 KiB
Python
# -*- 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)
|
||
# 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 / "임도기술교본"))
|
||
OUTMD = Path(str(ROOT_DIR / "0. 참조 법령·기준 목록.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 = OUTMD.parent
|
||
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"{d}/{folder.name}"
|
||
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}](<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 기준 현행본")
|
||
|
||
OUTMD.write_text("\n".join(L) + "\n", encoding="utf-8")
|
||
print(f"작성 완료: {OUTMD.name} / {total}건 / 무매칭 {miss}")
|