From 45c6f9aa07ea5592c6d80a98ca2123bf603485bb Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 29 Aug 2026 16:53:17 +0900 Subject: [PATCH] =?UTF-8?q?feat(B06):=20=EA=B8=B0=EC=A4=80=20=EC=B8=A1?= =?UTF-8?q?=EC=A0=90=20=EC=9D=B4=EB=8F=99=EC=9D=84=20B06=20=EA=B5=AC?= =?UTF-8?q?=EC=A1=B0=EB=AC=BC=20=EB=B0=B0=EC=B9=98=EC=97=90=EC=84=9C?= =?UTF-8?q?=EB=8F=84=20=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-08-29 사용자: 구조물 배치 컨테이너를 B06으로 가져온 이유가 이것이다 — 폼의 기준 측점 숫자를 바꾸면 관이 옮겨져야 한다. - 관 저장기에 이동 예약(queueMove)을 더했다. 세션 키 b06:culvertmove에 담고, [저장]·[확정]의 flush가 옵션 병합과 함께 pipe_points에 반영한다. 세부 배수유역 재분할은 서버가 관 목록을 다시 받아 처리한다. - 폼에서 측점을 옮기면 목록·폼 캐시가 즉시 새 자리를 따르고, 옵션은 옮기기 전 자리로 예약한다(저장 시점 관 목록이 그 자리 기준). - 이동 안내는 차단 문구에서 '저장 뒤 다시 계산' 알림으로 바꿨다. --- .../B06_Section_Api_Culvert_Options.ts | 50 ++++++++++++++++--- B06_Section/B06_Section_UI_Page.ts | 2 + .../B06_Section_UI_Page_Station_Controls.ts | 6 +++ .../B06_Section_UI_Page_Structures_Panel.ts | 22 +++++++- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/B06_Section/B06_Section_Api_Culvert_Options.ts b/B06_Section/B06_Section_Api_Culvert_Options.ts index a62ed184..130274e0 100644 --- a/B06_Section/B06_Section_Api_Culvert_Options.ts +++ b/B06_Section/B06_Section_Api_Culvert_Options.ts @@ -28,6 +28,9 @@ type PendingMap = Record; export interface CulvertOptionWriter { /** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */ queue: (chainageM: number, patch: CulvertOptionPatch) => void; + /** 기준 측점 이동을 예약한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). + * 세부 배수유역 재분할은 저장 시 서버가 관 목록을 다시 받아 처리한다. */ + queueMove: (fromChainageM: number, toChainageM: number) => void; /** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */ flush: () => Promise; } @@ -59,25 +62,47 @@ function writePending(sessionKey: string | null, pending: PendingMap): void { * 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서 * 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다. */ +/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). */ +type PendingMoves = Record; + +function readMoves(sessionKey: string | null): PendingMoves { + if (!sessionKey) return {}; + try { + const raw = window.sessionStorage.getItem(sessionKey); + return raw ? (JSON.parse(raw) as PendingMoves) : {}; + } catch { + return {}; + } +} + export async function flushCulvertOptions( projectId: string | null, sessionKey: string | null, + moveKey: string | null = null, ): Promise { const pending = readPending(sessionKey); - const entries = Object.entries(pending); - if (!projectId || entries.length === 0) return; + const moves = readMoves(moveKey); + if (!projectId || (Object.keys(pending).length === 0 && Object.keys(moves).length === 0)) return; const current = await fetchDetailPipePoints(projectId); let touched = false; const points: DetailPipeInput[] = current.pipe_points.map((point) => { // 좌표는 서버가 다시 계산하므로 싣지 않는다(DetailPipeInput 규약). const { lonlat: _lonlat, ...input } = point; - const patch = pending[keyOf(point.chainage_m)]; - if (!patch) return input; + const key = keyOf(point.chainage_m); + const patch = pending[key]; + const movedTo = moves[key]; + if (!patch && movedTo === undefined) return input; touched = true; - return { ...input, options: { ...(input.options ?? {}), ...patch } }; + const next: DetailPipeInput = patch + ? { ...input, options: { ...(input.options ?? {}), ...patch } } + : { ...input }; + // 기준 측점 이동 — 관 위치가 바뀌면 서버가 세부 배수유역을 다시 나눠 응답한다. + if (movedTo !== undefined) next.chainage_m = movedTo; + return next; }); if (touched) await saveDetailPipePoints(projectId, points); writePending(sessionKey, {}); + writePending(moveKey, {}); } /** @@ -88,6 +113,8 @@ export function createCulvertOptionWriter( projectId: () => string | null, sessionKey: () => string | null, onError?: (message: string) => void, + /** 이동 예약 세션 키 — 없으면 이동은 메모리에도 남지 않는다. */ + moveKey: () => string | null = () => null, ): CulvertOptionWriter { return { queue(chainageM, patch) { @@ -96,9 +123,20 @@ export function createCulvertOptionWriter( pending[keyOf(chainageM)] = { ...(pending[keyOf(chainageM)] ?? {}), ...patch }; writePending(key, pending); }, + queueMove(fromChainageM, toChainageM) { + const key = moveKey(); + if (!key) return; + const moves = readMoves(key); + // 이미 옮긴 관을 또 옮기면 **원래 자리** 기준으로 최종 위치만 남긴다. + const origin = + Object.keys(moves).find((entry) => Math.abs(moves[entry] - fromChainageM) < 0.005) ?? + keyOf(fromChainageM); + moves[origin] = toChainageM; + window.sessionStorage.setItem(key, JSON.stringify(moves)); + }, async flush() { try { - await flushCulvertOptions(projectId(), sessionKey()); + await flushCulvertOptions(projectId(), sessionKey(), moveKey()); } catch (error) { onError?.(error instanceof Error ? error.message : "배수관 구간값 저장 실패"); throw error; diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 40b312cc..fcc2ab0a 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -141,6 +141,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { focusChainage: focusStationAt, queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch), applyPipeOptions: (chainageM, patch) => applyPipeOptionsToCache(chainageM, patch), + movePipe: (fromChainageM, toChainageM) => + stationControls.queueCulvertMove(fromChainageM, toChainageM), }); /** diff --git a/B06_Section/B06_Section_UI_Page_Station_Controls.ts b/B06_Section/B06_Section_UI_Page_Station_Controls.ts index fb87de56..ac8c0db3 100644 --- a/B06_Section/B06_Section_UI_Page_Station_Controls.ts +++ b/B06_Section/B06_Section_UI_Page_Station_Controls.ts @@ -49,6 +49,7 @@ export interface StationControlDeps { | "extrawall" | "extraspan" | "culvertopt" + | "culvertmove" | "revetlink" | "fordadjust" | "boxadjust", @@ -98,6 +99,8 @@ export interface StationControls { flushCulvertOptions: () => Promise; /** 관 옵션 조각을 예약한다 — 좌측 폼의 [수정]도 이 경로로 정본에 간다(2026-08-29). */ queueCulvertOptions: (chainageM: number, patch: Record) => void; + /** 기준 측점 이동을 예약한다 — B06 구조물 배치 폼의 측점 칸이 쓴다(2026-08-29). */ + queueCulvertMove: (fromChainageM: number, toChainageM: number) => void; load: () => void; /** 전체 반영 — 개별 반폭을 전역값으로 덮는다(없으면 비운다). */ applyGlobalWidth: (requested: number | undefined, chainages: number[]) => void; @@ -528,6 +531,7 @@ export function createStationControls(deps: StationControlDeps): StationControls deps.projectId, () => deps.sessionKey("culvertopt"), deps.onSaveError, + () => deps.sessionKey("culvertmove"), ); const linkSession = createLinkFlagSession(() => deps.sessionKey("revetlink")); const linkDetached = linkSession.detached; @@ -813,6 +817,8 @@ export function createStationControls(deps: StationControlDeps): StationControls }, flushCulvertOptions: () => culvertOptions.flush(), queueCulvertOptions: (chainageM, patch) => culvertOptions.queue(chainageM, patch), + queueCulvertMove: (fromChainageM, toChainageM) => + culvertOptions.queueMove(fromChainageM, toChainageM), load: () => { loadStationWidths(); loadRevetShifts(); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 3cd853c4..5c686685 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -30,6 +30,8 @@ import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; const PIPE_ADD_GUIDE = "계곡 통과 시설의 추가·삭제는 B05(종단) 화면에서 합니다."; const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다."; +const PIPE_MOVE_NOTICE = + "기준 측점을 옮겼습니다 — 세부 배수유역과 횡단도는 [저장]·[확정] 뒤에 다시 계산됩니다."; export interface B06StructuresPanelDeps { projectId: string | null; @@ -43,6 +45,9 @@ export interface B06StructuresPanelDeps { /** 바뀐 값을 그 측점 횡단 캐시(culvert 스펙)에 얹고 카드를 다시 그린다 — * 조정창과 같은 흐름이라 폼 조작도 도면에 바로 보인다(2026-08-29 사용자 보고). */ applyPipeOptions?: (chainageM: number, patch: Record) => void; + /** 기준 측점 이동을 예약한다 — 세부 배수유역 재분할은 [저장]·[확정] 때 서버가 + * 관 목록을 다시 받아 처리한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다). */ + movePipe?: (fromChainageM: number, toChainageM: number) => void; } export interface B06StructuresPanel { @@ -108,14 +113,27 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc // 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다 // (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다). const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M; - // 이동이라도 함께 온 제원은 반영한다 — 위치만 못 옮길 뿐 같은 데이터다. - if (moved) showToast(PIPE_MOVE_GUIDE, "error"); + if (moved) { + if (deps.movePipe) { + deps.movePipe(fromChainageM, toChainageM); + const hit = pipeFacilities.find( + (entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M, + ); + if (hit) hit.chainage_m = toChainageM; + currentChainageM = toChainageM; + section.setPipeFacilities(pipeFacilities); + showToast(PIPE_MOVE_NOTICE, "success"); + } else { + showToast(PIPE_MOVE_GUIDE, "error"); + } + } const patch = attributes.options; if (!patch || !Object.keys(patch).length) return; if (!deps.queuePipeOptions) { showToast(PIPE_ADD_GUIDE, "error"); return; } + // 옵션은 **옮기기 전 자리**로 예약한다 — 저장 시점의 관 목록이 그 자리 기준이다. deps.queuePipeOptions(fromChainageM, patch as Record); deps.applyPipeOptions?.(fromChainageM, patch as Record); // 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.