Files
Aislo/resources/master_data/scripts/build_용도.py
T
eomsangdonandClaude Opus 5.5 c7cbef86a0 feat(826-2): M01 기계 = 한 대 한 파일 · M01 저장 길 줄이기
- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움)
- 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만
- 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차
- 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움
- 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음)
- 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 19:45:00 +09:00

226 lines
7.2 KiB
Python

# -*- 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 = {}
# 원문번호 절 이름이 표 내용과 안 맞는 예외 — 검증(ref/_검증_기계_용도.md) 반영.
OVERRIDE_공종 = {
"CC000074": "불도저·굴착기", # 8-4-8 은 「[70]기타기계」 절이나 내용은 불도저·굴착기 잡재료 비율
}
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")
import master_machine_files as mmf # 기계 = 폴더(826-2)
기계_d = mmf.table(MASTER)[0]
기계_이름 = 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("상세구분")
if 키 in OVERRIDE_공종:
공종 = OVERRIDE_공종[키]
else:
공종 = 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()