Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1
# Conflicts: # B08_Quantity/B08_Quantity_UI_Page.css
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
# -*- 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]
|
||||
@@ -220,7 +220,8 @@ def test_화면이_서버_칸을_그림():
|
||||
base = Path(__file__).resolve().parents[2]
|
||||
ui = (base / "B08_Quantity/B08_Quantity_UI_Summary.ts").read_text(encoding="utf-8")
|
||||
assert "/quantity/summary" in ui and "pipeColumns(pipe, comboPath)" in ui
|
||||
assert '(point.facility ?? "pipe") !== "pipe"' in ui # 배관 조합 열 대신 서버 열
|
||||
# 배관 · 횡단배수 조합 열 대신 서버 열(120 — 서버가 펼친 종류 `types` 를 뺌)
|
||||
assert '!served.has(point.facility ?? "pipe")' in ui
|
||||
assert "combosAt(things, station)" in ui # 모달은 배관 조합까지 그대로
|
||||
locale = (base / "ui_template/ui_template_locale_b4.ts").read_text(encoding="utf-8")
|
||||
assert 'B08_Quantity_Summary_Total: ["계", "Total"]' in locale
|
||||
|
||||
@@ -43,6 +43,8 @@ _COMPUTED_FIELDS = {
|
||||
# B08 하상경사 원천(110) — 관 경사 · 박스 원지반 경사(%) · 관 길이와 같은 다리.
|
||||
"pipe_slope_pct",
|
||||
"box_ground_slope_pct",
|
||||
# BOX 본체 길이(120) — 같은 다리.
|
||||
"box_length_m",
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user