From ce9a83a655d0da8b0db67963eb537715b7624a42 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 12 Sep 2026 15:54:08 +0900 Subject: [PATCH] =?UTF-8?q?feat(b06):=20=EC=A2=85=EB=8B=A8=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=EC=84=A0=C2=B7=EA=B5=AC=EC=A1=B0=EB=AC=BC=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD=EC=9D=84=20=EB=81=8C=EC=96=B4=20=EC=98=AE=EA=B8=B8=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit B05 에서만 되던 끌어 옮기기를 B06 종단에도 붙임 — 측점선과 알약 어느 쪽을 끌어도 같은 길을 타고, 관은 예약 이동(저장·확정 때 정본과 배수유역 재분할), 구조물은 정본 이동임. 끌기 판별과 대상 찾기는 새 모듈(B06_Section_UI_Section_View_Menu)로 모아, 종단 화면 본체가 더 커지지 않게 둠. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR --- B06_Section/B06_Section_UI_Page.ts | 5 ++ .../B06_Section_UI_Page_Structures_Panel.ts | 24 ++++++++++ B06_Section/B06_Section_UI_Section_View.ts | 17 ++++++- .../B06_Section_UI_Section_View_Chart.ts | 5 +- .../B06_Section_UI_Section_View_Menu.ts | 47 +++++++++++++++++++ 5 files changed, 95 insertions(+), 3 deletions(-) diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 5b9d296e..b9fe7344 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -495,6 +495,11 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { removeStructure: (structureId) => { structuresPanel.removeStructureById(structureId); }, + movePipe: (fromChainageM, toChainageM) => + structuresPanel.movePipeTo(fromChainageM, toChainageM), + moveStructure: (structureId, toChainageM) => { + structuresPanel.moveStructureTo(structureId, toChainageM); + }, }); // 폼 → 횡단 캐시 반영은 따로 뗀 모듈이 맡는다(2026-09-02 분리). const pipeOptionsContext: PipeOptionsContext = { diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 1a824da0..3a790a29 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -111,6 +111,8 @@ export interface B06StructuresPanel { removeStructureById: (structureId: string) => boolean; addPipeAt: (chainageM: number) => void; removePipeAt: (chainageM: number) => void; + movePipeTo: (fromChainageM: number, toChainageM: number) => void; + moveStructureTo: (structureId: string, toChainageM: number) => boolean; /** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */ facility: { setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void; @@ -434,6 +436,26 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc section.setPipeFacilities(pipeFacilities); } + /** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */ + function movePipeTo(fromChainageM: number, toChainageM: number): void { + if (!deps.movePipe) { + showToast(PIPE_MOVE_GUIDE, "error"); + return; + } + const to = Number(toChainageM.toFixed(2)); + deps.movePipe(fromChainageM, to); + const hit = pipeFacilities.find( + (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, + ); + if (hit) hit.chainage_m = to; + if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M) + currentChainageM = to; + pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m); + section.setPipeFacilities(pipeFacilities); + pushMarks(); + showToast(PIPE_MOVE_NOTICE, "success"); + } + /** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */ function removePipeAt(chainageM: number): void { if (!deps.removePipe) { @@ -463,6 +485,8 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc removeStructureById: (structureId) => section.removeById(structureId), addPipeAt: (chainageM) => section.addAt(chainageM, "pipe"), removePipeAt: (chainageM) => removePipeAt(chainageM), + movePipeTo: (fromChainageM, toChainageM) => movePipeTo(fromChainageM, toChainageM), + moveStructureTo: (structureId, toChainageM) => section.moveById(structureId, toChainageM), facility: { setInletStructure: (value) => section.facility.setInletStructure(value), onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler), diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index d5b940e1..51c76845 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -52,6 +52,7 @@ import { } from "../B05_Profile/B05_Profile_UI_Structures_Marks"; import { mountSectionStructureMenu, + moveMarkById, type SectionStructureEdit, } from "./B06_Section_UI_Section_View_Menu"; import { @@ -511,6 +512,14 @@ export function createSectionView( // 생겨 패널이 16,000px 까지 부푼다(2026-09-07 실측). 빼 두면 한 번에 수렴한다. const laneHeight = markStructures.length && markTypes.length ? STRUCTURE_LANE_HEIGHT_PX : 0; const heights = chartHeights(Math.max(lastChartAvailable - laneHeight, MIN_LONG_HEIGHT)); + const moveStationMark = (markId: string, toChainageM: number): void => { + if (!structureEdit) return; + moveMarkById(markId, toChainageM, { + structures: markStructures, + stations: detail.longitudinal.stations ?? [], + edit: structureEdit, + }); + }; const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval); const chartWidth = Math.max(renderWidth, minWidth); const chart = buildLongitudinalChart({ @@ -524,6 +533,10 @@ export function createSectionView( minWidth, scrollLeft: keepScrollLeft, viewportWidth: chartWrap.clientWidth || chartWidth, + // 측점선을 끌면 그 구조물이 옮겨 간다 — 관은 예약 이동, 구조물은 정본 이동. + onDragStation: structureEdit + ? (stationId, toChainageM) => moveStationMark(stationId, toChainageM) + : undefined, }); const longAxis = chart.axis; const nodes: Element[] = [chart.node]; @@ -555,8 +568,8 @@ export function createSectionView( } if (best) selectStation(best.id, true); }, - // 알약 끌기는 B05 몫이다(배수유역 재분할이 걸린다) — 여기서는 자리만 보여 준다. - onMove: () => undefined, + // 알약 끌기도 B05 와 같게 받는다(2026-09-12 일원화) — 정본은 [저장]·[확정]에서. + onMove: (structureId, toChainageM) => moveStationMark(structureId, toChainageM), }), ); } diff --git a/B06_Section/B06_Section_UI_Section_View_Chart.ts b/B06_Section/B06_Section_UI_Section_View_Chart.ts index 7ab41541..05701438 100644 --- a/B06_Section/B06_Section_UI_Section_View_Chart.ts +++ b/B06_Section/B06_Section_UI_Section_View_Chart.ts @@ -30,6 +30,8 @@ export interface LongitudinalChartInput { /** 지금 보이는 구간을 정하는 값 — 가로 스크롤 위치와 컨테이너 안쪽 폭. */ scrollLeft: number; viewportWidth: number; + /** 구조물(비정규) 측점선을 끌어 옮겼다. 안 넘기면 그 선은 못 잡는다. */ + onDragStation?: (stationId: string, toChainageM: number) => void; } export interface LongitudinalChartResult { @@ -110,7 +112,8 @@ export function buildLongitudinalChart(input: LongitudinalChartInput): Longitudi axis = next; }, undefined, - undefined, + // 구조물(비정규) 측점선 끌어 옮기기 — B05 와 같은 조작이다(2026-09-12 일원화). + input.onDragStation, 0, 0, visibleElevationRange(detail, fromM, toM) ?? undefined, diff --git a/B06_Section/B06_Section_UI_Section_View_Menu.ts b/B06_Section/B06_Section_UI_Section_View_Menu.ts index 6a0c1a24..f3f41f18 100644 --- a/B06_Section/B06_Section_UI_Section_View_Menu.ts +++ b/B06_Section/B06_Section_UI_Section_View_Menu.ts @@ -25,6 +25,53 @@ export interface SectionStructureEdit { addStructureType: (chainageM: number, typeId: string) => void; /** 구조물(관 아님)을 뺀다. */ removeStructure: (structureId: string) => void; + /** 관을 다른 측점으로 옮긴다(측점선·알약 끌기). */ + movePipe: (fromChainageM: number, toChainageM: number) => void; + /** 구조물(관 아님)을 다른 측점으로 옮긴다. */ + moveStructure: (structureId: string, toChainageM: number) => void; +} + +/** + * 측점선·알약을 끌어 놓았다 — 그래프 위 id 로 구조물을 찾아 같은 예약 경로로 보낸다. + * 종단 측점선의 id 는 **종단 정본 측점 id** 라 알약(구조물 id)과 다를 수 있다. 둘 다 받아 + * 같은 자리(누가거리)로 맞춘다. + */ +export function moveMarkById( + markId: string, + toChainageM: number, + input: { + structures: ReadonlyArray; + stations: ReadonlyArray<{ station_id: string; chainage_m: number }>; + edit: SectionStructureEdit; + }, +): void { + const direct = input.structures.find((entry) => String(entry.structure_id) === markId); + if (direct) { + moveStructureMark(input.structures, input.edit, markId, toChainageM); + return; + } + const station = input.stations.find((entry) => String(entry.station_id) === markId); + if (!station) return; + // 측점선 id 로 온 경우 — 그 측점 자리에 선 구조물을 찾는다(관 매칭과 같은 0.51m). + const near = input.structures.find( + (entry) => Math.abs(structureAnchorM(entry) - station.chainage_m) < 0.51, + ); + if (near) moveStructureMark(input.structures, input.edit, String(near.structure_id), toChainageM); +} + +/** 측점선·알약을 끌었을 때 — 관인지 구조물인지 갈라 같은 예약 경로로 보낸다. */ +function moveStructureMark( + structures: ReadonlyArray, + edit: SectionStructureEdit, + structureId: string, + toChainageM: number, +): void { + const hit = structures.find((entry) => String(entry.structure_id) === structureId); + if (!hit) return; + const from = structureAnchorM(hit); + if (Math.abs(from - toChainageM) < 0.005) return; + if (isPipeMark(hit)) edit.movePipe(from, toChainageM); + else edit.moveStructure(structureId, toChainageM); } /** 알약·우클릭이 쓰는 「관인가」 판정 — 관은 정본이 달라 지우는 길도 다르다. */