"""D 예정노선 받은 산림 · 환경 자료 조각 API (PLAN 622-1 · 요구 529). - `GET /api/projects/{id}/plan/vt/{name}/{z}/{x}/{y}.pbf` — 벡터 조각(forest_road · forest_soil · eco_forest · 공용 창고 · `common_util_vector_tiles`) - `GET …/plan/wmt/{name}/{z}/{x}/{y}.png` — 그림 조각 대리(ecology · landslide · 처음 받고 기억 · `common_util_wms_tiles`) · 빈 · 오류 조각 = 투명 PNG + `X-Tile-Empty` · 짧게 기억 권한 = 다른 plan 길과 같음(`main.py` 등록 때 회사 보호). 계산 자리 = ③ 서버 단독. """ from __future__ import annotations import asyncio from uuid import UUID from fastapi import APIRouter, HTTPException, Response from common_util import common_util_vector_tiles as vector_tiles from common_util import common_util_wms_tiles as wms_tiles router = APIRouter(prefix="/api/projects", tags=["D03 Plan Tiles"]) KEEP = "private, max-age=86400" SHORT = f"private, max-age={wms_tiles.SHORT_S}" @router.get("/{project_id}/plan/vt/{name}/{z}/{x}/{y}.pbf", response_model=None) async def plan_vector_tile(project_id: UUID, name: str, z: int, x: int, y: int) -> Response: """받은 산림 자료 벡터 조각 — 공용 창고 · 없으면 원자료에서 만듦 · 빈 조각 = 빈 MVT.""" if name not in vector_tiles.RULES: raise HTTPException(status_code=404, detail=f"모르는 겹입니다 — {name}") try: tile = await asyncio.to_thread(vector_tiles.ensure_tile, name, z, x, y) except KeyError as error: raise HTTPException(status_code=400, detail="조각 번호가 범위를 벗어났습니다.") from error return Response( content=tile, media_type="application/x-protobuf", headers={"Cache-Control": KEEP} ) @router.get("/{project_id}/plan/wmt/{name}/{z}/{x}/{y}.png", response_model=None) async def plan_wms_tile(project_id: UUID, name: str, z: int, x: int, y: int) -> Response: """그림 조각 대리 — 처음 받고 기억 · 키 없음 = 404 + 까닭.""" if name not in wms_tiles.SOURCES: raise HTTPException(status_code=404, detail=f"모르는 겹입니다 — {name}") try: png, status = await asyncio.to_thread(wms_tiles.ensure_tile, name, z, x, y) except wms_tiles.MissingKeyError as error: raise HTTPException(status_code=404, detail=str(error)) from error except KeyError as error: raise HTTPException(status_code=400, detail="조각 번호가 범위를 벗어났습니다.") from error headers = {"Cache-Control": SHORT if status else KEEP} if status: headers["X-Tile-Empty"] = status return Response(content=png, media_type="image/png", headers=headers)