- 표 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
134 lines
5.6 KiB
Python
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)}
|