perf(B04): 응답 압축 + 좌표 자릿수 정리로 내려보낼 양 축소

DB·파일에는 뜻 없는 자릿수가 붙어 있고(1.4000000000000001 꼴) 서버는 응답을
전혀 압축하지 않고 있었음. B05 한 번 진입에 118MB 수신(2026-09-06 실측).

- main.py: 압축 미들웨어(1KB 이상). 3D 예상형상(/corridor)은 예외 —
  숫자 배열이라 절반만 줄면서 압축에 493ms 듦(18.6->9.4MB).
- common_util_json: round_floats(value, digits) + LONLAT_DIGITS 7(1.1cm) ·
  METRE_DIGITS 6. 저장 파일은 그대로 두고 내려보낼 때만 줄임.
  브라우저가 다시 계산에 넣는 값(종횡단 지반선)은 줄이면 안 된다고 머리에 경고 적음.
- 도엽 GeoJSON: 자릿수만 줄인 표시용 사본(.display.geojson)을 한 번 만들어 서빙.
  원본이 새로 깔리면 자동 재생성, 실패하면 원본 그대로.
  등고선 도엽 65.9->36.8MB, 압축까지 11.3MB.
- 배수유역 응답: 위경도 7자리. 151->100KB, 압축 22KB.

ruff 통과, 테스트 390 통과·17 건너뜀.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 21:13:05 +09:00
co-authored by Claude Opus 5
parent c97e949792
commit da2130c988
4 changed files with 124 additions and 2 deletions
+18 -1
View File
@@ -37,6 +37,7 @@ from common_util.common_util_drainage_pipes import (
save_detail_basins,
save_pipe_points,
)
from common_util.common_util_json import LONLAT_DIGITS, round_floats
from common_util.common_util_route_geometry import StructureCandidate
from config.config_system import DRAINAGE_PIPE_MAX_SPACING_M, DRAINAGE_PIPE_MIN_SPACING_M
@@ -122,7 +123,23 @@ def _payload(
points: list[PipePoint],
saved: bool,
) -> dict[str, Any]:
"""관 목록과 세부유역을 화면 좌표(WGS84)로 정리한다."""
"""관 목록과 세부유역을 화면 좌표(WGS84)로 정리한다.
좌표는 위경도 7자리(약 1.1cm)로 맞춰 내보낸다 — 저장된 값에는 뜻 없는 자리가 붙어 있어
응답이 151KB → 100KB(압축까지 얹으면 22KB)가 된다(2026-09-06 실측). 화면이 그리는 값이고
다시 계산에 넣지 않으므로 줄여도 결과가 안 갈린다.
"""
return round_floats(_raw_payload(project_id, context, detail, points, saved), LONLAT_DIGITS)
def _raw_payload(
project_id: UUID,
context: DrainageContext,
detail: DrainageDetail,
points: list[PipePoint],
saved: bool,
) -> dict[str, Any]:
"""자릿수를 줄이기 전의 응답 본문."""
to_lonlat = context.to_lonlat
return {
"status": "success",
+41 -1
View File
@@ -10,6 +10,7 @@ from fastapi.responses import JSONResponse
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
from common_util.common_util_http_cache import cached_file_response
from common_util.common_util_json import LONLAT_DIGITS, round_floats
from common_util.common_util_storage import resolve_stored_project_path
from config.config_db import get_db_pool
@@ -150,7 +151,13 @@ async def get_project_geojson(
if layer in _SHEET_GEOJSON_FILES:
# 도엽 산출물은 자르거나 줄이지 않고 파일 그대로 보낸다(2026-08-01 사용자 지시).
# 재직렬화만 건너뛰어도 요청당 2초가 사라지고, ETag로 두 번째부터는 304가 된다.
return cached_file_response(request, filepath, "application/geo+json")
# 다만 **자릿수만 줄인 사본**을 한 번 만들어 그것을 보낸다(2026-09-06) — 형상은
# 그대로고 뜻 없는 자리만 사라진다.
return cached_file_response(
request,
await asyncio.to_thread(_display_copy, filepath),
"application/geo+json",
)
if layer == "등고선":
simplified_filepath = target_dir / "등고선_bounds_simplified.geojson"
@@ -349,3 +356,36 @@ async def build_sheet_surface(project_id: UUID, request: Request) -> dict[str, A
await connection.rollback()
raise
return {"status": "success", "method": method, "surface_model_ids": model_ids}
def _display_copy(source: Path) -> Path:
"""도엽 GeoJSON 의 **표시용 사본**(좌표 자릿수만 줄인 것) 경로를 돌려준다.
저장된 파일에는 `1.4000000000000001` 꼴로 뜻 없는 자리가 붙어 있다 — 위경도 7자리
(1.1cm)로 맞추면 형상은 그대로면서 등고선 도엽이 65.9MB → 36.8MB 가 된다(2026-09-06 실측).
압축까지 얹으면 11.3MB.
사본은 **한 번만** 만든다(66MB 기준 약 3초). 원본이 새로 깔리면 수정시각이 앞서므로
저절로 다시 만든다. 만들다 실패하면 원본을 그대로 보낸다 — 화면을 막지 않는다.
"""
target = source.with_suffix(".display.geojson")
try:
if target.exists() and target.stat().st_mtime >= source.stat().st_mtime:
return target
payload = json.loads(source.read_text(encoding="utf-8"))
target.write_text(
json.dumps(
round_floats(payload, LONLAT_DIGITS), ensure_ascii=False, separators=(",", ":")
),
encoding="utf-8",
)
logger.info(
"도엽 표시용 사본 생성: %s (%.1fMB → %.1fMB)",
source.name,
source.stat().st_size / 1e6,
target.stat().st_size / 1e6,
)
return target
except Exception as exc: # 사본 실패가 화면을 막으면 안 된다.
logger.warning("도엽 표시용 사본을 만들지 못해 원본을 보냅니다 (%s): %s", source.name, exc)
return source