From 6db3a2f6357ad9f3d7afb87657ed378de89f3dc4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 25 Oct 2026 20:45:43 +0900 Subject: [PATCH] =?UTF-8?q?fix(530-3):=20D03=20=ED=99=94=EB=A9=B4=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=9D=A0=20=E2=80=94=20=EC=A0=80=EC=9E=A5?= =?UTF-8?q?=EB=90=9C=20=EA=B0=88=EB=9E=98=20=EB=B0=9D=ED=9E=98=20=C2=B7=20?= =?UTF-8?q?=EC=9B=80=EC=A7=81=EC=9D=B8=20=EB=81=8C=EA=B8=B0=EB=A7=8C=20?= =?UTF-8?q?=EC=A7=80=EC=A0=90=20=EC=98=AE=EA=B9=80=20=C2=B7=20=ED=9A=A1?= =?UTF-8?q?=EB=8B=A8=20=EA=BE=B8=EB=B0=88=20=C2=B7=20=EC=A7=84=ED=96=89=20?= =?UTF-8?q?=EB=AA=BB=20=EC=9D=BD=EC=9C=BC=EB=A9=B4=20=EC=95=8C=EB=A6=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 카드 고름 = 서버 이은 누가(저장 갈래) 위에 캐시 고름 - 지점 누름(손 떨림 안)은 끌기 아님 · 집은 자리 차 지킴 - 횡단 판 B05 RouteEdit 꾸밈 css · 끌기 끝나면 옛 횡단 비움 - 진행 요청 실패 = 알림 뒤 화면 되돌림(서버 일은 그대로) Co-Authored-By: Claude Opus 5.5 --- D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts | 22 +++++++++++++++---- D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts | 18 ++++++++++----- D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts | 17 ++++++++++++-- 3 files changed, 46 insertions(+), 11 deletions(-) diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts index d8e7db7e4..00571394c 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Cross.ts @@ -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; }, }; } diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts index 219589be2..e5e59f21e 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Map.ts @@ -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); diff --git a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts index ebbe66146..c2d77ff32 100644 --- a/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts +++ b/D03_PlanAnalysis/D03_PlanAnalysis_UI_Page.ts @@ -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 { 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 { 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 { 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 { 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 { } 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 { rows: () => rows, nodes: () => nodes, pick: onStation, + cross: () => lastCross, }; try {