Files
Aislo/resources/tester/test_b08_structure_summary.py
T
eomsangdonandClaude Opus 5 d9c84aaaf0 feat(b08): 구조물 집계표 탭 — 측점별 한 줄·종류별 표·칸마다 출처
- 정본 둘(structures.json·pipe_points.json)과 B06 관 연장을 읽어 종류별 표로 세움
- 한 줄 = 측점 + 실치수(제원 칸) + 개소·연장 · 표마다 개소·연장 합·평균치수
- 칸 출처 자동·사용자·라이브러리(양식 기본값)·빈칸 · 배수관과 세월교는 다른 표
- 이 표에서 고친 값을 B05 가 바꾸면 빨간 테두리와 알림(조용히 안 사라짐)
- 구조물도 탭 앞에 탭 등록

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-13 20:12:24 +09:00

147 lines
6.0 KiB
Python

"""구조물 집계표 (2026-09-13, PLAN 2장).
겨누는 것
① 한 줄 = 측점 + 종류 + 실치수 + 개소·연장 · 종류별 표 · 개소·연장 합 · 평균치수
② 칸 출처 — 정본 값은 자동 · 빈 칸은 양식 기본값(라이브러리) · 둘 다 없으면 빈칸
③ 계곡 통과 시설은 관 지점 정본에서 — 배수관과 세월교는 **다른 표**(브레인 판정) ·
관 연장은 B06 횡단 값(0.5m 안) · 없으면 빈칸(0 아님)
④ 손댄 칸은 「사용자」 · 그 값을 B05 가 바꾸면 자동으로 돌아가되 **알림이 남음**
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT))
import B08_Quantity.B08_Quantity_Router_Material as material_module # noqa: E402
from B05_Profile.B05_Profile_Structures_Repository import save_structures # noqa: E402
from B05_Profile.B05_Profile_Structures_Schema import ( # noqa: E402
StructureInstance,
structure_type_map,
)
from B08_Quantity.B08_Quantity_Engine_StructureSummary import build_summary # noqa: E402
from B08_Quantity.B08_Quantity_Engine_StructureTemplate import load_template # noqa: E402
from common_util.common_util_drainage_pipes import pipe_points_path_in # noqa: E402
PROJECT_ID = "66666666-6666-6666-6666-666666666666"
WALLS = [
{
"structure_id": "w1",
"type_id": "masonry_wet",
"placement": "interval",
"start_m": 100.0,
"end_m": 110.0,
"options": {"height_m": 2.5, "back_len_cm": 35},
},
{
"structure_id": "w2",
"type_id": "masonry_wet",
"placement": "interval",
"start_m": 40.0,
"end_m": 46.0,
"options": {"height_m": 1.5, "length_m": 5.0},
},
]
POINTS = [
{"chainage_m": 60.2, "options": {"pipe_kind": "흄관", "pipe_diameter_mm": 800}},
{"chainage_m": 300.0},
{"chainage_m": 200.0, "facility": "ford_bridge", "options": {"pipe_count": 2}},
]
def _tables(user_cells=None) -> dict:
body = build_summary(
WALLS,
POINTS,
structure_type_map(),
{"masonry_wet": load_template("masonry_wet")},
{60.0: 8.0},
user_cells,
)
return {table["type_id"]: table for table in body["tables"]} | {"_notes": body["notes"]}
def test_종류별_표에_측점_실치수_개소_연장이_선다() -> None:
walls = _tables()["masonry_wet"]
assert [row["id"] for row in walls["rows"]] == ["w2", "w1"] # 측점 차례
w2, w1 = walls["rows"]
assert w1["length_m"] == pytest.approx(10.0) and w1["length_basis"] == "시·종점"
assert w2["length_m"] == pytest.approx(5.0) and w2["length_basis"] == "제원 연장"
assert walls["count"] == 2 and walls["length_total_m"] == pytest.approx(15.0)
assert walls["averages"]["height_m"] == pytest.approx(2.0)
# 칸 출처 — 정본 값 · 양식 기본(뒷길이 45) · 둘 다 없음.
assert w1["cells"]["back_len_cm"] == {"value": 35, "source": "auto"}
assert w2["cells"]["back_len_cm"] == {"value": 45, "source": "library"}
assert w2["cells"]["face_slope_ratio"] == {"value": None, "source": "empty"}
def test_배수관과_세월교는_다른_표이고_관_연장은_B06_값() -> None:
tables = _tables()
pipes = tables["pipe"]
assert [row["chainage_m"] for row in pipes["rows"]] == [60.2, 300.0]
near, far = pipes["rows"]
assert near["length_m"] == pytest.approx(8.0) and near["length_basis"] == "B06 횡단 관 연장"
assert far["length_m"] is None and pipes["length_missing"] == 1 # 0 으로 안 채움
assert near["cells"]["pipe_kind"]["value"] == "흄관"
assert tables["ford_bridge"]["rows"][0]["cells"]["pipe_count"]["value"] == 2
assert tables["ford_bridge"]["length_missing"] == 0 # 점 시설은 연장이 없는 것이 정상
def test_손댄_칸은_사용자이고_B05_가_바꾸면_알림이_남는다() -> None:
marks = {
"w1": {"height_m": {"value": 2.5, "was": 2.0}, "back_len_cm": {"value": 55, "was": 35}}
}
tables = _tables(marks)
w1 = tables["masonry_wet"]["rows"][1]
assert w1["cells"]["height_m"] == {"value": 2.5, "source": "user", "was": 2.0}
# 뒷길이는 55 로 적었는데 정본이 35 — B05 가 바꿈.
assert w1["cells"]["back_len_cm"]["source"] == "auto"
assert w1["cells"]["back_len_cm"]["replaced_user_value"] == 55
assert w1["replaced"] == ["뒷길이"]
assert any("자동값으로 돌아감" in note for note in tables["_notes"])
@pytest.fixture()
def client(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> TestClient:
root = tmp_path / "project"
root.mkdir()
save_structures(
str(root), [StructureInstance.model_validate(w) for w in WALLS], base_revision=0
)
path = pipe_points_path_in(root)
path.parent.mkdir(parents=True)
path.write_text(json.dumps({"route_signature": "", "points": POINTS}), encoding="utf-8")
async def fake_connection(func, *args):
return "stored"
async def designs(project_id):
return [{"chainage_m": 60.0, "design": {"pipe_length_m": 8.0}}]
monkeypatch.setattr(material_module, "run_with_connection", fake_connection)
monkeypatch.setattr(material_module, "resolve_stored_project_path", lambda _p: str(root))
monkeypatch.setattr(material_module, "_designs", designs)
app = FastAPI()
app.include_router(material_module.router)
return TestClient(app)
def test_창구가_정본_둘과_횡단_관_연장을_읽어_표를_낸다(client: TestClient) -> None:
response = client.get(f"/api/projects/{PROJECT_ID}/quantity/structure-summary")
assert response.status_code == 200, response.text
body = response.json()
names = [table["name"] for table in body["tables"]]
assert "배수관" in names and "세월교" in names and "돌쌓기(찰)" in names
pipes = next(table for table in body["tables"] if table["type_id"] == "pipe")
assert pipes["rows"][0]["length_m"] == pytest.approx(8.0)
assert body["revision"] == 1