diff --git a/B06_Section/B06_Section_UI_Page.ts b/B06_Section/B06_Section_UI_Page.ts index 976340fb..c0261fb4 100644 --- a/B06_Section/B06_Section_UI_Page.ts +++ b/B06_Section/B06_Section_UI_Page.ts @@ -466,6 +466,19 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise { ); // 횡단도 벽·구체 선택 → 좌측 「구조물 배치」 폼에 그 시설 로드(2026-08-29 일원화). wireStructureSelection(stationControls, () => sectionDetail, structuresPanel); + // 횡단도(카드) 자체를 골라도 그 측점 구조물 정보를 폼에 올린다(2026-08-29 사용자). + // 연동으로 옆에서 넘어온 카드는 **소유 측점** 시설을 보여 준다. + sectionView.setStationSelectListener((stationId) => { + const target = stationId + ? sectionDetail?.cross_sections.find((section) => section.station_id === stationId) + : null; + if (!target) { + structuresPanel.showAtChainage(null); + return; + } + const owner = stationControls.structureSpan.ownerOf(target); + structuresPanel.showAtChainage(owner?.chainage_m ?? target.chainage_m); + }); // 메인 영역: 종·횡단 도면(sectionView) 또는 안내 메시지를 표시한다. const mainArea = document.createElement("div"); diff --git a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts index 23f4c237..88e7aae5 100644 --- a/B06_Section/B06_Section_UI_Page_Structures_Panel.ts +++ b/B06_Section/B06_Section_UI_Page_Structures_Panel.ts @@ -47,10 +47,25 @@ export interface B06StructuresPanel { load: () => Promise; /** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */ showPipeAt: (chainageM: number | null) => void; + /** 고른 횡단도(측점)에 있는 구조물을 폼에 올린다 — 계곡 통과 시설이 먼저고, + * 없으면 그 측점을 덮는 구조물 정본 항목. 둘 다 없으면 해제(2026-08-29). */ + showAtChainage: (chainageM: number | null) => void; +} + +/** 관 매칭 허용 오차(m) — 관은 계획선에 스냅돼 측점과 살짝 어긋난다(B05와 같은 기준). */ +const PIPE_MATCH_M = 0.51; + +/** 이 구조물이 그 측점을 덮는가 — 점형은 근사 일치, 구간형은 시작~종료 안. */ +function coversChainage(structure: StructureInstance, chainageM: number): boolean { + if (structure.placement === "interval" && structure.start_m != null && structure.end_m != null) { + return chainageM >= structure.start_m - 0.01 && chainageM <= structure.end_m + 0.01; + } + return Math.abs(structureAnchorM(structure) - chainageM) < PIPE_MATCH_M; } export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel { let structures: StructureInstance[] = []; + let pipeFacilities: PipeFacilityItem[] = []; /** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야 * 하고, 서버는 빈 값일 때만 새로 발급한다(B05 다리와 같은 규칙). */ @@ -95,7 +110,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc // 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙). structures = readPendingStructures(projectId) ?? stored.structures; section.setStructures(structures); - const pipes: PipeFacilityItem[] = pipeResponse.pipe_points.map((pipe) => ({ + pipeFacilities = pipeResponse.pipe_points.map((pipe) => ({ chainage_m: pipe.chainage_m, facility: pipe.facility ?? "pipe", start_m: pipe.start_m, @@ -104,7 +119,7 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc options: pipe.options, design_flow_m3s: null, })); - section.setPipeFacilities(pipes); + section.setPipeFacilities(pipeFacilities); } catch (error) { showToast( error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.", @@ -118,6 +133,23 @@ export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06Struc listRoot: section.listRoot, load, showPipeAt: (chainageM) => section.selectPipeByChainage(chainageM), + showAtChainage: (chainageM) => { + if (chainageM === null) { + section.selectById(null); + return; + } + // 계곡 통과 시설이 먼저다 — 같은 측점에 구조물 정본 항목이 겹쳐도 횡단도에 + // 그려지는 것은 이쪽이다. + const pipe = pipeFacilities.find( + (entry) => Math.abs(entry.chainage_m - chainageM) < PIPE_MATCH_M, + ); + if (pipe) { + section.selectPipeByChainage(pipe.chainage_m); + return; + } + const hit = structures.find((entry) => coversChainage(entry, chainageM)); + section.selectById(hit?.structure_id ?? null); + }, }; } diff --git a/B06_Section/B06_Section_UI_Section_View.ts b/B06_Section/B06_Section_UI_Section_View.ts index fd0cbe01..27064096 100644 --- a/B06_Section/B06_Section_UI_Section_View.ts +++ b/B06_Section/B06_Section_UI_Section_View.ts @@ -115,6 +115,9 @@ export interface SectionViewController { /** 좌측 구조물 목록에서 고른 측점 카드를 선택하고 화면에 드러낸다 — 재클릭 토글 없음 * (2026-08-29 B05/B06 일원화: 목록 클릭 → 해당 카드 스크롤·강조). */ focusStation: (stationId: string) => void; + /** 카드(측점) 선택이 바뀔 때 알림 — 좌측 「구조물 배치」 폼이 그 측점 구조물을 + * 올린다(2026-08-29 일원화). null = 선택 해제. */ + setStationSelectListener: (listener: (stationId: string | null) => void) => void; clear: () => void; dispose: () => void; } @@ -356,10 +359,14 @@ export function createSectionView( * (2026-08-02 사용자 지적). 선택 표시가 바뀌는 카드는 이전·현재 둘뿐이고, 그 둘도 클래스와 * 강조 상태만 갈아 끼운다. */ + /** 카드 선택 변경 수신자(좌측 구조물 폼). 페이지가 등록한다. */ + let stationSelectListener: ((stationId: string | null) => void) | null = null; + const applySelection = (previousStationId: string | null): void => { markCard(previousStationId); if (selectedStationId !== previousStationId) markCard(selectedStationId); drawPanel(); + if (selectedStationId !== previousStationId) stationSelectListener?.(selectedStationId); }; /** 같은 측점을 다시 고르면 선택을 푼다(2026-08-02 사용자 지시) — 카드·측점선 어느 쪽이든. */ @@ -708,6 +715,9 @@ export function createSectionView( if (selectedStationId !== stationId) selectStation(stationId, true); else revealCard(stationId, "smooth"); }, + setStationSelectListener(listener) { + stationSelectListener = listener; + }, clear() { currentDetail = null; selectedStationId = null;