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:
2026-10-30 21:49:20 +09:00
co-authored by Claude Sonnet 5.5
parent 20ed22709c
commit 40cecf27a2
12 changed files with 629 additions and 53 deletions
+84 -5
View File
@@ -52,6 +52,7 @@ import {
fetchPlanSoils,
fetchPlanZeroOrder,
fetchPlanZones,
deletePlanForestRoad,
uploadPlanForestRoads,
type PlanForestRoads,
} from "./D03_PlanAnalysis_Api";
@@ -69,6 +70,7 @@ import {
readPref,
shadeImage,
type PlanLayerKey,
type RoadPick,
} from "./D03_PlanAnalysis_UI_LayerDefs";
import { layerCounts, legendRows, type Prepared } from "./D03_PlanAnalysis_UI_LayerLegend";
import { createCanvasLayers } from "./D03_PlanAnalysis_UI_LayerPaint";
@@ -110,6 +112,10 @@ export interface PlanLayers {
load: () => Promise<void>;
/** 임도망도 올리기(왼쪽 「위치」 칸) — 받으면 그 겹을 켬 · 못 받으면 알림 + null. */
uploadRoads: (files: File[]) => Promise<PlanForestRoads | null>;
/** 올린 묶음 하나 지움(605-1) — 지도에서 바로 빠짐 · 못 지우면 알림 + 거짓. */
deleteRoad: (bundle: string) => Promise<boolean>;
/** 올린 묶음 · 선 고름(605-2) — 지도 강조(꺼 있던 겹은 켬) · null = 강조 없앰. */
pickRoad: (pick: RoadPick | null) => void;
/** 검증 창구 — 켬끔 · 받은 겹 · 겹별 수(지물 · 넓이 m²). */
state: () => {
on: Record<PlanLayerKey, boolean>;
@@ -294,11 +300,7 @@ export function createPlanLayers(opts: {
const at = meta;
const job = uploadPlanForestRoads(projectId, files).then((info) => {
if (meta !== at) return info;
if (at)
data.forestRoadFile = { layer: prepareLayer(info.collection, createNormalizer(at)), info };
gl.setData("forestRoadFile", info.collection);
on.forestRoadFile = true;
writeByKey(PREF_KEY, JSON.stringify(on));
applyRoads(info);
showToast(
`임도망도 받음 — 범위 안 ${info.count}가닥 ${(info.length_m / 1000).toFixed(2)} km (파일 ${info.total}가닥)`,
"success",
@@ -328,6 +330,80 @@ export function createPlanLayers(opts: {
}
}
/** 올린 임도망 자료를 바꿔 그림(올림 · 지움) — 켜 둠 · null = 하나도 안 남음(끔). */
function applyRoads(info: PlanForestRoads | null): void {
if (info) {
if (meta)
data.forestRoadFile = {
layer: prepareLayer(info.collection, createNormalizer(meta)),
info,
};
gl.setData("forestRoadFile", info.collection);
} else {
delete data.forestRoadFile;
gl.setData("forestRoadFile", null);
roadPick = null;
gl.setRoadPick(null);
}
on.forestRoadFile = !!info;
writeByKey(PREF_KEY, JSON.stringify(on));
}
async function deleteRoad(bundle: string): Promise<boolean> {
try {
const info = await deletePlanForestRoad(projectId, bundle);
if (roadPick?.bundle === bundle) pickRoad(null);
applyRoads(info);
showToast(
info
? `임도망도 묶음 지움 — 남은 ${info.bundles.length}묶음`
: "임도망도 묶음 지움 — 남은 것 없음",
"success",
);
return true;
} catch (error) {
showToast(
`임도망도 묶음을 지우지 못함 — ${error instanceof Error ? error.message : String(error)}`,
"error",
);
return false;
} finally {
sync();
map.redrawBase();
}
}
/** 고른 묶음 · 선만 담은 캔버스 자료(옛 그림용) — 자료 · 고름이 같으면 그대로. */
let roadPick: RoadPick | null = null;
let pickMemo: { from: unknown; pick: RoadPick; layer: PreparedLayer } | null = null;
function pickLayer(): PreparedLayer | null {
const got = data.forestRoadFile;
if (!roadPick || !got || !meta) return null;
if (pickMemo?.from !== got || pickMemo.pick !== roadPick) {
const mine = roadPick;
const features = (got.info.collection.features ?? []).filter((f) => {
const props = f.properties ?? {};
return (
props["__bundle"] === mine.bundle &&
(mine.line === undefined || props["__line"] === mine.line)
);
});
pickMemo = {
from: got,
pick: mine,
layer: prepareLayer({ ...got.info.collection, features }, createNormalizer(meta)),
};
}
return pickMemo.layer;
}
function pickRoad(pick: RoadPick | null): void {
roadPick = pick;
gl.setRoadPick(pick);
if (pick && !on.forestRoadFile) toggle("forestRoadFile");
else map.redrawBase();
}
/** 받은 범위 지문 — 등고 · 세류 주소에 붙여 브라우저 보관함이 옛 범위를 안 줌. */
const haveTag = (): string => (opts.have() ?? []).map((v) => Math.round(v)).join("_");
const sheetLayer = (layer: string, extra = ""): Promise<GeoJsonCollection> =>
@@ -538,6 +614,7 @@ export function createPlanLayers(opts: {
on,
data,
meta: () => meta,
roadPick: pickLayer,
});
/** GL 이면 캔버스 바탕 겹은 쉼 — 등고 주곡선 간격(화면 안 가닥 수)만 셈. */
@@ -581,6 +658,8 @@ export function createPlanLayers(opts: {
layers,
load,
uploadRoads,
deleteRoad,
pickRoad,
state: () => ({
on: { ...on },
loaded: LAYERS.map((def) => def.key).filter(loadedOf),