diff --git a/M01_MasterData/M01_MasterData_Router.py b/M01_MasterData/M01_MasterData_Router.py index 97424138..a411a2cb 100644 --- a/M01_MasterData/M01_MasterData_Router.py +++ b/M01_MasterData/M01_MasterData_Router.py @@ -318,6 +318,12 @@ def get_combo(key: str) -> dict: return _call(combo_store.combo, key) +@router.get("/combo/{key}/inputs") +def get_combo_inputs(key: str) -> dict: + """조합 알약 — 담은 로직 결과단위 분모마다 물량 하나(같은 이름·단위는 합침).""" + return _call(combo_store.combo_inputs, key) + + @router.get("/logic/combos") def get_logic_combos(key: str) -> dict: """이 단가산출 로직을 담은 조합 — 로직 쪽에서 거꾸로 봄.""" diff --git a/M01_MasterData/M01_MasterData_Store_Combo.py b/M01_MasterData/M01_MasterData_Store_Combo.py index c363639f..c340e0de 100644 --- a/M01_MasterData/M01_MasterData_Store_Combo.py +++ b/M01_MasterData/M01_MasterData_Store_Combo.py @@ -8,6 +8,7 @@ from __future__ import annotations +import re from datetime import date from pathlib import Path @@ -141,6 +142,40 @@ def combo(key: str) -> dict: } +_PER = re.compile(r"원\s*/\s*(\d*\.?\d*)\s*([^\s·(]+)") +_TAIL = re.compile(r"\s*\([^()]*\)\s*$") + + +def combo_inputs(key: str) -> dict: + """조합 알약 — 담은 로직마다 결과단위 분모(원/㎥ → ㎥)가 물량 하나. + + 이름 = 로직 이름 끝 괄호 뗀 것(「(인력)」·「(장비)」 한 알약) · 같은 이름 · 단위면 합침 · + 키 = 그 알약 첫 로직 키 · `배` = 분모 앞 수(원/10㎡ → 10 · 1 이면 안 줌) · 돈 아닌 로직은 빠짐. + `조건` 은 자리만(설계 조건 고르기 · 범위는 알약에 안 넣음). + """ + row = _find(_read()[0], key) + whole = store.loaded()[1] + pills: dict[tuple[str, str], dict] = {} + for one in mcb.items(row): + code = str(one.get("로직") or "") if isinstance(one, dict) else "" + try: + got = whole.logic(code) + except cm.mf.FormulaError: + continue + per = _PER.findall(str(got.get("결과단위") or "")) + if not per: + continue + # ponytail: 분모가 여럿(「원/㎡ · 원/㎥(단위당 따름)」)이면 「㎡ · ㎥」 한 글 — 입력이 고름 + unit = " · ".join(dict.fromkeys(u for _, u in per)) + name = _TAIL.sub("", str(got.get("이름") or code)) or code + pill = pills.setdefault((name, unit), {"키": code, "이름": name, "단위": unit, "로직": []}) + pill["로직"].append(code) + times = per[0][0] + if len(per) == 1 and times and float(times) != 1: + pill["배"] = float(times) if "." in times else int(times) + return {"키": row.get("키"), "이름": row.get("이름"), "물량": list(pills.values()), "조건": []} + + def combos_of_logic(key: str) -> dict: """「이 로직을 쓰는 조합」 — 로직 화면이 거꾸로 볼 때(소요량 「용도」 와 같은 결).""" files, _ = store.loaded() diff --git a/resources/tester/test_m01_combo_inputs.py b/resources/tester/test_m01_combo_inputs.py new file mode 100644 index 00000000..0e7189d2 --- /dev/null +++ b/resources/tester/test_m01_combo_inputs.py @@ -0,0 +1,55 @@ +"""M01 조합 알약(`GET /api/m01/combo/{키}/inputs`) — 담은 로직 결과단위 분모가 물량 하나. + +못박는 것 + - 물량 = 로직 결과단위 분모(원/㎥ → ㎥) · 설계 조건(고르기 · 범위)은 안 넣음(`조건` 은 빈 자리). + - 이름 = 로직 이름 끝 괄호 뗀 것 · 같은 이름 · 단위면 한 알약(로직 여럿). + - 원/10㎡ 는 단위 ㎡ · `배` 10 · 없는 조합 404. +정본 파일을 읽기만 함. +""" + +from __future__ import annotations + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from M01_MasterData import M01_MasterData_Router as router_module + + +@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[("유로폼 설치 및 해체", "㎡")]["키"] == "GC000214" + 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["단위"], 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