feat(D03 겹): 노선분석 지도 산림입지도 겹 — 브이월드 WFS 토양형 · 묶음 셋째 · 처음 끔 (PLAN 581-2 · 요구 440-8)

- 서버: lt_c_fsdifrsts 받은 범위로 잘라 토양형 코드 · 이름 · 넓이 · 토양형별 합 · 덮으면 다시 안 받음 · 실패 = 502
- 화면: 단추 하나(잎 아이콘 · 부품 상자에 더함) · 토양형 색 --draw-map-soil-1~6 · 범례 = 토양형 이름 · ha · 바탕 캔버스
- 산사태위험지도(생활안전지도 = 공공누리 4유형 · 데이터 준비 중) · 단층 · 지질구조선(이용 조건 문서 없음)은 넣지 않음(브레인 결정 A)
- test_553_4: 겹 목록에 soils 더함(새 겹 · 기대 목록 갱신)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-29 15:15:10 +09:00
co-authored by Claude Opus 5.5
parent 63a9f16d32
commit f29fb01dd5
9 changed files with 272 additions and 16 deletions
+2 -1
View File
@@ -49,7 +49,7 @@ def test_d03_parts():
assert f'"{b05_only}"' not in tools, b05_only
paint = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_DrawPaint.ts") # 580 — 그림 나눔
assert "map.stations" in paint and "map.slopes" in paint
# 569-2 · 581-1 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억
# 569-2 · 581-1 · 581-2 — 바탕 겹 단추(공용 지도 도구줄 한 줄 · 묶음 셋) · 처음 켬 셋 · 취향 기억
defs = _src("D03_PlanAnalysis/D03_PlanAnalysis_UI_LayerDefs.ts")
assert "createMapTools({" in layers and "createIconButton({" in layers
assert "createToolbar(" not in layers
@@ -66,6 +66,7 @@ def test_d03_parts():
("forestRoad", "false"),
("cadastral", "false"),
("zones", "false"),
("soils", "false"),
("bands", "false"),
("building", "false"),
("place", "false"),
+97
View File
@@ -0,0 +1,97 @@
"""D 노선분석 GIS 겹 「다음」 묶음(PLAN 581-2) — 산림입지도 잘라 넓이 · 토양형 코드 차례
이름 없으면 글자 그대로 · 실패 = 오류(파일 안 씀) · 덮으면 다시 안 받음 · 화면 겹 정의 · 색 값.
./venv/Scripts/python.exe -m pytest resources/tester/test_581_2_plan_soils.py -q
"""
import json
import re
from pathlib import Path
import pytest
from common_util.common_util_crs import tm_to_lonlat
from D03_PlanAnalysis import D03_PlanAnalysis_Layers as layers
EPSG = 5186
BOX = (230000.0, 450000.0, 232000.0, 451000.0) # 2 × 1 km
ROOT = Path(__file__).resolve().parents[2]
def _square(x0: float, y0: float, side: float) -> dict:
ring = [(x0, y0), (x0 + side, y0), (x0 + side, y0 + side), (x0, y0 + side), (x0, y0)]
return {"type": "Polygon", "coordinates": [[list(tm_to_lonlat(x, y, EPSG)) for x, y in ring]]}
def _soil(gid: str, geometry: dict, cd: str, code: str, name: str | None) -> dict:
props = {"sltp_cd": cd, "toyanghyun": code, **({"name": name} if name else {})}
return {"id": gid, "geometry": geometry, "properties": props}
def test_clip_soils_area_order_names() -> None:
found = [
_soil("a", _square(230100.0, 450100.0, 100.0), "03", "B₃", "갈색적윤산림토양"),
_soil("b", _square(231800.0, 450800.0, 400.0), "01", "B₁", "갈색건조산림토양"), # 귀퉁이
_soil("b", _square(231800.0, 450800.0, 400.0), "01", "B₁", "갈색건조산림토양"), # 겹침
_soil("c", _square(230500.0, 450500.0, 100.0), "03", "B₃", "갈색적윤산림토양"),
_soil("d", _square(230800.0, 450100.0, 50.0), "99", "Xx", None), # 모르는 값
_soil("e", _square(240000.0, 460000.0, 100.0), "02", "B₂", "갈색약건산림토양"), # 밖
]
features, summary = layers.clip_soils(found, BOX, EPSG)
assert [row["code"] for row in summary] == ["B₁", "B₃", "Xx"]
assert summary[0]["area_m2"] == pytest.approx(200 * 200, rel=0.01)
assert summary[1] == {
"code": "B₃",
"name": "갈색적윤산림토양",
"count": 2,
"area_m2": pytest.approx(2 * 100 * 100, rel=0.01),
}
assert summary[2]["name"] == "Xx" # 이름 없으면 글자 그대로
assert len(features) == 4
assert {f["properties"]["code"] for f in features} == {"B₁", "B₃", "Xx"}
class FakeWfs:
def __init__(self, fail: bool = False) -> None:
self.fail, self.calls = fail, []
def __call__(self, typename, filename, bounds, output_dir) -> None:
self.calls.append(typename)
if self.fail:
return
feats = [_soil("a", _square(230500.0, 450200.0, 300.0), "01", "B₁", "갈색건조산림토양")]
body = {"type": "FeatureCollection", "features": feats}
(Path(output_dir) / filename).write_text(json.dumps(body), "utf-8")
def test_soils_fetch_once(tmp_path, monkeypatch) -> None:
fake = FakeWfs()
monkeypatch.setattr(layers, "request_vworld_wfs", fake)
got = layers.ensure_soils(tmp_path, BOX, EPSG)
assert got["fetched"] and fake.calls == [layers.SOIL_TYPENAME]
saved = json.loads((tmp_path / layers.SOIL_FILE).read_text("utf-8"))
assert saved["summary"][0]["count"] == 1 and len(saved["features"]) == 1
inner = (230500.0, 450100.0, 231000.0, 450900.0)
assert not layers.ensure_soils(tmp_path, inner, EPSG)["fetched"] # 덮으면 다시 안 받음
assert len(fake.calls) == 1
assert layers.ensure_soils(tmp_path, (229000.0, *BOX[1:]), EPSG)["fetched"] # 넓어지면 다시
def test_soils_fail_raises(tmp_path, monkeypatch) -> None:
monkeypatch.setattr(layers, "request_vworld_wfs", FakeWfs(fail=True))
with pytest.raises(RuntimeError):
layers.ensure_soils(tmp_path, BOX, EPSG)
assert not (tmp_path / layers.SOIL_FILE).exists()
def test_screen_def_and_colors() -> None:
defs = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_LayerDefs.ts").read_text("utf-8")
entry = re.search(r'key: "soils",\s*icon: "(\w+)",[^}]*on: (\w+)', defs)
assert entry and entry.group(2) == "false" # 처음 값 끔
icons = (ROOT / "ui_template" / "ui_template_icons.ts").read_text("utf-8")
assert re.search(rf"^\s+{entry.group(1)}:", icons, re.M)
theme = (ROOT / "ui_template" / "ui_template_theme.css").read_text("utf-8")
assert all(f"--draw-map-soil-{n}:" in theme for n in range(1, 7))
page = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_UI_Layers.ts").read_text("utf-8")
assert "if (!on.soils || !data.soils) return;" in page
assert page.count("base: true,") == page.count("draw: (context, map) =>") # 겹 모두 바탕(582)