_brief 응답에 구분·상세구분 추가(테스트 컨테이너 기계 후보 거름용) 표 상세화면 용도의 로직키를 누르면 로직 탭으로 전환 + 키 클립보드 복사(찾기용) 로직 목록을 직접 열지는 못함(로직 부분은 sub1 담당) — _화면_계약.md 한 줄 추가
534 lines
26 KiB
Python
534 lines
26 KiB
Python
"""M01 마스터 데이터 API — 계약 `resources/master_data/_화면_계약.md`.
|
||
|
||
임시 폴더에 master_data 첫 층 JSON 사본을 두고 읽기 · 시험 계산 · 저장 · 409 · 422.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import shutil
|
||
from decimal import Decimal
|
||
from functools import cache
|
||
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
|
||
|
||
REAL = store.FOLDER
|
||
LABOR = "인력.json"
|
||
LOGIC_SUB = "건설품셈 공통" # 구분 = 원문 + 부문
|
||
LOGIC_NUM = "3-3-1" # 원문번호는 절 번호만
|
||
LOGIC_NAME = "암발파(미진동굴착 TYPE-Ⅰ)" # 화약취급공 1016 · 보통인부 1002
|
||
LOGIC_INPUTS = {"지역": "전국평균", "보정작업": "해당 없음"}
|
||
|
||
|
||
@cache
|
||
def key_of(ref: str) -> str:
|
||
"""「ID:원문번호」 → 키(실제 폴더)."""
|
||
return _whole().get(ref)["키"]
|
||
|
||
|
||
@cache
|
||
def _whole():
|
||
return store.cm.master(REAL)
|
||
|
||
|
||
LOGIC_KEY = "GC000091" # 공통 3-3-1 암발파(미진동굴착 TYPE-Ⅰ)
|
||
|
||
|
||
@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 _calc(client: TestClient, **inputs) -> dict:
|
||
res = client.post(
|
||
"/api/m01/calc",
|
||
json={"key": LOGIC_KEY, "inputs": {**LOGIC_INPUTS, **inputs}},
|
||
)
|
||
assert res.status_code == 200, res.text
|
||
return res.json()
|
||
|
||
|
||
def _labor_row(client: TestClient, number: str) -> tuple[dict, str]:
|
||
got = _get(client, "/api/m01/rows", file=LABOR, q="", size=500)
|
||
return next(r for r in got["rows"] if r["원문번호"] == number), got["version"]
|
||
|
||
|
||
def test_그룹_파일_줄_쪽나눔_찾기(client: TestClient) -> None:
|
||
groups = {g["group"]: g for g in _get(client, "/api/m01/groups")["groups"]}
|
||
assert list(groups) == ["인력", "재료", "기계", "소요량", "계수", "환율", "요율", "로직"]
|
||
files = _get(client, "/api/m01/groups/인력/files")["files"]
|
||
assert LABOR in [f["file"] for f in files]
|
||
assert groups["인력"]["files"] == len(files)
|
||
page2 = _get(client, "/api/m01/rows", file=LABOR, page=2, size=10)
|
||
assert page2["page"] == 2 and len(page2["rows"]) == 10 and page2["total"] > 20
|
||
hit = _get(client, "/api/m01/rows", file=LABOR, q="보통인부")
|
||
assert [r["원문번호"] for r in hit["rows"]] == ["1002"]
|
||
assert _get(client, "/api/m01/rows", file=LABOR, q=key_of("LB:1002"))["total"] == 1
|
||
assert client.get("/api/m01/rows", params={"file": "없음.json"}).status_code == 404
|
||
assert client.get("/api/m01/rows", params={"file": "../main.py"}).status_code == 404
|
||
|
||
|
||
def test_표형_요소_표_목록과_표_하나(client: TestClient) -> None:
|
||
file = "소요량_건설품셈_03장_토공사.json"
|
||
listed = _get(client, "/api/m01/tables", file=file, q="암발파")
|
||
one = next(t for t in listed["tables"] if t["이름"] == LOGIC_NAME)
|
||
assert one["원문번호"] == LOGIC_NUM
|
||
assert one["count"] >= 1 and "줄" not in one and one["file"] == file
|
||
table = _get(client, "/api/m01/table", file=file, key=one["키"])["table"]
|
||
assert len(table["줄"]) == one["count"] and table["값칸"] == one["값칸"]
|
||
assert client.get("/api/m01/tables", params={"file": LABOR}).status_code == 400
|
||
|
||
|
||
def test_표_목록은_구분_상세구분으로_거름(client: TestClient) -> None:
|
||
"""파일을 가로지른 한 목록 — 구분 · 상세구분 · 찾기 · 쪽 나눔."""
|
||
every = _get(client, "/api/m01/tables", group="소요량", size=1)
|
||
kind = _get(client, "/api/m01/tables", group="소요량", sub=LOGIC_SUB, size=500)
|
||
assert 0 < kind["total"] < every["total"]
|
||
assert {t["구분"] for t in kind["tables"]} == {LOGIC_SUB}
|
||
assert len({t["file"] for t in kind["tables"]}) > 1 # 여러 장 파일이 한 목록에
|
||
one = _get(
|
||
client, "/api/m01/tables", group="소요량", sub=LOGIC_SUB, detail="03장 토공사", size=500
|
||
)
|
||
assert 0 < one["total"] < kind["total"]
|
||
assert {t["상세구분"] for t in one["tables"]} == {"03장 토공사"}
|
||
page = _get(client, "/api/m01/tables", group="소요량", size=5, page=2)
|
||
assert page["page"] == 2 and len(page["tables"]) == 5 and page["total"] == every["total"]
|
||
|
||
|
||
def test_표_용도_대상으로_거름(client: TestClient) -> None:
|
||
"""PLAN 1-1 셋째 줄 — 표마다 붙은 용도(공종·대상·로직키)로 거름."""
|
||
one = _get(client, "/api/m01/tables", group="소요량", size=1)["tables"][0]
|
||
usage = _get(client, "/api/m01/table", file=one["file"], key=one["키"])["table"]["용도"]
|
||
assert usage["공종"] and usage["대상"]
|
||
target = usage["대상"][0]
|
||
narrowed = _get(
|
||
client, "/api/m01/tables", group="소요량", q=one["이름"], usage=target, size=500
|
||
)
|
||
assert one["키"] in {t["키"] for t in narrowed["tables"]}
|
||
ALL_TARGETS = ("인력", "기계", "재료", "할증", "환산", "작업량 조건")
|
||
other = next((v for v in ALL_TARGETS if v not in usage["대상"]), None)
|
||
if other:
|
||
excluded = _get(
|
||
client, "/api/m01/tables", group="소요량", q=one["이름"], usage=other, size=500
|
||
)
|
||
assert one["키"] not in {t["키"] for t in excluded["tables"]}
|
||
|
||
|
||
def test_갈래_목록은_서버가_줌(client: TestClient) -> None:
|
||
"""소요량 · 계수 · 로직 컨테이너의 구분 · 상세구분 — 장 차례대로."""
|
||
for group in ("소요량", "계수", "로직"):
|
||
got = _get(client, "/api/m01/subs", group=group)
|
||
names = [s["name"] for s in got["subs"]]
|
||
assert got["slot"] == "구분" and names[0] == "산림품셈"
|
||
assert LOGIC_SUB in names
|
||
kind = next(s for s in got["subs"] if s["name"] == LOGIC_SUB)
|
||
details = kind["details"]
|
||
assert kind["book"] == "건설품셈" and all(d[:2].isdigit() and d[2] == "장" for d in details)
|
||
assert details == sorted(details) # 장 차례대로
|
||
|
||
|
||
def test_로직_목록_하나_시험계산(client: TestClient) -> None:
|
||
detail = "03장 토공사"
|
||
listed = _get(client, "/api/m01/logics", sub=LOGIC_SUB, detail=detail, q="암발파")["logics"]
|
||
assert LOGIC_KEY in [x["키"] for x in listed]
|
||
assert all((x["구분"], x["상세구분"]) == (LOGIC_SUB, detail) for x in listed)
|
||
assert not any(x["blocked"] for x in listed)
|
||
files = _get(client, "/api/m01/groups/로직/files")["files"]
|
||
assert files[0]["file"].startswith("로직_산림품셈_") and files[-1]["chapter"].startswith(
|
||
"유지관리"
|
||
)
|
||
assert _get(client, "/api/m01/logics", blocked=1)["logics"] == []
|
||
one = _get(client, "/api/m01/logic", key=LOGIC_KEY)
|
||
assert one["file"] == "로직_건설품셈_03장_토공사.json" and one["blocked"] is False
|
||
got = _calc(client)
|
||
assert got["ok"] is True
|
||
lines = {x["이름"]: x for x in got["lines"]}
|
||
assert Decimal(str(lines["화약취급공"]["금액"])) == Decimal(
|
||
str(lines["화약취급공"]["수량"])
|
||
) * Decimal(str(lines["화약취급공"]["단가"]))
|
||
assert got["sums"]["계"] == pytest.approx(sum(x["금액"] for x in got["lines"]))
|
||
stop = _calc(client, 없는입력=1)
|
||
assert stop["ok"] is False and "모르는 입력" in stop["reason"]
|
||
|
||
|
||
def test_저장_고침은_파일에_쓰고_낡은_판본은_409(client: TestClient) -> None:
|
||
row, version = _labor_row(client, "1016")
|
||
before = _calc(client)["sums"]["계"]
|
||
edited = {**row, "값": row["값"] + 1000}
|
||
body = {
|
||
"files": [
|
||
{
|
||
"file": LABOR,
|
||
"version": version,
|
||
"changes": [{"op": "edit", "key": row["키"], "row": edited}],
|
||
}
|
||
]
|
||
}
|
||
res = client.post("/api/m01/save", json=body)
|
||
assert res.status_code == 200, res.text
|
||
new_version = res.json()["files"][0]["version"]
|
||
assert new_version != version
|
||
assert _labor_row(client, "1016") == (edited, new_version)
|
||
assert _calc(client)["sums"]["계"] == pytest.approx(before + 1000 * 0.04) # 화약취급공 0.040 인
|
||
data = json.loads((store.FOLDER / LABOR).read_text(encoding="utf-8"))
|
||
assert (
|
||
data["그룹"] == "인력"
|
||
and len(data["줄"]) == _get(client, "/api/m01/rows", file=LABOR)["total"]
|
||
)
|
||
again = client.post("/api/m01/save", json=body) # 옛 판본 그대로
|
||
assert again.status_code == 409 and again.json()["detail"]["stale"] == [LABOR]
|
||
|
||
|
||
def test_저장_검사에_걸리면_아무것도_안_씀(client: TestClient) -> None:
|
||
row, version = _labor_row(client, "1016")
|
||
raw = (store.FOLDER / LABOR).read_bytes()
|
||
book = (store.FOLDER / store.mk.BOOK.name).read_bytes()
|
||
gone = {"file": LABOR, "version": version, "changes": [{"op": "delete", "key": row["키"]}]}
|
||
res = client.post("/api/m01/save", json={"files": [gone]})
|
||
assert res.status_code == 422 and any(row["키"] in x for x in res.json()["detail"]["errors"])
|
||
bad_form = {**_labor_row(client, "1002")[0], "키": "", "원문번호": "9999"}
|
||
del bad_form["출처"]
|
||
res = client.post(
|
||
"/api/m01/save", json={"files": [{**gone, "changes": [{"op": "add", "row": bad_form}]}]}
|
||
)
|
||
assert res.status_code == 422 and any("출처" in x for x in res.json()["detail"]["errors"])
|
||
dup = {**gone, "changes": [{"op": "add", "row": _labor_row(client, "1002")[0]}]}
|
||
res = client.post("/api/m01/save", json={"files": [dup]})
|
||
assert res.status_code == 422 and any(
|
||
"원문번호 겹침" in x for x in res.json()["detail"]["errors"]
|
||
)
|
||
rekey = {
|
||
**gone,
|
||
"changes": [{"op": "edit", "key": row["키"], "row": {**row, "키": "LB999999"}}],
|
||
}
|
||
assert client.post("/api/m01/save", json={"files": [rekey]}).status_code == 400
|
||
assert (store.FOLDER / LABOR).read_bytes() == raw
|
||
assert (store.FOLDER / store.mk.BOOK.name).read_bytes() == book
|
||
|
||
|
||
def test_더함_지움_로직_파일도_같은_길(client: TestClient) -> None:
|
||
one = _get(client, "/api/m01/logic", key=LOGIC_KEY)
|
||
copy = {**one["logic"], "키": "", "원문번호": LOGIC_NUM}
|
||
add = {"file": one["file"], "version": one["version"], "changes": [{"op": "add", "row": copy}]}
|
||
res = client.post("/api/m01/save", json={"files": [add]})
|
||
assert res.status_code == 200, res.text
|
||
book = json.loads((store.FOLDER / store.mk.BOOK.name).read_text(encoding="utf-8"))
|
||
new = f"GC{book['다음']['GC'] - 1:06d}" # 대장의 다음 번호를 받음
|
||
assert book["키"][new] == {"원문번호": LOGIC_NUM, "파일": one["file"]}
|
||
assert _get(client, "/api/m01/logic", key=new)["blocked"] is False
|
||
drop = {
|
||
"file": one["file"],
|
||
"version": res.json()["files"][0]["version"],
|
||
"changes": [{"op": "delete", "key": new}],
|
||
}
|
||
assert client.post("/api/m01/save", json={"files": [drop]}).status_code == 200
|
||
assert client.get("/api/m01/logic", params={"key": new}).status_code == 404
|
||
|
||
|
||
def test_요소_찾기_기계는_구분_상세구분_실림(client: TestClient) -> None:
|
||
"""테스트 컨테이너(sub3)가 기계 후보를 구분→상세구분으로 거르려면 필요."""
|
||
found = _get(client, "/api/m01/elements", group="기계", q="불도저")
|
||
assert found["items"]
|
||
assert all(x["구분"] and x["상세구분"] for x in found["items"])
|
||
|
||
|
||
def test_단가_자동_요소_찾기_저장전_시험계산(client: TestClient) -> None:
|
||
one = _get(client, "/api/m01/logic", key=LOGIC_KEY)
|
||
labor = one["prices"][key_of("LB:1016")]
|
||
assert labor["이름"] == "화약취급공" and labor["값"] > 0
|
||
found = _get(client, "/api/m01/elements", group="인력", q="보통인부")
|
||
assert key_of("LB:1002") in [x["ref"] for x in found["items"]]
|
||
tables = _get(client, "/api/m01/elements", group="소요량", q="암발파", limit=5)
|
||
assert tables["items"] and all(x["값칸"] for x in tables["items"])
|
||
raw = (store.FOLDER / one["file"]).read_bytes()
|
||
before = _calc(client)["sums"]["계"]
|
||
half = {**one["logic"], "호표": one["logic"]["호표"][:1]} # 보통인부 줄 뺌
|
||
res = client.post(
|
||
"/api/m01/calc",
|
||
json={"key": LOGIC_KEY, "inputs": LOGIC_INPUTS, "row": half},
|
||
)
|
||
lines = res.json()["lines"]
|
||
assert [x["이름"] for x in lines] == ["화약취급공"] and res.json()["sums"]["계"] < before
|
||
new = {**half, "키": "", "원문번호": "새 로직"}
|
||
body = {
|
||
"key": "",
|
||
"inputs": LOGIC_INPUTS,
|
||
"row": new,
|
||
"file": one["file"],
|
||
}
|
||
assert client.post("/api/m01/calc", json=body).json()["ok"] is True
|
||
assert (store.FOLDER / one["file"]).read_bytes() == raw # 시험 계산은 안 씀
|
||
|
||
|
||
def test_쓰기_모양은_읽은_값과_같음() -> None:
|
||
for name in (
|
||
"소요량_건설품셈_13장_플랜트설비공사.json",
|
||
"로직_건설품셈_02장_하천공사.json",
|
||
LABOR,
|
||
):
|
||
data = json.loads((REAL / name).read_text(encoding="utf-8"), parse_float=Decimal)
|
||
text = store.dump(data)
|
||
assert json.loads(text, parse_float=Decimal) == data
|
||
assert "\r" not in text and text.endswith("}\n")
|
||
|
||
|
||
def test_시험계산_글로_온_수도_받음(client: TestClient) -> None:
|
||
def calc(**inputs):
|
||
body = {"key": key_of("GF000219"), "inputs": inputs}
|
||
return client.post("/api/m01/calc", json=body).json()
|
||
|
||
base = {"돌": "견치돌", "쌓기": "골쌓기", "높이": 1, "초과증가율": 80}
|
||
assert calc(뒷길이=35, **base)["sums"]["계"] == pytest.approx(203281.2)
|
||
as_text = calc(**{**base, "뒷길이": "35", "높이": "1"})
|
||
assert as_text["ok"] is True and as_text["sums"]["계"] == pytest.approx(203281.2)
|
||
assert "고르기 밖" in calc(뒷길이="36", **base)["reason"]
|
||
|
||
|
||
def test_저장은_고친_줄만_바꿈(client: TestClient) -> None:
|
||
old = (store.FOLDER / LABOR).read_text(encoding="utf-8").split("\n")
|
||
row, version = _labor_row(client, "1016")
|
||
change = {"op": "edit", "key": row["키"], "row": {**row, "값": row["값"] + 1}}
|
||
body = {"files": [{"file": LABOR, "version": version, "changes": [change]}]}
|
||
assert client.post("/api/m01/save", json=body).status_code == 200
|
||
new = (store.FOLDER / LABOR).read_text(encoding="utf-8").split("\n")
|
||
assert len(new) == len(old) and sum(a != b for a, b in zip(old, new)) == 1
|
||
one = _get(client, "/api/m01/logic", key=LOGIC_KEY)
|
||
path = store.FOLDER / one["file"]
|
||
old = path.read_text(encoding="utf-8")
|
||
edited = {**one["logic"], "비고": "고침"}
|
||
change = {"op": "edit", "key": LOGIC_KEY, "row": edited}
|
||
body = {"files": [{"file": one["file"], "version": one["version"], "changes": [change]}]}
|
||
assert client.post("/api/m01/save", json=body).status_code == 200
|
||
new = path.read_text(encoding="utf-8")
|
||
at = old.index(f'"키": "{LOGIC_KEY}"')
|
||
head = old[: old.rfind("\n", 0, at)] # 고친 요소 앞은 글자 그대로
|
||
assert new.startswith(head) and "\r" not in new
|
||
tail = old[old.index('"키"', at + 1) :] # 다음 요소부터도 그대로
|
||
assert new.endswith(tail)
|
||
|
||
|
||
def test_main_은_시스템관리자만() -> None:
|
||
main = (Path(__file__).resolve().parents[2] / "main.py").read_text(encoding="utf-8")
|
||
assert "app.include_router(m01_master_data_router, dependencies=system_admin_only)" in main
|
||
|
||
|
||
def test_고르기_자재품목_직종(client: TestClient) -> None:
|
||
assert client.get("/api/m01/pick", params={"kind": "procure"}).status_code == 404
|
||
market = _get(client, "/api/m01/pick", kind="price", q="H형강 관급", limit=50)
|
||
# 조달청 값이 아직 안 이어진 줄도 섞여 있음 — 이어진 줄이 하나라도 있으면 됨
|
||
assert any(i["관급"] and i["값"] > 0 for i in market["items"])
|
||
job = _get(client, "/api/m01/pick", kind="job", q="보통인부")
|
||
assert job["items"][0]["값"] > 0 and job["items"][0]["ref"] == key_of("LB:1002")
|
||
assert _get(client, "/api/m01/pick", kind="job", q="CAD설계사")["items"][0]["ref"] == key_of(
|
||
"LB:1"
|
||
)
|
||
assert all(i["값"] is not None for i in _get(client, "/api/m01/pick", kind="job")["items"])
|
||
assert _get(client, "/api/m01/pick", kind="job", q="기술사(건설)")["items"][0]["ref"] == key_of(
|
||
"LB:기술사(건설)"
|
||
)
|
||
assert (
|
||
not _get(client, "/api/m01/pick", kind="job", q="중급기술자(기계")["items"][0].get("구분")
|
||
== "미확보"
|
||
)
|
||
|
||
|
||
def test_구분_목록(client: TestClient) -> None:
|
||
"""구분 목록 API — 파일 머리에 등록된 갈래 그대로(화면이 목록을 안 품음)."""
|
||
got = _get(client, "/api/m01/subs", file=LABOR)
|
||
names = [s["name"] for s in got["subs"]]
|
||
assert got["slot"] == "구분" and names[0] == "건설업" and names[-1] == "산림"
|
||
survey = next(s for s in got["subs"] if s["name"] == "측량")
|
||
assert survey["book"] == "측량노임" and "기능계 도화" in survey["details"]
|
||
machine = _get(client, "/api/m01/subs", file="기계.json")
|
||
names = [s["name"] for s in machine["subs"]]
|
||
assert machine["slot"] == "구분" and names[0] == "[00]토공기계" and names[-1] == "임업기계"
|
||
earthwork = next(s for s in machine["subs"] if s["name"] == "[00]토공기계")
|
||
assert earthwork["book"] == "건설품셈" and "불도저" in earthwork["details"]
|
||
assert _get(client, "/api/m01/subs", file="재료_자재품목.json")["subs"] == []
|
||
|
||
|
||
def test_못_이은_줄만_거름(client: TestClient) -> None:
|
||
every = _get(client, "/api/m01/rows", file="재료_품셈재료.json", size=500)
|
||
cut = _get(client, "/api/m01/rows", file="재료_품셈재료.json", size=500, unlinked=1)
|
||
assert 0 < cut["total"] < every["total"] and not any(r["연결"] for r in cut["rows"])
|
||
|
||
|
||
def test_품셈재료_단가는_연결의_낮은_값과_출처(client: TestClient) -> None:
|
||
key = key_of("GC000628")
|
||
one = _get(client, "/api/m01/logic", key=key)
|
||
cement = one["prices"][key_of("MP:시멘트")]
|
||
assert cement["값"] > 0 and cement["출처"].startswith("MT004176" + ".")
|
||
|
||
|
||
def test_전력은_계약종별을_골라야_값이_섬(client: TestClient) -> None:
|
||
"""후보 조건 「계약종별: 입력」 — 화면에선 값 없음 · 계산 때 로직 입력으로 정해짐."""
|
||
one = _get(client, "/api/m01/logic", key=key_of("GC000893"))
|
||
power = one["prices"][key_of("MP:전력")]
|
||
assert power["값"] is None and power["출처"] is None
|
||
assert "전력 계약종별" in {x["이름"] for x in one["logic"]["입력"]}
|
||
|
||
|
||
def test_하위_거름과_참조_이름(client: TestClient) -> None:
|
||
every = _get(client, "/api/m01/rows", file=LABOR, size=500)
|
||
survey = _get(client, "/api/m01/rows", file=LABOR, size=500, sub="측량")
|
||
assert 0 < survey["total"] < every["total"] and {r["구분"] for r in survey["rows"]} == {"측량"}
|
||
field = _get(client, "/api/m01/rows", file=LABOR, size=500, sub="측량", detail="기능계 도화")
|
||
assert 0 < field["total"] < survey["total"]
|
||
assert {r["상세구분"] for r in field["rows"]} == {"기능계 도화"}
|
||
machine = _get(client, "/api/m01/rows", file="기계.json", size=500, sub="산림품셈")
|
||
assert 0 < machine["total"] < 100 and {r["원문"] for r in machine["rows"]} == {"산림품셈"}
|
||
linked = _get(client, "/api/m01/rows", file="재료_품셈재료.json", size=500)
|
||
keyed = [r for r in linked["rows"] if isinstance(r["연결"], str)]
|
||
assert keyed and all(linked["refs"][r["연결"]] for r in keyed)
|
||
|
||
|
||
def test_자재품목_저장은_띄어쓰기를_안_바꿈(client: TestClient) -> None:
|
||
market = "재료_자재품목.json"
|
||
old = (store.FOLDER / market).read_text(encoding="utf-8").split("\n")
|
||
rows = _get(client, "/api/m01/rows", file=market, size=1)
|
||
row = rows["rows"][0]
|
||
row["물가자료"] = 1
|
||
change = {"op": "edit", "key": row["키"], "row": row}
|
||
body = {"files": [{"file": market, "version": rows["version"], "changes": [change]}]}
|
||
assert client.post("/api/m01/save", json=body).status_code == 200
|
||
new = (store.FOLDER / market).read_text(encoding="utf-8").split("\n")
|
||
diff = [(a, b) for a, b in zip(old, new) if a != b]
|
||
assert len(new) == len(old) and len(diff) == 1
|
||
assert diff[0][1].replace('"물가자료": 1,', '"물가자료": X,').count("{ ") == diff[0][0].count(
|
||
"{ "
|
||
)
|
||
|
||
|
||
def test_준용_저장은_직종_값을_다시_읽고_상태_거름(client: TestClient) -> None:
|
||
subs = _get(client, "/api/m01/subs", file=LABOR)
|
||
assert subs["states"] == ["공표", "산정", "미공표", "미확보", "추정"]
|
||
guess = _get(client, "/api/m01/rows", file=LABOR, size=500, state="추정")
|
||
assert 0 < guess["total"] < 30 and {r["상태"] for r in guess["rows"]} == {"추정"}
|
||
row = next(r for r in guess["rows"] if r["이름"] == "제관공")
|
||
other, version = _labor_row(client, "1016") # 화약취급공
|
||
edited = {**row, "준용": other["키"], "값": 1} # 화면이 보낸 값은 믿지 않음
|
||
body = {
|
||
"files": [
|
||
{
|
||
"file": LABOR,
|
||
"version": guess["version"],
|
||
"changes": [{"op": "edit", "key": row["키"], "row": edited}],
|
||
}
|
||
]
|
||
}
|
||
assert client.post("/api/m01/save", json=body).status_code == 200
|
||
saved = next(r for r in _get(client, "/api/m01/rows", file=LABOR, q=row["키"])["rows"])
|
||
assert saved["준용"] == other["키"] and saved["값"] == other["값"]
|
||
|
||
|
||
def test_유가전력_구분_상세구분_거름(client: TestClient) -> None:
|
||
subs = _get(client, "/api/m01/subs", file="재료_유가전력.json")["subs"]
|
||
assert {s["name"] for s in subs} == {"유류", "전력"}
|
||
power = next(s for s in subs if s["name"] == "전력")
|
||
assert "주택용 저압" in power["details"]
|
||
hit = _get(
|
||
client,
|
||
"/api/m01/rows",
|
||
file="재료_유가전력.json",
|
||
size=500,
|
||
sub="전력",
|
||
detail="주택용 저압",
|
||
)
|
||
assert hit["total"] > 0 and {r["상세구분"] for r in hit["rows"]} == {"주택용 저압"}
|
||
|
||
|
||
def test_재료_고르기_후보_목록(client: TestClient) -> None:
|
||
"""구분·상세구분·규격으로 좁힌 후보 · `기본` = 시험 계산이 쓸 줄 · 자재지역."""
|
||
market = store.FOLDER / "재료_자재품목.json"
|
||
data = json.loads(market.read_text(encoding="utf-8"))
|
||
for row, sub, detail, name, spec in (
|
||
(data["줄"][0], "관·수로", "흄관", "흄관-경상북도(울진군)", "D300 B종"),
|
||
(data["줄"][1], "관·수로", "흄관", "흄관-경상북도(포항시)", "D300 B종"),
|
||
(data["줄"][2], "관·수로", "파형강관", "파형강관", "D300"),
|
||
):
|
||
row.update({"구분": sub, "상세구분": detail, "이름": name, "규격": spec})
|
||
market.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||
|
||
assert client.get("/api/m01/materials").status_code == 422 # 구분은 반드시
|
||
whole = _get(client, "/api/m01/materials", sub="관·수로")
|
||
assert whole["total"] == 3 and whole["기본"] == whole["items"][0]["ref"]
|
||
pipes = _get(client, "/api/m01/materials", sub="관·수로", detail="흄관")
|
||
assert [i["ref"] for i in pipes["items"]] == [r["키"] for r in data["줄"][:2]]
|
||
assert pipes["items"][0]["단위"] and pipes["items"][0]["값"] is not None
|
||
spec = _get(client, "/api/m01/materials", sub="관·수로", spec="D300 B종")
|
||
assert spec["total"] == 2
|
||
near = _get(client, "/api/m01/materials", sub="관·수로", detail="흄관", region="울진")
|
||
assert [i["ref"] for i in near["items"]] == [data["줄"][0]["키"]]
|
||
assert _get(client, "/api/m01/materials", sub="없는구분") == {
|
||
"total": 0,
|
||
"기본": None,
|
||
"items": [],
|
||
}
|
||
|
||
|
||
def test_재료_고르기_후보_다섯_값(client: TestClient) -> None:
|
||
"""후보마다 값 열 다섯을 다 주고 `값` 에 시험 계산이 쓸 낮은 값 · 그 줄이 맨 앞."""
|
||
market = store.FOLDER / "재료_자재품목.json"
|
||
data = json.loads(market.read_text(encoding="utf-8"))
|
||
blank, priced = data["줄"][0], data["줄"][1]
|
||
for row in (blank, priced):
|
||
row.update({"구분": "돌망태붙임", "상세구분": "돌망태", "이름": "돌망태", "규격": "1x1x1"})
|
||
for slot in store.cm.mf.SLOTS:
|
||
row[slot] = None
|
||
priced["물가정보"], priced["관급"] = 20000, 18000
|
||
market.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
|
||
|
||
got = _get(client, "/api/m01/materials", sub="돌망태붙임")
|
||
assert got["total"] == 2 and got["기본"] == priced["키"]
|
||
first, second = got["items"]
|
||
assert first["ref"] == priced["키"] # 값 있는 줄이 맨 앞 — 화면 단가가 비지 않음
|
||
assert first["값들"] == {
|
||
"물가자료": None,
|
||
"유통물가": None,
|
||
"물가정보": 20000,
|
||
"거래가격": None,
|
||
"관급": 18000,
|
||
}
|
||
assert first["값"] == 18000 and first["관급"] is True
|
||
assert second["ref"] == blank["키"] and second["값"] is None
|
||
|
||
|
||
def test_로직_고르기_줄_단가_미리보기(client: TestClient) -> None:
|
||
"""호표 요소가 고르기 조건인 줄 — `prices` 열쇠 = 조건 글 · 후보 수와 값 열 다섯."""
|
||
for path in sorted(store.FOLDER.glob("로직_*.json")):
|
||
rows = json.loads(path.read_text(encoding="utf-8"))["줄"]
|
||
hit = next(
|
||
(
|
||
(row, item["요소"])
|
||
for row in rows
|
||
for item in row.get("호표") or []
|
||
if isinstance(item.get("요소"), dict)
|
||
),
|
||
None,
|
||
)
|
||
if hit:
|
||
break
|
||
row, cond = hit
|
||
got = _get(client, "/api/m01/logic", key=row["키"])
|
||
brief = got["prices"][store.cm.mf.cond_text(cond)]
|
||
assert brief["조건"] == cond and brief["후보"] >= 1
|
||
assert set(brief["값들"]) == set(store.cm.mf.SLOTS)
|