Files
Aislo/resources/tester/test_m02_matset.py
T
eomsangdonandClaude Sonnet 5.5 90e478fe0b fix(M02): 자재 조합 이름 오류 글 — 파일명 · 키 없이 쉬운 말 한 줄 (PLAN 21-10 흠 7 서버)
- 같은 이름 · 60자 넘는 이름을 틀 검사 앞에서 「이름」 한 줄로 막음
- 오류 글에 이름은 30자에서 자름(…)
- 시험: 겹침 글 · 긴 이름 자름 · 60자 넘음

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EUypcnp5d1gU2aeKh9F2H7
2026-09-29 13:31:59 +09:00

141 lines
7.4 KiB
Python

"""M02 자재 조합(`/api/m02/material-combos…` · 마스터 템플릿 `자재조합.json` · 키 UM).
못박는 것
- 정본 표는 마스터 템플릿 자리 · 틀대로(머리 「다음」) · 키 대장에 UM 없음 · M01 길 없음.
- 새로 만들면 키 = 표 「다음」 번호 · 열 한 벌 · 담은 자재는 네 칸 · 지운 키는 다시 안 씀.
- 없는 자재 코드 · 같은 자재 두 번 · 이름 없음 · 이름 겹침 · 없는 본뜬키는 막음.
- 고치기 · 지우기는 낡은 판이면 409 · 본뜬키는 고쳐도 그대로 · 쓰는 열이 있으면 못 지움.
- 정본은 읽기만 함(사본 폴더 · 견본 집계표).
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from M01_MasterData import M01_MasterData_Router as m01_router
from M01_MasterData import M01_MasterData_Store as m01
from M02_MasterTemplete import M02_MasterTemplete_Matset as matset
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete.M02_MasterTemplete_Router import router
from resources.tester.test_m02_structure import write_sample_table
ROOT = Path(__file__).resolve().parents[2]
M1, M2 = (
{"코드": "MT000008", "이름": "시멘트", "규격": "40kg", "단위": "포"},
{"코드": "MT000009", "이름": "모래", "규격": "", "단위": "㎥"},
)
URL = "/api/m02/material-combos"
TABLE = "/api/m02/templates/table/구조물집계표"
@pytest.fixture
def client(tmp_path: Path, monkeypatch) -> TestClient:
write_sample_table(tmp_path)
monkeypatch.setattr(store, "FOLDER", tmp_path)
app = FastAPI()
app.include_router(router)
return TestClient(app)
def _new(client: TestClient, name: str, mats: list[dict], 판: str | None = None, **more):
if 판 is None:
판 = client.get(URL).json()["판"]
return client.post(URL, json={"판": 판, "줄": {"이름": name, "담은자재": mats, **more}})
def test_정본_표는_마스터_템플릿_자리_틀대로_UM_은_대장_밖() -> None:
path = ROOT / "resources/master_template/자재조합.json"
data = json.loads(path.read_text(encoding="utf-8"))
assert data["그룹"] == "자재조합" and isinstance(data["다음"], int)
assert matset.mmc.check_file(path) == []
assert not (ROOT / "resources/master_data/자재조합.json").exists()
book = (ROOT / "resources/master_data/_키대장.json").read_text(encoding="utf-8")
assert '"UM"' not in book.splitlines()[1] # 대장 머리 「다음」
assert not any("matset" in getattr(r, "path", "") for r in m01_router.router.routes)
def test_새로_만들면_다음_번호_열_한_벌_지운_키는_다시_안_씀(client) -> None:
res = _new(client, "합판 자재", [{**M1, "덤": 1}])
assert res.status_code == 200, res.text
made = res.json()
assert made["키"] == "UM000001" and tuple(made["줄"]) == matset.mmc.COLS
assert made["줄"]["담은자재"] == [M1] and made["줄"]["본뜬키"] is None
listed = client.get(URL).json()
assert listed["판"] == made["판"] and [r["키"] for r in listed["줄"]] == ["UM000001"]
assert client.get(f"{URL}/UM000001").json()["이름"] == "합판 자재"
assert client.delete(f"{URL}/UM000001", params={"판": made["판"]}).status_code == 200
again = _new(client, "다음 것", [M2]).json()
assert again["키"] == "UM000002" # 지운 번호는 다시 안 씀
assert json.loads((store.FOLDER / matset.FILE).read_text(encoding="utf-8"))["다음"] == 3
def test_없는_자재_두_번_이름_본뜬키는_막는다(client) -> None:
assert _new(client, "없는 코드", [{**M1, "코드": "MT999999"}]).status_code == 422
assert _new(client, "두 번", [M1, M1]).status_code == 422
assert _new(client, " ", [M1]).status_code == 400
assert _new(client, "빈 것", []).status_code == 422
assert _new(client, "없는 원본", [M1], 본뜬키="UM999999").status_code == 400
ok = _new(client, "원본", [M1]).json()["키"]
copy = _new(client, "본뜬 것", [M1, M2], 본뜬키=ok).json()
assert copy["줄"]["본뜬키"] == ok and copy["키"] != ok
dup = _new(client, "원본", [M2]) # 이름 겹침 — 파일명 · 키 없이 쉬운 말 한 줄
assert dup.status_code == 422 and dup.json()["detail"]["errors"] == [
"「원본」 이름의 자재 조합이 이미 있음 — 다른 이름으로"
]
long = "긴" * 40
assert _new(client, long, [M1]).status_code == 200
cut = _new(client, long, [M2]).json()["detail"]["errors"][0]
assert cut.startswith("「" + "긴" * 30 + "…」") and matset.FILE not in cut and "UM0" not in cut
assert _new(client, "긴" * 61, [M1]).json()["detail"]["errors"] == ["이름이 너무 김 — 60자까지"]
assert _new(client, "낡은 판", [M2], 판="0000").status_code == 409
def test_고치기는_낡은_판이면_409_본뜬키는_그대로(client) -> None:
src = _new(client, "원본", [M1]).json()["키"]
made = _new(client, "고칠 것", [M1], 본뜬키=src).json()
row = {**made["줄"], "이름": "고침", "담은자재": [M1, M2], "본뜬키": None}
res = client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row})
assert res.status_code == 200, res.text
assert res.json()["줄"]["이름"] == "고침" and res.json()["줄"]["본뜬키"] == src
assert client.put(f"{URL}/{made['키']}", json={"판": made["판"], "줄": row}).status_code == 409
assert client.put(f"{URL}/UM999999", json={"판": "", "줄": row}).status_code == 404
def test_쓰는_열이_있으면_못_지우고_없으면_지워진다(client) -> None:
made = _new(client, "쓰는 것", [M1]).json()
table = client.get(TABLE).json()
for col in table["문서"]["열"]:
if col["id"] == "rv":
col["자재조합"] = made["키"]
assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200
res = client.delete(f"{URL}/{made['키']}", params={"판": made["판"]})
assert client.get(URL).json()["쓰는곳"] == {made["키"]: ["돌기슭막이 (형태마다) H=(높이마다)"]}
assert res.status_code == 409 and res.json()["detail"]["users"] == [
"돌기슭막이 (형태마다) H=(높이마다)"
]
rows = client.get("/api/m02/structure-combos").json()["줄"]
assert next(r for r in rows if r["열"] == "rv")["자재조합"] == made["키"] # SS 장부 칸
table = client.get(TABLE).json()
for col in table["문서"]["열"]:
col.pop("자재조합", None)
assert client.put(TABLE, json={"판": table["판"], "문서": table["문서"]}).status_code == 200
assert client.delete(f"{URL}/{made['키']}", params={"판": "낡음"}).status_code == 409
assert client.delete(f"{URL}/{made['키']}", params={"판": made["판"]}).status_code == 200
assert client.get(f"{URL}/{made['키']}").status_code == 404
def test_M01_마스터는_UM_을_모른다() -> None:
assert "UM" not in m01.cm.mk.IDS and "자재조합" not in m01.cm.mk.ROW_ORDER
def test_새로_본떠_만들기는_열자마자_이름_칸에_초점() -> None:
ui = (ROOT / "M02_MasterTemplete/M02_MasterTemplete_Matset.ts").read_text(encoding="utf-8")
assert "'[data-matset-field=\"이름\"]')?.focus()" in ui.split("if (isNew) {")[1]
copy = ui.split("btnCopy.addEventListener")[1].split("});")[0]
assert '이름: ""' in copy # 본떠 만들기는 이름 칸 비움