feat(508-2 B05 · B06 남은 셋): 3D 길 모형 D3 = 서버 — 낡은 저장본 = 옛 모형 세움(빈 3D 아님) · [3D 업데이트]만 서버 창구(/calc corridor.mesh · 같은 TS · 라이브 계획선 샘플)가 지어 바꿔 끼움 · 같은 입력 다시 안 지음 · 실패 = 옛 모형 · 늦음 남김 · 창 열 때 자동 안 지음(요구 324) · both 그대로 · 추천 창 [재계산] = 저장본 없음(409) 안내 줄 옆 · 칸 바꿈 뒤만 · 노선 편집 도엽 = 놓은 뒤(잠잠) 범위 밖이면 지금 노선 bbox 로 잘린 사본 다시 받아 바꿔 끼움(서버 bbox · 열쇠 = 사본 지문 + 범위 · 여는 때도 통째 사본 안 받음 · 배수유역 범위 그대로) · 시험 test_508_2 셋 · 낡은 기대 셋(383_3 · 309_2 · 345_4) 고침 — 요구 323

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 17:13:41 +09:00
co-authored by Claude Opus 5.5
parent 4cf9e5d0a0
commit f1a6de017e
19 changed files with 701 additions and 58 deletions
@@ -6,7 +6,8 @@
- 범위(사업지 m · 바깥으로 `SNAP_M` 눈금에 맞춤 → 노선을 조금 옮겨도 같은 열쇠)
- `route` = 저장 노선 bbox + 띠 `ROUTE_BAND_M` + 여유 `ROUTE_SPARE_M` — 편집이 이 밖으로
나가면 화면이 통째 사본을 받는다(`clip_lonlat` 으로 판정).
나가면 화면이 놓은 뒤 편집 중 노선 bbox(`route_bounds` · 508-2)로 다시 받는다
(`clip_lonlat` 으로 판정).
- `drainage` = 세부 유역(노선 교차 관으로 모이는 유역) ∪ 줌아웃 한계(노선 bbox ×2)
+ `DRAIN_SPARE_M`.
- 파생 저장본 = `_derived/sheet_<범위>/<열쇠>.json`(GeoJSON 그대로 · 열쇠 = 사본 지문 + 범위)
@@ -144,9 +145,25 @@ def _basin_points(project_root: Path, meta: dict[str, Any]) -> list[tuple[float,
return points
def clip_rect_m(project_root: Path, scope: str, meta: dict[str, Any]) -> Rect | None:
"""범위 사각형(사업지 m · 눈금 맞춤). 노선이 없으면(또는 배수유역도에 유역이 없으면) None."""
route = _bounds(_route_points(project_root))
def parse_route_bbox(text: str | None) -> Rect | None:
"""화면이 보낸 편집 중 노선 bbox(`x0,y0,x1,y1` m) — 숫자 넷 · 차례 맞음만 · 아니면 None."""
try:
values = [float(part) for part in (text or "").split(",")]
except ValueError:
return None
if len(values) != 4 or not all(math.isfinite(v) for v in values):
return None
if values[0] > values[2] or values[1] > values[3]:
return None
return values[0], values[1], values[2], values[3]
def clip_rect_m(
project_root: Path, scope: str, meta: dict[str, Any], route_bounds: Rect | None = None
) -> Rect | None:
"""범위 사각형(사업지 m · 눈금 맞춤). 노선이 없으면(또는 배수유역도에 유역이 없으면) None.
`route_bounds` = 저장 노선 대신 쓸 노선 bbox(편집 중 · 508-2)."""
route = route_bounds or _bounds(_route_points(project_root))
if route is None:
return None
if scope == "route":
@@ -214,11 +231,18 @@ def _write_compact(path: Path, value: Any) -> None:
raise
def clipped_sheet(project_root: Path, source: Path, scope: str) -> Path:
"""`scope` 범위로 자른 표시본 경로. 범위를 못 정하거나 실패하면 `source` 그대로."""
def clipped_sheet(
project_root: Path, source: Path, scope: str, route_bounds: Rect | None = None
) -> Path:
"""`scope` 범위로 자른 표시본 경로. 범위를 못 정하거나 실패하면 `source` 그대로.
열쇠 = 사본 지문 + 범위 — 같은 범위면 노선을 어디서 재든 같은 저장본(다시 안 셈)."""
try:
meta = _read_meta(project_root / "B04_PreProcess" / "processed")
rect_m = clip_rect_m(project_root, scope, meta) if meta and scope in SCOPES else None
rect_m = (
clip_rect_m(project_root, scope, meta, route_bounds)
if meta and scope in SCOPES
else None
)
if rect_m is None or meta is None:
return source
west, south = _to_lonlat(meta, rect_m[0], rect_m[1])
+14 -3
View File
@@ -18,7 +18,10 @@ logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/projects", tags=["B04 Surface GIS"])
tiles_router = APIRouter(tags=["B04 MVT Tiles"])
from B04_PreProcess.B04_PreProcess_Engine_SheetClip import clipped_sheet # noqa: E402
from B04_PreProcess.B04_PreProcess_Engine_SheetClip import ( # noqa: E402
clipped_sheet,
parse_route_bbox,
)
# 수치지형도 도엽 병합 레이어 (SheetParser 산출과 1:1 — 정의는 SHEET_LAYERS가 유일)
from B04_PreProcess.B04_PreProcess_Engine_SheetParser import SHEET_LAYERS # noqa: E402
@@ -98,13 +101,18 @@ async def get_vworld_map(
# GeoJSON 조회 API
@router.get("/{project_id}/geojson", response_model=None)
async def get_project_geojson(
project_id: UUID, layer: str, request: Request, scope: str | None = None
project_id: UUID,
layer: str,
request: Request,
scope: str | None = None,
bbox: str | None = None,
) -> dict[str, Any] | Response | JSONResponse:
"""저장된 프로젝트의 특정 GeoJSON 레이어 데이터를 반환합니다.
도엽 레이어는 분석용 원본 대신 **표시용 사본**(프로젝트 주변만 잘라 좌표를 줄인 것)을
파일 그대로 내보낸다. 원본을 매번 읽어 재직렬화하면 등고선 한 장에 2초가 든다.
`scope`(route · drainage)를 주면 그 화면이 보는 범위만 자른 사본을 준다(509-1 · 509-6).
`bbox`(route 만 · 사업지 m `x0,y0,x1,y1`) = 저장 노선 대신 편집 중 노선 bbox(508-2).
"""
pool = get_db_pool()
try:
@@ -158,7 +166,10 @@ async def get_project_geojson(
# 그대로고 뜻 없는 자리만 사라진다.
display = await asyncio.to_thread(_display_copy, filepath)
if scope:
display = await asyncio.to_thread(clipped_sheet, project_root, display, scope)
bounds = parse_route_bbox(bbox) if scope == "route" else None
display = await asyncio.to_thread(
clipped_sheet, project_root, display, scope, bounds
)
return cached_file_response(request, display, "application/geo+json")
if layer == "등고선":