knowledge(마스터): 기계 상세구분 다듬음 · 소요량·계수 표에 용도 칸 신설

기계.json — 상세구분을 구동방식·규격 뺀 굵은 기종으로(654줄 중 302줄 값 바뀜, 세부분류 칸은 그대로 둠 — 200개 이름이 170개 기종으로 묶임)
소요량_*.json · 계수_*.json 83개 표 3029개 항목에 용도 칸 신설(공종·대상·로직키) — scripts/build_용도.py
 - 공종: 원문번호로 해당 원문 md 의 절 제목을 찾아 채움(3029개 전부 찾음 · 산림품셈 1-1-3 형식은 마침표 포함 확인)
 - 대상: 값칸·줄 값을 인력.json·기계.json·재료_품셈재료.json 이름과 맞대 인력/기계/재료 판정, 이름에 「할증」「환산」 있으면 추가, 조건이 있거나 아무 것도 안 맞으면 작업량 조건
 - 로직키: 로직_*.json(54개, 읽기만) 의 식·수량·호표 문자열에서 표 키를 거꾸로 찾아 채움(3029개 중 1852개에 참조 있음)
스크립트는 재실행해도 같은 결과(기존 용도 줄을 지우고 다시 씀)

check_master.py 틀 0 · 로직 0 유지 확인 · 본문 1건(CC000074, 8-4-8 결손 1)은 이 작업 전부터 있던 것(용도 뺀 상태로 스태시해 재확인)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0146J72jAMHpcmW4n2Y8WakE
This commit is contained in:
2026-09-20 22:02:08 +09:00
co-authored by Claude Sonnet 5
parent 826495f199
commit 0b794cea7d
85 changed files with 3545 additions and 302 deletions
@@ -0,0 +1,214 @@
# -*- coding: utf-8 -*-
"""소요량 · 계수 표마다 「용도」(공종 · 대상 · 로직키) 채우기.
담당: 소요량_*.json · 계수_*.json (읽기+쓰기) · 로직_*.json · 원문 md (읽기만).
다시 돌리면 같은 결과 — 순수 함수 + 정렬.
"""
import glob
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[3]
MASTER = ROOT / "resources" / "master_data"
ORIG_KS = ROOT / "resources" / "knowledge" / "original" / "원가계산" / "건설공사_표준품셈" / "본문"
ORIG_SR = ROOT / "resources" / "knowledge" / "original" / "원가계산" / "산림_표준품셈" / "본문"
CODE_RE = re.compile(r"\b([A-Z]{2}\d{6})\b")
HEADING_RE = re.compile(r"^#{2,4}\s+([0-9]+(?:-[0-9]+)*)\.?\s+(.+?)\s*$")
TRAIL_PAREN_RE = re.compile(r"(\s*\([^()]*\))+\s*$")
_heading_cache = {}
def _find_부문_dir(부문):
if not ORIG_KS.exists():
return None
for d in sorted(ORIG_KS.iterdir()):
if d.is_dir() and d.name.endswith(부문 + "부문"):
return d
return None
def _find_장_dir(base, ):
m = re.match(r"(\d+)", or "")
if not m or not base or not base.exists():
return None
num = m.group(1).zfill(2)
for d in sorted(base.iterdir()):
if d.is_dir() and d.name.startswith(f"{num}"):
return d
return None
def _heading_index(folder):
key = str(folder)
if key in _heading_cache:
return _heading_cache[key]
idx = {}
if folder and folder.exists():
for md in sorted(folder.glob("*.md")):
for line in md.read_text(encoding="utf-8").splitlines():
m = HEADING_RE.match(line)
if not m:
continue
num, text = m.group(1), m.group(2)
text = TRAIL_PAREN_RE.sub("", text).strip()
idx.setdefault(num, text)
_heading_cache[key] = idx
return idx
def resolve_공종(원문, 부문, , 원문번호):
if 원문 == "건설품셈":
base = _find_부문_dir(부문 or "")
folder = _find_장_dir(base, or "")
elif 원문 == "산림품셈":
folder = _find_장_dir(ORIG_SR, or "")
else:
folder = None
idx = _heading_index(folder)
num = 원문번호 or ""
parts = num.split("-")
while parts:
cand = "-".join(parts)
if cand in idx:
return idx[cand]
parts = parts[:-1]
return None
def _load_names(path, key="이름"):
d = json.load(open(path, encoding="utf-8"))
return set(r[key] for r in d[""] if r.get(key))
def load_masters():
인력_이름 = _load_names(MASTER / "인력.json")
기계_d = json.load(open(MASTER / "기계.json", encoding="utf-8"))
기계_이름 = set()
for r in 기계_d[""]:
기계_이름.add(r["이름"])
if r.get("상세구분"):
기계_이름.add(r["상세구분"])
재료_이름 = _load_names(MASTER / "재료_품셈재료.json")
return 인력_이름, 기계_이름, 재료_이름
def _strip_spec(key):
return key.split("(", 1)[0].strip()
def classify_대상(item, 인력_이름, 기계_이름, 재료_이름):
대상 = set()
이름 = item.get("이름", "")
if "할증" in 이름:
대상.add("할증")
if "환산" in 이름:
대상.add("환산")
조건 = item.get("조건") or {}
if 조건:
대상.add("작업량 조건")
값칸 = item.get("값칸") or {}
keys = list(값칸.keys())
row_vals = set()
for row in item.get("") or []:
if isinstance(row, dict):
for v in row.values():
if isinstance(v, str):
row_vals.add(v)
candidates = set(keys) | row_vals
for c in candidates:
base = _strip_spec(c)
if c in 인력_이름 or base in 인력_이름:
대상.add("인력")
elif c in 기계_이름 or base in 기계_이름:
대상.add("기계")
elif c in 재료_이름 or base in 재료_이름:
대상.add("재료")
for k in keys:
if "시공량" in k:
대상.add("작업량 조건")
if not 대상:
대상.add("작업량 조건")
return sorted(대상)
def build_로직_index():
idx = {}
for fp in sorted(glob.glob(str(MASTER / "로직_*.json"))):
d = json.load(open(fp, encoding="utf-8"))
for row in d.get("", []):
own = row.get("")
text = json.dumps(row, ensure_ascii=False)
for code in set(CODE_RE.findall(text)):
if code == own:
continue
idx.setdefault(code, set()).add(own)
return idx
def main():
인력_이름, 기계_이름, 재료_이름 = load_masters()
로직_idx = build_로직_index()
files = sorted(glob.glob(str(MASTER / "소요량_*.json"))) + sorted(
glob.glob(str(MASTER / "계수_*.json"))
)
총_항목 = 0
공종_미확인 = []
for fp in files:
d = json.load(open(fp, encoding="utf-8"))
원문 = d.get("원문")
부문 = d.get("부문")
용도_map = {}
for item in d.get("", []):
총_항목 += 1
= item[""]
= item.get("상세구분")
공종 = resolve_공종(원문, 부문, , item.get("원문번호"))
if 공종 is None:
공종_미확인.append((, fp, item.get("원문번호")))
공종 = item.get("이름", "").split("(", 1)[0].strip()
대상 = classify_대상(item, 인력_이름, 기계_이름, 재료_이름)
로직키 = sorted(로직_idx.get(, []))
용도_map[] = {"공종": 공종, "대상": 대상, "로직키": 로직키}
text = open(fp, encoding="utf-8").read()
lines = text.split("\n")
out = []
cur_키 = None
key_re = re.compile(r'"":\s*"([A-Z]{2}\d{6})"')
src_re = re.compile(r'^(\s*)"출처":\s*".*",\s*$')
old_용도_re = re.compile(r'^\s*"용도":\s*\{.*\},\s*$')
for line in lines:
if old_용도_re.match(line):
continue
m = key_re.search(line)
if m:
cur_키 = m.group(1)
out.append(line)
sm = src_re.match(line)
if sm and cur_키 in 용도_map:
indent = sm.group(1)
payload = json.dumps(용도_map[cur_키], ensure_ascii=False)
out.append(f'{indent}"용도": {payload},')
new_text = "\n".join(out)
with open(fp, "w", encoding="utf-8") as f:
f.write(new_text)
print("총 항목:", 총_항목, "/ 공종 미확인:", len(공종_미확인))
if 공종_미확인:
for , fp, 번호 in 공종_미확인[:30]:
print(" 미확인:", , 번호, fp)
if __name__ == "__main__":
main()