Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/sub_laptop_2' into sub_laptop_3

This commit is contained in:
2026-10-29 20:13:28 +09:00
2 changed files with 68 additions and 5 deletions
+36 -5
View File
@@ -3,7 +3,7 @@
- `GET /api/projects/{id}/plan/terrain/extent` — 받기 전 범위(상자 · 도엽 · 칸 수 · 받은 범위)
- `POST …/plan/terrain` — 사업 전체 지형 한 벌 받기(뒤에서 · 누를 때만) · 끝나면 `surface_models` 행
- `GET …/plan/progress` — 일의 진행률
- `GET …/plan/tile/{z}/{x}/{y}` — 위성 조각 대리(586-1 GL 바탕 시험)
- `GET …/plan/tile/{z}/{x}/{y}` — 위성 조각 대리 · 서버 기억(586 GL 바탕)
- `GET …/plan/layers/sheet?key=` · `…/cadastral` · `…/raster?kind=slope|hillshade|landslide` ·
`…/zones` · `…/soils` · `…/bands` · `…/lines?kind=` · `…/zero-order` ·
`…/forest-roads`(GET · POST 올리기) · `…/landslide-zones` — 지도 겹
@@ -21,7 +21,7 @@ import json
import logging
from pathlib import Path
from typing import Any
from uuid import UUID
from uuid import UUID, uuid4
from fastapi import APIRouter, File, HTTPException, Request, Response, UploadFile
from pydantic import BaseModel
@@ -350,22 +350,53 @@ async def plan_layer_landslide_zones(project_id: UUID) -> dict[str, Any]:
return {"status": "success", **value}
# 위성 조각 기억 — 프로젝트 → 실경로(조각마다 DB 를 안 부름)
_TILE_ROOTS: dict[UUID, Path] = {}
TILE_KIND = "plan_tile"
async def _tile_root(project_id: UUID) -> Path:
root = _TILE_ROOTS.get(project_id)
if root is None:
async with get_db_pool().acquire() as connection:
stored = await get_project_storage_relative_path(connection, project_id)
if not stored:
raise HTTPException(status_code=404, detail="프로젝트 저장 경로를 찾을 수 없습니다.")
root = _TILE_ROOTS[project_id] = Path(resolve_stored_project_path(stored))
return root
@router.get("/{project_id}/plan/tile/{z}/{x}/{y}", response_model=None)
async def plan_satellite_tile(project_id: UUID, z: int, x: int, y: int) -> Response:
"""위성 조각(PLAN 586-1 GL 바탕 시험 · 브이월드 Satellite z6~19) — 키가 화면에 안 새게 대리."""
"""위성 조각(PLAN 586 GL 바탕 · 브이월드 Satellite z6~19) — 키가 화면에 안 새게 대리 ·
받은 조각 = `_derived/plan_tile/Satellite/{z}/{x}_{y}.jpeg`(지문 = 겹 · 조각 번호 ·
지워도 다시 받음)."""
import urllib.request
from B04_PreProcess.B04_PreProcess_Engine_VWorld import VWORLD_API_KEY
from common_util.common_util_derived_store import derived_dir
_ = project_id
if not (6 <= z <= 19 and 0 <= x < 2**z and 0 <= y < 2**z):
raise HTTPException(status_code=400, detail="조각 번호가 범위를 벗어났습니다.")
path = (
derived_dir(await _tile_root(project_id), TILE_KIND)
/ "Satellite"
/ str(z)
/ f"{x}_{y}.jpeg"
)
url = f"http://api.vworld.kr/req/wmts/1.0.0/{VWORLD_API_KEY}/Satellite/{z}/{y}/{x}.jpeg"
def fetch() -> bytes:
if path.is_file():
return path.read_bytes()
request = urllib.request.Request(url, headers={"User-Agent": "Aislo/1.0"})
with urllib.request.urlopen(request, timeout=8) as response:
return response.read()
tile = response.read()
path.parent.mkdir(parents=True, exist_ok=True)
part = path.with_suffix(f".{uuid4().hex}.part")
part.write_bytes(tile)
part.replace(path) # 반쯤 쓴 조각을 남기지 않음
return tile
try:
tile = await asyncio.to_thread(fetch)
+32
View File
@@ -134,3 +134,35 @@ def test_위성_조각_대리는_범위_밖을_막음():
with pytest.raises(HTTPException) as error:
asyncio.run(plan_satellite_tile(uuid.uuid4(), z, x, y))
assert error.value.status_code == 400
def test_위성_조각은_서버에_기억해_다시_안_받음(tmp_path, monkeypatch):
"""586-2 — 받은 조각 = 프로젝트 `_derived/plan_tile` · 두 번째는 브이월드에 안 물음."""
import asyncio
import urllib.request
import uuid
from D03_PlanAnalysis import D03_PlanAnalysis_Router as router
project = uuid.uuid4()
monkeypatch.setitem(router._TILE_ROOTS, project, tmp_path)
calls = []
class Reply:
def __enter__(self):
return self
def __exit__(self, *_):
return False
def read(self):
return b"jpeg"
monkeypatch.setattr(urllib.request, "urlopen", lambda *a, **k: calls.append(1) or Reply())
for _ in range(2):
reply = asyncio.run(router.plan_satellite_tile(project, 14, 13990, 6380))
assert reply.body == b"jpeg"
assert len(calls) == 1
saved = tmp_path / "_derived" / "plan_tile" / "Satellite" / "14" / "13990_6380.jpeg"
assert saved.read_bytes() == b"jpeg"
assert not list(saved.parent.glob("*.part"))