feat(D 예정노선): 올린 임도망 묶음 쌓기 · 목록(고르기 · 지움) · 지도 밝은 분홍 + 검은 테 (PLAN 605-1 · 605-2 · 요구 476-1)
- 서버 — 올릴 때마다 묶음 하나(`D03_PlanAnalysis/임도망도/<번호>`) · 옛 한 파일 꼴 = 묶음 legacy · 합친 값 + 묶음 줄 · 목록 GET · 묶음 DELETE · 20 묶음까지 - 화면 — 위치 칸 목록(묶음 줄 · 펼치면 선 줄 · 고름 = 지도 강조 + 맞춤 · 지움 = 물음 뒤) · 색 주황 → 밝은 분홍(`--draw-map-forest-road-file`) - 시험 584_3 — 묶음 쌓기 · 지움 · legacy · 20 묶음 · 화면 정의 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -62,8 +62,8 @@ def test_save_roads_files_clip_and_saved(tmp_path) -> None:
|
||||
assert got["crs"] == ["EPSG:5179"]
|
||||
assert got["collection"]["features"][0]["properties"]["임도명"] == "임도0" # cp949 dbf
|
||||
assert got["source"]["files"] == [name for name, _ in files]
|
||||
assert (tmp_path / fr.FOLDER / fr.SOURCE_ZIP).is_file()
|
||||
assert not list((tmp_path / fr.FOLDER).glob("*.new"))
|
||||
assert len(list((tmp_path / fr.FOLDER / fr.BUNDLE_DIR).glob("*.zip"))) == 1
|
||||
assert not list((tmp_path / fr.FOLDER).rglob("*.new"))
|
||||
assert fr.road_layer(tmp_path, BOX, EPSG) == json.loads(json.dumps(got)) # 다시 열면 저장본
|
||||
|
||||
|
||||
@@ -77,7 +77,8 @@ def test_zip_in_folder_and_missing_prj_is_5179(tmp_path) -> None:
|
||||
def test_bad_uploads_keep_old(tmp_path) -> None:
|
||||
files = _shp_files(tmp_path / "shp", LINES)
|
||||
first = fr.save_roads(tmp_path, files, BOX, EPSG)
|
||||
stamp = (tmp_path / fr.FOLDER / fr.SOURCE_ZIP).stat().st_mtime_ns
|
||||
folder = tmp_path / fr.FOLDER / fr.BUNDLE_DIR
|
||||
before = sorted(p.name for p in folder.iterdir())
|
||||
bad = [
|
||||
([("a.zip", b"not a zip")], "zip 파일을 열지"),
|
||||
([(n, d) for n, d in files if not n.endswith(".dbf")], ".dbf"),
|
||||
@@ -87,9 +88,8 @@ def test_bad_uploads_keep_old(tmp_path) -> None:
|
||||
for upload, text in bad:
|
||||
with pytest.raises(ValueError, match=re.escape(text)):
|
||||
fr.save_roads(tmp_path, upload, BOX, EPSG)
|
||||
assert (tmp_path / fr.FOLDER / fr.SOURCE_ZIP).stat().st_mtime_ns == stamp # 옛 것 그대로
|
||||
assert sorted(p.name for p in folder.iterdir()) == before # 쌓인 것 그대로
|
||||
assert fr.road_layer(tmp_path, BOX, EPSG)["count"] == first["count"]
|
||||
assert not list((tmp_path / fr.FOLDER).glob("*.new"))
|
||||
|
||||
|
||||
def test_clip_box_site_5km_and_distance(tmp_path) -> None:
|
||||
@@ -112,6 +112,67 @@ def test_clip_box_site_5km_and_distance(tmp_path) -> None:
|
||||
fr.save_roads(tmp_path, far, frame, EPSG, site)
|
||||
|
||||
|
||||
def test_bundles_stack_and_delete(tmp_path) -> None:
|
||||
"""605-1 — 올릴 때마다 묶음 · 합친 값 + 묶음 줄 · 지움 = 그 묶음만 · 옛 한 파일 꼴 = legacy."""
|
||||
files = _shp_files(tmp_path / "shp", LINES)
|
||||
one = fr.save_roads(tmp_path, files, BOX, EPSG)
|
||||
two = fr.save_roads(tmp_path, [("copy.zip", _zip(dict(files)))], BOX, EPSG)
|
||||
assert [b["name"] for b in two["bundles"]] == [
|
||||
"47250",
|
||||
"copy.zip",
|
||||
] # 낱 파일 = 첫 파일 이름 줄기
|
||||
assert two["count"] == 2 * one["count"] and two["total"] == 6
|
||||
assert two["length_m"] == pytest.approx(2 * one["length_m"], abs=0.2)
|
||||
assert {f["properties"]["__bundle"] for f in two["collection"]["features"]} == {
|
||||
b["id"] for b in two["bundles"]
|
||||
}
|
||||
assert all(b["count"] == 2 and b["length_m"] > 1200 for b in two["bundles"])
|
||||
for b in two["bundles"]: # 묶음 안 선 차례
|
||||
mine = [
|
||||
f["properties"]
|
||||
for f in two["collection"]["features"]
|
||||
if f["properties"]["__bundle"] == b["id"]
|
||||
]
|
||||
assert [p["__line"] for p in mine] == [0, 1]
|
||||
gone = two["bundles"][0]["id"]
|
||||
fr.delete_bundle(tmp_path, gone)
|
||||
rest = fr.road_layer(tmp_path, BOX, EPSG)
|
||||
assert [b["id"] for b in rest["bundles"]] == [two["bundles"][1]["id"]]
|
||||
assert rest["count"] == one["count"]
|
||||
for bad in (gone, "../x", "zz"):
|
||||
with pytest.raises(FileNotFoundError):
|
||||
fr.delete_bundle(tmp_path, bad)
|
||||
fr.delete_bundle(tmp_path, rest["bundles"][0]["id"])
|
||||
with pytest.raises(FileNotFoundError):
|
||||
fr.road_layer(tmp_path, BOX, EPSG)
|
||||
|
||||
|
||||
def test_legacy_single_file_reads_as_one_bundle(tmp_path) -> None:
|
||||
folder = tmp_path / fr.FOLDER
|
||||
folder.mkdir()
|
||||
(folder / fr.SOURCE_ZIP).write_bytes(fr.pack(_shp_files(tmp_path / "shp", LINES)))
|
||||
(folder / fr.SOURCE_META).write_text(
|
||||
json.dumps({"files": ["옛.zip"], "bytes": 1, "uploaded_at": "2026-01-01T00:00:00Z"}),
|
||||
"utf-8",
|
||||
)
|
||||
got = fr.road_layer(tmp_path, BOX, EPSG)
|
||||
assert [(b["id"], b["name"]) for b in got["bundles"]] == [(fr.LEGACY, "옛.zip")]
|
||||
assert got["count"] == 2
|
||||
new = fr.save_roads(tmp_path, _shp_files(tmp_path / "n", LINES), BOX, EPSG)
|
||||
assert [b["id"] for b in new["bundles"]][0] == fr.LEGACY and len(new["bundles"]) == 2
|
||||
fr.delete_bundle(tmp_path, fr.LEGACY)
|
||||
assert not (folder / fr.SOURCE_ZIP).exists() and not (folder / fr.SOURCE_META).exists()
|
||||
assert len(fr.road_layer(tmp_path, BOX, EPSG)["bundles"]) == 1
|
||||
|
||||
|
||||
def test_bundle_limit(tmp_path) -> None:
|
||||
files = _shp_files(tmp_path / "shp", LINES)
|
||||
for _ in range(fr.MAX_BUNDLES):
|
||||
fr.save_roads(tmp_path, files, BOX, EPSG)
|
||||
with pytest.raises(ValueError, match="하나를 지우고"):
|
||||
fr.save_roads(tmp_path, files, BOX, EPSG)
|
||||
|
||||
|
||||
def test_no_upload_is_not_found(tmp_path) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
fr.road_layer(tmp_path, BOX, EPSG)
|
||||
@@ -121,6 +182,9 @@ def test_upload_limit() -> None:
|
||||
router = (ROOT / "D03_PlanAnalysis" / "D03_PlanAnalysis_Router.py").read_text("utf-8")
|
||||
assert fr.MAX_BYTES == 100 * 1024 * 1024
|
||||
assert "status_code=413" in router and "forest_road.MAX_BYTES" in router
|
||||
assert (
|
||||
"forest-roads/bundles" in router and "forest-roads/{bundle}" in router
|
||||
) # 605-1 목록 · 지움
|
||||
|
||||
|
||||
# ── 산사태취약지역 표 ──
|
||||
@@ -281,6 +345,18 @@ def test_screen_defs() -> None:
|
||||
assert "uploadRoads" in page and "uploadPlanForestRoads(" in page
|
||||
assert "<input" not in page and 'createElement("input")' not in page
|
||||
assert paint.count("base: true,") == paint.count("draw: (context, map) =>") # 겹 모두 바탕
|
||||
# 605-2 — 밝은 분홍 · 목록(부품 상자만) · 고름 = 강조(GL 거름 · 캔버스) · 지움 = 물음 뒤
|
||||
assert re.search(r"--draw-map-forest-road-file: #ff2fd0;", theme)
|
||||
mapgl = (folder / "D03_PlanAnalysis_UI_MapGl.ts").read_text("utf-8")
|
||||
roads = (folder / "D03_PlanAnalysis_UI_RoadList.ts").read_text("utf-8")
|
||||
assert "forestRoadFile-pick-halo" in mapgl and "setRoadPick" in mapgl
|
||||
assert "roadPick" in paint and "deletePlanForestRoad(" in page and "pickRoad" in page
|
||||
assert "askConfirm(" in roads and "danger: true" in roads and "createListRow" in roads
|
||||
assert (
|
||||
"<button" not in roads and "<input" not in roads and 'createElement("button")' not in roads
|
||||
)
|
||||
assert "createRoadList(" in site and "roads.reload()" in site
|
||||
assert len((folder / "D03_PlanAnalysis_UI_RoadList.ts").read_text("utf-8").splitlines()) <= 700
|
||||
assert "임도 짐작" in legend and "올린 임도망도" in legend # 범례 구분
|
||||
assert "이 시도 자료 없음" in legend
|
||||
for name in (
|
||||
|
||||
Reference in New Issue
Block a user