- 자동 맞춤 견본 = 옵션 그룹 · 컨테이너 복사 건(복사본 …__그룹) 빼고 본체만 — 지정 항목 모드 몫이라 자동 맞춤에서 두 번 셈(46-3 · 54-6) - 관 유출 흙막이 열 id 「rv|돌쌓기(찰)|1.5」(기본 높이 54-36) · 세월교 열 「fb|1000|2」(련수 기본 2 · 65-1) - 템플릿 층 채운 표 알림 = 배관 한 개 컨테이너 셋 → 미지정 4건 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVPp2o6Mh2FZJr72yW3ofZ
230 lines
11 KiB
Python
230 lines
11 KiB
Python
"""M02 구조물 집계표 채우기 — 펼침 열 · 측점 줄 · 관 줄 나눔 · 손 값 지킴 (임시 프로젝트)."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
import pytest
|
||
|
||
from M02_MasterTemplete import M02_Table_Fill as fill
|
||
from resources.tester.helper_m02_master_sample import sample_table
|
||
|
||
|
||
def _master() -> dict[str, Any]:
|
||
return sample_table() # 시험 안 견본(PLAN 26-5a) — 실제 마스터 안 읽음
|
||
|
||
|
||
@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 _auto(project: Path, document: dict[str, Any], lengths: dict[float, float]) -> dict[str, Any]:
|
||
"""바인딩 자동 맞춤(`fill_document` 기본) — 프로젝트 `fill_table` 은 지정 항목 모드(PLAN 21-3)라
|
||
펼침 · 묶음 · 손 값 규칙은 이 길로 봄."""
|
||
# 옵션 그룹 · 컨테이너마다 더 서는 건(복사본 `…__그룹`)은 지정 항목 모드 몫 — 자동 맞춤은 본체만 셈
|
||
items = [i for i in fill.collect_items(project, lengths) if "__" not in i["복사본"]]
|
||
return fill.fill_document(document, items)
|
||
|
||
|
||
def _filled(project: Path, document: dict[str, Any] | None = None) -> dict[str, Any]:
|
||
return _auto(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 # 펼침 마스터 칸은 빠지고 고정 열은 남음
|
||
# 관 유출 기슭막이는 레지스트리 기본값(찰 · 흙막이 기본 높이 1.5 — 54-36)으로 열이 섬 · id 는 설계 값
|
||
assert [i for i in ids if i.startswith("rv|")] == ["rv|돌쌓기(찰)|1.5"]
|
||
assert _col(doc, "rv|돌쌓기(찰)|1.5")["머리"] == ["돌기슭막이", "찰쌓기", "H=1.5"]
|
||
# 세월교 기본값(65-1) — 련수 칸 기본 2
|
||
assert _col(doc, "fb|1000|2")["머리"] == ["세월교", "Φ1000×2련", 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 = _auto(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)
|
||
# 식을 지운 계산 열(pv_a · pv_jt · pp_cp) = 빈 값 — 값은 면적산출 알약 연결로 받음(PLAN 13장)
|
||
for col_id in ("pv_a|20", "pv_jt|20", "pp_cp|파형강관|1000", "pp_cp|흄관|1000"):
|
||
assert "식" not in _col(doc, col_id), col_id
|
||
assert all(
|
||
col_id not in row["값"] and col_id not in (row.get("식") or {}) for row in doc["줄"]
|
||
)
|
||
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|돌쌓기(찰)|1.5"] == second["rv|돌쌓기(찰)|1.5"] == 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 "식" not in rows["s200.00~230.00"] # 줄눈 간격이 달라도 열 식이 없으면 줄 식 없음
|
||
assert doc["알림"] == []
|
||
bare = _auto(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 # 다음 저장에도 남음
|
||
|
||
|
||
def test_집계_길이는_구간형이면_시작_종료가_먼저(project: Path) -> None:
|
||
# 노면공 87-2 — 범위를 고쳐도 옛 length_m 이 안 앞섬
|
||
moved = {"start_m": 200, "end_m": 245, "options": {"length_m": 30}}
|
||
assert fill._length(moved) == 45
|
||
assert fill._length({"options": {"length_m": 12}}) == 12 # 점형 · 구간 없음 = 입력 길이
|
||
assert fill._length({"options": {}}) is None
|