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
+31
View File
@@ -17,9 +17,11 @@ import signal
import subprocess
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
from A06_Login.A06_Login_Router import router as a06_login_router
@@ -329,6 +331,35 @@ app.add_middleware(
allow_headers=["*"],
)
# ─────────────────────────────────────────────────────────────────────────
# 응답 압축 (2026-09-06 실측으로 도입)
# ─────────────────────────────────────────────────────────────────────────
# 화면 한 번 여는 데 수십 MB 가 오간다 — 종횡단 상세 1.56MB → 0.34MB(압축 13ms),
# 배수유역 151KB → 37KB, 지형 도엽 GeoJSON 은 훨씬 크다. 사무실 랜에서는 티가 덜 나지만
# 현장에서 인터넷으로 열면 이 차이가 곧 대기 시간이다.
#
# 딱 하나 예외 — 3D 예상형상(코리도)은 숫자 배열이라 절반밖에 안 줄면서 압축에만 493ms 가
# 든다(18.6MB → 9.4MB). 그 자리는 「덜 보내기」가 아니라 「안 보내기」로 푼다.
_COMPRESS_MIN_BYTES = 1024
def _skips_compression(path: str) -> bool:
"""압축에서 뺄 경로인가 — 지금은 코리도 하나뿐."""
return path.endswith("/corridor")
class _SelectiveGZipMiddleware(GZipMiddleware):
"""경로 몇 개만 빼고 압축한다. 나머지 동작은 표준 미들웨어 그대로."""
async def __call__(self, scope: Any, receive: Any, send: Any) -> None:
if scope["type"] == "http" and _skips_compression(scope.get("path", "")):
await self.app(scope, receive, send)
return
await super().__call__(scope, receive, send)
app.add_middleware(_SelectiveGZipMiddleware, minimum_size=_COMPRESS_MIN_BYTES)
# ─────────────────────────────────────────────────────────────────────────
# 정적 파일 서빙 (프론트엔드)
# ─────────────────────────────────────────────────────────────────────────