- 측구 줄 맨 오른쪽 [상세] · 투명 막 · 카드 안 가리는 넓은 쪽 · 카드 다시 그리면 갱신 - 구조물 글 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
146 lines
6.4 KiB
Python
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"]
|