Files
Aislo/resources/tester/test_z01_work_items.py
T
eomsangdonandClaude Opus 5 76f70b675f feat(z01): 총칙 기본 감춤의 기준을 잣대로 · 목록에서는 「공종 아님 · 규정·기준」
랩탑 서브가 낸 기본 감춤(2ee6a7ed)·갈래 펴기(0ff186c1)를 받고, 판정 자리만 옮김.

- 서버에 박혀 있던 `DEFAULTS` → `axis_policy.json` 의 `list_default` 를 읽음
  (총칙 범위는 기관이 정하는 값 — 코드에 박으면 관리자가 못 고침 · 못 읽을 때만 대비값)
- 구실마다 `list_label` — 거르기 상자에서는 「총칙」이 아니라 **「공종 아님 · 규정·기준」**
  · 기본 감춤이되 **지운 것이 아니라 감춘 것**임이 드러남
- 지금 수 — 산림 전부 627 중 공종 568 · 공종 아님 59 / 건설 전부 997 중 공종 983 · 공종 아님 14

⚠ 갈래 열쇠 흠 하나 기록 — `@id` 가 `FW-00105#1.1∼1.5` 꼴로 갈래 문자열을 그대로 담음.
원문이 물결표를 둘(∼ U+223C 26 · ~ U+FF5E 8)로 쓰고 자간 공백만 다른 짝이 6쌍이라,
문자열을 손질하면 열쇠가 갈림. 잣대 `known_defects` 에 적고 수를 시험으로 못박음(갈래 335).
건설은 갈래 키가 비어 편 수 = 줄 수(997).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsGw1Dz9HmhuAxGisxmDPF
2026-09-17 17:04:55 +09:00

200 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Z01 공종 축 API 틀 — 산림·건설 두 kind 를 기초단가와 같은 길로(2026-09-17 브레인 · PLAN_공종축 8-4).
못박는 것: 평평한 잎 목록 · 전체 경로 이름(겹침 없음) · 계산 규칙(choose_one 91 · sum_steps 3) ·
불변 열쇠가 `@id` · 건설도 같은 길(자료가 있어야 상자가 섬) · 고치기는 막힘.
"""
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from Z01_MasterData import Z01_MasterData_Overrides as overrides
from Z01_MasterData import Z01_MasterData_Router as router_module
from Z01_MasterData import Z01_MasterData_WorkItems as work_items
@pytest.fixture
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
from common_util.common_util_auth import verify_session
monkeypatch.setattr(overrides, "OVERRIDE_DIR", tmp_path / "data_master_override")
app = FastAPI()
app.include_router(router_module.router)
app.dependency_overrides[verify_session] = lambda: {"user_id": 7, "role": "SYSTEM_ADMIN"}
return TestClient(app)
def _get(client: TestClient, kind: str, **params) -> dict:
res = client.get(f"/api/master-data/base-prices/{kind}", params=params)
assert res.status_code == 200, res.text
return res.json()
def test_산림_평평한_잎_목록과_계산_규칙(client: TestClient) -> None:
listed = client.get("/api/master-data/base-prices").json()["kinds"]
forest = next(i for i in listed if i["kind"] == "work_item_forest")
assert forest["group"] == "work_item" and forest["rows"] == 627 # 갈래마다 한 줄
# 상자 이름은 이름표 것 — 영문 키(work_item)로 뜨던 자리(2026-09-17 브레인 ①)
assert {i["group"]: i["group_label"] for i in listed} == {
"base_price": "기초단가",
"pumsem_basis": "품셈 기준",
"work_item": "공종 축",
}
table = _get(client, "work_item_forest", size=500, axis_role="") # 「전부」 — 총칙까지
assert table["total"] == 627 and table["stats"] == {"work_items": 365, "units": 627}
assert table["editable"] == [] and set(table["locked"]) == set(table["sortable"])
rows = table["rows"] + _get(client, "work_item_forest", size=500, page=2, axis_role="")["rows"]
assert len({r["@id"] for r in rows}) == 627
assert (
len({r["@key"] for r in rows}) == len({r["name"] for r in rows}) == 365
) # 잎 이름 겹침 — 경로로 풀림 · 목차 오기 바로잡아 358 → 360(12-17-2 · 12-24-1)
by_code = {r["work_item_code"]: r for r in rows}
assert by_code["FP-09-03-02"]["name"] == "토공 › 토사깍기 › 기계"
modes = [r["mode"] for r in table["rules"]]
assert modes.count("choose_one") == 91 and modes.count("sum_steps") == 3
blast = next(r for r in table["rules"] if r["work_item_code"] == "FP-09-05")
assert [s["weight"] for s in blast["steps"]] == ["0.1", "0.9", "1"]
# 계산 규칙 칸은 이름표 이름 — 「sum_steps」 영문으로는 더해야 하는 줄로 안 읽힘(브레인 ②)
assert by_code["FP-09-05-02"]["rule"] == "아래를 단계로 다 더함"
assert by_code["FP-09-03-02"]["rule"] == "아래 하나를 고름"
assert by_code["FP-09-05-02"]["step_weight"] == "0.9"
# 2026-09-17 FW- 가 옴(데스크탑 서브) — 알림이 사라지는 것이 곧 열쇠가 섰다는 증거.
assert all(r["@id"].startswith("FW-") for r in rows) # 불변 열쇠(데스크탑 서브 7dd2c515)
assert not any("불변 열쇠" in line for line in table["notice"])
found = _get(client, "work_item_forest", q="발파암 ")
assert found["total"] == 5 # 절취 · 깍기(연암·보통암·경암) · 집토
def test_건설_공종_축도_같은_길(client: TestClient) -> None:
"""건설(2026-09-17 뽑음) — 부문이 뿌리라 경로가 부문부터 · 열쇠 CW- · 계산 규칙은 전부 choose_one."""
table = _get(client, "work_item_const", size=500, axis_role="")
assert table["total"] == table["stats"]["work_items"] > 900
rows = table["rows"]
assert all(r["@id"].startswith("CW-") for r in rows)
assert all(r["name"].split(" ")[0].endswith("부문") for r in rows)
assert {r["mode"] for r in table["rules"]} == {"choose_one"}
assert not any("불변 열쇠" in line for line in table["notice"])
found = _get(client, "work_item_const", q="굴착(인력/토사)")
assert [r["name"] for r in found["rows"]] == ["공통부문 › 토공사 › 굴착 › 굴착(인력/토사)"]
def test_불변_열쇠가_오면_id_로_씀_자료_파일은_판_id_로_가림(
client: TestClient, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
folder = tmp_path / "data_work_item_master"
folder.mkdir()
fake = {
"dataset_version": {"dataset_id": "pum_const"},
"effective_date": "2026-01-01",
"work_items": [
{"work_item_code": "CP-01", "work_item_key": "CW-00001", "number": "1",
"name": "토공", "parent_code": None, "tables": [], "parent_mode": "sum_steps",
"steps": [{"code": "CP-01-01", "weight": "0.5"}], "steps_basis": "시험", "variant_keys": []},
{"work_item_code": "CP-01-01", "work_item_key": "CW-00002", "number": "1-1",
"name": "인력", "parent_code": "CP-01", "tables": [{"pum_table_id": "C0001"}],
"variant_keys": ["갑", "을"]},
],
} # fmt: skip
(folder / "const_work_item_master_2026-01-01.json").write_text(
json.dumps(fake, ensure_ascii=False), encoding="utf-8"
)
monkeypatch.setattr(work_items, "FOLDER", folder)
table = _get(client, "work_item_const")
assert [r["@id"] for r in table["rows"]] == ["CW-00002#갑", "CW-00002#을"] # 갈래마다 한 줄
assert {r["@key"] for r in table["rows"]} == {"CW-00002"}
assert table["rows"][0]["work_item_code"] == "CP-01-01" # 목차 코드는 칸으로 남음
assert table["rows"][0]["name"] == "토공 인력"
assert table["stats"] == {"work_items": 1, "units": 2}
assert table["rules"][0]["steps"] == [{"@id": "CW-00002", "weight": "0.5"}]
assert _get(client, "work_item_forest")["total"] == 0 # 산림 파일은 이 폴더에 없음
res = client.put(
"/api/master-data/base-prices/work_item_const/CW-00002%23갑",
json={"values": {"name": "바꿈"}},
)
assert res.status_code == 400 and "공종 축 뼈대" in res.json()["detail"]
def test_거르기는_자료에_있는_가름만_서버가_판정(client: TestClient) -> None:
"""화면(서브 b890fba0)이 `filters` 를 받으면 상자를 세우고 고른 값만 보냄 — 판정은 서버 한 곳.
건설 부문 다섯 · 줄 구실(이름은 axis_policy.json). ⚠ 「임도가 쓰는 것」은 잣대가 없어 안 냄."""
const = _get(client, "work_item_const", size=1, axis_role="")
by_key = {f["key"]: f for f in const["filters"]}
assert set(by_key) == {"division", "axis_role"}
division = by_key["division"]
assert division["default"] == ""
assert division["options"][0] == {"value": "", "label": "전부", "rows": const["total"]}
assert [o["value"] for o in division["options"][1:]] == [
"공통부문",
"토목부문",
"건축부문",
"기계설비부문",
"유지관리부문",
]
assert sum(o["rows"] for o in division["options"][1:]) == const["total"]
# 2026-09-17 목록에서는 「총칙」이 아니라 **공종이 아님**을 드러내는 이름(잣대 `list_label`).
assert {o["value"]: o["label"] for o in by_key["axis_role"]["options"]}[
"general_provision"
] == "공종 아님 · 규정·기준"
assert by_key["axis_role"]["default"] == "work_item" # 감춘 것이지 지운 것이 아님
civil = _get(client, "work_item_const", size=500, division="토목부문", axis_role="")
assert civil["total"] == next(
o["rows"] for o in division["options"] if o["value"] == "토목부문"
)
assert all(r["name"].startswith("토목부문 ") for r in civil["rows"])
rules = _get(
client, "work_item_const", size=500, division="공통부문", axis_role="general_provision"
)
assert rules["total"] > 0
assert all(r["name"].startswith("공통부문 적용기준") for r in rules["rows"])
assert [f["key"] for f in _get(client, "work_item_forest", size=1)["filters"]] == ["axis_role"]
assert _get(client, "labor", size=1, division="토목부문")["total"] == 261 # 다른 표는 안 거름
def test_갈래마다_한_줄이라_편_하나를_집는다(client: TestClient) -> None:
"""2026-09-17 브레인 ③ — 일위대가는 갈래마다 다름. 교목굴취(나무높이) 한 줄이면 어느 높이 값인지 못 고름."""
rows = _get(client, "work_item_forest", size=50, q="교목굴취(나무높이)")["rows"]
assert [r["variant"] for r in rows] == [
"1.0 이하", "1.11.5", "1.62.0", "2.12.5", "2.63.0",
"3.13.5", "3.64.0", "4.14.5", "4.65.0",
] # fmt: skip
assert rows[1]["@id"] == "FW-00105#1.11.5" and rows[1]["@key"] == "FW-00105"
# 갈래 없는 공종은 한 줄 · 열쇠에 # 없음
plain = _get(client, "work_item_forest", q="교목굴취(근원직경)")["rows"]
assert [(r["@id"], r["variant"]) for r in plain] == [("FW-00106", None)]
# 갈래 이름으로도 찾힘
assert _get(client, "work_item_forest", q="2.12.5")["total"] >= 1
def test_총칙_줄은_기본_감춤_전부를_고르면_보임(client: TestClient) -> None:
"""2026-09-17 브레인 — 값을 안 보내면 상자 기본값(공종)으로 거름 · 화면은 그 기본값을 상자에 보임."""
for kind in ("work_item_forest", "work_item_const"):
shown = _get(client, kind, size=500)
role = next(f for f in shown["filters"] if f["key"] == "axis_role")
assert role["default"] == "work_item"
counts = {o["value"]: o["rows"] for o in role["options"]}
assert counts["general_provision"] > 0
# 기본 = 공종만 · 찾기 결과로 기본이 흔들리지 않음(총칙만 걸리는 낱말도 기본은 감춤)
assert shown["total"] == counts["work_item"]
assert (
_get(client, kind, size=500, q="적용기준")["total"]
< _get(client, kind, size=500, q="적용기준", axis_role="")["total"]
)
# 「전부」(빈 값을 보냄) → 총칙까지
assert _get(client, kind, size=1, axis_role="")["total"] == counts[""]
assert (
_get(client, kind, size=1, axis_role="general_provision")["total"]
== (counts["general_provision"])
)