# Conflicts: # B08_Quantity/B08_Quantity_Router_Summary.py # B08_Quantity/B08_Quantity_UI_Page.css # B08_Quantity/B08_Quantity_UI_Summary.ts # main.py
245 lines
10 KiB
Python
245 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""B08 구조물 집계표 「횡단배수 | 배관」 실무 꼴 — PLAN 98-1.
|
||
|
||
2공구 구조물집계표(`4.2 수량산출(2공구).xlsx`) 꼴 견본:
|
||
71+13 Φ1200 L22 · 유입 흙막이(찰 H2.0) · 유출 메 H2.0
|
||
· 흙막이 1(메 H2.0 · 선 단 1 — 흙막이 2 는 안 섬) · 찰붙임 10 ㎡ → 커플링 올림(22/6)−1 = 3
|
||
78+7 Φ800 L18 · 집수정 ㄷ(U) · 유출 메 H2.0 10 m · 메붙임 20 ㎡ → 커플링 2
|
||
84+0 Φ800 L16 · 집수정 ㄴ(L) · 유출 메 H2.0 → 커플링 올림(16/6)−1 = 2 · 1본 8 m 면 1
|
||
89+10 Φ800 L(없음) · 유출 높이 빈칸 → 관경 최소 1.8 · 집수정 기록 없음 → 알림
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
from B08_Quantity import B08_Quantity_Router_Summary as summary
|
||
from M02_MasterTemplete import M02_Template_Layers as layers
|
||
from M02_MasterTemplete.StructOptions import M02_SO_Definition as definition
|
||
|
||
COMBO = {"layer": "system", "owner": None, "key": "SS000011", "name": "횡단배수 배관"}
|
||
OUT = {"layer": "company", "owner": "3", "key": "SC3-000001", "name": "기슭막이 메쌓기"}
|
||
WALL = {"side": "outlet", "key": "extra0"}
|
||
|
||
|
||
def _revet(side: str, form: str, height: float | None) -> dict:
|
||
found = {
|
||
f"{side}_type": "기슭막이",
|
||
f"{side}_revet_form": form,
|
||
f"{side}_revet_before_m": 5,
|
||
f"{side}_revet_after_m": 5,
|
||
f"{side}_revet_length_m": 99, # 전 + 후가 앞섬
|
||
}
|
||
if height is not None:
|
||
found[f"{side}_revet_height_m"] = height
|
||
return found
|
||
|
||
|
||
def _chute(bond: str, area: float) -> dict:
|
||
return {"chute_add_1_structure": "돌붙임", "chute_add_1_bond": bond, "chute_add_1_area": area}
|
||
|
||
|
||
POINTS = [
|
||
{
|
||
"chainage_m": 1433.0,
|
||
"options": {
|
||
"pipe_diameter_mm": 1200,
|
||
**_revet("inlet", "돌쌓기(찰)", 2.0),
|
||
**_revet("outlet", "돌쌓기(메)", 2.0),
|
||
"soil_add_1_form": "돌쌓기(메)",
|
||
"soil_add_1_height": 2,
|
||
"soil_add_1_before": 5,
|
||
"soil_add_1_after": 5,
|
||
"soil_add_2_form": "돌쌓기(메)",
|
||
"soil_add_2_height": 2,
|
||
**_chute("찰붙임", 10),
|
||
},
|
||
"summary_item": COMBO,
|
||
"group_items": {"g9lq9uk": OUT},
|
||
},
|
||
{
|
||
"chainage_m": 1567.0,
|
||
"options": {
|
||
"pipe_diameter_mm": 800,
|
||
"inlet_type": "집수정",
|
||
"inlet_structure": "auto",
|
||
**_revet("outlet", "돌쌓기(메)", 2.0),
|
||
**_chute("메붙임", 20),
|
||
},
|
||
"summary_item": COMBO,
|
||
},
|
||
{
|
||
"chainage_m": 1680.0,
|
||
"options": {
|
||
"pipe_diameter_mm": "800",
|
||
"inlet_type": "집수정",
|
||
**_revet("outlet", "돌쌓기(메)", 2.0),
|
||
"chute_add_1_structure": "돌붙임", # 붙임 형식 · 면적 빈칸 → 안 셈
|
||
},
|
||
},
|
||
{
|
||
"chainage_m": 1790.0,
|
||
"options": {"pipe_diameter_mm": 800, "inlet_type": "집수정", **_revet("outlet", "", None)},
|
||
},
|
||
]
|
||
DESIGNS = [
|
||
{
|
||
"chainage_m": 1433.0,
|
||
"design": {"pipe_length_m": 22, "inlet_basin": None, "extra_walls": [WALL]},
|
||
},
|
||
{"chainage_m": 1567.2, "design": {"pipe_length_m": 18, "inlet_basin": "U", "extra_walls": []}},
|
||
{"chainage_m": 1680.0, "design": {"pipe_length_m": 16, "inlet_basin": "L", "extra_walls": []}},
|
||
{"chainage_m": 1790.0, "design": {"extra_walls": []}},
|
||
]
|
||
|
||
|
||
@pytest.fixture
|
||
def root(tmp_path: Path) -> Path:
|
||
found = tmp_path / "proj"
|
||
edits = found / "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"
|
||
)
|
||
return found
|
||
|
||
|
||
def _run(root: Path, one_m: float | None = 6.0) -> dict:
|
||
names = definition.read()["종류"]["pipe"]["이름"]
|
||
return summary.pipe_summary(root, DESIGNS, names, one_m)
|
||
|
||
|
||
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_관경마다_관매설_커플링_관보호공(root):
|
||
out = _run(root)
|
||
assert _cell(out, 1433, "pipe:len|1200") == 22
|
||
assert _cell(out, 1433, "pipe:cp|1200") == 3 # 올림(22/6) − 1
|
||
assert _cell(out, 1567, "pipe:len|800") == 18
|
||
assert _cell(out, 1567, "pipe:cp|800") == 2
|
||
assert _cell(out, 1680, "pipe:cp|800") == 2 # 올림(16/6) − 1
|
||
assert _cell(out, 1567, "pipe:basin|800|U") == 1 # 실제로 선 모양(자동 → 지형 규칙 결과)
|
||
assert _cell(out, 1680, "pipe:basin|800|L") == 1
|
||
assert _cell(out, 1790, "pipe:len|800") is None # 연장 없음 = 빈칸(0 아님)
|
||
assert _run(root, 8.0)["rows"][2]["values"]["pipe:cp|800"] == 1 # 1본 8 m: 올림(16/8) − 1
|
||
ids = [c["id"] for c in out["columns"]]
|
||
assert "pipe:basin|1200|U" not in ids and not any(i.startswith("pipe:basin|1200") for i in ids)
|
||
|
||
|
||
def test_흙막이_형태_높이마다_길이(root):
|
||
out = _run(root)
|
||
# 71+13: 유입 찰 10 · 유출 메 10 + 흙막이 1 메 10(선 단 1 — 흙막이 2 는 안 셈)
|
||
assert _cell(out, 1433, "pipe:revet|돌쌓기(찰)|2.0") == 10
|
||
assert _cell(out, 1433, "pipe:revet|돌쌓기(메)|2.0") == 20
|
||
# 집수정 유입은 흙막이로 안 셈 · 유출만
|
||
assert _cell(out, 1567, "pipe:revet|돌쌓기(찰)|2.0") is None
|
||
assert _cell(out, 1567, "pipe:revet|돌쌓기(메)|2.0") == 10
|
||
# 빈 높이 = 횡단도 관경 최소(Φ800 → 1.8) · 정의 기본 1.5 아님 · 빈 형태 = 미입력
|
||
assert _cell(out, 1790, "pipe:revet|-|1.8") == 10
|
||
head = next(c for c in out["columns"] if c["id"] == "pipe:revet|-|1.8")
|
||
assert head["group"][1:] == ["미입력", "H=1.8"]
|
||
|
||
|
||
def test_돌붙임은_형식_면적이_다_있을_때만(root):
|
||
out = _run(root)
|
||
assert _cell(out, 1433, "pipe:chute|찰붙임") == 10
|
||
assert _cell(out, 1567, "pipe:chute|메붙임") == 20
|
||
assert _cell(out, 1680, "pipe:chute|메붙임") is None
|
||
|
||
|
||
def test_머리_단위_차례_조합_계(root):
|
||
out = _run(root)
|
||
cols = {c["id"]: c for c in out["columns"]}
|
||
assert cols["pipe:len|800"]["group"] == ["관 공", "Φ800m/m", "관매설"]
|
||
assert cols["pipe:len|800"]["label"] == "m"
|
||
assert cols["pipe:cp|800"]["label"] == "ea"
|
||
assert cols["pipe:basin|800|U"]["group"][2] == "관보호공"
|
||
assert cols["pipe:basin|800|U"]["label"] == "ㄷ형" # M02 정의 이름(inlet.form=U)
|
||
assert cols["pipe:chute|찰붙임"]["label"] == "㎡"
|
||
assert cols["pipe:revet|돌쌓기(메)|2.0"]["group"][2] == "H=2.0"
|
||
# 차례: 흙막이 → 돌붙임 → 관 공(관경 오름 · 관매설 · 커플링 · 관보호공 I · L · U)
|
||
ids = [c["id"] for c in out["columns"]]
|
||
assert ids.index("pipe:revet|돌쌓기(찰)|2.0") < ids.index("pipe:revet|돌쌓기(메)|2.0")
|
||
assert ids.index("pipe:revet|돌쌓기(메)|2.0") < ids.index("pipe:chute|찰붙임")
|
||
assert ids.index("pipe:chute|메붙임") < ids.index("pipe:len|800")
|
||
assert ids[ids.index("pipe:len|800") :] == [
|
||
"pipe:len|800",
|
||
"pipe:cp|800",
|
||
"pipe:basin|800|L",
|
||
"pipe:basin|800|U",
|
||
"pipe:len|1200",
|
||
"pipe:cp|1200",
|
||
]
|
||
# 열마다 조합 id(95-3 모달 · 맨 윗 머리) — 그 값을 낸 컨테이너에 이은 조합
|
||
assert cols["pipe:len|800"]["combos"] == ["system||SS000011"]
|
||
assert cols["pipe:revet|돌쌓기(메)|2.0"]["combos"] == ["company|3|SC3-000001"]
|
||
# 계 줄 = 측점 값 합
|
||
assert out["total"]["pipe:len|800"] == 34
|
||
assert out["total"]["pipe:cp|800"] == 4
|
||
assert out["total"]["pipe:revet|돌쌓기(메)|2.0"] == 40 # 20 + 10 + 10
|
||
assert [round(r["chainage_m"]) for r in out["rows"]] == [1433, 1567, 1680, 1790]
|
||
|
||
|
||
def test_알림_연장_집수정_1본(root):
|
||
notes = _run(root, None)["notes"]
|
||
assert any("관 연장 없음 1곳" in n for n in notes)
|
||
assert any("집수정 모양 기록 없음 1곳" in n for n in notes)
|
||
assert any("파형강관_1본_m" in n for n in notes)
|
||
|
||
|
||
def test_1본_길이는_프로젝트_층_변수(root, tmp_path, monkeypatch):
|
||
system = tmp_path / "master"
|
||
(system / "table").mkdir(parents=True)
|
||
(system / "table/구조물집계표.json").write_text(
|
||
json.dumps({"변수": {"파형강관_1본_m": 8}}), encoding="utf-8"
|
||
)
|
||
monkeypatch.setattr(layers, "SYSTEM_ROOT", system)
|
||
assert summary.one_pipe_length(root) == 8
|
||
mine = layers.template_path(layers.project_dir(root), "table", "구조물집계표")
|
||
mine.parent.mkdir(parents=True)
|
||
mine.write_text(json.dumps({"변수": {"파형강관_1본_m": 6}}), encoding="utf-8")
|
||
assert summary.one_pipe_length(root) == 6
|
||
mine.write_text(json.dumps({"변수": {"파형강관_1본_m": ""}}), encoding="utf-8")
|
||
assert summary.one_pipe_length(root) is None
|
||
|
||
|
||
def test_관마다_길이_칸_화면이_1본_바꿔_다시_셈(root):
|
||
# 122 — 응답 `pipes` = 관마다 길이 · 커플링 열(화면 recountCouplings 가 같은 식으로 셈)
|
||
six, eight = _run(root, 6.0), _run(root, 8.0)
|
||
assert six["pipes"] == eight["pipes"]
|
||
assert [(round(p["chainage_m"]), p["column"], p["length_m"]) for p in six["pipes"]] == [
|
||
(1433, "pipe:cp|1200", 22),
|
||
(1567, "pipe:cp|800", 18),
|
||
(1680, "pipe:cp|800", 16),
|
||
(1790, "pipe:cp|800", None),
|
||
]
|
||
for out, one in ((six, 6.0), (eight, 8.0)):
|
||
total = sum(
|
||
summary.couplings(p["length_m"], one) or 0 for p in out["pipes"] if "800" in p["column"]
|
||
)
|
||
assert out["total"]["pipe:cp|800"] == total
|
||
|
||
|
||
def test_라우터_등록():
|
||
text = (Path(__file__).resolve().parents[2] / "main.py").read_text(encoding="utf-8")
|
||
assert "app.include_router(b08_summary_router, dependencies=protected_with_company)" in text
|
||
|
||
|
||
def test_화면이_서버_칸을_그림():
|
||
"""98-4 — 화면은 서버 배관 열 · 칸 · 계만 그림(계산 없음) · 배관 조합 열은 서버 열로 바뀜."""
|
||
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
|
||
# 배관 · 횡단배수 조합 열 대신 서버 열(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
|