Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Matset.py
T
eomsangdonandClaude Opus 5.5 4abfc5b265 refactor(M02): 자재 조합 표를 마스터 템플릿으로 옮김 · M02 좌측 「자재 조합」 컨테이너
- 표 resources/master_data/자재조합.json → resources/master_template/자재조합.json(git mv) · 키 UM 은 표 안 「다음」 번호 · _키대장 · master_keys 의 UM 뺌 · check_master 틀이 새 자리를 같이 봄
- 서버: M01 /api/m01/matset* 뺌 · M02 저장소 M02_MasterTemplete_Matset.py(목록 · 새로 · 고치기 · 지우기 · 쓰는 열 검사) · 길 GET/POST /api/m02/material-combos · PUT/DELETE /api/m02/material-combos/{키} · 갈라 내기(branchMaterialCombo)도 이 저장소로
- 화면: M01 좌측 「자재 조합」 컨테이너 뺌 · M02 좌측 「자재 조합」 컨테이너(찾기 · 키 맨 앞 목록 · 새로 · 본떠 만들기 · 지우기) · 메인 칸 상세(이름 · 비고 · 본뜬키 · 담은 자재 표 · 자재 더하기 · 빼기 · 저장) · 쓰는 열이 있으면 지우기는 확인 창 없이 알림
- 시험: test_m01_matset → test_m02_matset · test_m02_combo_branch 사본 자리 · 실제 자재 코드

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VwJQ5eRrCKf3nH9EFeKvJz
2026-09-28 22:25:30 +09:00

134 lines
5.6 KiB
Python

"""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)}