feat(b06): 종단 그래프 우클릭으로 구조물·계곡 통과 시설을 넣고 빼게 함
B05 와 B06 은 한 페이지인데 구조물을 넣고 빼는 자리가 B05 뿐이라 화면을 오가야 했음. 종단 그래프 렌더러·알약 레인·좌측 배치 폼은 이미 공용이었으므로, 빠져 있던 조작층만 B05 와 같은 부품(mountStructureMenu)으로 B06 에도 붙임. 관 추가·삭제는 이동과 같은 예약 경로를 씀 — 세션에 쌓고 [저장]·[확정]에서 정본 (pipe_points.json)으로 나가며, 세부 배수유역 재분할은 그때 서버가 처리함. 넣었다 바로 빼거나 뺐다 다시 넣으면 예약만 걷어 내 정본이 흔들리지 않음. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Fe1QWPTfw11PaKh2LjwXdR
This commit is contained in:
@@ -39,8 +39,13 @@ import {
|
||||
import type { SectionDetailResponse } from "./B06_Section_Api_Fetch";
|
||||
import type { StationControls } from "./B06_Section_UI_Page_Station_Controls";
|
||||
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설의 추가·삭제는 B05(종단) 화면에서 합니다.";
|
||||
const PIPE_ADD_GUIDE = "계곡 통과 시설을 넣고 빼려면 프로젝트를 먼저 여세요.";
|
||||
const PIPE_MOVE_GUIDE = "기준 측점 이동은 배수유역을 다시 나눠야 해 B05(종단) 화면에서 합니다.";
|
||||
/** B05·B06 은 한 페이지라 넣고 빼는 자리도 같아야 한다(2026-09-12 사용자). 재분할은 저장 뒤. */
|
||||
const PIPE_ADD_NOTICE =
|
||||
"계곡 통과 시설을 넣었습니다 — 세부 배수유역과 횡단도는 [저장]·[확정] 뒤에 다시 계산됩니다.";
|
||||
const PIPE_REMOVE_NOTICE =
|
||||
"계곡 통과 시설을 뺐습니다 — 세부 배수유역과 횡단도는 [저장]·[확정] 뒤에 다시 계산됩니다.";
|
||||
/** 고른 것 없이 폼만 만졌을 때 — 값이 어디로도 가지 않으므로 이유를 알린다. */
|
||||
const PIPE_PICK_GUIDE = "먼저 횡단도나 목록에서 구조물을 고르세요 — 고른 것에만 값이 반영됩니다.";
|
||||
const PIPE_MOVE_NOTICE =
|
||||
@@ -75,6 +80,10 @@ export interface B06StructuresPanelDeps {
|
||||
/** 기준 측점 이동을 예약한다 — 세부 배수유역 재분할은 [저장]·[확정] 때 서버가
|
||||
* 관 목록을 다시 받아 처리한다(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;
|
||||
@@ -97,6 +106,11 @@ export interface B06StructuresPanel {
|
||||
showAtChainage: (chainageM: number | null) => void;
|
||||
/** 지금 폼에 올라온 계곡 통과 시설의 누가거리(없으면 null). */
|
||||
currentChainage: () => number | null;
|
||||
/** 종단 그래프 우클릭이 쓰는 길 — 좌측 폼·목록과 **같은 함수**를 탄다(2026-09-12). */
|
||||
addStructureAt: (chainageM: number, typeId: string) => void;
|
||||
removeStructureById: (structureId: string) => boolean;
|
||||
addPipeAt: (chainageM: number) => void;
|
||||
removePipeAt: (chainageM: number) => void;
|
||||
/** 유입구 "구조"에 합쳐진 B06 유입측 형식 — 조정창 값과 맞춘다(2026-08-29 지시 5). */
|
||||
facility: {
|
||||
setInletStructure: (value: "auto" | "revet" | "I" | "L" | "U") => void;
|
||||
@@ -248,7 +262,27 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
},
|
||||
getInterval: deps.stationInterval,
|
||||
onReveal: () => deps.reveal?.(),
|
||||
onPipeAdd: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
// 관 추가 — 정본은 [저장]·[확정]에서 나간다. 화면 목록·알약은 바로 세운다.
|
||||
onPipeAdd: (chainageM, attributes) => {
|
||||
if (!deps.addPipe) {
|
||||
showToast(PIPE_ADD_GUIDE, "error");
|
||||
return;
|
||||
}
|
||||
const at = Number(chainageM.toFixed(2));
|
||||
if (pipeFacilities.some((entry) => Math.abs(entry.chainage_m - at) < PIPE_MATCH_M)) {
|
||||
showToast("그 자리에는 계곡 통과 시설이 이미 있습니다.", "error");
|
||||
return;
|
||||
}
|
||||
const facility = attributes.facility ?? "pipe";
|
||||
deps.addPipe(at, facility);
|
||||
pipeFacilities.push({ chainage_m: at, facility, options: attributes.options ?? {} });
|
||||
pipeFacilities.sort((left, right) => left.chainage_m - right.chainage_m);
|
||||
currentChainageM = at;
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
deps.onStructuresChanged?.();
|
||||
showToast(PIPE_ADD_NOTICE, "success");
|
||||
},
|
||||
// 값 수정은 B06에서도 받는다 — 조정창 구간값과 같은 저장 경로(캐시 예약 →
|
||||
// [저장]·[확정])로 보낸다. 기준점 이동만 배수유역 재분할이 걸려 B05 몫이다.
|
||||
onPipeUpdate: (fromChainageM, toChainageM, attributes) => {
|
||||
@@ -289,7 +323,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
if (hit) hit.options = { ...(hit.options ?? {}), ...patch };
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
},
|
||||
onPipeRemove: () => showToast(PIPE_ADD_GUIDE, "error"),
|
||||
onPipeRemove: (chainageM) => removePipeAt(chainageM),
|
||||
onPipeSelect: (chainageM) => {
|
||||
// 목록에서 고른 것도 세션에 남긴다 — B05로 돌아가면 그 시설이 그대로 열린다.
|
||||
writeStructurePick(deps.projectId, chainageM);
|
||||
@@ -400,11 +434,35 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
}
|
||||
|
||||
/** 관 빼기 — 목록·폼·종단 우클릭이 같은 길을 탄다. 정본은 [저장]·[확정]에서 나간다. */
|
||||
function removePipeAt(chainageM: number): void {
|
||||
if (!deps.removePipe) {
|
||||
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;
|
||||
section.setPipeFacilities(pipeFacilities);
|
||||
pushMarks();
|
||||
deps.onStructuresChanged?.();
|
||||
showToast(PIPE_REMOVE_NOTICE, "success");
|
||||
}
|
||||
|
||||
return {
|
||||
root: section.root,
|
||||
listRoot: section.listRoot,
|
||||
load,
|
||||
currentChainage: () => currentChainageM,
|
||||
// 종단 우클릭 → 좌측 폼·목록과 같은 함수. 폼이 열리고 목록·알약도 함께 선다.
|
||||
addStructureAt: (chainageM, typeId) => section.addAt(chainageM, typeId),
|
||||
removeStructureById: (structureId) => section.removeById(structureId),
|
||||
addPipeAt: (chainageM) => section.addAt(chainageM, "pipe"),
|
||||
removePipeAt: (chainageM) => removePipeAt(chainageM),
|
||||
facility: {
|
||||
setInletStructure: (value) => section.facility.setInletStructure(value),
|
||||
onInletStructureChange: (handler) => section.facility.onInletStructureChange(handler),
|
||||
|
||||
Reference in New Issue
Block a user