fix(530-3): D03 화면 검증 흠 — 저장된 갈래 밝힘 · 움직인 끌기만 지점 옮김 · 횡단 꾸밈 · 진행 못 읽으면 알림

- 카드 고름 = 서버 이은 누가(저장 갈래) 위에 캐시 고름
- 지점 누름(손 떨림 안)은 끌기 아님 · 집은 자리 차 지킴
- 횡단 판 B05 RouteEdit 꾸밈 css · 끌기 끝나면 옛 횡단 비움
- 진행 요청 실패 = 알림 뒤 화면 되돌림(서버 일은 그대로)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 20:45:43 +09:00
co-authored by Claude Opus 5.5
parent 134515b9cb
commit 6db3a2f635
3 changed files with 46 additions and 11 deletions
+18 -4
View File
@@ -4,18 +4,28 @@
* `generate_sections` · 격자 = 그 연도 DEM 평활본) · 그림 = B05 `buildCrossSvg` 지반만.
* ========================================================================== */
import "../B05_Profile/B05_Profile_UI_Style_RouteEdit.css";
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import { stationLabel } from "../common_util/common_util_station";
import type { DtmGrid } from "../common_util/common_util_surface_sampler";
import { buildCrossSvg } from "../B05_Profile/B05_Profile_UI_RouteEdit_Cross_Draw";
import { sectionsGround } from "../B05_Profile/logic/B05_Profile_Sections_Ground";
import {
sectionsGround,
type StationGround,
} from "../B05_Profile/logic/B05_Profile_Sections_Ground";
import type { Vertex } from "./logic/D03_PlanAnalysis_Plan";
import { PLAN_STATION_INTERVAL_M } from "./D03_PlanAnalysis_UI_Map";
export interface PlanCross {
root: HTMLElement;
/** `chainage` = 등록 누가 · `localM` = 그 연도 노선 위 누가. */
show: (chainage: number, year: number, line: Vertex[], localM: number, grid: DtmGrid) => void;
/** `chainage` = 등록 누가 · `localM` = 그 연도 노선 위 누가 · 돌려줌 = 그린 측점 지반(못 읽으면 null). */
show: (
chainage: number,
year: number,
line: Vertex[],
localM: number,
grid: DtmGrid,
) => StationGround | null;
clear: (text: string) => void;
}
@@ -48,7 +58,10 @@ export function createPlanCross(): PlanCross {
clear,
show(chainage, year, line, localM, grid) {
const station = sectionsGround(line, [localM], grid).stations[0];
if (!station) return clear("이 측점의 지반을 읽지 못했습니다.");
if (!station) {
clear("이 측점의 지반을 읽지 못했습니다.");
return null;
}
const name = stationLabel(chainage, PLAN_STATION_INTERVAL_M, 1);
const z = station.center_z === null ? "—" : `${station.center_z.toFixed(2)} m`;
head.textContent = `${year}년 · ${name} · 중심 지반고 ${z}`;
@@ -71,6 +84,7 @@ export function createPlanCross(): PlanCross {
true,
);
body.replaceChildren(plot.svg);
return station;
},
};
}
+13 -5
View File
@@ -289,7 +289,8 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
};
// 지점 끌기 — 지도 옮기기보다 먼저 듣고 지점을 집었으면 옮기기에 안 넘김
let dragging: { index: number; id: number } | null = null;
// 집은 자리와 지점 가운데 차(px)를 지킴 · 손 떨림(CLICK_SLOP_PX) 안이면 끌기 아님(누름)
let dragging: { index: number; id: number; dx: number; dy: number; moved: boolean } | null = null;
let press: [number, number] | null = null;
canvas.addEventListener("pointerdown", (event) => {
press = [event.clientX, event.clientY];
@@ -297,7 +298,9 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
const index = nodeAt(...at(event));
if (index < 0) return;
event.stopImmediatePropagation();
dragging = { index, id: event.pointerId };
const [px, py] = at(event);
const [sx, sy] = toScreen(scene.nodes[index]);
dragging = { index, id: event.pointerId, dx: sx - px, dy: sy - py, moved: false };
canvas.setPointerCapture(event.pointerId);
canvas.style.cursor = "grabbing";
});
@@ -307,14 +310,19 @@ export function createPlanMap(events: PlanMapEvents): PlanMap {
canvas.style.cursor = nodeAt(...at(event)) >= 0 ? "grab" : "";
return;
}
events.onDrag(dragging.index, screenToMetric(meta, view, ...at(event)));
if (!press) return;
const far = Math.hypot(event.clientX - press[0], event.clientY - press[1]) > CLICK_SLOP_PX;
if (!dragging.moved && !far) return;
dragging.moved = true;
const [px, py] = at(event);
events.onDrag(dragging.index, screenToMetric(meta, view, px + dragging.dx, py + dragging.dy));
});
const endDrag = (event: PointerEvent): void => {
if (!dragging || event.pointerId !== dragging.id) return;
const index = dragging.index;
const { index, moved } = dragging;
dragging = null;
canvas.style.cursor = "";
events.onDragEnd(index);
if (moved) events.onDragEnd(index);
};
canvas.addEventListener("pointerup", endDrag);
canvas.addEventListener("pointercancel", endDrag);
+15 -2
View File
@@ -36,6 +36,7 @@ import {
} from "../B04_PreProcess/B04_PreProcess_UI_MapRender";
import { prepareLayer } from "../B04_PreProcess/logic/B04_PreProcess_UI_MapRender_Prepare";
import { stationLabel } from "../common_util/common_util_station";
import type { StationGround } from "../B05_Profile/logic/B05_Profile_Sections_Ground";
import {
fetchPlanAnalysis,
fetchPlanPoints,
@@ -139,6 +140,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
let selected: number | null = null;
let editable = false;
let busy: "terrain" | "analyze" | null = null;
let lastCross: StationGround | null = null;
const moved = (): boolean => !sameNodes(nodes, savedNodes);
const sentPoints = () => (moved() ? pointsOf(years, nodes) : null);
@@ -171,7 +173,9 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
routes: {},
},
);
rows = chainRows(results, choices, startM);
// 고름 = 저장된 갈래(서버 이은 누가) 위에 캐시 고름
const saved = Object.fromEntries((analysis?.chain ?? []).map((row) => [row.year, row.mode]));
rows = chainRows(results, { ...saved, ...choices }, startM);
const line = joinedLine(results, rows);
const others: Vertex[][] = results.flatMap((result, i) => {
const routes = PLAN_MODES.filter((mode) => mode !== rows[i].mode)
@@ -264,6 +268,9 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
i === index - 1 || i === index ? { ...entry, analysis: null } : entry,
),
};
selected = null;
lastCross = null;
cross.clear("지점을 옮겼습니다 — [분석] 뒤 측점을 누르세요.");
keepDraft();
refresh();
}
@@ -275,7 +282,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
const result = results.find((entry) => entry.year === row.year)!;
selected = chainage;
map.update({ selected });
cross.show(
lastCross = cross.show(
chainage,
row.year,
result.routes[row.mode].planned ?? [],
@@ -356,9 +363,14 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
}
await sleep(POLL_MS);
}
} catch (error) {
// 진행을 못 읽음(세션 끊김 등) — 서버 일은 그대로 돎 · 다시 열면 이어 지켜봄
showToast(`진행을 읽지 못했습니다 — ${errorText(error)}`, "error");
return null;
} finally {
busy = null;
bar.hidden = true;
refresh();
}
}
@@ -449,6 +461,7 @@ export async function renderD03PlanAnalysis(root: HTMLElement): Promise<void> {
rows: () => rows,
nodes: () => nodes,
pick: onStation,
cross: () => lastCross,
};
try {