"""M01 재료 기본 검색어 · 고름 · 단위 환산 · 단위 경고 — PLAN 8-1 · 8-2 계약. 검색어 든 견본 로직 줄을 시험 안에서 만들어(`POST /calc` 의 `row` — 저장 안 함) 돌림. 정본 폴더는 읽기만. """ from __future__ import annotations from decimal import Decimal 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 mf = store.cm.mf mp = mf.mp @pytest.fixture(scope="module") def client() -> TestClient: app = FastAPI() app.include_router(router_module.router) return TestClient(app) def _sample(term: str, unit: str, **cond) -> dict: """재료 줄 하나짜리 견본 로직 — 검색어 줄 하나 · 수량 1.""" return { "이름": "검색어 견본", "구분": "자체", "입력": [], "호표": [ { "종류": "재료", "요소": {"검색어": term, **cond}, "이름": "견본 재료", "단위": unit, "수량": "1", "비목": "재료비", } ], } def _line(client: TestClient, row: dict, picks: dict | None = None, path: str = "calc") -> dict: body = {"key": "GF999999", "inputs": {}, "row": row, **({"고름": picks} if picks else {})} got = client.post(f"/api/m01/{path}", json=body).json() assert got.get("ok", True) is True, got return got["lines"][0] if path == "calc" else got def test_검색어로_찾고_지역_낱말이_맞는_줄이_먼저(client) -> None: seoul = _line(client, _sample("합판(내수) 12t 서울", "㎡")) assert seoul["출처"].startswith("MT000018") and seoul["단가"] == 11354 # 값 열 가운데 낮은 값 busan = _line(client, _sample("합판(내수) 12t 부산", "㎡")) assert busan["출처"].startswith("MT000020") and busan["단가"] == 11488 assert "단위경고" not in seoul def test_고름은_그_줄만_그_품목으로_셈(client) -> None: row = _sample("합판(내수) 12t 서울", "㎡") got = _line(client, row, {"0": "MT000019"}) assert got["출처"].startswith("MT000019") and got["단가"] == 11556 assert _line(client, row)["단가"] == 11354 # 요청마다 — 저장 안 함 text = _line(client, row, {"0": "MT000019"}, "text") assert text["ok"] is True # 읽는 식도 같은 몸으로 돎 def test_같은_성격_단위는_줄_단위로_환산(client) -> None: got = _line(client, _sample("어닐링철선 서울 4.0mm", "톤")) # 자재 kg → 줄 톤 assert got["단가"] == 1610000 and "단위경고" not in got got = _line(client, _sample("어닐링철선 서울 4.0mm", "g")) assert got["단가"] == pytest.approx(1.61) def test_성격이_다르면_계산은_하고_단위경고(client) -> None: got = _line(client, _sample("어닐링철선 서울 4.0mm", "m")) assert got["단가"] == 1610 and got["단위경고"] == "줄 m · 자재 kg" assert got["금액"] == 1610 def test_검색어_후보가_없으면_그_줄만_비움(client) -> None: got = client.post( "/api/m01/calc", json={"key": "GF999999", "inputs": {}, "row": _sample("없는물건zzz", "개")} ).json() assert got["ok"] is True and got["lines"][0]["금액"] is None assert "후보 없음" in got["lines"][0]["까닭"] def test_기계_입력은_목록_밖_원문번호도_받음(client) -> None: key = "GC000267" inputs = {**store.auto(key)["값"], "기계": "0101-0007"} # 고르기 목록 밖 · 기계 마스터엔 있음 got = client.post("/api/m01/calc", json={"key": key, "inputs": inputs}).json() assert got["ok"] is True, got bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**inputs, "기계": "9999-9999"}}) assert bad.json()["ok"] is False # 기계 마스터에 없는 번호는 그대로 막힘 def test_단위_환산_표() -> None: assert mp.convert(Decimal(1610), "kg", "톤") == 1610000 assert mp.convert(Decimal(5), "Ton", "kg") == Decimal("0.005") assert mp.convert(Decimal(100), "㎡", "㎠") == Decimal("0.01") assert mp.convert(Decimal(7), "kg", "m") == 7 # 성격이 다르면 그대로 assert mp.same_kind("ℓ", "㎥") and not mp.same_kind("kg", "m") def test_check_master_검색어_후보0_단위경고(client) -> None: whole = store.loaded()[1] assert mf.check_pick(whole, "줄", {"검색어": "합판(내수) 12t 서울"}) == [] assert "후보 0" in mf.check_pick(whole, "줄", {"검색어": "없는물건zzz"})[0] assert mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "kg") == [] assert "성격이 다름" in mf.check_unit(whole, "줄", {"검색어": "어닐링철선 서울 4.0mm"}, "m")[0] def test_기계_입력은_EQ_키도_원문번호로_풀어_받음(client) -> None: key = "GC000267" number = "0101-0007" # 고르기 목록 밖 eq = next(k for k, r in store.loaded()[1].index["EQ"].items() if r.get("원문번호") == number) base = {**store.auto(key)["값"]} by_number = client.post( "/api/m01/calc", json={"key": key, "inputs": {**base, "기계": number}} ).json() by_key = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": eq}}).json() assert by_key["ok"] is True, by_key assert by_key["sums"] == by_number["sums"] # EQ 키 = 원문번호와 같은 계산 bad = client.post("/api/m01/calc", json={"key": key, "inputs": {**base, "기계": "EQ999999"}}) assert bad.json()["ok"] is False def test_재료_줄마다_품목_칸(client) -> None: key = "GF000160" body = {"key": key, "inputs": {k: str(v) for k, v in store.auto(key)["값"].items()}} lines = client.post("/api/m01/calc", json=body).json()["lines"] idx = next( i for i, x in enumerate(lines) if x.get("품목", {}).get("이름", "").startswith("합판(내수)") ) first = lines[idx]["품목"] assert first["이름"] == "합판(내수), 서울" and first["값칸"] assert {"키", "이름", "규격", "단위", "값칸"} <= set(first) lines = client.post("/api/m01/calc", json={**body, "고름": {str(idx): "MT000019"}}).json()[ "lines" ] assert lines[idx]["품목"]["키"] == "MT000019" and "인천" in lines[idx]["품목"]["이름"] def test_목록_검색은_띄어쓰기를_무시(client) -> None: for q in ("함석", "골 함 석", "골함석"): got = client.get("/api/m01/rows", params={"file": "재료_자재품목.json", "q": q}).json() assert any(r["이름"] == "골 함 석" for r in got["rows"]), q