Files
Aislo/resources/tester/test_704_4_spec.py
T
eomsangdonandClaude Opus 5.5 90273507c5 fix(규격 표 화면): 열 합계 한 자리 · 규격 줄 칸 폭 · B08 · B09 금액 = 열 수량 × 단가 시험 (PLAN 704-4)
- 격자 합계 줄 규격 칸 = 금액 칸처럼 소수 한 자리
- 규격 이름 · 열 이름 칸 6em — 좁은 카드에서 한 줄
- 임시 storage 시험 — 사본 셈 반영합 = 층 문서 그 열 셈(B08) · 대가 줄 금액 = 열 수량 × 단가(B09)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:41:13 +09:00

250 lines
12 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.
"""704-4 규격 열 — 설계 고르기 = 열(summary_item `spec`) · 사본이 고른 열을 굳힘 ·
열 바꿈 뒤 B09 덮개 남음 · B07 한 장 판정 = 항목 + 열 · 열 지움 막음(쓰는 사본).
임시 저장소(실제 마스터 · storage 안 씀 · M01 읽기만).
"""
from __future__ import annotations
import json
import shutil
from pathlib import Path
from typing import Any
import pytest
from B07_DesignDetail.B07_DesignDetail_Router_Support_Structure import structure_sheets
from B09_Estimation.B09_Estimation_Calc_Labor import compute_labor
from B09_Estimation.B09_Estimation_Labor_Source import daega_inputs
from common_util.common_util_summary_item import parse_summary_item
from config import config_system
from M02_MasterTemplete import M02_Item_Snapshot as snap
from M02_MasterTemplete import M02_Labor_Spec as spec
from M02_MasterTemplete import M02_Template_Layers as layers
from resources.tester.helper_m02_master_sample import SAMPLE
MS = {"layer": "system", "owner": None, "key": "SS000022", "name": "돌쌓기"}
SPEC = {
"이름": "H",
"열": ["1.5", "2.5"],
"기본": "1.5",
"수량": {"GF000219": {"1.5": 2, "2.5": 3}},
}
LABOR = {
"종류": "일위대가",
"열": "ms",
"단위": "m",
"담은로직": [
{"로직": "GF000219", "수량": {"손": 2}}, # 메쌓기 — 열마다 수량
{"로직": "GF000119", "수량": {"손": 0.5}}, # 노체포설 — 공통
],
"자재": [],
"규격": SPEC,
}
# ── summary_item spec ──────────────────────────────────────────────────────
def test_summary_item_spec_있음_없음():
assert parse_summary_item({**MS, "spec": " 2.5 "}) == {**MS, "spec": "2.5"}
assert parse_summary_item(MS) == MS # 옛 기록 = 칸 없음 그대로
with pytest.raises(ValueError, match="모르는 칸"):
parse_summary_item({**MS, "열": "2.5"})
for bad in ("", "x" * 21, 2.5):
with pytest.raises(ValueError, match="spec"):
parse_summary_item({**MS, "spec": bad})
# ── 굳히기 ─────────────────────────────────────────────────────────────────
def test_굳히기_고른_열_지운_열_옛_문서():
got = spec.freeze({**LABOR, "계산값": {"x": 1}}, "2.5")
assert got["담은로직"][0]["수량"] == {"손": 3} and got["담은로직"][1]["수량"] == {"손": 0.5}
assert got["규격"] == {"이름": "H", "열": ["2.5"], "기본": "2.5", "수량": {}}
assert "계산값" not in got
assert spec.freeze(LABOR, "지운열")["담은로직"][0]["수량"] == {"손": 2} # = 기본
old = {k: v for k, v in LABOR.items() if k != "규격"}
assert spec.freeze(old, "2.5") is old
# ── 임시 저장소 사본 ───────────────────────────────────────────────────────
@pytest.fixture
def world(tmp_path: Path, monkeypatch) -> dict[str, Any]:
system = tmp_path / "master"
system.mkdir()
shutil.copy2(SAMPLE / layers.COMBO_FILE, system / layers.COMBO_FILE)
for kind in ("table", "structure", "area"):
shutil.copytree(SAMPLE / kind, system / kind)
(system / "labor").mkdir()
(system / "labor" / "ms.json").write_text(json.dumps(LABOR), encoding="utf-8")
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
storage = tmp_path / "storage"
monkeypatch.setattr(config_system, "STORAGE_BASE_DIR", str(storage))
root = storage / "3" / "12" / "proj"
(root / "B05_Profile/route").mkdir(parents=True)
(root / "B04_PreProcess/drainage/edits").mkdir(parents=True)
return {"system": system, "root": root}
def _save(root: Path, specs: list[str | None]) -> dict[str, list[str]]:
rows = [
{
"structure_id": f"st{n}",
"type_id": "masonry_dry",
"placement": "interval",
"chainage_m": 85 + n * 100,
"start_m": 80 + n * 100,
"end_m": 90 + n * 100,
"options": {"height_m": 2.0, "length_m": 10},
"summary_item": {**MS, **({"spec": s} if s else {})},
}
for n, s in enumerate(specs)
]
(root / "B05_Profile/route/structures.json").write_text(
json.dumps({"revision": 1, "structures": rows}, ensure_ascii=False), encoding="utf-8"
)
(root / "B04_PreProcess/drainage/edits/pipe_points.json").write_text(
'{"points": []}', encoding="utf-8"
)
return snap.sync_snapshots(root)
def _block(root: Path, owner: str) -> tuple[dict, dict]:
got = daega_inputs(root)
return got, next(b for b in got["blocks"] if b["id"] == got["owners"][owner])
def test_사본이_고른_열을_굳힘_manifest_규격(world):
root = world["root"]
assert _save(root, ["2.5"])["만듦"] == ["st0"]
folder = snap.structures_dir(root) / "st0"
doc = json.loads((folder / "일위대가.json").read_text(encoding="utf-8"))["줄"]
assert [x["수량"] for x in doc["담은로직"]] == [{"손": 3}, {"손": 0.5}]
manifest = json.loads((folder / snap.MANIFEST_NAME).read_text(encoding="utf-8"))
assert manifest["규격"] == {"이름": "H", "열": "2.5"} and manifest["원본"]["spec"] == "2.5"
assert _save(root, ["2.5"])["만듦"] == [] # 같은 항목 · 같은 열 = 그대로
def test_열_바꿈_뒤_덮개_남음(world):
root = world["root"]
_save(root, ["1.5"])
_, before = _block(root, "st0")
cids = [ln["cid"] for ln in before["lines"]]
assert before["cover"] == "S:st0" and [ln["qty"] for ln in before["lines"]] == [2, 0.5]
cover = {before["cover"]: {cids[1]: {"qty": 0.7}}} # 공통 줄 품을 고침
got = _save(root, ["2.5"])
assert got["지움"] == ["st0"] and got["만듦"] == ["st0"] # 열 바꿈 = 새 사본
after_all, after = _block(root, "st0")
assert after["cover"] == before["cover"] and [ln["cid"] for ln in after["lines"]] == cids
sheets = {"labor": [], "material": [], "machine": [], "gaps": []}
out = compute_labor(after_all, sheets, {}, cover)
lines = next(b for b in out["daega_blocks"] if b["id"] == after["id"])["lines"]
assert lines[0]["qty"] == 3 # 새 열 수량
assert lines[1]["qty"] == 0.7 and lines[1]["covered"] == ["qty"] # 덮개 그대로
assert out["daega_orphans"] == []
def test_B07_한_장_판정_항목_더하기_열(world):
root = world["root"]
_save(root, ["1.5", "1.5", "2.5"])
ids = [s["id"] for s in structure_sheets(root) if s["id"].startswith("structure_system")]
assert len(set(ids)) == 2 # 같은 열 둘 = 한 장 · 다른 열 = 따로
def test_열_지움_쓰는_사본_찾기(world):
root = world["root"]
_save(root, ["2.5", None])
assert spec.users(world["system"], {"SS000022"}, {"2.5"}) == ["proj/st0"]
assert spec.users(world["system"], {"SS000022"}, {"1.5"}) == []
assert spec.users(world["system"], {"SS000001"}, {"2.5"}) == []
def test_쓰는_열_지움_409_안_쓰는_열_됨(world):
from M02_MasterTemplete import M02_MasterTemplete_Store as store
root = world["root"]
_save(root, ["2.5"])
path = world["system"] / "labor" / "ms.json"
with store.at(world["system"]):
with pytest.raises(store.StoreError) as got:
spec.guard(path, {"규격": {**SPEC, "열": ["1.5"]}}, "ms")
assert got.value.status == 409 and "proj/st0" in str(got.value.detail)
spec.guard(path, {"규격": {**SPEC, "열": ["2.5", "3.0"]}}, "ms") # 1.5 = 안 씀
spec.guard(path, {"규격": SPEC}, "ms")
# ── 화면 자료(TS · Node) — 격자 규격 열 · 고르기 창 spec ─────────────────────
def test_격자_규격_열_수량_칸_열마다():
from resources.tester import test_704_3_hopyo_grid as g
files = [g.GRID, "M02_MasterTemplete/M02_MasterTemplete_Labor_Calc.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Spec.ts",
"M02_MasterTemplete/M02_MasterTemplete_Labor_Grid.ts"] # fmt: skip
doc = {**g.DOC, "규격": {"이름": "H", "열": ["1.5", "2.0"], "기본": "2.0",
"수량": {"GF1": {"1.5": 1.5, "2.0": 2.09}, "MT1": {"1.5": 4}}}} # fmt: skip
doc["자재"] = [{**doc["자재"][0], "수량": {"손": 5}}, {**doc["자재"][1], "수량": {"손": 2}}]
out = g._node(files, (
f"\nconst b = laborBlock({json.dumps(doc, ensure_ascii=False)}, {json.dumps(g.CALC, ensure_ascii=False)}, new Map());"
"\nconst grid = hopyoGrid([b]);"
"\nconsole.log(JSON.stringify({ cols: grid.columns.map((c) => [c.key, c.label, c.group]),"
" rows: grid.rows, specs: b.specs }));"
), g.STUBS) # fmt: skip
cols = [c for c in out["cols"] if c[0].startswith("qty")]
assert cols == [["qty_0", "1.5", ["H 열"]], ["qty_1", "2.0 (기본)", ["H 열"]]]
line = out["rows"][1] # 첫 구성 줄
assert line["qty_0"] == 1.5 and line["qty_1"] == 2.09
mt1 = next(r for r in out["rows"] if r["name"] == "ㄴ 큰돌")
assert mt1["qty_0"] == 4 and mt1["qty_1"] == 5 # 2.0 빈 열 = 공통 수량
t15, t20 = out["specs"]["totals"]
assert t20 == pytest.approx(112559.55) # 기본 열 = 금액 칸 합
assert t15 == pytest.approx(529950 * 1.5 / 10 + 1800) # 열 수량 · 별도계상 MT1 = 합 밖
def test_고르기_창_spec_돌려줌():
import subprocess
data = Path(__file__).resolve().parents[2] / "ui_template/ui_template_item_picker_data.ts"
body = (
data.read_text("utf-8")
+ """
const row = { key: "SS000022", 열: "ms", 경로: ["석축", "메쌓기"],
labor: { 규격: { 이름: "H", 기본: "2.0", 열: [{ 이름: "1.5", 합계: 1 }, { 이름: "2.0", 합계: 2 }] } } };
const one = { ...row, labor: { 규격: { 이름: "H", 기본: "1.5", 열: [{ 이름: "1.5", 합계: 1 }] } } };
const plain = { key: "SS000001", 열: "rv" };
const cur = { layer: "system", owner: null, key: "SS000022", name: "x", spec: "1.5" };
console.log(JSON.stringify([toSummary(row, "1.5"), toSummary(one, "1.5"), toSummary(plain, "1.5"),
startSpec(row, cur), startSpec(row, null), startSpec(row, { ...cur, spec: "지운" }), startSpec(plain, cur)]));
"""
)
got = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings", "--input-type=module-typescript"],
input=body, capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert got.returncode == 0, got.stderr
two, single, plain, *starts = json.loads(got.stdout)
assert two == {"layer": "system", "owner": None, "key": "SS000022",
"name": "석축 메쌓기 (H=1.5)", "spec": "1.5"} # fmt: skip
assert single["name"] == "석축 메쌓기" and single["spec"] == "1.5" # 열 하나 = 이름 그대로
assert "spec" not in plain # 규격 없는 옛 항목
assert starts == ["1.5", "2.0", "2.0", None]
def test_B08_B09_금액_열_수량_곱하기_단가(world):
"""B08 = 사본 1 단위 값(사본 셈 반영합) = 층 문서 그 열 셈 · B09 = 대가 줄 금액 = 열 수량 × 단가."""
from M02_MasterTemplete import M02_Item_Prices as item_prices
from M02_MasterTemplete import M02_Labor_Doc as labor
root = world["root"]
_save(root, ["2.5"])
folder = snap.structures_dir(root) / "st0"
read = lambda name: json.loads((folder / name).read_text(encoding="utf-8")) # noqa: E731
copy = item_prices.calc(read("일위대가.json"), read("단가.json"), read("입력값.json"), {}, None)
layer = labor.clean(LABOR)
want = labor.settle(labor.calc(layer, {}), layer, {}, "2.5")["반영합"]
assert copy["반영합"]["계"] == pytest.approx(want["계"]) # B08 1 단위 값 = 2.5 열
first = labor.settle(labor.calc(layer, {}), layer, {}, "1.5")["반영합"]
assert want["계"] != pytest.approx(first["계"])
got, block = _block(root, "st0")
price = {ln["key"]: {"labor": 1000.0, "material": 0.0, "expense": 0.0} for ln in block["lines"]}
sheets = {"labor": [], "material": [], "machine": [], "gaps": []}
out = compute_labor(got, sheets, price, {})
lines = next(b for b in out["daega_blocks"] if b["id"] == block["id"])["lines"]
assert [ln["amounts"]["labor"] for ln in lines] == [3000, 500] # 3 × 1000 · 공통 0.5 × 1000