Files
Aislo/resources/tester/test_m01_combo_inputs.py
T
eomsangdonandClaude Sonnet 5 0fb69c053b feat(M01): 일위대가 조합에 칸 「본뜬키」 더함 · 목록 · 상세에 보임 (계약 더함)
- 열 한 벌에 「본뜬키」(소유 다음 · 비고 앞) — 비면 원본 · 있으면 본뜬 원본 조합 키
- 서버가 없는 키 · 자기 키를 400 으로 막고 검사(check_master)도 같은 규칙
- 정본 조합 37 줄에 본뜬키 null 채움 · 목록 「본뜬키」 열 · 상세 「원본 / 본뜬키」 줄
- _틀.md 10장 · _화면_계약.md 9장 갱신 · 시험 하나 더함

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

144 lines
6.5 KiB
Python

"""M01 조합 알약(`GET /api/m01/combo/{키}/inputs`) — 담은 로직 결과단위 분모가 물량 하나.
못박는 것
- 물량 = 로직 결과단위 분모(원/㎥ → ㎥) · 설계 조건(고르기 · 범위)은 안 넣음(`조건` 은 빈 자리).
- 이름 = 로직 이름 끝 괄호 뗀 것 · 같은 이름 · 단위면 한 알약(로직 여럿).
- 원/10㎡ 는 단위 ㎡ · `배` 10 · 없는 조합 404.
- 알약 키 = 「이름|단위」 — 로직을 빼고 · 더하고 · 차례를 바꿔도 같은 물량이면 같은 키.
- 조합 지우기 — 구조물 열이 「일위대가」 로 쓰면 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
@pytest.fixture(scope="module")
def client() -> TestClient:
app = FastAPI()
app.include_router(router_module.router)
return TestClient(app)
def _pills(client: TestClient, key: str) -> dict:
res = client.get(f"/api/m01/combo/{key}/inputs")
assert res.status_code == 200, res.text
return res.json()
def test_기슭막이_찰쌓기_인력_장비는_한_알약(client) -> None:
got = _pills(client, "UA000004")
assert got["조건"] == []
by = {(p["이름"], p["단위"]): p for p in got["물량"]}
assert len(got["물량"]) == 10 # 담은 로직 11 · 찰쌓기 둘이 합침
assert by[("돌쌓기 찰쌓기", "㎡")]["로직"] == ["GF000222", "GF000221"]
assert by[("레디믹스트콘크리트 타설", "㎥")]["로직"] == ["GC000198"]
assert by[("유로폼 설치 및 해체", "㎡")]["키"] == "유로폼 설치 및 해체|㎡"
assert set(got["물량"][0]) == {"키", "이름", "단위", "로직"}
def test_원_10제곱미터는_배_10(client) -> None:
got = _pills(client, "UA000031")
pill = next(p for p in got["물량"] if p["로직"] == ["GF000226"])
assert pill["키"] == f"{pill['이름']}|㎡"
assert (pill["단위"], pill["배"]) == ("㎡", 10)
def test_준공표지석_물량_여섯(client) -> None:
got = _pills(client, "UA000002")
assert [p["단위"] for p in got["물량"]] == ["㎥", "㎡", "㎥", "㎥", "㎥", "㎡"]
def test_없는_조합은_404(client) -> None:
assert client.get("/api/m01/combo/UA999999/inputs").status_code == 404
@pytest.fixture
def own(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 _new(client: TestClient, name: str, codes: list[str]) -> str:
combo = {"이름": name, "담은로직": [{"로직": c, "메모": None} for c in codes]}
res = client.post("/api/m01/combo/new", json={"combo": combo})
assert res.status_code == 200, res.text
return res.json()["key"]
def test_알약_키는_로직을_빼고_더하고_차례를_바꿔도_같다(own) -> None:
both = ["GF000222", "GF000221", "GC000198"] # 찰쌓기 인력 · 장비 + 타설
full = _pills(own, _new(own, "전부", both))["물량"]
fewer = _pills(own, _new(own, "장비 뺌", ["GF000222", "GC000198"]))["물량"]
swapped = _pills(own, _new(own, "차례 바꿈", both[::-1]))["물량"]
keys = {p["키"] for p in full}
assert keys == {"돌쌓기 찰쌓기|㎡", "레디믹스트콘크리트 타설|㎥"}
assert {p["키"] for p in fewer} == keys
assert {p["키"] for p in swapped} == keys
assert len({p["키"] for p in full}) == len(full) # 키 겹침 없음
def _use(sheet: Path, key: str) -> None:
cols = [
{"id": "c5", "머리": ["돌기슭막이", "찰쌓기", "", ""], "일위대가": key},
{"id": "c6", "머리": ["돌붙임", "", "", ""], "일위대가": "UA000009"},
]
sheet.write_text(json.dumps({"열": cols}, ensure_ascii=False), encoding="utf-8")
def test_쓰는_열이_있으면_조합을_못_지우고_구조물_이름을_알린다(own) -> None:
key = _new(own, "쓰는 조합", ["GF000222"])
_use(combo_store.USERS[0], key)
version = own.get(f"/api/m01/combo?key={key}").json()["version"]
res = own.post("/api/m01/combo/delete", json={"key": key, "version": version})
assert res.status_code == 409
detail = res.json()["detail"]
assert detail["users"] == ["돌기슭막이 찰쌓기"]
assert "돌기슭막이 찰쌓기" in detail["errors"][0]
assert own.get(f"/api/m01/combo?key={key}").status_code == 200 # 그대로 남음
def test_안_쓰는_조합은_지워진다(own) -> None:
key = _new(own, "안 쓰는 조합", ["GF000222"])
_use(combo_store.USERS[0], "UA000009") # 다른 조합을 씀
version = own.get(f"/api/m01/combo?key={key}").json()["version"]
assert (
own.post("/api/m01/combo/delete", json={"key": key, "version": version}).status_code == 200
)
assert own.get(f"/api/m01/combo?key={key}").status_code == 404
def test_본뜬키는_목록에_나오고_없는_키_자기_키는_400(own) -> None:
base = _new(own, "원본", ["GF000222"])
combo = {"이름": "본뜬 것", "본뜬키": base, "담은로직": [{"로직": "GF000222", "메모": None}]}
got = own.post("/api/m01/combo/new", json={"combo": combo}).json()
assert got["combo"]["본뜬키"] == base
rows = {r["키"]: r for r in own.get("/api/m01/combos").json()["combos"]}
assert rows[got["key"]]["본뜬키"] == base and rows[base]["본뜬키"] is None
bad = {**combo, "이름": "없는 원본", "본뜬키": "UA999999"}
assert own.post("/api/m01/combo/new", json={"combo": bad}).status_code == 400
version = own.get(f"/api/m01/combo?key={base}").json()["version"]
me = {"이름": "원본", "본뜬키": base, "담은로직": combo["담은로직"]}
res = own.post("/api/m01/combo/edit", json={"key": base, "version": version, "combo": me})
assert res.status_code == 400