"""Z01 공종 축 — 산림·건설 두 kind 를 기초단가와 **같은 계약**으로(2026-09-17 브레인 · PLAN_공종축 8-4). 줄 = **잎**(아래 마디 없고 품셈 표가 붙은 마디) — 일위대가가 붙는 단위. 갈래(`variant_keys`)는 칸으로(편 수 = `stats.units`). 이름 = 뿌리부터 전체 경로(「토공 › 토사깍기 › 기계」 · 건설은 부문부터) — 잎 이름만으론 겹침. 계산 규칙 = 표 수준 `rules`(부모 마디 choose_one · sum_steps) + 줄 칸 `rule`·`step_weight` — 빠지면 발파암(절취 0.1 + 깍기 0.9 + 집토 1) 금액이 틀어짐. `@id` = 불변 열쇠(FW-·CW-) — 없는 줄은 목차 코드로 서고 알림에 드러냄. ⚠ 읽기만 — 고칠 칸 없음(원문 값 풀기 8-7 뒤). 원본 `work_item_code`(FP-)는 B08·B09 가 씀 — 안 건드림. """ from __future__ import annotations import json from functools import lru_cache from pathlib import Path from typing import Any from Z01_MasterData import Z01_MasterData_Tables as tables GROUP = "work_item" #: kind → 원본 품셈 dataset_id — 공종 축 파일 `dataset_version.dataset_id` 로 가림(파일 이름에 기대지 않음) SOURCE_OF = { "work_item_forest": "pum_forest", "work_item_const": "pum_const", } # 이름표 `work_item_axis` 와 같은 이름 FOLDER = tables.RESOURCES / "data_work_item_master" #: 불변 열쇠 칸 — ⚠ 데스크탑 서브 자료가 오면 칸 이름을 맞출 것(여기 한 곳) KEY_FIELD = "work_item_key" PATH_JOIN = " › " _LOCKED = "공종 축 뼈대 — 품셈 원문으로만 바뀜(값 고치기는 원문 값 풀기 뒤)" @lru_cache(maxsize=8) def _read(path: str, _mtime_ns: int) -> dict[str, Any]: return json.loads(Path(path).read_text(encoding="utf-8")) def doc(kind: str) -> tuple[dict[str, Any], str] | None: """그 품셈의 공종 축 끝 판(문서 · 파일 이름) — 없으면 None(건설은 아직 없음).""" found = [] for path in sorted(FOLDER.glob("*work_item_master_*.json")): # 건설은 `const_` 앞꼬리 d = _read(str(path), path.stat().st_mtime_ns) if (d.get("dataset_version") or {}).get("dataset_id") == SOURCE_OF[kind]: found.append((d, path.name)) return max(found, key=lambda each: each[0].get("effective_date") or "", default=None) #: 자료가 있는 품셈만 kind 로 섬 — 빈 상자는 이름만 뜨고 안 열림 · 건설은 자료가 오면 서버 재시작 뒤 저절로 KINDS = tuple(kind for kind in SOURCE_OF if doc(kind)) def _key(item: dict[str, Any]) -> str: return item.get(KEY_FIELD) or item["work_item_code"] def _tree(d: dict[str, Any]) -> tuple[list[dict], dict[str, dict], dict[str, list[dict]]]: items = d.get("work_items") or [] children: dict[str, list[dict]] = {} for item in items: if item.get("parent_code"): children.setdefault(item["parent_code"], []).append(item) return items, {i["work_item_code"]: i for i in items}, children def _path(item: dict[str, Any] | None, by: dict[str, dict]) -> str: names = [] while item: names.append(item["name"]) item = by.get(item.get("parent_code")) return PATH_JOIN.join(reversed(names)) def base_rows(kind: str) -> list[dict[str, Any]]: found = doc(kind) if found is None: return [] d, name = found items, by, children = _tree(d) rows = [] for item in items: if item["work_item_code"] in children or not item.get("tables"): continue parent = by.get(item.get("parent_code")) or {} weight = next( (s["weight"] for s in parent.get("steps") or [] if s["code"] == item["work_item_code"]), None, ) rows.append( { "@id": _key(item), "@source": name, "work_item_code": item["work_item_code"], # 목차 코드 — 열쇠 아님, 칸으로만 "number": item["number"], "name": _path(item, by), "variant_keys": item.get("variant_keys") or [], "pum_tables": [t["pum_table_id"] for t in item["tables"]], "rule": parent.get("parent_mode"), "step_weight": weight, "effective_date": d.get("pum_edition") or d.get("effective_date"), } ) return rows def rules(kind: str) -> list[dict[str, Any]]: """부모 마디 계산 규칙 — choose_one(아래 중 하나) · sum_steps(아래를 무게대로 더해야 한 단위).""" found = doc(kind) if found is None: return [] items, by, children = _tree(found[0]) out = [] for item in items: mode = item.get("parent_mode") if not mode: continue rule = { "@id": _key(item), "work_item_code": item["work_item_code"], "name": _path(item, by), "mode": mode, "children": [_key(c) for c in children.get(item["work_item_code"], [])], } if mode == "sum_steps": rule["steps"] = [ {"@id": _key(by[s["code"]]), "weight": s["weight"]} for s in item["steps"] ] rule["basis"] = item.get("steps_basis") out.append(rule) return out def spec(columns: list[str]) -> dict[str, Any]: return {"editable": [], "locked": dict.fromkeys(columns, _LOCKED), "formula": {}} def notice(kind: str) -> list[str]: lines = ["공종 축은 읽기만 — 고칠 칸 없음(품셈 원문 값 풀기 뒤에 열림)"] found = doc(kind) if found and any(KEY_FIELD not in i for i in found[0].get("work_items") or []): lines.append( "불변 열쇠(FW-·CW-) 아직 없음 — 목차 코드(FP-)를 열쇠로 씀 · 품셈 개정으로 번호가 밀리면 딴 줄을 가리킬 수 있음" ) return lines def extra(kind: str) -> dict[str, Any]: """표 수준 덧붙임 — 계산 규칙 · 잎 수 · 갈래 편 수(쪽 나누기와 무관하게 전체).""" leaves = base_rows(kind) return { "rules": rules(kind), "stats": { "leaves": len(leaves), "units": sum(max(1, len(r["variant_keys"])) for r in leaves), }, }