Files
Aislo/resources/tester/test_131_b08_material_fix.py
T
eomsangdonandClaude Opus 5.5 fd0ab2817d fix(B08): 자재 집계표 끝수 줄마다(엑셀 꼴) · 총괄 막자갈 줄 · 빈 칸 까닭 안내 · 자재 열 머리 줄바꿈 · 세월교 관 길이(B06 새 키) → 파형강관 · 커플링(131-1)
- 총괄 끝수 = 레미콘 ROUND 2 · 철근 · 와이어메쉬 ROUND 3 · 시멘트 ROUND 0 · 그 밖 ROUNDUP 0 (TS · 파이썬 짝)
- 총괄 막자갈 줄(할증 4 % · 수량 빈칸 — 채집)
- 총괄 · 콘크리트소요 위 「단위수량 빈 줄 n개」 + 구조물 집계표 알림 한 줄
- B06 ford_pipe_length_m(그린 관 한 련 길이 1 m 올림 · 주인 측점만) → 구조물 집계표 관매설 · 커플링 × 련
- test_121 기대값 31.62 · 새 test_131

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PSXsvyDxjNmufteN5bqmt
2026-10-08 20:23:16 +09:00

216 lines
9.6 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.
"""131-1 B08 자재 집계표 고침 — 끝수 줄마다 · 막자갈 줄 · 세월교 관 길이 → 파형강관 · 커플링 ·
안내 글.
견본 = test_121 의 2공구 꼴 · test_120 의 세월교(400 · Φ1000×2련) · B06 견본 횡단(173.087 세월교).
"""
from __future__ import annotations
import asyncio
import copy
import json
import re
import subprocess
from pathlib import Path
import pytest
from test_120_b08_drain_summary import POINTS, STRUCTURES
from test_121_b08_material_sheets import SUMMARY, UNITS, _run_ts
from B08_Quantity import B08_Quantity_Router_Material as router_mod
from B08_Quantity import B08_Quantity_Router_Summary as summary_mod
from B08_Quantity.B08_Quantity_Material_Calc import clean_overrides, compute, finish
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = Path(__file__).parent / "fixtures"
IMPORTS = re.compile(r"^import[^;]*;\n", re.MULTILINE)
needs_node = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
def _rows(sheets):
return {row["key"]: row for row in sheets["material"]["rows"]}
# ── 끝수 처리 줄마다(엑셀 G열 꼴) ──────────────────────────────────────────
def test_끝수_규칙():
assert finish(31.62, "r2") == 31.62 and finish(31.625, "r2") == 31.63 # 반 올림
assert finish(1.271123, "r3") == 1.271 and finish(1.271123, "u0") == 2
assert finish(243.4, "r0") == 243 and finish(243.5, "r0") == 244
assert finish(15.0000000001, "u0") == 15 # 떠도는 끝수는 안 올림
#: 철근 D13 1234.1 kg · 와이어메쉬 10.0004 ㎡ · 레미콘 25-24 10.004 ㎥ — 규칙마다 갈리는 값
EXTRA = {"pipe:chute|메붙임": {"rebar_d13": 1234.1 / 90, "wire_mesh": 10.0004 / 90}}
def _extra_over():
units = {
**UNITS,
"pipe:chute|메붙임": {**UNITS["pipe:chute|메붙임"], **EXTRA["pipe:chute|메붙임"]},
}
units["pipe:revet|돌쌓기(찰)|2.0"] = {
**UNITS["pipe:revet|돌쌓기(찰)|2.0"],
"remicon_25_24_15": 10.004 / 20,
}
return clean_overrides({"units": units})
def test_총괄_끝수_줄마다():
rows = _rows(compute(SUMMARY, _extra_over()))
assert rows["remicon_25_18_80"]["amount"] == 31.62 # ROUND(31 × 1.02, 2)
assert rows["remicon_25_24_15"]["amount"] == 10.1 # ROUND(10.004 × 1.01, 2) = 10.10
assert rows["rebar_d13"]["amount"] == 1.271 # ROUND(1.2341 × 1.03, 3)
assert rows["wire_mesh"]["amount"] == 10.0 # ROUND(10.0004, 3) — 올림 0 이면 11
cement = rows["cement"]
assert cement["amount"] == round(cement["qty"] * 1.03) # ROUND 0
assert rows["stone_buy_45"]["amount"] == 39 # 그 밖 = 올림(38.4)
assert (cement["rounding"], rows["sod"]["rounding"]) == ("round", "up")
assert (rows["rebar_d13"]["places"], rows["remicon_25_18_80"]["places"]) == (3, 2)
def test_막자갈_줄_할증_4_수량_빈칸():
sheets = compute(SUMMARY, clean_overrides({"units": UNITS}))
keys = [row["key"] for row in sheets["material"]["rows"]]
assert keys[:4] == ["sand", "gravel", "rubble", "mixed_aggregate"]
rubble = _rows(sheets)["rubble"]
assert (rubble["group"], rubble["spec"], rubble["unit"]) == ("골재", "막자갈", "㎥")
assert rubble["surcharge"] == 4
assert rubble["qty"] is None and rubble["amount"] is None # 채집 — 자재수량 계가 있어도
assert sheets["quantity"]["total"]["rubble"] == 84.94
def test_거울_끝수_막자갈():
for over in (_extra_over(), clean_overrides({"units": UNITS})):
assert _run_ts(SUMMARY, over) == compute(SUMMARY, over)
# ── 세월교 관 길이 → 구조물 집계표 관매설 · 커플링 → 총괄 파형강관 · 커플링 ────────
def _ford_summary(tmp_path: Path, designs: list[dict]) -> dict:
root = tmp_path / "proj"
edits = root / "B04_PreProcess/drainage/edits"
edits.mkdir(parents=True)
points = [{**p, "source": "user"} for p in POINTS]
(edits / "pipe_points.json").write_text(
json.dumps({"points": points}, ensure_ascii=False), encoding="utf-8"
)
route = root / "B05_Profile/route"
route.mkdir(parents=True)
(route / "structures.json").write_text(
json.dumps({"revision": 1, "structures": STRUCTURES}, ensure_ascii=False),
encoding="utf-8",
)
names = definition.read()["종류"]["pipe"]["이름"]
return summary_mod.pipe_summary(root, designs, names, 6.0)
def _cell(out: dict, at: float, cid: str):
row = next(r for r in out["rows"] if abs(r["chainage_m"] - at) < 0.01)
return row["values"].get(cid)
def test_세월교_관_길이_련수만큼_관매설_커플링(tmp_path):
designs = [
{"chainage_m": 300.0, "design": {"pipe_length_m": 18, "box_length_m": 9.6}},
{"chainage_m": 400.0, "design": {"ford_pipe_length_m": 9}},
]
out = _ford_summary(tmp_path, designs)
assert _cell(out, 400, "pipe:len|1000") == 18 # 9 m × 2련
assert _cell(out, 400, "pipe:cp|1000") == 2 # (올림(9 ÷ 6) − 1) × 2련
column = next(c for c in out["columns"] if c["id"] == "pipe:len|1000")
assert column["group"] == ["관 공", "Φ1000m/m", "관매설"]
fords = [p for p in out["pipes"] if p["column"] == "pipe:cp|1000"]
assert len(fords) == 2 and all(p["length_m"] == 9 for p in fords) # 1본 다시 셈 = 련마다
assert not any("세월교 관 길이" in n for n in out["notes"])
sheets = compute({"columns": out["columns"], "total": out["total"]}, clean_overrides({}))
rows = _rows(sheets)
assert rows["pipe_1000"]["amount"] == 18 and rows["coupling_1000"]["amount"] == 2
assert rows["pipe_800"]["amount"] == 18 # 관 몫은 그대로
def test_세월교_관_길이_없으면_빈칸_알림(tmp_path):
out = _ford_summary(tmp_path, [{"chainage_m": 300.0, "design": {"pipe_length_m": 18}}])
assert _cell(out, 400, "pipe:len|1000") is None
assert any("세월교 관 길이 없음 1곳" in n for n in out["notes"])
@needs_node
def test_서버가_세월교_관_길이를_주인_측점에만():
from B06_Section.B06_Section_Server_Calc_Prebuild import _NPM_SCRIPT, BUNDLE
from common_util.common_util_node_bundle import run_bundle_json
data = json.loads((FIXTURES / "b06_73_4_body_guard_sections.json").read_text("utf-8"))
sections = data["cross_sections"]
ford_at = next(s for s in sections if s.get("ford"))
neighbour = next(s for s in sections if s["chainage_m"] == 180)
neighbour["ford"] = copy.deepcopy(ford_at["ford"])
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
rows = {round(r["chainage_m"], 3): r for r in output["areas"]}
length = rows[173.087]["ford_pipe_length_m"]
assert length > 0 and length == int(length) # 1 m 올림
assert "ford_pipe_length_m" not in rows.get(180, {})
def test_B06_새_키_한_벌():
from B06_Section.B06_Section_Schema import CrossSectionPatch
from B06_Section.B06_Section_Server_Calc_Prebuild import _AREA_KEYS
assert "ford_pipe_length_m" in _AREA_KEYS
assert CrossSectionPatch(chainage_m=1, ford_pipe_length_m=9).ford_pipe_length_m == 9
with pytest.raises(ValueError):
CrossSectionPatch(chainage_m=1, ford_pipe_length_m=-1)
layouts = (ROOT / "B06_Section" / "B06_Section_Structure_Layouts.ts").read_text("utf-8")
block = layouts[layouts.index("STRUCTURE_ROW_KEYS = [") :][:900]
assert '"ford_pipe_length_m"' in block
# ── 안내 글 · 열 머리 줄바꿈 ─────────────────────────────────────────────
def test_단위수량_빈_줄_수_TS():
calc = ROOT / "B08_Quantity" / "B08_Quantity_Material_Calc.ts"
full = json.dumps({"units": UNITS, "mix": {}, "surcharge": {}}, ensure_ascii=False)
body = (
f"\nconst s = {json.dumps(SUMMARY, ensure_ascii=False)};"
f"\nconst full = computeMaterialSheets(s, {full});"
"\nconst none = computeMaterialSheets(s, {units: {}, mix: {}, surcharge: {}});"
"\nconsole.log(JSON.stringify([blankUnitRows(full), blankUnitRows(none)]));"
)
script = IMPORTS.sub("", calc.read_text(encoding="utf-8")) + body
result = subprocess.run(
["node", "--experimental-strip-types", "--no-warnings",
"--input-type=module-typescript", "-e", script],
capture_output=True, text=True, encoding="utf-8", timeout=60,
) # fmt: skip
assert result.returncode == 0, result.stderr
# 자재 줄 10(관매설 3 은 기본 단위 1) — 다 넣으면 0 · 안 넣으면 관매설 뺀 7
assert json.loads(result.stdout) == [0, 7]
def test_서버_알림이_자재_응답에(tmp_path, monkeypatch):
async def summary(_project_id):
return {**SUMMARY, "rows": [], "notes": ["집수정 모양 기록 없음 9곳 — B06 횡단 [저장] 뒤"]}
async def project_root(_project_id):
return tmp_path
monkeypatch.setattr(router_mod, "structure_summary", summary)
monkeypatch.setattr(router_mod, "_project_root", project_root)
payload = asyncio.run(router_mod.material_sheets("p"))
assert payload["notes"] == ["집수정 모양 기록 없음 9곳 — B06 횡단 [저장] 뒤"]
def test_화면_안내_글_열_머리():
ui = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Material.ts").read_text(encoding="utf-8")
assert "단위수량 빈 줄 ${blank}개 — 자재수량 탭에서 넣음" in ui
assert "payload.notes" in ui and 'sheet !== "quantity"' in ui
css = (ROOT / "B08_Quantity" / "B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
assert "[data-b08-material] thead th" in css