feat(B06): 기준 측점 이동을 B06 구조물 배치에서도 한다

2026-08-29 사용자: 구조물 배치 컨테이너를 B06으로 가져온 이유가 이것이다 —
폼의 기준 측점 숫자를 바꾸면 관이 옮겨져야 한다.

- 관 저장기에 이동 예약(queueMove)을 더했다. 세션 키 b06:culvertmove에 담고,
  [저장]·[확정]의 flush가 옵션 병합과 함께 pipe_points에 반영한다. 세부 배수유역
  재분할은 서버가 관 목록을 다시 받아 처리한다.
- 폼에서 측점을 옮기면 목록·폼 캐시가 즉시 새 자리를 따르고, 옵션은 옮기기 전
  자리로 예약한다(저장 시점 관 목록이 그 자리 기준).
- 이동 안내는 차단 문구에서 '저장 뒤 다시 계산' 알림으로 바꿨다.
This commit is contained in:
2026-08-29 16:53:17 +09:00
parent effd5e59d3
commit 45c6f9aa07
4 changed files with 72 additions and 8 deletions
+44 -6
View File
@@ -28,6 +28,9 @@ type PendingMap = Record<string, CulvertOptionPatch>;
export interface CulvertOptionWriter {
/** 측점 옵션을 세션에 예약한다. 같은 측점의 앞선 예약과는 합쳐진다. */
queue: (chainageM: number, patch: CulvertOptionPatch) => void;
/** 기준 측점 이동을 예약한다(2026-08-29 사용자: B06에서도 옮길 수 있어야 한다).
* 세부 배수유역 재분할은 저장 시 서버가 관 목록을 다시 받아 처리한다. */
queueMove: (fromChainageM: number, toChainageM: number) => void;
/** 예약분을 즉시 내보낸다([저장]·[확정]에서만 부른다). */
flush: () => Promise<void>;
}
@@ -59,25 +62,47 @@ function writePending(sessionKey: string | null, pending: PendingMap): void {
* 하지 않는다. 성공하면 세션을 비우고, 실패하면 **그대로 두어** 다음 [저장]에서
* 다시 시도한다 — 조용히 잃으면 화면과 저장분이 갈린다.
*/
/** 예약된 이동 — 옛 측점 키 → 새 누가거리(m). */
type PendingMoves = Record<string, number>;
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<void> {
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;
+2
View File
@@ -141,6 +141,8 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
focusChainage: focusStationAt,
queuePipeOptions: (chainageM, patch) => stationControls.queueCulvertOptions(chainageM, patch),
applyPipeOptions: (chainageM, patch) => applyPipeOptionsToCache(chainageM, patch),
movePipe: (fromChainageM, toChainageM) =>
stationControls.queueCulvertMove(fromChainageM, toChainageM),
});
/**
@@ -49,6 +49,7 @@ export interface StationControlDeps {
| "extrawall"
| "extraspan"
| "culvertopt"
| "culvertmove"
| "revetlink"
| "fordadjust"
| "boxadjust",
@@ -98,6 +99,8 @@ export interface StationControls {
flushCulvertOptions: () => Promise<void>;
/** 관 옵션 조각을 예약한다 — 좌측 폼의 [수정]도 이 경로로 정본에 간다(2026-08-29). */
queueCulvertOptions: (chainageM: number, patch: Record<string, number | string>) => 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();
@@ -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<string, number | string>) => 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<string, number | string>);
deps.applyPipeOptions?.(fromChainageM, patch as Record<string, number | string>);
// 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.