"""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