# -*- 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 맹암거(그룹 조합) 143-1 — 칸은 그 칸을 낸 조합 사본이 있을 때만 셈: 견본은 칸을 내는 잇기를 모두 두고 연결된 사본 = 지정 전부 · 사본 없으면 없음은 test_143_1 """ 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 import M02_Item_Snapshot as snapshots 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}, "summary_item": _item("SS000100")}, { "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"), "gboxin": _item("SS000106"), "gboxout": _item("SS000107"), }, }, { "chainage_m": 340.0, "facility": "box_culvert", "options": { "body_width_m": 2.5, "body_height_m": 1.5, "wing_in": "없음", "wing_out": "날개벽", }, "summary_item": _item("SS000101"), "group_items": {"gboxout": _item("SS000107")}, }, { "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"), "gfordout": _item("SS000108"), }, }, { "chainage_m": 500.0, "facility": "ford_pavement", "options": { "ford_width_m": 8, "ford_height_m": 1.0, # 물넘이 포장 도수로/돌붙임(136-2) — 관 · 박스 · 세월교와 같은 열 "ford_chute_1_structure": "돌붙임", "ford_chute_1_bond": "찰붙임", "ford_chute_1_area": 12, }, "summary_item": _item("SS000109"), "group_items": {"gfordpavechute1": _item("SS000112")}, }, ] STRUCTURES = [ { "structure_id": "s-ditch", "type_id": "open_ditch", "placement": "point", "chainage_m": 600, "summary_item": _item("SS000110"), }, { "structure_id": "s-exposed", "type_id": "cross_drain_exposed", "placement": "point", "chainage_m": 600, "summary_item": _item("SS000111"), }, { "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"]["이름"] linked = {owner: {} for owner in snapshots.assignments(root)} # 지정 전부 사본 있음 return summary.pipe_summary(root, DESIGNS, names, 6.0, linked=linked) 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 assert _cell(out, 500, "pipe:chute|찰붙임") == 12 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") + ( ROOT / "B08_Quantity/B08_Quantity_Summary_Table.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]