fix(349): 횡단 상세 안쪽 호출 길 복구 — GET 처리기만 ETag · 조립 함수는 request 없이

343-2 ETag 가 get_section_detail 에 request 인자를 더해 [저장] 뒤 서버 재계산 · 3D 길 모형 서버
짓기 · B09 인쇄 도면 · B07 덧그림 범위가 TypeError 로 끊기던 흠. 처리기 = get_section_detail_route
(ETag · 304) · get_section_detail = 조립만(안쪽 호출 넷 그대로). 진짜 처리 길 시험 test_349.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
This commit is contained in:
2026-10-16 13:21:55 +09:00
co-authored by Claude Opus 5.5
parent 109d5e9a8f
commit b8b4421ff0
2 changed files with 125 additions and 8 deletions
+13 -8
View File
@@ -318,13 +318,21 @@ def _read_balloon_offsets(row: dict[str, Any]) -> dict[str, list[float]] | None:
@router.get("/{project_id}/sections/{route_id}/detail", response_model=SectionDetailResponse)
async def get_section_detail(
async def get_section_detail_route(
project_id: UUID, route_id: int, request: Request
) -> SectionDetailResponse | JSONResponse | Response:
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플을 반환한다.
"""GET 처리기 — ETag = 응답 본문 해시 · 내용이 같으면 304(1.5 MB 다시 안 받음 · HTTP 캐시)."""
detail = await get_section_detail(project_id, route_id)
if isinstance(detail, JSONResponse):
return detail
return _etag_json_response(request, detail.model_dump_json().encode("utf-8"))
ETag = 응답 본문 해시 — 내용이 같으면 304(1.5 MB 다시 안 받음 · 브라우저 HTTP 캐시).
"""
async def get_section_detail(
project_id: UUID, route_id: int
) -> SectionDetailResponse | JSONResponse:
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플 — 안쪽 호출(서버 재계산 · 3D 길 모형 · 인쇄 도면)도
이것을 부름(실패 = JSONResponse)."""
try:
# 서로 기다릴 이유가 없는 읽기 셋 — 원격 DB 라 순차로 내면 왕복이 그대로 더해진다
# (질의 하나 약 12ms, 2026-09-06 실측). 같이 보내 가장 느린 하나의 시간만 쓴다.
@@ -365,10 +373,7 @@ async def get_section_detail(
project_root,
standard,
)
body = SectionDetailResponse(
**detail, balloon_offsets=_read_balloon_offsets(longitudinal)
).model_dump_json()
return _etag_json_response(request, body.encode("utf-8"))
return SectionDetailResponse(**detail, balloon_offsets=_read_balloon_offsets(longitudinal))
except FileNotFoundError as exc:
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
except (OSError, ValueError, json.JSONDecodeError) as exc:
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
"""349 — 안쪽 호출이 진짜 `get_section_detail` 길로 종횡단 상세를 받는지(요구 231 · 232 흠).
343-2 ETag 가 GET 처리기에 `request` 를 더해 안쪽 호출([저장] 뒤 서버 재계산 · 3D 길 모형 서버
짓기 · B09 인쇄 도면)이 TypeError 로 끊겼음. 이 시험은 `get_section_detail` 을 갈음하지 않고 그 아래
DB · 파일 읽기만 가짜로 둠 — 안쪽 함수 모양이 다시 바뀌면 빨강.
"""
from __future__ import annotations
import asyncio
import copy
import sys
from pathlib import Path
from types import SimpleNamespace
from uuid import uuid4
import pytest
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import B05_Profile.B05_Profile_Corridor_Prebuild as corridor # noqa: E402
import B06_Section.B06_Section_Router as b06_router # noqa: E402
import B06_Section.B06_Section_Router_Design as b06_design # noqa: E402
import B06_Section.B06_Section_Server_Calc_Prebuild as prebuild # noqa: E402
from B09_Estimation.draft.B09_Draft_Print_Drawings_Source import section_detail # noqa: E402
DETAIL = {
"longitudinal": {"length_m": 40.0, "stations": [{"chainage_m": 0.0}]},
"cross_sections": [
{"chainage_m": 0.0, "design": {"ground_type": "soil"}},
{"chainage_m": 20.0, "design": {"ground_type": "rock"}},
],
}
class _Stop(Exception):
"""상세를 받은 뒤 서버 재계산을 멈춤 — 그 뒤 길은 342-2 시험 몫."""
@pytest.fixture
def real_detail(monkeypatch, tmp_path):
"""`get_section_detail` 아래 읽기만 가짜 — 처리 함수 자체는 진짜."""
async def run_with(fn, *args):
if fn is b06_router.get_longitudinal_section:
return {"longitudinal_file_path": "longitudinal.json", "data": None}
if fn is b06_router.get_project_storage_relative_path:
return "p"
return [] # 횡단 설계 행
monkeypatch.setattr(b06_router, "run_with_connection", run_with)
monkeypatch.setattr(b06_router, "resolve_stored_project_path", lambda path: str(tmp_path))
monkeypatch.setattr(b06_router, "_read_section_detail", lambda *a: copy.deepcopy(DETAIL))
monkeypatch.setattr(b06_router, "_attach_default_designs", lambda *a: None)
return tmp_path
def test_corridor_prebuild_reads_detail(real_detail) -> None:
detail = asyncio.run(corridor._section_detail(uuid4(), 3))
assert detail is not None
assert [row["chainage_m"] for row in detail["cross_sections"]] == [0.0, 20.0]
def test_server_recompute_reads_detail(real_detail, monkeypatch) -> None:
seen: list = []
async def run_with(fn, *args):
if fn is prebuild.get_project_storage_relative_path:
return "p"
return {}
async def haul_inputs(project_id):
return {}
def enforce(longitudinal, sections, *rest):
seen.append(copy.deepcopy(sections))
raise _Stop
monkeypatch.setattr(prebuild, "run_with_connection", run_with)
monkeypatch.setattr(prebuild, "haul_inputs_for", haul_inputs)
monkeypatch.setattr(prebuild, "resolve_stored_project_path", lambda path: str(real_detail))
monkeypatch.setattr(prebuild, "get_db_pool", lambda: None)
monkeypatch.setattr(b06_design, "stored_standard_cross_section", lambda row: None)
monkeypatch.setattr(b06_design, "enforce_design_lines", enforce)
with pytest.raises(_Stop):
asyncio.run(prebuild.recompute_server_side(uuid4(), 3))
assert [row["chainage_m"] for row in seen[0]] == [0.0, 20.0]
def test_print_drawings_reads_detail(real_detail) -> None:
detail = asyncio.run(section_detail(uuid4(), 3))
assert detail is not None and len(detail["cross_sections"]) == 2
def test_route_handler_keeps_etag(real_detail) -> None:
def request(tag):
return SimpleNamespace(headers={"if-none-match": tag} if tag else {})
handler = next(
route.endpoint
for route in b06_router.router.routes
if getattr(route, "path", "").endswith("/sections/{route_id}/detail")
and "GET" in route.methods
)
first = asyncio.run(handler(uuid4(), 3, request(None)))
etag = first.headers["ETag"]
assert first.status_code == 200 and b"cross_sections" in first.body
assert asyncio.run(handler(uuid4(), 3, request(etag))).status_code == 304