- M01_LB_Calc.py — POST /api/m01/lb/calc {row, inputs, tables}: /calc 와 같은 셈 · 부를 때마다 새 사본에 표를 바꿔 끼움(전역 캐시 · 파일 안 건드림 · 저장 안 함)
- GET /api/m01/lb/table-users — 표 「용도.로직키」 + 로직 글 대조
- main.py 등록 한 줄(시스템 관리자 · 폴더째 지우면 이 줄도 지움)
- M01_LB_Api.ts — lbCalcLater(0.3초 · 최신만 · 쉬운 말) · tableUsers · fetchLbTable
- 시험 test_m01_lb_calc.py — 견본 표 덮어 값 바뀜 · 캐시 · 파일 그대로 · 표 없으면 /calc 와 같음 · 404 · 사용처 · 403
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
182 lines
7.3 KiB
Python
182 lines
7.3 KiB
Python
"""로직 만들기 판 시험 길(`M01_MasterData/LogicBuilder/M01_LB_Calc.py`) 시험 — PLAN 35-2.
|
|
|
|
· 고친 표 사본으로 셈하면 값이 바뀌고 · 전역 캐시 · 파일은 그대로
|
|
· 표 없이 부르면 `/calc` 와 같은 값 · 없는 표는 404
|
|
· 사용처 — 표 「용도.로직키」 + 로직 글 대조
|
|
· 권한 — 시스템 관리자 아니면 403
|
|
견본 표 · 로직 줄은 시험 안에 둠(임시 폴더 사본에 견본 표를 더함 — 진짜 마스터 값에 안 기댐).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import json
|
|
import shutil
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from fastapi import Depends, FastAPI
|
|
from fastapi.testclient import TestClient
|
|
|
|
from common_util.common_util_auth import require_system_admin, verify_session
|
|
from M01_MasterData import M01_MasterData_Router as router_module
|
|
from M01_MasterData import M01_MasterData_Store as store
|
|
from M01_MasterData.LogicBuilder import M01_LB_Calc as lb
|
|
|
|
REAL = store.FOLDER
|
|
OWN = "로직_자체.json"
|
|
SAMPLE = "QF999999" # 견본 표 — 사본 폴더에만 있음
|
|
TABLE = {
|
|
"키": SAMPLE,
|
|
"원문번호": "",
|
|
"구분": "",
|
|
"상세구분": "",
|
|
"이름": "견본 쌓기 표",
|
|
"기준": "1㎡",
|
|
"출처": "자체",
|
|
"용도": {"공종": "견본", "대상": ["인력"], "로직키": []},
|
|
"조건": {"돌": "고르기"},
|
|
"값칸": {"석공": "인"},
|
|
"줄": [{"돌": "깬돌", "석공": 0.5}, {"돌": "견치돌", "석공": 0.7}],
|
|
"주": [],
|
|
}
|
|
|
|
|
|
def _labor() -> tuple[str, Decimal]:
|
|
rows = json.loads((store.FOLDER / "인력.json").read_text(encoding="utf-8"))["줄"]
|
|
row = next(r for r in rows if r.get("상태") == "공표" and isinstance(r.get("값"), (int, float)))
|
|
return row["키"], Decimal(str(row["값"]))
|
|
|
|
|
|
def _row(labor: str) -> dict:
|
|
"""견본 로직 — 입력 「돌」 로 견본 표에서 석공 품을 찾아 노임에 곱함."""
|
|
return {
|
|
"키": "",
|
|
"원문번호": "",
|
|
"이름": "견본 쌓기",
|
|
"결과단위": "원/㎡",
|
|
"출처": "자체",
|
|
"입력": [{"이름": "돌", "고르기": ["깬돌", "견치돌"]}],
|
|
"중간": [],
|
|
"호표": [
|
|
{
|
|
"종류": "인력",
|
|
"요소": labor,
|
|
"이름": "석공",
|
|
"단위": "인",
|
|
"수량": f"찾기({SAMPLE}, 돌=돌).석공",
|
|
"비목": "노무비",
|
|
}
|
|
],
|
|
"덧줄": [],
|
|
"끝수": None,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def folder(tmp_path: Path, monkeypatch) -> Path:
|
|
for path in REAL.glob("*.json"):
|
|
if path.name == OWN:
|
|
continue
|
|
if not path.name.startswith("_") or path.name == store.mk.BOOK.name:
|
|
shutil.copy(path, tmp_path / path.name)
|
|
host = sorted(tmp_path.glob("소요량_*.json"))[0] # 견본 표를 사본 소요량 파일 하나에 더함
|
|
data = json.loads(host.read_text(encoding="utf-8"))
|
|
data["표"].append(TABLE)
|
|
host.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
monkeypatch.setattr(store, "FOLDER", tmp_path)
|
|
return tmp_path
|
|
|
|
|
|
@pytest.fixture
|
|
def client(folder: Path) -> TestClient:
|
|
app = FastAPI()
|
|
app.include_router(router_module.router)
|
|
app.include_router(lb.router)
|
|
return TestClient(app)
|
|
|
|
|
|
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 test_고친_표로_값이_바뀌고_캐시와_파일은_그대로(client: TestClient, folder: Path) -> None:
|
|
labor, price = _labor()
|
|
row = _row(labor)
|
|
before = {p.name: p.read_bytes() for p in folder.glob("*.json")}
|
|
|
|
plain = _post(client, "/api/m01/lb/calc", {"row": row, "inputs": {"돌": "깬돌"}})
|
|
assert plain["ok"] is True
|
|
assert Decimal(str(plain["sums"]["계"])) == Decimal("0.5") * price
|
|
|
|
edited = copy.deepcopy(TABLE)
|
|
edited["줄"][0]["석공"] = 0.8 # 깬돌 줄 품 0.5 → 0.8
|
|
body = {"row": row, "inputs": {"돌": "깬돌"}, "tables": {SAMPLE: edited}}
|
|
changed = _post(client, "/api/m01/lb/calc", body)
|
|
assert Decimal(str(changed["sums"]["계"])) == Decimal("0.8") * price
|
|
assert changed["lines"][0]["수량"] == 0.8
|
|
|
|
# 전역 캐시 · 파일 · 같은 요청 뒤 표 없는 셈은 그대로
|
|
files = store.loaded()[0].values()
|
|
cached = next(
|
|
t for d in files if d.get("그룹") == "소요량" for t in d["표"] if t["키"] == SAMPLE
|
|
)
|
|
assert cached["줄"][0]["석공"] == Decimal("0.5")
|
|
assert {p.name: p.read_bytes() for p in folder.glob("*.json")} == before
|
|
again = _post(client, "/api/m01/lb/calc", {"row": row, "inputs": {"돌": "깬돌"}})
|
|
assert again == plain
|
|
|
|
# 키는 못 바꿈 — 사본 표의 「키」 가 달라도 그 자리 키로 셈
|
|
_post(client, "/api/m01/lb/calc", {**body, "tables": {SAMPLE: {**edited, "키": "QF000001"}}})
|
|
|
|
|
|
def test_표_없이_부르면_calc_와_같고_없는_표는_404(client: TestClient) -> None:
|
|
labor, _ = _labor()
|
|
row = _row(labor)
|
|
for stone in ("깬돌", "견치돌"):
|
|
mine = _post(client, "/api/m01/lb/calc", {"row": row, "inputs": {"돌": stone}})
|
|
theirs = _post(client, "/api/m01/calc", {"key": "", "inputs": {"돌": stone}, "row": row})
|
|
assert mine == theirs
|
|
|
|
stop = _post(client, "/api/m01/lb/calc", {"row": row, "inputs": {"돌": "호박돌"}})
|
|
assert stop["ok"] is False and stop == _post(
|
|
client, "/api/m01/calc", {"key": "", "inputs": {"돌": "호박돌"}, "row": row}
|
|
)
|
|
_post(client, "/api/m01/lb/calc", {"row": row, "tables": {"QF999998": TABLE}}, want=404)
|
|
|
|
|
|
def test_사용처_용도와_로직_글(client: TestClient, folder: Path) -> None:
|
|
labor, _ = _labor()
|
|
made = _post(client, "/api/m01/logic/new", {"logic": _row(labor)})
|
|
first = next(
|
|
r["키"] for p in sorted(folder.glob("로직_*.json")) if p.name != OWN
|
|
for r in json.loads(p.read_text(encoding="utf-8"))["줄"]
|
|
) # fmt: skip
|
|
host = sorted(folder.glob("소요량_*.json"))[0]
|
|
data = json.loads(host.read_text(encoding="utf-8"))
|
|
next(t for t in data["표"] if t["키"] == SAMPLE)["용도"]["로직키"] = [first, made["key"]]
|
|
host.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
store.cache.stale([host.name])
|
|
|
|
res = client.get("/api/m01/lb/table-users", params={"key": SAMPLE})
|
|
assert res.status_code == 200, res.text
|
|
users = {u["key"]: u["via"] for u in res.json()["users"]}
|
|
assert users == {first: "용도", made["key"]: "둘"}
|
|
assert client.get("/api/m01/lb/table-users", params={"key": "QF999998"}).status_code == 404
|
|
|
|
|
|
def test_시스템_관리자가_아니면_403(folder: Path) -> None:
|
|
app = FastAPI()
|
|
app.include_router(
|
|
lb.router, dependencies=[Depends(verify_session), Depends(require_system_admin)]
|
|
)
|
|
app.dependency_overrides[verify_session] = lambda: {"role": "USER", "company_id": 1}
|
|
client = TestClient(app)
|
|
assert client.post("/api/m01/lb/calc", json={"row": {}}).status_code == 403
|
|
assert client.get("/api/m01/lb/table-users", params={"key": SAMPLE}).status_code == 403
|
|
app.dependency_overrides[verify_session] = lambda: {"role": "SYSTEM_ADMIN", "company_id": None}
|
|
assert client.get("/api/m01/lb/table-users", params={"key": SAMPLE}).status_code == 200
|