wip(M01): 자재 조합 M01 길 초안 — 합치기 전 임시(sub4 자재조합 표 위에 얹을 예정)

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:00:53 +09:00
co-authored by Claude Sonnet 5
parent 0fb69c053b
commit 1904cd5072
5 changed files with 296 additions and 3 deletions
+16
View File
@@ -196,6 +196,22 @@
- 못 도는 줄은 통째로 멈추지 않고 그 줄만 `ok` false · `까닭` · `멈춤` 에 한 줄 — 나머지 줄의 합계는 그대로 냄.
- 답 — 200 · 400(틀린 몸 · 이름 없음 · 모르는 소유) · 404(없는 키) · 409(낡은 판본) · 422 `{detail: {errors: […]}}`(검사 걸림 — 없는 로직 키 · 같은 로직 두 번 · 빈 조합 · 조합을 담음).
### 자재 조합
재료산출에서 쓸 **자재 목록 묶음**(파일 `자재조합.json` · 키 `UM` · `_틀.md` 11장). 담은 자재 `{코드, 이름, 규격, 단위}` 만 지님 — 수량 · 단가 없음. 요소 그룹이 아님(`/groups` · `/rows` 에 안 끼고 제 길로만).
| 길 | 받음 | 줌 |
| --------------------- | ------------------------------- | ------------------------------------------- |
| `GET /matsets` | `q?` `owner?` | `{matsets: […]}` — 줄마다 `count` (자재 수) |
| `GET /matset` | `key` | `{file, version, matset}` |
| `POST /matset/new` | `{matset, owner?}` | `{file, version, key, matset}` |
| `POST /matset/edit` | `{key, version, matset, owner?}` | `{file, version, matset}` |
| `POST /matset/delete` | `{key, version}` | `{file, version}` |
- 답 — 200 · 400(이름 없음 · 모르는 소유 · 없는/자기 본뜬키) · 404 · 409(낡은 판본 · 지우기 막음) · 422(없는 자재 코드 · 같은 자재 두 번 · 자재 줄 모양).
- 지우기는 구조물집계표 · `구조물집계표조합` 의 「자재조합」 칸으로 쓰는 열이 있으면 409 `{detail: {errors: […], users: [구조물 이름…]}}` — 지우지 않음.
- M02 화면이 새로 만드는 조합도 같은 파일 · 같은 길이라 M01 목록에 같이 보임.
## 10. 재료 · 기계 고르기
재료 줄은 키 대신 기본 검색어로 품목을 찾음 — 파일 모양 · 찾기 규칙 · 자동값 차례 · 단위 환산은 `_틀.md` 7장.
+107
View File
@@ -0,0 +1,107 @@
"""M01 자재 조합(`/api/m01/matset…` · 파일 `자재조합.json` · 키 UM) — 담은 자재 목록만 지님.
못박는 것
- 새로 만들면 키를 서버가 `UM` 다음 번호로 줌 · 열 한 벌 · 담은 자재는 네 칸.
- 없는 자재 코드 · 같은 자재 두 번 · 이름 없음 · 없는 본뜬키는 막음.
- 고치기는 낡은 판본이면 409 · 지우기는 구조물 열이 「자재조합」 칸으로 쓰면 409 + 구조물 이름들.
- 정본은 읽기만 함(사본 폴더 · 사본 집계표).
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M01_MasterData import M01_MasterData_Router as router_module
from M01_MasterData import M01_MasterData_Store as store
from M01_MasterData import M01_MasterData_Store_Combo as combo_store
ROOT = Path(__file__).resolve().parents[2]
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
for path in store.FOLDER.glob("*.json"):
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
shutil.copy(path, tmp_path / path.name)
monkeypatch.setattr(store, "FOLDER", tmp_path)
sheet = tmp_path / "집계표.json"
sheet.write_text(json.dumps({"열": []}), encoding="utf-8")
monkeypatch.setattr(combo_store, "USERS", (sheet,))
app = FastAPI()
app.include_router(router_module.router)
return TestClient(app)
def _mat(client: TestClient) -> dict:
"""자재품목에서 실제 줄 하나 — 자재 찾기 길 그대로."""
got = client.get("/api/m01/pick", params={"kind": "price", "q": "합판", "limit": 2}).json()
one = got["items"][0]
return {"코드": one["ref"], "이름": one["이름"], "규격": one["규격"], "단위": one["단위"]}
def _new(client: TestClient, name: str, mats: list[dict], **more) -> dict:
body = {"matset": {"이름": name, "담은자재": mats, **more}}
return client.post("/api/m01/matset/new", json=body)
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 '"UM"' in (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8")
def test_새로_만들면_UM_키와_열_한_벌(client) -> None:
mat = _mat(client)
res = _new(client, "합판 자재", [{**mat, "덤": 1}])
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 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)]
got = client.get("/api/m01/matset", params={"key": made["key"]}).json()
assert got["matset"]["이름"] == "합판 자재"
def test_없는_자재_두_번_담기_이름_본뜬키는_막는다(client) -> None:
mat = _mat(client)
assert _new(client, "없는 코드", [{**mat, "코드": "MT999999"}]).status_code == 422
assert _new(client, "두 번", [mat, mat]).status_code == 422
assert _new(client, " ", [mat]).status_code == 400
assert _new(client, "없는 원본", [mat], 본뜬키="UM999999").status_code == 400
ok = _new(client, "원본", [mat]).json()["key"]
assert _new(client, "본뜬 것", [mat], 본뜬키=ok).json()["matset"]["본뜬키"] == ok
assert _new(client, "원본", [mat]).status_code == 422 # 이름 겹침
def test_고치기는_낡은_판본이면_409(client) -> None:
mat = _mat(client)
key = _new(client, "고칠 것", [mat]).json()["key"]
one = client.get("/api/m01/matset", params={"key": key}).json()
body = {"key": key, "version": one["version"], "matset": {**one["matset"], "이름": "고침"}}
assert client.post("/api/m01/matset/edit", json=body).status_code == 200
assert client.post("/api/m01/matset/edit", json=body).status_code == 409 # 같은 옛 판본
def test_쓰는_열이_있으면_못_지우고_없으면_지워진다(client) -> None:
mat = _mat(client)
key = _new(client, "쓰는 것", [mat]).json()["key"]
cols = [{"id": "c9", "머리": ["돌붙임", "찰붙임", "", ""], "자재조합": key}]
combo_store.USERS[0].write_text(json.dumps({"열": cols}, ensure_ascii=False), encoding="utf-8")
version = client.get("/api/m01/matset", params={"key": key}).json()["version"]
res = client.post("/api/m01/matset/delete", json={"key": key, "version": version})
assert res.status_code == 409 and res.json()["detail"]["users"] == ["돌붙임 찰붙임"]
combo_store.USERS[0].write_text(json.dumps({"열": []}), encoding="utf-8")
assert (
client.post("/api/m01/matset/delete", json={"key": key, "version": version}).status_code
== 200
)
assert client.get("/api/m01/matset", params={"key": key}).status_code == 404