"""M01 복사해서 만들기(PLAN 3-4) — 별칭 꼴 식 펴기 · 되돌려 자체 로직으로 저장. 계약 `resources/master_data/_화면_계약.md` 7장 · 규칙 `resources/master_data/ref/_설계_식_복사.md`. 못박는 것 - 13-4-1 을 식으로 펴 아무것도 안 고치고 되돌려 저장하면 원본과 계산 결과가 같음. - 12-4 처럼 같은 표를 다른 인자로 쓰는 로직은 별칭이 번호로 갈림. - 로직 전부(1,354줄)가 펴고 되돌리면 원래 식 글자 그대로 돌아옴. """ from __future__ import annotations import shutil import sys from decimal import Decimal 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 sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "master_data/scripts")) import master_copy as mcp # noqa: E402 REAL = store.FOLDER STACK = "GF000219" # 13-4-1 메쌓기(인력) — 같은 표를 같은 인자로 두 번(별칭 하나로 합침) FORM = "GF000160" # 12-4 합판거푸집 — 같은 표를 다른 인자로 일곱 번(번호로 갈림) @pytest.fixture def client(tmp_path: Path, monkeypatch) -> TestClient: for path in REAL.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) app = FastAPI() app.include_router(router_module.router) return TestClient(app) def _get(client: TestClient, url: str, **params) -> dict: res = client.get(url, params=params) assert res.status_code == 200, res.text return res.json() def _post(client: TestClient, url: str, body: dict, want: int = 200) -> dict: res = client.post(url, json=body) assert res.status_code == want, res.text return res.json() def _inputs(key: str) -> dict: """그 로직의 일괄 시험 입력 한 벌(`check_master.시험입력`).""" row = store.cm.master(REAL).logic(key) return {k: str(v) for k, v in store.cm.시험입력(row).items()} def _sums(client: TestClient, key: str, inputs: dict) -> dict: got = _post(client, "/api/m01/calc", {"key": key, "inputs": inputs}) assert got["ok"] is True, got return got["sums"] # ── 펴기 ────────────────────────────────────────────────────────────── def test_별칭이_식에서_이름_하나로_보이고_원문은_뒤에_남는다(client: TestClient) -> None: got = _get(client, "/api/m01/logic/formula", key=STACK) names = {one["이름"]: one for one in got["별칭"]} assert set(names) == {"메쌓기인력", "높이에대한증가율표"} hit = names["메쌓기인력"] assert hit["참조"] == "QF000421" assert hit["원문"] == "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기)" assert hit["자리"] == ["호표[0].수량", "호표[1].수량"] # 완전히 같은 부르기는 하나로 합침 assert "석공" in hit["값칸"] 가로 = {one["자리"]: one["식"] for one in got["줄"]} assert 가로["호표[0].수량"] == "메쌓기인력.석공 * (1 + 증가율 / 100)" # 값 칸은 그대로 둠 assert "찾기(" not in 가로["중간[0].식"] assert "높이에대한증가율표.증가율" in 가로["중간[0].식"] def test_같은_표를_다른_인자로_쓰면_별칭이_번호로_갈린다(client: TestClient) -> None: got = _get(client, "/api/m01/logic/formula", key=FORM) 형틀 = [one for one in got["별칭"] if one["참조"] == "QF000357"] assert [one["이름"] for one in 형틀] == [f"합판거푸집{n}" for n in range(1, 8)] assert [one["원문"] for one in 형틀][:2] == [ "찾기(QF000357, 구분='합판')", "찾기(QF000357, 구분='각재')", ] # 같은 표를 같은 인자로 두 번 쓰는 자리(QF000358)는 번호 없이 하나 한벌 = [one for one in got["별칭"] if one["참조"] == "QF000358"] assert len(한벌) == 1 and 한벌[0]["자리"] == ["중간[0].식", "중간[1].식"] 가로 = {one["자리"]: one["식"] for one in got["줄"]} assert 가로["호표[0].수량"] == "합판거푸집1.기준수량_1회사용 * 재료율" assert 가로["호표[1].수량"] == "합판거푸집2.기준수량_1회사용 * 재료율" def test_로직_부르기도_별칭으로_보인다(client: TestClient) -> None: got = _get(client, "/api/m01/logic/formula", key="GC000994") 부름 = [one for one in got["별칭"] if one["종류"] == "로직"] assert 부름, got["별칭"] assert all(one["원문"].startswith("로직(") for one in 부름) 요소 = [one.get("요소") for one in got["줄"] if one["갈래"] == "호표"] assert any(str(one) in {x["이름"] for x in 부름} for one in 요소) # 호표 요소도 별칭으로 def test_로직_전부가_펴고_되돌리면_원래_식으로_돌아온다() -> None: files = store.cm.load(folder=REAL) master = store.cm.mf.Master(files) bad, rows = [], 0 for data in files.values(): if data.get("그룹") != "로직": continue for row in data.get("줄") or []: rows += 1 bad += mcp.check(master, row) assert rows > 1000, rows assert not bad, bad[:5] # ── 되돌려 저장 ──────────────────────────────────────────────────────── def test_안_고치고_되돌려_저장하면_원본과_계산이_같다(client: TestClient) -> None: inputs = _inputs(STACK) was = _sums(client, STACK, inputs) view = _get(client, "/api/m01/logic/formula", key=STACK) made = _post( client, "/api/m01/logic/formula/save", {"로직": view["로직"], "별칭": view["별칭"], "본뜬키": STACK, "이름": "메쌓기 복사본"}, ) key = made["key"] assert key.startswith("GX"), made assert made["logic"]["소유"] == "현장" and made["logic"]["구분"] == "자체" assert f"{STACK} 를 본뜸" in made["logic"]["비고"] # 별칭은 저장되지 않음 — 식은 원문 그대로 식 = made["logic"]["호표"][0]["수량"] assert 식 == "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공 * (1 + 증가율 / 100)" assert Decimal(str(_sums(client, key, inputs)["계"])) == Decimal(str(was["계"])) assert Decimal(str(was["계"])) > 0 # 원본은 그대로 assert _get(client, "/api/m01/logic", key=STACK)["logic"]["호표"][0]["수량"] == 식 def test_식을_고쳐_저장하면_그_값으로_계산된다(client: TestClient) -> None: inputs = _inputs(FORM) was = _sums(client, FORM, inputs) view = _get(client, "/api/m01/logic/formula", key=FORM) row = view["로직"] row["호표"][6]["수량"] = "합판거푸집6.기준수량_1회사용 * 인력율 * 2" # 형틀목공 품 두 배 made = _post( client, "/api/m01/logic/formula/save", {"로직": row, "별칭": view["별칭"], "본뜬키": FORM}, ) saved = made["logic"]["호표"][6]["수량"] assert saved == "찾기(QF000357, 구분='형틀목공').기준수량_1회사용 * 인력율 * 2" now = _sums(client, made["key"], inputs) assert Decimal(str(now["노무비"])) > Decimal(str(was["노무비"])) assert Decimal(str(now["재료비"])) == Decimal(str(was["재료비"])) def test_별칭과_같은_이름의_사용자_변수를_만들면_저장을_막는다(client: TestClient) -> None: """일감 15 — 교차 검증 어긋남 1건. 가림 못 하는 이름 겹침은 조용히 안 바꾸고 막음.""" view = _get(client, "/api/m01/logic/formula", key=STACK) row = view["로직"] row["중간"].insert(0, {"이름": "메쌓기인력", "식": "1"}) # 별칭과 같은 이름 for h in row["호표"]: if h["이름"] == "석공": h["수량"] = "메쌓기인력 * 2" got = _post( client, "/api/m01/logic/formula/save", {"로직": row, "별칭": view["별칭"], "본뜬키": STACK}, want=400, ) assert "메쌓기인력" in str(got["detail"]) # 미리보기 길도 같이 막힘 got2 = _post( client, "/api/m01/logic/formula/preview", {"로직": row, "별칭": view["별칭"]}, want=400 ) assert "메쌓기인력" in str(got2["detail"]) def test_별칭_원문이_부르기가_아니면_막는다(client: TestClient) -> None: view = _get(client, "/api/m01/logic/formula", key=STACK) marks = [{**one} for one in view["별칭"]] marks[0]["원문"] = "1 + 1" got = _post( client, "/api/m01/logic/formula/save", {"로직": view["로직"], "별칭": marks}, want=400, ) assert "찾기" in str(got["detail"]) def test_미리보기는_저장_없이_원문으로_되돌린_로직만_준다(client: TestClient) -> None: """3-4 화면의 [시험 계산] 이 쓰는 길 — `/calc` 의 `row`+`file` 로 그대로 넣어 씀.""" inputs = _inputs(STACK) was = _sums(client, STACK, inputs) view = _get(client, "/api/m01/logic/formula", key=STACK) before_keys = _get(client, "/api/m01/logics")["logics"] preview = _post( client, "/api/m01/logic/formula/preview", {"로직": view["로직"], "별칭": view["별칭"]} ) resolved = preview["logic"]["호표"][0]["수량"] assert resolved == "찾기(QF000421, 뒷길이=뒷길이, 돌=돌, 쌓기=쌓기).석공 * (1 + 증가율 / 100)" after_keys = _get(client, "/api/m01/logics")["logics"] assert after_keys == before_keys # 아무것도 저장 안 함 calc = _post( client, "/api/m01/calc", {"key": STACK, "inputs": inputs, "row": preview["logic"], "file": view["file"]}, ) assert calc["ok"], calc assert Decimal(str(calc["sums"]["계"])) == Decimal(str(was["계"])) def test_미리보기도_별칭_원문이_부르기가_아니면_막는다(client: TestClient) -> None: view = _get(client, "/api/m01/logic/formula", key=STACK) marks = [{**one} for one in view["별칭"]] marks[0]["원문"] = "아무거나" got = _post( client, "/api/m01/logic/formula/preview", {"로직": view["로직"], "별칭": marks}, want=400, ) assert "찾기" in str(got["detail"]) def test_정본_로직은_이_길로도_안_바뀐다(client: TestClient) -> None: """저장은 늘 새 자체 로직(`GX`) — 본뜬 원본 파일은 판본까지 그대로.""" before = _get(client, "/api/m01/logic", key=STACK) view = _get(client, "/api/m01/logic/formula", key=STACK) _post( client, "/api/m01/logic/formula/save", {"로직": view["로직"], "별칭": view["별칭"], "본뜬키": STACK}, ) after = _get(client, "/api/m01/logic", key=STACK) assert after["version"] == before["version"] assert after["logic"] == before["logic"]