diff --git a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts index c6ea363a..00985513 100644 --- a/B05_Profile/B05_Profile_UI_Drainage_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Drainage_Panel.ts @@ -88,6 +88,9 @@ export interface DrainagePanel { savePipes: () => Promise; /** 밖에서 유역을 고른다(그래프 측점선·사이드 패널 선택과 맞추기 위함). 이미 같으면 무시. */ 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( diff --git a/B05_Profile/B05_Profile_UI_Page.ts b/B05_Profile/B05_Profile_UI_Page.ts index d6537b51..5b918a2f 100644 --- a/B05_Profile/B05_Profile_UI_Page.ts +++ b/B05_Profile/B05_Profile_UI_Page.ts @@ -248,6 +248,8 @@ export async function renderB05Route(root: HTMLElement): Promise { 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 { // 통합 「구조물 배치」 목록의 계곡 통과 시설 항목을 강조한다(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 { profilePanel.setSelectedStation(id); // 통합 목록에서 배관 항목을 고르면 배수유역도의 그 유역도 함께 강조한다. syncBasinHighlight(id); + // 배수유역 마커 선택 + 부속 옵션 폼도 연다(2026-08-17 전역 선택 동기화). + profilePanel.drainage.selectPipeAtChainage(chainage); } finally { selectionSyncing = false; } diff --git a/B05_Profile/B05_Profile_UI_Profile_Panel.ts b/B05_Profile/B05_Profile_UI_Profile_Panel.ts index 14aab46b..372216a9 100644 --- a/B05_Profile/B05_Profile_UI_Profile_Panel.ts +++ b/B05_Profile/B05_Profile_UI_Profile_Panel.ts @@ -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); diff --git a/B05_Profile/B05_Profile_UI_Selection.ts b/B05_Profile/B05_Profile_UI_Selection.ts index 4f2da651..bfb09e74 100644 --- a/B05_Profile/B05_Profile_UI_Selection.ts +++ b/B05_Profile/B05_Profile_UI_Selection.ts @@ -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); }