feat(B06): 기준 측점 이동을 B06 구조물 배치에서도 한다
2026-08-29 사용자: 구조물 배치 컨테이너를 B06으로 가져온 이유가 이것이다 — 폼의 기준 측점 숫자를 바꾸면 관이 옮겨져야 한다. - 관 저장기에 이동 예약(queueMove)을 더했다. 세션 키 b06:culvertmove에 담고, [저장]·[확정]의 flush가 옵션 병합과 함께 pipe_points에 반영한다. 세부 배수유역 재분할은 서버가 관 목록을 다시 받아 처리한다. - 폼에서 측점을 옮기면 목록·폼 캐시가 즉시 새 자리를 따르고, 옵션은 옮기기 전 자리로 예약한다(저장 시점 관 목록이 그 자리 기준). - 이동 안내는 차단 문구에서 '저장 뒤 다시 계산' 알림으로 바꿨다.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>);
|
||||
// 화면 캐시(목록·폼 재로드용)도 같이 맞춘다 — 저장 전에도 값이 유지된다.
|
||||
|
||||
Reference in New Issue
Block a user