From b8b4421ff0f3cc754d067644173bb078def49eeb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 16 Oct 2026 13:21:55 +0900 Subject: [PATCH] =?UTF-8?q?fix(349):=20=ED=9A=A1=EB=8B=A8=20=EC=83=81?= =?UTF-8?q?=EC=84=B8=20=EC=95=88=EC=AA=BD=20=ED=98=B8=EC=B6=9C=20=EA=B8=B8?= =?UTF-8?q?=20=EB=B3=B5=EA=B5=AC=20=E2=80=94=20GET=20=EC=B2=98=EB=A6=AC?= =?UTF-8?q?=EA=B8=B0=EB=A7=8C=20ETag=20=C2=B7=20=EC=A1=B0=EB=A6=BD=20?= =?UTF-8?q?=ED=95=A8=EC=88=98=EB=8A=94=20request=20=EC=97=86=EC=9D=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD --- B06_Section/B06_Section_Router.py | 21 ++-- .../tester/test_349_section_detail_inner.py | 112 ++++++++++++++++++ 2 files changed, 125 insertions(+), 8 deletions(-) create mode 100644 resources/tester/test_349_section_detail_inner.py diff --git a/B06_Section/B06_Section_Router.py b/B06_Section/B06_Section_Router.py index 3db908f61..6f996baec 100644 --- a/B06_Section/B06_Section_Router.py +++ b/B06_Section/B06_Section_Router.py @@ -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: diff --git a/resources/tester/test_349_section_detail_inner.py b/resources/tester/test_349_section_detail_inner.py new file mode 100644 index 000000000..e96eef200 --- /dev/null +++ b/resources/tester/test_349_section_detail_inner.py @@ -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