Files
Aislo/M01_MasterData/M01_MasterData_Store_Matset.py
T
eomsangdonandClaude Sonnet 5 d35269e32a feat(M01): 자재 조합 M01 길 — 목록 · 하나 · 새로 · 고치기 · 지우기(쓰는 열 있으면 막음)
- /api/m01/matsets · matset · matset/new|edit|delete — sub4 의 자재조합.json(UM) 위에 얹음 · 담은 자재 코드가 마스터에 없으면 422 · 본뜬키는 있는 다른 UM 만
- 지우기 전 집계표 · 구조물집계표조합의 「자재조합」 칸을 훑어 쓰는 열이 있으면 409 + 구조물 이름들
- 일위대가 · 자재 조합 지우기 검사가 users_of 한 곳을 씀 · 계약 9장 「자재 조합」 표 더함
- 시험 다섯(resources/tester/test_m01_matset.py)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoyUwuTFiDJfvCfHpvKKzp
2026-09-28 19:08:20 +09:00

137 lines
5.6 KiB
Python

"""M01 자재 조합 저장소 — 목록 · 하나 읽기 · 만들기 · 고치기 · 지우기.
계약: `_화면_계약.md` 9장 끝 · 파일 모양: `_틀.md` 11장 · 엔진: `scripts/master_matset.py`.
조합은 담은 자재 목록(코드 · 이름 · 규격 · 단위)만 지님 — 수량 · 단가는 두지 않음(재료산출 몫).
자리는 `자재조합.json` 한 파일 · 키는 `_키대장.json` 의 `UM` 다음 번호(서버가 줌).
"""
from __future__ import annotations
from datetime import date
from pathlib import Path
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Combo as combos
cm = store.cm
msk = cm.mmc # sub4 가 세운 자재 조합 엔진(`scripts/master_material_combo.py`)
FILE = msk.FILE
OWNERS = msk.OWNERS
def _path() -> Path:
return store.FOLDER / FILE
def _make_file() -> None:
"""파일이 없으면 머리만 두고 만듦."""
if not _path().is_file():
head = {"그룹": msk.GROUP, "판": str(date.today().year), "줄": []}
_path().write_text(store.dump(head), encoding="utf-8", newline="\n")
def _read() -> tuple[dict, str]:
_make_file()
return store.read(FILE)
def _keys() -> list[str]:
return [str(r.get("키")) for r in _read()[0].get("줄") or []]
def _find(data: dict, key: str) -> dict:
for row in data.get("줄") or []:
if str(row.get("키")) == key:
return row
raise store.StoreError(404, f"없는 자재 조합 「{key}」")
def _clean(one: dict, owner: str, key: str | None = None) -> dict:
"""서버가 잡는 칸 — 키 · 소유 · 열 한 벌(안 준 칸은 null · 자재 줄은 네 칸만)."""
if not isinstance(one, dict):
raise store.StoreError(400, "자재 조합 줄이 묶음 아님")
name = one.get("이름")
if not isinstance(name, str) or not name.strip():
raise store.StoreError(400, "자재 조합 줄에 「이름」 이 없거나 글자 아님")
if owner not in OWNERS:
raise store.StoreError(400, f"모르는 소유 「{owner}」 — {' · '.join(OWNERS)}")
origin = one.get("본뜬키")
if origin is not None and (origin == key or origin not in _keys()):
raise store.StoreError(400, f"본뜬키 「{origin}」 — 비었거나 있는 다른 자재 조합 키여야 함")
got = one.get("담은자재")
if isinstance(got, list): # 화면이 덤을 실어도 자재 줄은 네 칸만 · 안 준 칸은 빈 글
got = [{c: m.get(c, "") for c in msk.ITEM_COLS} if isinstance(m, dict) else m for m in got]
rest = {k: v for k, v in one.items() if k not in ("키", "원문번호", "소유")}
_check_codes(got)
row = {**({"키": key} if key else {}), "소유": owner, **rest, "담은자재": got or []}
for col in msk.COLS:
row.setdefault(col, None)
return {k: row[k] for k in msk.COLS if k in row}
def _check_codes(got) -> None:
"""담은 자재 코드가 마스터 어느 줄에도 없으면 422 — 틀 검사는 저장 길(`store.save`)이 함."""
if not isinstance(got, list) or not got:
return
known = {str(r.get("키")) for d in store.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)}"]})
def _write(changes: list[dict], version: str | None = None) -> dict:
_make_file()
return store.save(
[{"file": FILE, "version": version or store.read(FILE)[1], "changes": changes}]
)[0]
def matsets(q: str = "", owner: str = "") -> list[dict]:
"""목록 — 찾기(이름 · 키)와 소유로 거름 · 줄마다 자재 수."""
out = []
for row in store.loaded()[0].get(FILE, {}).get("줄") or []:
if owner and row.get("소유") != owner:
continue
if not store._hit(row, q):
continue
out.append(
{k: row.get(k) for k in msk.COLS if k != "담은자재"}
| {"file": FILE, "count": len(msk.items(row))}
)
return out
def matset(key: str) -> dict:
data, version = _read()
return {"file": FILE, "version": version, "matset": _find(data, key)}
def matset_new(one: dict, owner: str = OWNERS[0]) -> dict:
"""새 자재 조합 — 키는 서버가 `UM` 다음 번호로 줌."""
before = set(_keys())
done = _write([{"op": "add", "row": _clean(one, owner)}])
key = next((k for k in reversed(_keys()) if k not in before), "")
return {**done, "key": key, "matset": _find(_read()[0], key) if key else {}}
def matset_edit(key: str, version: str, one: dict, owner: str = "") -> dict:
data, _ = _read()
was = _find(data, key)
if store.read(FILE)[1] != version:
raise store.StoreError(409, {"stale": [FILE]})
fixed = _clean(one, owner or str(was.get("소유") or OWNERS[0]), key)
done = _write([{"op": "edit", "key": key, "row": fixed}], version)
return {**done, "matset": _find(_read()[0], key)}
def matset_delete(key: str, version: str) -> dict:
"""지우기 — 구조물 열이 「자재조합」 칸으로 쓰면 409 + 구조물 이름들(지우지 않음)."""
_find(_read()[0], key)
if users := combos.users_of("자재조합", key):
msg = f"구조물 {len(users)} 곳이 쓰는 자재 조합이라 못 지움 — {' · '.join(users)}"
raise store.StoreError(409, {"errors": [msg], "users": users})
if store.read(FILE)[1] != version:
raise store.StoreError(409, {"stale": [FILE]})
return _write([{"op": "delete", "key": key}], version)