feat(D03 겹): 노선분석 지도 산사태위험지도 겹 — 행안부 생활안전지도 WMS · 키 없으면 그 겹만 꺼짐 (PLAN 584-1 · 요구 444)

- 서버: IF_0046_WMS 겹 A2SM_SANSATAI 를 받은 범위 경위도 상자로 한 장(2 m/px · 한 변 2,048) → 작업 좌표계 상자로 옮겨 그림 · 덮으면 저장본 · 키 없음 = 503 「키 없음」 · 받기 실패 · 그림 아님 = 502
- 키 = .env SAFEMAP_API_KEY 새 칸(값 비움)
- 화면: 단추 하나(alert) · 묶음 셋째 · 처음 끔 · 바탕 캔버스 · 범례 1~5 등급(--draw-map-landslide-1~5)
- 켬끔 취향 읽기를 LayerDefs 로 옮김(700줄 한도)
- test_553_4: 겹 목록에 landslide 더함(새 겹 · 기대 목록 갱신)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 16:59:11 +09:00
co-authored by Claude Opus 5.5
parent 7d4a94a887
commit 84f243cee1
9 changed files with 262 additions and 25 deletions
+1
View File
@@ -67,6 +67,7 @@ def test_d03_parts():
("cadastral", "false"),
("zones", "false"),
("soils", "false"),
("landslide", "false"),
("faults", "false"),
("lineaments", "false"),
("zeroOrder", "false"),
+59 -1
View File
@@ -1,9 +1,12 @@
"""D 노선분석 GIS 겹 더함 2(PLAN 584-1) — 단층 · 지질구조선 잘라 가닥 · 길이 · 실패 = 오류 ·
덮으면 다시 안 받음 · 0차곡 흐름누적(웅덩이 · 평지 넘김) · 임계 · 선 뽑기 · 저장본 · 화면 겹 정의.
덮으면 다시 안 받음 · 산사태위험지도(키 없음 · 그림 아님 = 오류 · 옮겨 그림 · 덮으면 저장본) ·
0차곡 흐름누적(웅덩이 · 평지 넘김) · 임계 · 선 뽑기 · 저장본 · 화면 겹 정의.
./venv/Scripts/python.exe -m pytest resources/tester/test_584_1_plan_geo_lines.py -q
"""
import base64
import io
import json
import math
import re
@@ -13,6 +16,7 @@ import numpy as np
import pytest
from common_util.common_util_crs import tm_to_lonlat
from D03_PlanAnalysis import D03_PlanAnalysis_Landslide as landslide
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
from D03_PlanAnalysis import D03_PlanAnalysis_ZeroOrder as zo
@@ -81,6 +85,60 @@ def test_line_kinds_vworld_names() -> None:
assert layers.LINE_KINDS["lineaments"][1] == "lt_l_gimslinea"
# ── 산사태위험지도 ──
def _png(width: int, height: int, rgba: tuple[int, int, int, int]) -> bytes:
from PIL import Image
buffer = io.BytesIO()
Image.new("RGBA", (width, height), rgba).save(buffer, format="PNG")
return buffer.getvalue()
def test_landslide_no_key(tmp_path, monkeypatch) -> None:
monkeypatch.setenv(landslide.LANDSLIDE_KEY_ENV, " ")
with pytest.raises(landslide.MissingKeyError, match="키 없음"):
landslide.ensure_landslide(tmp_path, BOX, EPSG)
assert not list(tmp_path.iterdir())
def test_landslide_not_image(tmp_path, monkeypatch) -> None:
class Reply(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *exc) -> None:
pass
monkeypatch.setenv(landslide.LANDSLIDE_KEY_ENV, "k")
monkeypatch.setattr(landslide.urllib.request, "urlopen", lambda *a, **k: Reply(b"<xml/>"))
with pytest.raises(RuntimeError, match="그림이 아닌"):
landslide.ensure_landslide(tmp_path, BOX, EPSG)
assert not list(tmp_path.iterdir())
def test_landslide_warp_and_saved(tmp_path, monkeypatch) -> None:
from PIL import Image
calls = []
def fake(bounds, width, height):
calls.append((width, height))
return _png(width, height, (220, 38, 38, 200))
monkeypatch.setattr(landslide, "request_wms", fake)
value = landslide.ensure_landslide(tmp_path, BOX, EPSG)
assert calls == [(1000, 500)] # 2 m/px
assert value["box_m"] == list(BOX)
raw = base64.b64decode(value["image"].split(",", 1)[1])
image = np.asarray(Image.open(io.BytesIO(raw)))
assert image.shape == (500, 1000, 4)
assert tuple(image[250, 500]) == (220, 38, 38, 200) # 가운데 = 받은 색
inner = (230100.0, 450100.0, 231000.0, 450900.0)
assert landslide.ensure_landslide(tmp_path, inner, EPSG) == value and len(calls) == 1
# ── 0차곡 ──