Files
Aislo/resources/tester/test_138_cross_detail.py
T
eomsangdonandClaude Opus 5.5 48c4f7d7d4 feat(B06): 횡단 카드 [상세] 모달 — 구조물 글 · CAD 꼴 그림 · B07 횡단 수량표(138-4)
- 측구 줄 맨 오른쪽 [상세] · 투명 막 · 카드 안 가리는 넓은 쪽 · 카드 다시 그리면 갱신
- 구조물 글 structure_note — USER_TOUCHED_KEYS 두 벌 · CrossSectionPatch · 세션 초안 → [저장]
- 표 값 = 서버 cross-quantity-table 한 길(B07 _quantity_table 을 캐시 설계로) · 칸 배치 B07 그대로
- test_138_cross_detail

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JywHxQuWPk6UYXTebyrBoU
2026-10-09 15:01:01 +09:00

146 lines
6.4 KiB
Python

# -*- coding: utf-8 -*-
"""B06 횡단 [상세] 모달(PLAN 138-4) — 구조물 글 새 키 저장 왕복 · B07 수량표 값 POST 한 길.
- 글(`structure_note`): 브라우저 세션 초안 → [저장] patch → 서버 스키마 → 정본 병합 →
재계산(포장 · 세월교 · 사토장)에서도 남음.
- 표: `cross-quantity-table` 이 B07 `_quantity_table` 을 캐시 설계로 돌린 값과 칸 배치를 줌.
./venv/Scripts/python.exe -m pytest resources/tester/test_138_cross_detail.py -q
"""
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from fastapi import FastAPI # noqa: E402
from fastapi.testclient import TestClient # noqa: E402
from B06_Section import B06_Section_Router_CrossTable as table_mod # noqa: E402
from B06_Section import B06_Section_Router_Design as design_mod # noqa: E402
from B06_Section.B06_Section_Schema import CrossSectionPatch # noqa: E402
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Table import QUANTITY_VALUE_KEYS # noqa: E402
# ── 글 저장 왕복 ─────────────────────────────────────────────────────────
def test_글은_patch_스키마를_지나_정본_병합_값으로_남는다():
patch = CrossSectionPatch(chainage_m=40.0, structure_note="L형 측구 · 집수정 보강")
dumped = patch.model_dump()
# 확정 · 저장 경로와 같은 걸름(`_apply_section_edits`) — 최상위 None 만 걷음
merged = {k: v for k, v in dumped.items() if k != "chainage_m" and v is not None}
assert merged == {"structure_note": "L형 측구 · 집수정 보강"}
# 빈 글 = 지움 — 걸러지지 않고 실림
empty = CrossSectionPatch(chainage_m=40.0, structure_note="").model_dump()
assert empty["structure_note"] == ""
def test_글은_재계산에서도_남는다(monkeypatch):
monkeypatch.setattr(design_mod, "pavement_ranges", lambda root: [(0.0, 100.0)])
monkeypatch.setattr(design_mod, "ford_surface_drops", lambda root: [])
monkeypatch.setattr(design_mod, "design_elevation_from_longitudinal", lambda lon, ch: 100.0)
monkeypatch.setattr(design_mod, "curve_widening_args", lambda section: {})
monkeypatch.setattr(
design_mod, "compute_cross_design", lambda samples, elev, **kw: {"paved": kw["paved"]}
)
sections = [{"chainage_m": 40.0, "design": {"paved": False, "structure_note": "메모"}}]
assert design_mod.enforce_pavement_ranges({}, sections, Path("."), None) == 1
assert sections[0]["design"]["structure_note"] == "메모"
def test_브라우저가_글을_저장_patch_에_싣는다():
persist = (PROJECT_ROOT / "B06_Section" / "B06_Section_UI_Page_Persist.ts").read_text(
encoding="utf-8"
)
assert "readStructureNotes(" in persist and ".structure_note = note" in persist
state = (PROJECT_ROOT / "A00_Common" / "b_page_state.ts").read_text(encoding="utf-8")
assert '"structure-notes": { bucket: "draft", scope: "route" }' in state
# ── 수량표 값 ────────────────────────────────────────────────────────────
SOURCE = {"chainage_m": 40.0, "center_z": 101.5, "samples": []}
DESIGN = {
"design_elevation_m": 100.25,
"cut_soil_area_m2": 1.234,
"cut_rock_area_m2": 0.5,
"fill_area_m2": 2.0,
}
def test_표_값은_B07_수량표_한_벌():
sheet = table_mod.cross_quantity_sheet(SOURCE, DESIGN)
table = sheet["quantity_table"]
assert set(QUANTITY_VALUE_KEYS) <= set(table)
assert table["ground"] == 101.5 and table["planned"] == 100.25
assert round(table["cut"], 6) == 1.25 and table["fill"] == 0.0
assert table["cut_soil"] == 1.234 and table["embankment"] == 2.0
# 글은 B07 표에 찍히는 꼴 그대로 · 값 없음 = 「-」
assert sheet["text"]["cut_soil"] == "1.23"
assert sheet["text"]["fence"] == "-"
layout = sheet["layout"]
assert [row[1] for row in layout["header"]] == ["ground", "planned", "fill", "cut"]
assert len(layout["left"]) == len(layout["middle"]) == len(layout["right"]) == 6
def test_설계_없으면_원본_칸만():
sheet = table_mod.cross_quantity_sheet(SOURCE, None)
assert sheet["quantity_table"]["ground"] == 101.5
assert sheet["quantity_table"]["planned"] is None
assert sheet["text"]["cut_soil"] == "-"
class _Pool:
def acquire(self):
pool = self
class _Ctx:
async def __aenter__(self):
return pool
async def __aexit__(self, *exc):
return False
return _Ctx()
def test_POST_한_길이_캐시_설계로_표를_낸다(monkeypatch):
seen = {}
async def longitudinal(connection, project_id, route_id):
return {"longitudinal_file_path": "B05_Profile/route/longitudinal.json"}
async def storage(connection, project_id):
return "proj"
def read_inputs(root, long_path, chainage):
seen["chainage"] = chainage
return [], 100.0, False, SOURCE
monkeypatch.setattr(table_mod, "get_db_pool", lambda: _Pool())
monkeypatch.setattr(table_mod, "get_longitudinal_section", longitudinal)
monkeypatch.setattr(table_mod, "get_project_storage_relative_path", storage)
monkeypatch.setattr(table_mod, "resolve_stored_project_path", lambda rel: "C:/x")
monkeypatch.setattr(table_mod, "read_cross_design_inputs", read_inputs)
app = FastAPI()
app.include_router(table_mod.router)
client = TestClient(app)
url = "/api/projects/00000000-0000-0000-0000-000000000001/sections/7/cross-quantity-table"
response = client.post(url, json={"chainage_m": 40.0, "design": DESIGN})
assert response.status_code == 200, response.text
body = response.json()
assert seen["chainage"] == 40.0
assert body["quantity_table"]["cut_soil"] == 1.234
assert body["text"]["planned"] == "100.25"
# 측점 파일이 없으면 404 · 사유 그대로
def missing(*args):
raise FileNotFoundError("해당 측점의 횡단 상세 파일을 찾을 수 없습니다.")
monkeypatch.setattr(table_mod, "read_cross_design_inputs", missing)
response = client.post(url, json={"chainage_m": 41.0, "design": None})
assert response.status_code == 404
assert "횡단 상세 파일" in response.json()["message"]