Files
Aislo/resources/tester/test_m01_copy.py
T
eomsangdon e73ef5d3a0 feat(M01): PLAN 3-4 복사해서 만들기 화면 — 별칭 꼴 식 편집·표 바꾸기·로직 부르기 갈래·시험 계산·저장
- 새 M01_MasterData_UI_LogicLab_Copy.ts — [본떠 만들기] → GET /logic/formula(별칭 폼) 편집
  → 표 바꾸기 모달(소요량·계수 검색) · 로직 부르기 그대로/같이 복사 갈래 · 식 줄 고치기·덧줄 더하기
  → POST /logic/formula/preview 로 되돌린 실제 식을 buildCalc 로 재사용해 시험 계산
  → POST /logic/formula/save 로 자체 로직(GX) 저장 · afterSave 로 LogicLab 이 새 키를 바로 엶
- 서버: POST /logic/formula/preview 새로 더함(저장 없이 별칭만 원문으로 되돌림) · 계약 7장 반영
- LogicLab.ts 는 「본떠 만들기」 단추 한 줄만 붙임(정본 로직 화면·New 화면은 안 건드림)
- ORCA 로 13-4-1 을 본떠 저장 — 안 고치면 79,279.668 로 원본과 같음 · 석공 줄 곱 2배로 고치면
  131,716.728 로 바뀜 확인 · 시험 흔적(GX000001·로직_자체.json·키대장 GX 번호) 지움·되돌림
- 시험 `test_m01_copy.py` 에 미리보기 시험 둘 더함 · M01 시험 130 통과 · typecheck 통과
2026-09-21 21:12:43 +09:00

234 lines
10 KiB
Python

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