perf(영구저장): 343-2 도엽 3D 면 열쇠 · 지표면 모델 id 유지 · 비용면 메모리 기억 · 추천 노선 열쇠 · 횡단 상세 ETag
- 도엽 3D 면 — 결과 옆 dtm_sheet_{방식}.key.json(등고선 이름:mtime:크기 · 노선 · CRS · 격자 · 여유 · 스무딩 · 화면 상자 · ALGO) 같으면 건너뜀
- 지표면 모델 — (방식 · 필터 · 파일) + 파일 지문 같으면 행 id 살려 고쳐 씀 · 이번 결과에 없는 옛 행만 지움
- 비용면 · 계류 메모리 기억(비용면 서명 · 계류 파일 지문 · 좌표계) · 추천 열쇠 = 서명(배열 통째 해시 없앰)
- 추천 저장본 종류(갈래 · 지형)당 최근 2벌 · 꺼내면 최근으로
- 횡단 상세 GET — 측점 파일 안 지움(valid_cross_names) · 본문 해시 ETag 304
- 시험 test_343_2_persist_keys 13건
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014utrjfoW831Vc5wJUd13kL
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""B06 종횡단 생성 FastAPI 라우터."""
|
||||
|
||||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -8,15 +9,15 @@ from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
import aiomysql
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi import APIRouter, Depends, Request
|
||||
from fastapi.responses import JSONResponse, Response
|
||||
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_Profile.B05_Profile_Engine_Grade_Profile import rebuild_alignment_profile
|
||||
from B05_Profile.B05_Profile_Engine_Sections import (
|
||||
prune_stale_cross_files,
|
||||
relabel_pipe_stations,
|
||||
run_section_generation,
|
||||
valid_cross_names,
|
||||
)
|
||||
from B05_Profile.B05_Profile_Engine_Sections_Core import SectionGenerationOptions
|
||||
from B06_Section.B06_Section_Engine_Attach import attach_saved_shapes
|
||||
@@ -85,6 +86,7 @@ from B06_Section.B06_Section_Schema import (
|
||||
)
|
||||
from B06_Section.B06_Section_Server_Calc_Prebuild import conversion_factors_for, haul_limits_for
|
||||
from common_util.common_util_auth import verify_session
|
||||
from common_util.common_util_http_cache import CACHE_CONTROL
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_surface_confirmation import get_surface_confirmation_params
|
||||
from common_util.common_util_workflow_state import get_workflow_state
|
||||
@@ -266,7 +268,8 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
|
||||
|
||||
longitudinal = json.loads(longitudinal_path.read_text(encoding="utf-8"))
|
||||
stations = longitudinal.get("stations") if isinstance(longitudinal, dict) else None
|
||||
valid_names = prune_stale_cross_files(cross_dir, stations if isinstance(stations, list) else [])
|
||||
# 조회는 지우지 않음 — 잔재는 걸러 읽기만(지우기는 측점을 쓰는 쪽 몫)
|
||||
valid_names = valid_cross_names(cross_dir, stations if isinstance(stations, list) else [])
|
||||
cross_sections = [
|
||||
json.loads(path.read_text(encoding="utf-8"))
|
||||
for path in sorted(cross_dir.glob("cross_*.json"))
|
||||
@@ -282,6 +285,15 @@ def _read_section_detail(project_root: Path, longitudinal_file_path: str) -> dic
|
||||
return {"longitudinal": longitudinal, "cross_sections": cross_sections}
|
||||
|
||||
|
||||
def _etag_json_response(request: Request, body: bytes) -> Response:
|
||||
"""본문 해시 ETag JSON 응답 — 브라우저가 같은 ETag 를 들고 오면 304 만."""
|
||||
etag = f'"{hashlib.blake2b(body, digest_size=16).hexdigest()}"'
|
||||
headers = {"ETag": etag, "Cache-Control": CACHE_CONTROL}
|
||||
if request.headers.get("if-none-match") == etag:
|
||||
return Response(status_code=304, headers=headers)
|
||||
return Response(content=body, media_type="application/json", headers=headers)
|
||||
|
||||
|
||||
def _read_balloon_offsets(row: dict[str, Any]) -> dict[str, list[float]] | None:
|
||||
"""확정 시 저장해 둔 유토곡선 balloon 위치를 꺼낸다. 없거나 형태가 깨졌으면 None."""
|
||||
data = row.get("data")
|
||||
@@ -307,9 +319,12 @@ 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(
|
||||
project_id: UUID, route_id: int
|
||||
) -> SectionDetailResponse | JSONResponse:
|
||||
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플을 반환한다."""
|
||||
project_id: UUID, route_id: int, request: Request
|
||||
) -> SectionDetailResponse | JSONResponse | Response:
|
||||
"""경로의 SVG 렌더링용 종단·횡단 원시 샘플을 반환한다.
|
||||
|
||||
ETag = 응답 본문 해시 — 내용이 같으면 304(1.5 MB 다시 안 받음 · 브라우저 HTTP 캐시).
|
||||
"""
|
||||
try:
|
||||
# 서로 기다릴 이유가 없는 읽기 셋 — 원격 DB 라 순차로 내면 왕복이 그대로 더해진다
|
||||
# (질의 하나 약 12ms, 2026-09-06 실측). 같이 보내 가장 느린 하나의 시간만 쓴다.
|
||||
@@ -350,7 +365,10 @@ async def get_section_detail(
|
||||
project_root,
|
||||
standard,
|
||||
)
|
||||
return SectionDetailResponse(**detail, balloon_offsets=_read_balloon_offsets(longitudinal))
|
||||
body = SectionDetailResponse(
|
||||
**detail, balloon_offsets=_read_balloon_offsets(longitudinal)
|
||||
).model_dump_json()
|
||||
return _etag_json_response(request, body.encode("utf-8"))
|
||||
except FileNotFoundError as exc:
|
||||
return JSONResponse(status_code=404, content={"status": "error", "message": str(exc)})
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
|
||||
Reference in New Issue
Block a user