feat(B05): 구조물 전역 선택 동기화 — 어느 화면에서 골라도 부속 옵션 폼 개방

- 갭: 배관 부속 폼(시설 편집)이 배수유역도 마커 직접 클릭에서만 열림 —
  리스트·종단 그래프·3D·유역 클릭은 강조만 되고 폼 미개방 (2026-08-17 확인)
- DrainagePanel.selectPipeAtChainage 신설: 밖에서 관 선택 시 마커+폼+유역
  강조까지 (같으면 무시). onPipeSelected 콜백: 유역 없는 관도 전 화면 전파,
  같은 값 재알림 억제(재계산 후 선택 풀림 방지)
- SelectionSync.selectPipeForm 포트: 그래프·3D(syncIrregularSelection)·유역
  (selectStationOfPipe) 경로에서 폼 개방. 리스트(onPipeFacilitySelect) 배선
- 재진입은 기존 selectionSyncing 가드 차단. typecheck/build 통과
This commit is contained in:
2026-08-17 14:27:56 +09:00
parent df73d1e933
commit 97dd7cb968
4 changed files with 44 additions and 0 deletions
@@ -88,6 +88,9 @@ export interface DrainagePanel {
savePipes: () => Promise<number>;
/** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */
selectBasinByChainage: (chainageM: number | null) => void;
/** 밖(리스트·그래프·3D)에서 관을 고른다 — 마커 선택·시설 폼·유역 강조까지 연다.
* 이미 같으면 무시(2026-08-17 전역 선택 동기화). */
selectPipeAtChainage: (chainageM: number | null) => void;
/** 측점 선택 마킹 — 계획선 위 해당 누가거리에 표식을 그린다(null이면 지움). */
markStation: (chainageM: number | null) => void;
dispose: () => void;
@@ -107,6 +110,9 @@ export interface DrainagePanelCallbacks {
) => void;
/** 유역을 고르거나 풀 때 그 관의 누가거리(없으면 null)를 넘긴다 — 그래프·사이드 패널 동기화용. */
onBasinSelected?: (chainageM: number | null) => void;
/** 관 마커 선택이 바뀔 때(클릭·해제) 누가거리를 알린다 — 유역이 없는 관도 전 화면
* (그래프·3D·리스트)이 같은 것을 가리키게 한다(2026-08-17 전역 선택 동기화). */
onPipeSelected?: (chainageM: number | null) => void;
/** 배수유역도 우클릭으로 배관 외 구조물을 넣을 때(누가거리는 계획선 투영값). */
onStructureAdd?: (chainageM: number, type: "기성막이" | "대피로" | "기타") => void;
}
@@ -219,6 +225,8 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
let selectedBasin: number | null = null;
// 측점 선택 마킹(계획선 위 누가거리). 유역이 없는 구조물 측점도 위치를 보여 준다.
let markedChainage: number | null = null;
// 마지막으로 밖에 알린 관 선택(누가거리) — 같은 값 재알림을 막는다.
let lastNotifiedPipeChainage: number | null = null;
// 배관 편집기 — 마커 선택/추가/이동/삭제 시 재그리기와 버튼 상태만 갱신한다.
const pipeEditor = createPipeEditor(
() => {
@@ -370,6 +378,14 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
picked ? ((picked.reason || "user") as PipeSource) : undefined,
);
selectBasin(basinIndexOfPipe(basins, picked ?? null));
// 유역 유무와 무관하게 선택 자체를 알린다 — 그래프·3D·리스트 동기화(재진입은
// Page의 selectionSyncing 가드가 끊는다). 값이 안 바뀌면 조용히 — 재계산(apply)
// 뒤의 재호출이 다른 화면 선택을 풀어 버리지 않게.
const pickedChainage = picked ? picked.chainage_m : null;
if (pickedChainage !== lastNotifiedPipeChainage) {
lastNotifiedPipeChainage = pickedChainage;
callbacks.onPipeSelected?.(pickedChainage);
}
}
function scheduleDraw(): void {
@@ -744,6 +760,18 @@ export function createDrainagePanel(callbacks: DrainagePanelCallbacks = {}): Dra
pipeEditor.select(index);
pipeEditor.deleteSelected();
},
selectPipeAtChainage(chainageM) {
const index =
chainageM === null
? null
: pipeEditor.pipes().findIndex((pipe) => Math.abs(pipe.chainage_m - chainageM) < 0.51);
const next = index === -1 ? null : index;
if (pipeEditor.selected() === next) return;
// select()는 onChange를 울리지 않는다 — 폼·유역·재그리기를 직접 맞춘다.
pipeEditor.select(next);
syncPipeSelection();
scheduleDraw();
},
async savePipes() {
if (!projectId) return 0;
const response = await saveDetailPipePoints(
+6
View File
@@ -248,6 +248,8 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
onIrregularSelect: (station) => syncIrregularSelection(irregularStationId(station.id)),
// 배수유역도에서 유역을 고르면 그 관의 구조물 측점을 그래프·3D·사이드 패널에서도 고른다.
onBasinSelected: (chainageM) => selectStationOfPipe(chainageM),
// 관 마커 선택(유역 없는 관 포함)도 같은 경로로 전 화면을 맞춘다(2026-08-17).
onPipeSelected: (chainageM) => selectStationOfPipe(chainageM),
// 구조물 서클마크 — 그래프에서 고르면 사이드 폼도 같은 항목을 연다.
onStructureSelect: (structureId) => panel.structures.selectById(structureId),
onStructureMarkMove: (structureId, toChainage) =>
@@ -301,6 +303,8 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
// 통합 「구조물 배치」 목록의 계곡 통과 시설 항목을 강조한다(2026-08-17 통합).
selectSidebar: (chainageM) => panel.structures.selectPipeByChainage(chainageM),
selectBasin: (chainageM) => profilePanel.drainage.selectBasinByChainage(chainageM),
// 배관이면 배수유역 마커 선택 + 부속 옵션 폼까지 연다(2026-08-17 전역 선택 동기화).
selectPipeForm: (chainageM) => profilePanel.drainage.selectPipeAtChainage(chainageM),
markStation: (chainageM) => profilePanel.drainage.markStation(chainageM),
});
@@ -369,6 +373,8 @@ export async function renderB05Route(root: HTMLElement): Promise<void> {
profilePanel.setSelectedStation(id);
// 통합 목록에서 배관 항목을 고르면 배수유역도의 그 유역도 함께 강조한다.
syncBasinHighlight(id);
// 배수유역 마커 선택 + 부속 옵션 폼도 연다(2026-08-17 전역 선택 동기화).
profilePanel.drainage.selectPipeAtChainage(chainage);
} finally {
selectionSyncing = false;
}
@@ -238,6 +238,8 @@ export interface RouteProfilePanelCallbacks {
onStructureTypeAdd?: (chainageM: number, typeId: string) => void;
/** 배수유역도에서 유역을 고름 — 그 관의 누가거리(해제면 null). 그래프·사이드 패널을 맞춘다. */
onBasinSelected?: (chainageM: number | null) => void;
/** 배수유역도에서 관 마커를 고름(유역 없는 관 포함) — 전 화면 동기화용(2026-08-17). */
onPipeSelected?: (chainageM: number | null) => void;
}
export function createRouteProfilePanel(
@@ -298,6 +300,7 @@ export function createRouteProfilePanel(
const drainagePanel = createDrainagePanel({
onPipesChanged: (pipes) => callbacks?.onPipesChanged?.(pipes),
onBasinSelected: (chainageM) => callbacks?.onBasinSelected?.(chainageM),
onPipeSelected: (chainageM) => callbacks?.onPipeSelected?.(chainageM),
onStructureAdd: (chainage, type) => callbacks?.onStructureAdd?.(chainage, type),
});
content.append(bodyWrap, drainagePanel.root);
+7
View File
@@ -30,6 +30,9 @@ export interface SelectionSyncPorts {
selectBasin: (chainageM: number | null) => void;
/** 배수유역도 계획선 위 측점 마킹(누가거리 기준). 유역 없는 구조물도 위치를 보여 준다. */
markStation?: (chainageM: number | null) => void;
/** + ( )
* (2026-08-17 ). */
selectPipeForm?: (chainageM: number | null) => void;
}
export interface SelectionSync {
@@ -72,6 +75,7 @@ export function createSelectionSync(ports: SelectionSyncPorts): SelectionSync {
ports.setSyncing(true);
try {
ports.selectSidebar(station ? station.chainage_m : null);
ports.selectPipeForm?.(station && isPipeStation(station) ? station.chainage_m : null);
} finally {
ports.setSyncing(false);
}
@@ -93,6 +97,9 @@ export function createSelectionSync(ports: SelectionSyncPorts): SelectionSync {
ports.selectMarker(id);
ports.selectGraph(id);
ports.selectSidebar(matched ? matched.chainage_m : null);
// 유역·마커 어느 쪽에서 왔든 시설 폼도 그 관을 연다(투영 측점이 없어도
// 누가거리로 근사 매칭 — Drainage 쪽이 못 찾으면 해제된다).
ports.selectPipeForm?.(matched ? matched.chainage_m : chainageM);
} finally {
ports.setSyncing(false);
}