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
This commit is contained in:
2026-09-28 19:08:20 +09:00
co-authored by Claude Sonnet 5
parent 625c1012c8
commit d35269e32a
2 changed files with 15 additions and 4 deletions
+13 -2
View File
@@ -14,7 +14,7 @@ from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Combo as combos
cm = store.cm
msk = cm.msk
msk = cm.mmc # sub4 가 세운 자재 조합 엔진(`scripts/master_material_combo.py`)
FILE = msk.FILE
OWNERS = msk.OWNERS
@@ -63,12 +63,23 @@ def _clean(one: dict, owner: str, key: str | None = None) -> dict:
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(
@@ -79,7 +90,7 @@ def _write(changes: list[dict], version: str | None = None) -> dict:
def matsets(q: str = "", owner: str = "") -> list[dict]:
"""목록 — 찾기(이름 · 키)와 소유로 거름 · 줄마다 자재 수."""
out = []
for _, row in msk.rows(store.loaded()[0]):
for row in store.loaded()[0].get(FILE, {}).get("줄") or []:
if owner and row.get("소유") != owner:
continue
if not store._hit(row, q):
+2 -2
View File
@@ -53,7 +53,7 @@ def _new(client: TestClient, name: str, mats: list[dict], **more) -> dict:
def test_정본_자재조합_파일은_틀대로다() -> None:
data = json.loads((ROOT / "resources/master_data/자재조합.json").read_text(encoding="utf-8"))
assert data["그룹"] == "자재조합" and isinstance(data["줄"], list)
assert store.cm.msk.check_form("자재조합.json", data) == []
assert store.cm.mmc.check_form("자재조합.json", data) == []
assert '"UM"' in (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8")
@@ -63,7 +63,7 @@ def test_새로_만들면_UM_키와_열_한_벌(client) -> None:
assert res.status_code == 200, res.text
made = res.json()
assert made["key"].startswith("UM") and len(made["key"]) == 8
assert tuple(made["matset"]) == store.cm.msk.COLS
assert tuple(made["matset"]) == store.cm.mmc.COLS
assert made["matset"]["담은자재"] == [mat] and made["matset"]["본뜬키"] is None
rows = client.get("/api/m01/matsets").json()["matsets"]
assert [(r["키"], r["count"]) for r in rows] == [(made["key"], 1)]