refactor(b06): 관 목록을 담는 자리를 한 벌로 합침

B05 는 관 목록 전체를 세션 스냅샷으로, B06 은 바뀐 것만 따로 담고 있어 담는 꼴이 둘이었음.
저장 순서를 맞춰 결과는 같게 했으나 자리가 둘인 한 같은 사고가 다시 남.

B06 의 추가·삭제·이동을 모두 B05 가 쓰는 스냅샷(pipes)에 쓰도록 바꾸고, B06 전용 예약
경로(culvertedit 세션·queueAdd·queueRemove·queueMove)를 걷어 냄. 옛 세션에 남아 있던
이동 예약만 저장 때 한 번 흘려보내게 읽기를 남김.

이제 관을 넣고 빼고 옮기는 길이 좌측 폼·목록·종단 우클릭·측점선 끌기 모두 한 함수이고,
담기는 자리도 한 곳임.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
2026-09-12 16:16:19 +09:00
co-authored by Claude Opus 5
parent cb5ea7ede9
commit b1ba37b004
7 changed files with 104 additions and 214 deletions
@@ -24,7 +24,7 @@ import {
type StructureInstance,
type StructureType,
} from "../B05_Profile/B05_Profile_Api_Structures";
import { readPendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
import { readPendingPipes, writePendingPipes } from "../B05_Profile/B05_Profile_Api_Pipes_Draft";
import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch";
import {
readStructurePick,
@@ -78,13 +78,6 @@ 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;
/** 관(계곡 통과 시설)을 넣는다 — 이동과 같은 예약 경로(2026-09-12 B05·B06 일원화). */
addPipe?: (chainageM: number, facility?: string) => void;
/** 관을 뺀다 — 같은 예약 경로. */
removePipe?: (chainageM: number) => void;
/** 구조물 목록이 바뀌었다 — 벽(C군)은 횡단 제원으로 얹혀 **면적까지 달라지므로**
* 화면이 횡단을 다시 받아 그려야 한다(2026-09-06 사용자 확정). */
onStructuresChanged?: () => void;
@@ -267,7 +260,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
onReveal: () => deps.reveal?.(),
// 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다.
onPipeAdd: (chainageM, attributes) => {
if (!deps.addPipe) {
if (!deps.projectId) {
showToast(PIPE_ADD_GUIDE, "error");
return;
}
@@ -276,38 +269,28 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
showToast("그 자리에는 계곡 통과 시설이 이미 있습니다.", "error");
return;
}
const facility = attributes.facility ?? "pipe";
deps.addPipe(at, facility);
pipeFacilities.push({ chainage_m: at, facility, options: attributes.options ?? {} });
pipeFacilities.push({
chainage_m: at,
facility: attributes.facility ?? "pipe",
source: "user",
options: attributes.options ?? {},
});
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
currentChainageM = at;
persistPipes();
section.setPipeFacilities(pipeFacilities);
pushMarks();
deps.onStructuresChanged?.();
showToast(PIPE_ADD_NOTICE, "success");
},
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
// 값 수정·기준점 이동 모두 B06 에서 받는다 — 이동은 종단 끌기와 **같은 함수**를 탄다
// (2026-09-12 일원화). 재분할은 [저장]·[확정] 뒤 서버 몫이다.
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
// 위치가 "옮겨졌다"고 볼 기준은 관 매칭과 같은 0.51m다 — 폼의 측점 표기는
// 0.1m로 반올림되므로 0.005m 기준으로 보면 값만 고쳐도 이동으로 잡힌다
// (2026-08-29 사용자 보고: 높이만 바꿨는데 이동 안내가 떴다).
const moved = Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M;
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);
pushMarks();
showToast(PIPE_MOVE_NOTICE, "success");
} else {
showToast(PIPE_MOVE_GUIDE, "error");
}
}
if (Math.abs(fromChainageM - toChainageM) > PIPE_MATCH_M)
movePipeTo(fromChainageM, toChainageM);
const patch = attributes.options;
if (!patch || !Object.keys(patch).length) return;
if (!deps.queuePipeOptions) {
@@ -324,6 +307,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
(entry) => Math.abs(entry.chainage_m - fromChainageM) < PIPE_MATCH_M,
);
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
persistPipes();
section.setPipeFacilities(pipeFacilities);
},
onPipeRemove: (chainageM) => removePipeAt(chainageM),
@@ -440,14 +424,33 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
section.setPipeFacilities(pipeFacilities);
}
/**
* 관 목록을 **미저장 스냅샷**으로 남긴다 — B05 가 쓰는 바로 그 세션이다(2026-09-12
* 사용자: 담는 꼴도 한 벌이어야 한다). 종전에는 B06 만 「바뀐 것만」 따로 담아
* 두 페이지가 서로 다른 자리를 보고 저장 순서에 따라 결과가 갈렸다.
*/
function persistPipes(): void {
if (!deps.projectId) return;
writePendingPipes(
deps.projectId,
pipeFacilities.map((pipe) => ({
chainage_m: pipe.chainage_m,
source: pipe.source ?? "user",
facility: pipe.facility,
...(pipe.start_m !== undefined ? { start_m: pipe.start_m } : {}),
...(pipe.end_m !== undefined ? { end_m: pipe.end_m } : {}),
...(pipe.options ? { options: pipe.options } : {}),
})),
);
}
/** 관 옮기기 — 폼의 측점 칸과 종단 끌기가 같은 길을 탄다. 재분할은 [저장]·[확정] 뒤. */
function movePipeTo(fromChainageM: number, toChainageM: number): void {
if (!deps.movePipe) {
if (!deps.projectId) {
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,
);
@@ -455,6 +458,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
if (currentChainageM !== null && Math.abs(currentChainageM - fromChainageM) < PIPE_MATCH_M)
currentChainageM = to;
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
persistPipes();
section.setPipeFacilities(pipeFacilities);
pushMarks();
showToast(PIPE_MOVE_NOTICE, "success");
@@ -462,17 +466,17 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
function removePipeAt(chainageM: number): void {
if (!deps.removePipe) {
if (!deps.projectId) {
showToast(PIPE_ADD_GUIDE, "error");
return;
}
const at = Number(chainageM.toFixed(2));
deps.removePipe(at);
pipeFacilities = pipeFacilities.filter(
(entry) => Math.abs(entry.chainage_m - at) >= PIPE_MATCH_M,
);
if (currentChainageM !== null && Math.abs(currentChainageM - at) < PIPE_MATCH_M)
currentChainageM = null;
persistPipes();
section.setPipeFacilities(pipeFacilities);
pushMarks();
deps.onStructuresChanged?.();