"""M02 구조물 집계표 채우기 — 펼침 열 · 측점 줄 · 관 줄 나눔 · 손 값 지킴 (임시 프로젝트).""" from __future__ import annotations import json from pathlib import Path from typing import Any import pytest from config import config_system from M02_MasterTemplete import M02_Table_Fill as fill MASTER = config_system.PROJECT_ROOT / "resources/master_template/table/구조물집계표.json" def _master() -> dict[str, Any]: return json.loads(MASTER.read_text(encoding="utf-8")) @pytest.fixture def project(tmp_path: Path) -> Path: root = tmp_path / "proj" route = root / "B05_Profile/route" route.mkdir(parents=True) structures = [ { "type_id": "masonry_dry", "chainage_m": 85, "start_m": 80, "end_m": 90, "options": {"height_m": 2.0, "length_m": 10}, }, { "type_id": "masonry_wet", "chainage_m": 80, "start_m": 80, "end_m": 90, "options": {"height_m": 2.5, "length_m": 10}, }, { "type_id": "pavement_concrete", "chainage_m": 205, "start_m": 200, "end_m": 230, "options": {"length_m": 30, "width_m": 3, "thickness_cm": 20, "joint_spacing_m": 5}, }, {"type_id": "position_sign", "chainage_m": 300}, { "type_id": "boulder_masonry", "chainage_m": 325, "start_m": 320, "end_m": 330, "options": {"height_m": 2.5, "length_m": 10, "bond": "메쌓기", "stone_cm": "40~60"}, }, ] for index, item in enumerate(structures): item.setdefault("structure_id", f"st{index}") item.setdefault("placement", "interval" if "start_m" in item else "point") (route / "structures.json").write_text( json.dumps({"revision": 1, "structures": structures}, ensure_ascii=False), encoding="utf-8" ) edits = root / "B04_PreProcess/drainage/edits" edits.mkdir(parents=True) points = [ {"chainage_m": 85.05, "source": "user", "options": {"pipe_diameter_mm": "1000"}}, {"chainage_m": 85.05, "source": "user", "options": {"pipe_kind": "흄관"}}, {"chainage_m": 173.09, "source": "user", "facility": "ford_bridge", "options": {}}, ] (edits / "pipe_points.json").write_text(json.dumps({"points": points}), encoding="utf-8") return root def _filled(project: Path, document: dict[str, Any] | None = None) -> dict[str, Any]: return fill.fill_table(project, document or _master(), {85.0: 14.0}) def _col(doc: dict[str, Any], col_id: str) -> dict[str, Any]: return next(column for column in doc["열"] if column["id"] == col_id) def test_펼침_열은_설계에_쓰인_값만_차례대로(project: Path) -> None: doc = _filled(project) ids = [column["id"] for column in doc["열"]] assert [i for i in ids if i.startswith("ms|")] == ["ms|찰쌓기|2.5", "ms|메쌓기|2.0"] assert _col(doc, "ms|메쌓기|2.0")["머리"] == ["돌쌓기", "메쌓기", "H=2.0"] assert [i for i in ids if i.startswith("pp_len|")] == [ "pp_len|흄관|1000", "pp_len|파형강관|1000", ] assert not any(i.startswith("rw|") for i in ids) # 옹벽 없음 → 열 없음 assert "rv" not in ids and "ps" in ids # 펼침 마스터 칸은 빠지고 고정 열은 남음 # 관 유입·유출 기슭막이는 레지스트리 기본값(찰 · 메 · 높이 없음)으로 열이 섬 · id 는 설계 값 assert "rv|돌쌓기(찰)|-" in ids and "rv|돌쌓기(메)|-" in ids assert _col(doc, "rv|돌쌓기(찰)|-")["머리"] == ["돌기슭막이", "찰쌓기", "높이 미입력"] assert _col(doc, "fb|1000|-")["머리"] == ["세월교", "Φ1000×련수 미입력", None] assert not any(i.startswith("pg_in|") for i in ids) # 날개벽·집수정 형식 없음 → 관보호공 없음 def test_묶음_열은_값마다_한데(project: Path) -> None: ids = [column["id"] for column in _filled(project)["열"]] assert [i for i in ids if i.startswith("pp_")] == [ "pp_len|흄관|1000", "pp_cp|흄관|1000", "pp_len|파형강관|1000", "pp_cp|파형강관|1000", ] assert [i for i in ids if i.startswith("pv_")] == [ f"pv_{k}|20" for k in ("b", "l", "w", "a", "jt") ] def test_계산_열은_설계값_딱지_없음(project: Path) -> None: master = _master() assert not [c["id"] for c in master["열"] if c.get("식") and c.get("바인딩")] doc = _filled(project) for col_id in ("pv_a|20", "pv_jt|20", "pp_cp|파형강관|1000", "bm_area|메쌓기|40~60|2.5"): column = _col(doc, col_id) assert "바인딩" not in column and column["펼침틀"]["원열"]["id"] == col_id.split("|")[0] assert _col(doc, "pp_cp|파형강관|1000")["머리"] == ["관공", "파형강관 Φ1000", "커플링밴드"] # 다시 저장 · 채워도 같은 열(계산 열도 원열로 되돌림) again = fill.fill_table(project, fill.strip_design(doc), {85.0: 14.0}) assert [c["id"] for c in again["열"]] == [c["id"] for c in doc["열"]] def test_계산_열_식은_같은_묶음_열로_바뀌고_빈_변수면_안_셈(project: Path) -> None: doc = _filled(project) assert _col(doc, "pv_a|20")["식"] == 'IF([pv_l|20]>0,[pv_b|20]*[pv_l|20]+[pv_w|20],"")' cp = _col(doc, "pp_cp|파형강관|1000")["식"] assert "[pp_len|파형강관|1000]" in cp and "[$파형강관_1본_m]" in cp assert "식" not in _col(doc, "pp_cp|흄관|1000") # 흄관 1본 길이 빈칸 → 계산 안 함 assert _col(doc, "bm_area|메쌓기|40~60|2.5")["식"] == ( 'IF([bm_len|메쌓기|40~60|2.5]>0,[bm_len|메쌓기|40~60|2.5]*2.5,"")' ) def test_측점_줄_같은_측점은_한_줄_관은_나눔(project: Path) -> None: doc = _filled(project) rows = {row["id"]: row for row in doc["줄"]} assert doc["줄"][0]["고정"] == "전구간" interval = rows["s80.00~90.00"]["값"] assert interval["sta"] == "NO.4~NO.4+10" assert interval["ms|찰쌓기|2.5"] == 10 and interval["ms|메쌓기|2.0"] == 10 first, second = rows["s85.05"]["값"], rows["s85.05#1"]["값"] assert first["sta"] == second["sta"] == "NO.4+5.05" assert first["pp_len|파형강관|1000"] == 14.0 and "pp_len|흄관|1000" not in first assert second["pp_len|흄관|1000"] == 14.0 # 한 측점 횡단 관 연장은 하나 — 두 관이 같이 씀 assert first["rv|돌쌓기(찰)|-"] == second["rv|돌쌓기(찰)|-"] == 10 assert rows["s300.00"]["값"]["ps"] == 1 assert [row["값"].get("no") for row in doc["줄"][1:]] == [str(i) for i in range(1, 8)] assert rows["s200.00~230.00"]["식"]["pv_jt|20"].count("/5)") == 1 # 줄눈 간격 5 박음 assert doc["알림"] == [] bare = fill.fill_table(project, _master(), {}) assert bare["알림"] == ["관 연장 없음 2곳 — B06 횡단 설계 전이면 빈칸"] assert not any("pp_len" in key for row in bare["줄"] for key in row["값"]) def test_손_값과_전구간_줄은_다시_채워도_지킴(project: Path) -> None: doc = _filled(project) doc["줄"][0]["값"]["ps"] = 3 next(row for row in doc["줄"] if row["id"] == "s300.00")["값"]["h_intake"] = 2 doc["줄"].append({"id": "u1", "값": {"sta": "NO.9", "h_rockfall": 4}, "손": True}) doc["줄"].append({"id": "s999.00", "값": {"sta": "NO.49+19", "h_waste": 1.5}}) again = _filled(project, doc) rows = {row["id"]: row for row in again["줄"]} assert rows["all"]["값"]["ps"] == 3 assert rows["s300.00"]["값"]["h_intake"] == 2 assert rows["u1"]["값"]["h_rockfall"] == 4 assert rows["s999.00"]["손"] is True and rows["s999.00"]["값"]["h_waste"] == 1.5 assert [c["id"] for c in again["열"]] == [c["id"] for c in doc["열"]] # 두 번 펼치지 않음 def test_측점_표기() -> None: assert fill.station_label(0) == "NO.0" assert fill.station_label(38) == "NO.1+18" assert fill.station_label(59.999) == "NO.3" assert fill.station_label(2413.5) == "NO.120+13.5" def test_저장은_양식과_손_값만_다시_채우면_제자리(project: Path) -> None: master = _master() doc = _filled(project) rows = {row["id"]: row for row in doc["줄"]} rows["s300.00"]["값"]["h_intake"] = 2 rows["s85.05#1"]["값"]["memo"] = "흄관 확인" doc["줄"][0]["값"]["ps"] = 3 saved = fill.strip_design({**doc, "결과": {"계산": {}}}) assert [c["id"] for c in saved["열"]] == [ c["id"] for c in master["열"] ] # 펼침 규칙 열로 되돌림 assert "알림" not in saved and "결과" not in saved assert saved["줄"] == [ {"id": "all", "값": {"sta": "전구간", "ps": 3}, "고정": "전구간"}, {"id": "s85.05#1", "값": {"sta": "NO.4+5.05", "memo": "흄관 확인"}}, {"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}}, ] again = {row["id"]: row for row in _filled(project, saved)["줄"]} assert again["s300.00"]["값"]["h_intake"] == 2 and again["s300.00"]["값"]["ps"] == 1 assert again["s85.05#1"]["값"]["memo"] == "흄관 확인" assert again["s85.05#1"]["값"]["pp_len|흄관|1000"] == 14.0 def test_측점이_없어진_손_값은_맨_아래_비고_줄로(project: Path) -> None: saved = fill.strip_design(_filled(project)) saved["줄"].append({"id": "s300.00", "값": {"sta": "NO.15", "h_intake": 2}}) saved["줄"].append({"id": "s555.00", "값": {"sta": "NO.27+15", "h_intake": 1}}) doc = _filled(project, saved) last = doc["줄"][-1] assert last["id"] == "s555.00" and last["손"] is True assert last["값"] == {"sta": "NO.27+15", "h_intake": 1, "memo": "설계에서 측점 없어짐"} kept = fill.strip_design(doc) assert kept["줄"][-1]["값"]["h_intake"] == 1 # 다음 저장에도 남음