feat(B06): 횡단도를 고르면 그 측점 구조물을 좌측 폼에 올린다
- SectionView.setStationSelectListener 신설 — 카드(측점) 선택 변경 알림 - 좌측 패널 showAtChainage: 계곡 통과 시설(pipe_points) 우선, 없으면 그 측점을 덮는 구조물 정본 항목(점형 근사·구간형 범위), 둘 다 없으면 선택 해제 - 연동 링크 카드는 structureSpan.ownerOf로 소유 측점 시설을 보여 준다 검증(공용 브라우저): 4+4.3 배수관·7+9.7 세월교·10+0.9 BOX암거 카드 클릭 시 종류·기준 측점·목록 강조 일치. 링크 카드 4+0.0→4+4.3, 14+0.0→13+15.7 소유 측점 표시. 구조물 없는 0+0.0 카드는 폼 해제. typecheck 통과.
This commit is contained in:
@@ -466,6 +466,19 @@ export async function renderB06ProfileCross(root: HTMLElement): Promise<void> {
|
||||
);
|
||||
// 횡단도 벽·구체 선택 → 좌측 「구조물 배치」 폼에 그 시설 로드(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");
|
||||
|
||||
@@ -47,10 +47,25 @@ export interface B06StructuresPanel {
|
||||
load: () => Promise<void>;
|
||||
/** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(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);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user