Files
Aislo/resources/tester/test_m01_make.py
T
eomsangdonandClaude Opus 5 aa86e02b8f feat(M01): 끝수 계산 붙임 · 절 목록·절 거르기·표 조건 값·소유 거름·자체 로직 API
- 엔진 — 로직 줄의 묶음 `끝수`{대상, 자리, 방법}를 계산 끝에 붙임(비목 합마다 끊고 계 = 그 합 · 돈 아닌 로직은 결과) · 글자 끝수는 설명 그대로 · 검사가 묶음 모양을 봄
- 서버 — `GET /sections`(원문 본문 절 목록 + 그 절의 표·로직 수) · `/tables` 의 `section`(13-4 가 13-40 에 안 걸림) · `GET /table/options`(조건 값만) · `/logics` 의 `owner`
- 자체 로직 — `POST /logic/new|copy|edit|delete` · 파일 `로직_자체.json` · 키 GX 서버 발급(원문번호 같아도 줄마다 다른 키) · 정본은 403 · 저장 안 한 초안은 `/calc` 에 `key` "" 로
- 저장소가 700줄을 넘어 쓰기 모양(`keep_shape`·`dump`)을 `M01_MasterData_Store_Shape.py` 로 가름
- 시험 `resources/tester/test_m01_make.py` 8건 통과 · 기존 M01 시험 25건 통과 · 일괄 시험 계산 1,291 그대로

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
2026-09-21 09:31:45 +09:00

179 lines
7.8 KiB
Python

"""M01 「만들기」 API — 절 목록 · 절 거르기 · 표 조건 값 · 소유 거름 · 자체 로직 · 끝수.
계약 `resources/master_data/_화면_계약.md` 2장·5장·6장. 임시 폴더 사본에서 씀.
"""
from __future__ import annotations
import shutil
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
from M01_MasterData import M01_MasterData_Store_Make as make
REAL = store.FOLDER
LABOR = "LB000002" # 보통인부
SAMPLE = "GF000219" # 산림 13-4-1 메쌓기(인력)
@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 _draft(끝수=None) -> dict:
"""보통인부 1/7 인 한 줄짜리 초안 — 저장 안 함."""
return {
"원문번호": "",
"이름": "시험 초안",
"결과단위": "원/㎡",
"출처": "자체",
"소유": "현장",
"입력": [],
"중간": [],
"호표": [
{
"종류": "인력",
"요소": LABOR,
"이름": "보통인부",
"단위": "인",
"수량": "1 / 7",
"비목": "노무비",
}
],
"덧줄": [],
"끝수": 끝수,
}
# ── 끝수 ──────────────────────────────────────────────────────────────
def test_끝수_묶음이_계에_붙고_글자는_안_붙는다(client: TestClient) -> None:
plain = _post(client, "/api/m01/calc", {"key": "", "inputs": {}, "row": _draft()})
assert plain["ok"] is True, plain
raw = Decimal(str(plain["sums"]["계"]))
assert raw != raw.to_integral_value() # 172068 / 7 — 원 미만이 남음
cut = _post(
client,
"/api/m01/calc",
{"key": "", "inputs": {}, "row": _draft({"대상": "계", "자리": 0, "방법": "버림"})},
)
assert Decimal(str(cut["sums"]["계"])) == raw.to_integral_value(rounding="ROUND_DOWN")
assert cut["sums"]["노무비"] == cut["sums"]["계"] # 비목 합마다 끊고 계 = 그 합
text = _post(client, "/api/m01/calc", {"key": "", "inputs": {}, "row": _draft("원 미만 버림")})
assert Decimal(str(text["sums"]["계"])) == raw # 글자 끝수는 설명일 뿐
def test_끝수_묶음_모양을_검사가_잡는다() -> None:
bad = {"키": "GX000001", "원문번호": "", "호표": [], "끝수": {"대상": "줄", "방법": "내림"}}
found = store.cm.mf.check_logic(store.cm.master(REAL), bad)[0]
assert any("대상" in x for x in found) and any("방법" in x for x in found)
# ── 절 목록 · 절 거르기 · 표 조건 값 ──────────────────────────────────
def test_절_목록과_절로_표_거르기(client: TestClient) -> None:
got = _get(client, "/api/m01/sections", book="산림품셈", chapter="13")
one = next(s for s in got["sections"] if s["section"] == "13-4-1")
assert one["title"].startswith("메쌓기") and one["chapter"] == "13장 구조물"
assert one["tables"] >= 1 and one["logics"] >= 1
assert client.get("/api/m01/sections", params={"book": "없는품셈"}).status_code == 404
hits = _get(client, "/api/m01/tables", group="소요량", section="13-4", size=500)["tables"]
assert hits and all(store.in_section(t["원문번호"], "13-4") for t in hits)
assert not store.in_section("13-40", "13-4") and store.in_section("13-4", "13-4")
def test_표_조건_값만_받는다(client: TestClient) -> None:
listed = _get(client, "/api/m01/tables", group="소요량", section="13-4-1", size=50, q="메쌓기")[
"tables"
]
one = listed[0]
got = _get(client, "/api/m01/table/options", file=one["file"], key=one["키"])
assert got["count"] >= 1 and "줄" not in got
for name, kind in got["조건"].items():
assert name in got["값"]
if kind == "고르기":
assert len(got["값"][name]) == len(set(map(str, got["값"][name])))
def test_로직_목록을_소유로_거른다(client: TestClient) -> None:
every = _get(client, "/api/m01/logics")["logics"]
assert all("소유" in x for x in every)
only = _get(client, "/api/m01/logics", owner="공용")["logics"]
assert only and all(x["소유"] == "공용" for x in only)
assert _get(client, "/api/m01/logics", owner="현장")["logics"] == []
# ── 자체 로직 ─────────────────────────────────────────────────────────
def test_자체_로직_만들기_본뜨기_고치기_지우기(client: TestClient) -> None:
made = _post(client, "/api/m01/logic/new", {"logic": _draft(), "owner": "현장"})
assert made["key"].startswith("GX") and made["file"] == make.OWN
assert made["logic"]["구분"] == "자체" and made["logic"]["소유"] == "현장"
assert (store.FOLDER / make.OWN).is_file()
again = _post(client, "/api/m01/logic/new", {"logic": _draft(), "owner": "현장"})
assert again["key"] != made["key"] # 원문번호가 같아도 키는 하나씩
copied = _post(client, "/api/m01/logic/copy", {"key": SAMPLE, "이름": "메쌓기 본뜸"})
assert copied["key"] != SAMPLE and f"{SAMPLE} 를 본뜸" in copied["logic"]["비고"]
assert copied["logic"]["이름"] == "메쌓기 본뜸" and copied["logic"]["소유"] == "현장"
assert copied["logic"]["상세구분"] == "13장 구조물"
fixed = dict(copied["logic"], 이름="고친 이름")
done = _post(
client,
"/api/m01/logic/edit",
{"key": copied["key"], "version": copied["version"], "logic": fixed},
)
assert done["logic"]["이름"] == "고친 이름" and done["logic"]["키"] == copied["key"]
stale = {"key": copied["key"], "version": copied["version"], "logic": fixed}
_post(client, "/api/m01/logic/edit", stale, want=409)
gone = _post(
client, "/api/m01/logic/delete", {"key": copied["key"], "version": done["version"]}
)
assert gone["file"] == make.OWN
assert client.get("/api/m01/logic", params={"key": copied["key"]}).status_code == 404
def test_정본_로직은_못_고친다(client: TestClient) -> None:
got = _get(client, "/api/m01/logic", key=SAMPLE)
body = {"key": SAMPLE, "version": got["version"], "logic": got["logic"]}
_post(client, "/api/m01/logic/edit", body, want=403)
_post(client, "/api/m01/logic/delete", {"key": SAMPLE, "version": got["version"]}, want=403)
_post(client, "/api/m01/logic/new", {"logic": _draft(), "owner": "남의것"}, want=400)
def test_만든_자체_로직이_시험_계산에_돈다(client: TestClient) -> None:
made = _post(client, "/api/m01/logic/new", {"logic": _draft(), "owner": "공용"})
got = _post(client, "/api/m01/calc", {"key": made["key"], "inputs": {}})
assert got["ok"] is True and Decimal(str(got["sums"]["노무비"])) > 0
listed = _get(client, "/api/m01/logics", owner="공용")["logics"]
assert made["key"] in [x["키"] for x in listed]