Files
Aislo/resources/tester/test_120_b08_drain_summary.py
T
eomsangdonandClaude Opus 5.5 be5ec734b0 feat(B08): 구조물 집계표 서버 종류 조합 열 빼기 · 열 폭 맨 아래 단 44~100 · 머리 줄바꿈(120-1 · 120-2)
- 화면 — 서버가 펼친 종류(types)를 조합 열에서 뺌
- 열 폭 — 맨 아래 단 글 · 칸 글만 잼(한글 12 · 그 밖 7 + 16) · 최소 44 · 최대 100 · 측점 열도
- B08 범위 CSS 로 집계표 머리 줄바꿈(공용 표 CSS 그대로)
- 시험 — 새 test_120(BOX · 세월교 · 물넘이 · 개거 열 · 값 · 폭 수치 · 서버 Node 박스 길이) · test_98 223 기대 고침

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYJJRCfHfdCGNhmXTjQKV7
2026-10-08 18:32:59 +09:00

270 lines
10 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.
# -*- coding: utf-8 -*-
"""B08 구조물 집계표 「횡단배수」 관 밖 구조물 · 열 폭 — PLAN 120.
견본:
300 관 Φ800 L18 + BOX 2.0×2.0(같은 측점 · 키 겹침 막음) 본체 9.6 m · 유입 옛 「있음」 = 날개벽 ·
유출 흙막이 메 · 높이 빈칸 → 박스 높이 2.0
340 BOX 사용자 입력 2.5×1.5 · 본체 길이 없음(B06 [저장] 전) · 유출 날개벽
400 세월교 Φ1000×2련 월류 폭 12 · 유입 날개벽 · 유출 흙막이 찰(높이 빈칸 → 미입력) 3 + 4
500 물넘이 포장 월류 폭 8 · H=1.0
600 개거 + 노출형 횡단수로 · 700 맹암거(그룹 조합)
"""
from __future__ import annotations
import copy
import json
import re
import subprocess
from pathlib import Path
import pytest
from B06_Section.B06_Section_Schema import CrossSectionPatch
from B08_Quantity import B08_Quantity_Router_Summary as summary
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
ROOT = Path(__file__).resolve().parents[2]
FIXTURES = Path(__file__).parent / "fixtures"
needs_node = pytest.mark.skipif(
not (ROOT / "config" / "node_modules").is_dir(), reason="프론트엔드 의존성 미설치"
)
def _item(key: str) -> dict:
return {"layer": "system", "owner": None, "key": key, "name": key}
POINTS = [
{"chainage_m": 300.0, "options": {"pipe_diameter_mm": 800}},
{
"chainage_m": 300.0,
"facility": "box_culvert",
"options": {
"body_width_m": 2,
"body_height_m": 2,
"wing_in": "있음",
"wing_out": "흙막이",
"outlet_revet_form": "돌쌓기(메)",
"outlet_revet_before_m": 5,
"outlet_revet_after_m": 5,
},
"summary_item": _item("SS000101"),
"group_items": {"gboxbody": _item("SS000102")},
},
{
"chainage_m": 340.0,
"facility": "box_culvert",
"options": {
"body_width_m": 2.5,
"body_height_m": 1.5,
"wing_in": "없음",
"wing_out": "날개벽",
},
},
{
"chainage_m": 400.0,
"facility": "ford_bridge",
"options": {
"pipe_diameter_mm": 1000,
"pipe_count": 2,
"ford_width_m": 12,
"inlet_type": "날개벽",
"wing_in": "있음",
"outlet_type": "흙막이",
"wing_out": "없음",
"outlet_revet_form": "돌쌓기(찰)",
"outlet_revet_before_m": 3,
"outlet_revet_after_m": 4,
},
"group_items": {"gford": _item("SS000103"), "gfordin": _item("SS000104")},
},
{
"chainage_m": 500.0,
"facility": "ford_pavement",
"options": {"ford_width_m": 8, "ford_height_m": 1.0},
},
]
STRUCTURES = [
{"structure_id": "s-ditch", "type_id": "open_ditch", "placement": "point", "chainage_m": 600},
{
"structure_id": "s-exposed",
"type_id": "cross_drain_exposed",
"placement": "point",
"chainage_m": 600,
},
{
"structure_id": "s-ud",
"type_id": "cross_underdrain",
"placement": "point",
"chainage_m": 700,
"group_items": {"gcrossud": _item("SS000105")},
},
]
DESIGNS = [
{"chainage_m": 300.0, "design": {"pipe_length_m": 18, "box_length_m": 9.6, "inlet_basin": ""}},
{"chainage_m": 340.0, "design": {}},
]
@pytest.fixture
def out(tmp_path: Path) -> 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.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 _col(out: dict, cid: str) -> dict:
return next(c for c in out["columns"] if c["id"] == cid)
def test_같은_측점_관_박스_따로(out):
assert _cell(out, 300, "pipe:len|800") == 18
assert _cell(out, 300, "pipe:box|body|2.0×2.0") == 9.6
def test_박스_본체_날개벽_흙막이(out):
body = _col(out, "pipe:box|body|2.0×2.0")
assert body["group"] == ["BOX암거", "2.0 × 2.0", "본체"] and body["label"] == "m"
assert body["combos"] == ["system||SS000101", "system||SS000102"]
assert _cell(out, 300, "pipe:box_culvert|wing|inlet") == 1 # 옛 「있음」 = 날개벽
assert _cell(out, 300, "pipe:box_culvert|wing|outlet") is None # 유출은 흙막이
wing = _col(out, "pipe:box_culvert|wing|inlet")
assert wing["group"] == ["BOX암거", "날개벽", "유입"] and wing["label"] == "개소"
# 흙막이 빈 높이 = 박스 높이 · 공용 흙막이 열
assert _cell(out, 300, "pipe:revet|돌쌓기(메)|2.0") == 10
# 사용자 입력 규격 · 본체 길이 없음 = 빈칸 + 알림
assert _col(out, "pipe:box|body|2.5×1.5")["group"][1] == "2.5×1.5"
assert _cell(out, 340, "pipe:box|body|2.5×1.5") is None
assert _cell(out, 340, "pipe:box_culvert|wing|outlet") == 1
assert _cell(out, 340, "pipe:box_culvert|wing|inlet") is None
assert any("BOX 본체 길이 없음 1곳" in n for n in out["notes"])
def test_세월교_월류폭_개소_날개벽_흙막이_미입력(out):
width = _col(out, "pipe:ford|1000|2|width")
assert width["group"] == ["세월교", "Φ1000×2련", "월류 폭"] and width["label"] == "m"
assert width["combos"] == ["system||SS000103"] # 정의 그룹 밖 잇기 = 저장된 잇기에서
assert _cell(out, 400, "pipe:ford|1000|2|width") == 12
assert _cell(out, 400, "pipe:ford|1000|2|count") == 1
assert _col(out, "pipe:ford|1000|2|count")["label"] == "개소"
assert _cell(out, 400, "pipe:ford_bridge|wing|inlet") == 1
assert _col(out, "pipe:ford_bridge|wing|inlet")["combos"] == ["system||SS000104"]
assert _cell(out, 400, "pipe:ford_bridge|wing|outlet") is None
soil = _col(out, "pipe:revet|돌쌓기(찰)|-")
assert soil["group"] == ["흙막이", "돌 쌓기(찰)", "미입력"]
assert _cell(out, 400, "pipe:revet|돌쌓기(찰)|-") == 7
def test_물넘이_H별_월류폭(out):
pave = _col(out, "pipe:pave|1.0")
assert pave["group"] == ["물넘이포장", "월류 폭", "H=1.0"] and pave["label"] == "m"
assert _cell(out, 500, "pipe:pave|1.0") == 8
def test_개거_노출형_맹암거_개소(out):
assert _cell(out, 600, "pipe:open_ditch") == 1
assert _cell(out, 600, "pipe:cross_drain_exposed") == 1
assert _cell(out, 700, "pipe:cross_underdrain") == 1
assert _col(out, "pipe:open_ditch")["group"] == ["개거(겉도랑)"]
assert _col(out, "pipe:cross_underdrain")["combos"] == ["system||SS000105"]
def test_차례_계_종류(out):
ids = [c["id"] for c in out["columns"]]
order = [
"pipe:revet|돌쌓기(찰)|-",
"pipe:revet|돌쌓기(메)|2.0",
"pipe:len|800",
"pipe:box|body|2.0×2.0",
"pipe:box|body|2.5×1.5",
"pipe:box_culvert|wing|inlet",
"pipe:ford|1000|2|width",
"pipe:ford|1000|2|count",
"pipe:ford_bridge|wing|inlet",
"pipe:pave|1.0",
"pipe:cross_drain_exposed",
"pipe:open_ditch",
"pipe:cross_underdrain",
]
assert [ids.index(i) for i in order] == sorted(ids.index(i) for i in order)
assert out["total"]["pipe:box_culvert|wing|outlet"] == 1
assert set(out["types"]) == {
"pipe",
"box_culvert",
"ford_bridge",
"ford_pavement",
"cross_drain_exposed",
"open_ditch",
"cross_underdrain",
}
def _width(*texts: str) -> int:
source = (ROOT / "B08_Quantity/B08_Quantity_Summary_Data.ts").read_text(encoding="utf-8")
source = re.sub(r"^import[^;]*;\n", "", source, flags=re.MULTILINE)
script = f"{source}\nconsole.log(widthOf(...{json.dumps(texts, ensure_ascii=False)}));"
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
return int(result.stdout)
def test_열_폭_맨_아래_단_글만_44_100():
assert _width("m") == 44 # 최소
assert _width("관매설") == 52 # 한글 12 × 3 + 16
assert _width("H=2.0") == 51 # 7 × 5 + 16
assert _width("m", "1,234.56") == 72 # 칸 글이 길면 칸 글
assert _width("흙막이 돌쌓기(메) 아주 긴 조합 이름") == 100 # 최대 · 머리는 줄바꿈
def test_화면_조합_열_빼기_폭_머리_줄바꿈():
ui = (ROOT / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
assert "served.has(point.facility" in ui and "served.has(item.type_id)" in ui
assert "widthOf(column.label, ...texts)" in ui and "widthOf(column.name)" in ui
assert "widthOf(stationHead, ...labels)" in ui
css = (ROOT / "B08_Quantity/B08_Quantity_UI_Page.css").read_text(encoding="utf-8")
assert '[data-b08-summary="table"] thead th' in css and "white-space: normal" in css
def test_패치가_박스_길이를_받는다():
assert CrossSectionPatch(chainage_m=1, box_length_m=9.6).box_length_m == 9.6
with pytest.raises(ValueError):
CrossSectionPatch(chainage_m=1, box_length_m=-1)
@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"]
box_at = next(s for s in sections if s.get("box"))
neighbour = next(s for s in sections if s["chainage_m"] == 340)
neighbour["box"] = copy.deepcopy(box_at["box"])
output = run_bundle_json(BUNDLE, _NPM_SCRIPT, {"detail": {"cross_sections": sections}})
rows = {round(r["chainage_m"], 3): r for r in output["areas"]}
assert rows[352.14]["box_length_m"] > 0
assert "box_length_m" not in rows[340]