"""M02 자재 조합 저장소 — 마스터 템플릿 표 `자재조합.json`(구조물집계표조합.json 과 같은 자리). 줄 = 키 UM · 이름 · 담은자재(코드 · 이름 · 규격 · 단위) · 본뜬키 · 소유 · 비고 — 수량 · 단가는 안 둠 (재료산출 몫). 키는 표 안 「다음」 번호(지운 키는 다시 안 씀) · 집계표 열 `자재조합` 칸이 가리킴. 틀 검사 = `master_material_combo.check_form`(check_master 와 같은 것) · 판 = 파일 sha256 앞 16자. """ from __future__ import annotations import json from typing import Any from common_util.common_util_json import atomic_write_json from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_MasterTemplete_Store as store mmc = m01.cm.mmc # 자재 조합 틀(`resources/master_data/scripts/master_material_combo.py`) FILE = mmc.FILE OWNERS = mmc.OWNERS _HEAD = "UM" def _load() -> tuple[dict[str, Any], str]: """(표, 판) — 파일이 없으면 빈 표 · 판 빈 글.""" path = store.FOLDER / FILE if not path.is_file(): return {"그룹": mmc.GROUP, "판": "2026", "비고": [], "다음": 1, "줄": []}, "" raw = path.read_bytes() return json.loads(raw), store.version_of(raw) def _find(doc: dict, key: str) -> dict[str, Any]: row = next((r for r in doc["줄"] if r.get("키") == key), None) if row is None: raise store.StoreError(404, f"없는 자재 조합 「{key}」") return row def listing() -> dict[str, Any]: """표 통째 — 판 · 줄(담은 자재 포함 · 작아서 한 번에) · 쓰는곳(키 → 쓰는 구조물 열 이름들).""" doc, version = _load() used: dict[str, list[str]] = {} for col in store._structure_columns(): if key := col.get("자재조합"): used.setdefault(key, []).append(_name(col)) return {"판": version, "줄": doc["줄"], "쓰는곳": used} def _name(col: dict) -> str: return " ".join(h for h in store._heads(col) if h) or col["id"] def one(key: str) -> dict[str, Any]: return _find(_load()[0], key) def users_of(key: str) -> list[str]: """구조물집계표에서 `자재조합` 칸이 이 키인 열 — 머리 글(없으면 열 id).""" out = [] for col in store._structure_columns(): if col.get("자재조합") == key: out.append(_name(col)) return out def _clean(row: Any, key: str, keep: dict | None = None) -> dict[str, Any]: """서버가 잡는 칸 — 키 · 열 한 벌 · 자재 줄 네 칸 · 본뜬키는 만들 때만(고칠 때는 있던 것).""" if not isinstance(row, dict): raise store.StoreError(400, "자재 조합 줄이 묶음 아님") name = row.get("이름") if not isinstance(name, str) or not name.strip(): raise store.StoreError(400, "자재 조합 줄에 「이름」 이 없거나 글자 아님") got = row.get("담은자재") got = [ {c: m.get(c) for c in mmc.ITEM_COLS} if isinstance(m, dict) else m for m in (got if isinstance(got, list) else []) ] known = {str(r.get("키")) for d in m01.loaded()[0].values() for r in d.get("줄") or []} lost = [str(m.get("코드")) for m in got if isinstance(m, dict) and m.get("코드") not in known] if lost: raise store.StoreError(422, {"errors": [f"마스터에 없는 자재 {' · '.join(lost)}"]}) base = keep or row owner = base.get("소유") or OWNERS[0] return { "키": key, "이름": name.strip(), "담은자재": got, "본뜬키": base.get("본뜬키") or None, "소유": owner, "비고": row.get("비고") or None, } def _save(doc: dict, version: str) -> str: """틀 검사 → 판이 같을 때만 씀 → 새 판.""" if errors := mmc.check_form(FILE, doc): raise store.StoreError(422, {"errors": errors}) path = store.FOLDER / FILE with store._LOCK: have = store.version_of(path.read_bytes()) if path.is_file() else "" if have != (version or ""): raise store.StoreError(409, {"stale": [FILE], "판": have}) atomic_write_json(path, doc) return store.version_of(path.read_bytes()) def create(row: Any, version: str | None = None) -> dict[str, Any]: """새 줄 — 키 = 「다음」 번호 · 본뜬키는 있는 다른 키만. `version` None = 지금 판(갈라 내기).""" doc, have = _load() key = f"{_HEAD}{int(doc.get('다음') or 1):06d}" fixed = _clean(row, key) if fixed["본뜬키"] and fixed["본뜬키"] not in {r.get("키") for r in doc["줄"]}: raise store.StoreError(400, f"본뜬키 「{fixed['본뜬키']}」 — 있는 다른 자재 조합 키여야 함") doc = {**doc, "다음": int(doc.get("다음") or 1) + 1, "줄": [*doc["줄"], fixed]} return {"키": key, "판": _save(doc, have if version is None else version), "줄": fixed} def edit(key: str, row: Any, version: str) -> dict[str, Any]: doc, _ = _load() fixed = _clean(row, key, keep=_find(doc, key)) doc = {**doc, "줄": [fixed if r.get("키") == key else r for r in doc["줄"]]} return {"키": key, "판": _save(doc, version), "줄": fixed} def delete(key: str, version: str) -> dict[str, Any]: """지우기 — 구조물 열이 쓰면 409 + 열 이름들(지우지 않음).""" doc, _ = _load() _find(doc, key) if users := users_of(key): msg = f"구조물 {len(users)} 곳이 쓰는 자재 조합이라 못 지움 — {' · '.join(users)}" raise store.StoreError(409, {"errors": [msg], "users": users}) doc = {**doc, "줄": [r for r in doc["줄"] if r.get("키") != key]} return {"판": _save(doc, version)}