/* ============================================================================= * B06_Section_UI_Page_Structures_Selection.ts * 횡단도 선택 → 좌측 폼 연동 배선(`wireStructureSelection`) — `_Page_Structures_Panel.ts` * 에서 떼어 냄(700줄). 폼 · 목록 패널 자체는 그 파일 몫. * ========================================================================== */ import { refreshAdjustSlots } from "./B06_Section_UI_Adjust_Dock"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; import type { RevetPick } from "./B06_Section_UI_Cross_Culvert"; import type { B06StructuresPanel } from "./B06_Section_UI_Page_Structures_Panel"; /** * 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그 * 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계). * 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다. */ /** 지금 처리 중인 선택이 **도면에서 벽·구체를 찍은 것**인가 — 카드 선택 리스너가 * 이 값으로 "카드만 고른 경우"를 가려낸다(2026-08-29 사용자: 측점 선택 때 구조물이 * 자동으로 골라지면 안 된다). 같은 턴에서만 참이다. */ let wallSelecting = false; export function isWallSelecting(): boolean { return wallSelecting; } function markWallSelecting(): void { wallSelecting = true; queueMicrotask(() => { wallSelecting = false; }); } export function wireStructureSelection( stationControls: StationControls, detail: () => SectionDetailResponse | null, panel: B06StructuresPanel, /** 고른 부재를 세션에 남긴다(2026-09-04). 해제(null)는 부르지 않는다 — 조정창 닫기가 * 카드(측점) 선택까지 지우면 안 된다. 측점 해제는 카드 선택 리스너가 따로 적는다. */ remember?: (chainageM: number, key: string) => void, /** 폼에서 고른 벽을 카드 조정창에도 세움(54-30 · 목록 넘김 `applyStructurePick` 과 같은 차례). */ refreshCard?: (chainageM: number) => void, /** 폼에 올린 시설로 보기를 옮김 — 목록에서 고른 길(54-47). */ reveal?: (chainageM: number) => void, ): { syncInletStructure: () => void } { const ownerChainageOf = (chainageM: number): number => { const sectionAt = detail()?.cross_sections.find( (entry) => Math.abs(entry.chainage_m - chainageM) < 0.01, ); return (sectionAt && stationControls.structureSpan.ownerOf(sectionAt)?.chainage_m) ?? chainageM; }; // 유입구 "구조" ↔ B06 유입측 형식 — 폼에서 고르면 조정값에 반영하고, 시설을 폼에 // 올릴 때는 조정값을 폼으로 되읽는다(2026-08-29 지시 5 병합). const sectionAt = (chainageM: number) => detail()?.cross_sections.find((entry) => Math.abs(entry.chainage_m - chainageM) < 0.01); panel.facility.onInletStructureChange((value) => { const chainageM = panel.currentChainage(); if (chainageM === null) return; stationControls.inletStructure.set(chainageM, value); }); /** 폼에 올라온 시설의 유입측 형식을 폼 드롭다운에 되싣는다. */ const syncInletStructure = (): void => { const chainageM = panel.currentChainage(); const target = chainageM === null ? undefined : sectionAt(chainageM); if (target) panel.facility.setInletStructure(stationControls.inletStructure.valueFor(target)); }; const origRevet = stationControls.revetOffset.select; stationControls.revetOffset.select = (chainageM, key) => { // 다른 카드 부재를 고르는 중에 옛 카드가 제 선택을 푸는 것 — 폼 · 선택 칸은 새 것 몫(53-1) const nested = wallSelecting; // 푸는 것은 고름이 아님 — 표시를 켜면 뒤이은 카드 고름이 선택 칸에 안 적힘(53-1) if (key) markWallSelecting(); origRevet(chainageM, key); if (key) remember?.(ownerChainageOf(chainageM), key); if (key) panel.showAtChainage(ownerChainageOf(chainageM)); else if (!nested) { remember?.(ownerChainageOf(chainageM), ""); // 푼 부재가 B05 ↔ B06 오갈 때 다시 안 열림 panel.releasePipe(); } if (key) syncInletStructure(); // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 목적지 칸이 그제야 선다. refreshAdjustSlots(); }; // 관 상자 칸을 고치거나 고르면 그 부분을 고름 — 횡단에서 누른 것과 같은 길(54-30) const keyOf = (box: HTMLElement): RevetPick | null => { const id = box.dataset.specContainer; if (id === "inlet" || id === "outlet" || id === "pipe") return id; return id === "soil_add" ? `extra${Number(box.dataset.specIndex)}` : null; }; const pickBox = (box: HTMLElement): void => { const chainageM = panel.currentChainage(); const target = chainageM === null ? undefined : sectionAt(chainageM); const key = keyOf(box); if (!key || !target?.culvert || stationControls.revetOffset.highlightFor(target) === key) return; stationControls.revetOffset.select(target.chainage_m, key); refreshCard?.(target.chainage_m); }; // 그 측점 카드 · 알약으로 감(목록에서 고른 길 · 54-47) — 같은 칸을 잇달아 고치면 한 번만(다시 그려도 칸 이름으로) const fieldOf = (el: HTMLElement): string => { const box = el.closest("fieldset[data-spec-container]"); const item = el.closest("[data-spec-item]")?.dataset.specItem; return `${box?.dataset.specContainer}:${box?.dataset.specIndex}:${item}`; }; let revealed: string | null = null; const revealAt = (chainageM: number, field: string | null): void => { if (field && field === revealed) return; revealed = field; reveal?.(chainageM); }; panel.root.addEventListener("focusin", (event) => { if (fieldOf(event.target as HTMLElement) !== revealed) revealed = null; }); panel.root.addEventListener("change", ({ target: el }) => { const box = (el as HTMLElement).closest("fieldset[data-spec-container]"); const chainageM = panel.currentChainage(); if (!box || chainageM === null) return; revealAt(chainageM, fieldOf(el as HTMLElement)); // 단 수 칸은 고른 단을 지킴(단 수 바꿈 뒤 강조 유지) — 새 단은 다시 그린 뒤 아래에서 고름 if (!(el as HTMLElement).closest('[data-spec-item="outlet.tiers"]')) pickBox(box); }); // 상자가 새로 서면(단 늘림 · [추가]) 그 상자를 고름 — 같은 시설을 다시 그린 때만 let painted: { chainageM: number | null; keys: Set } | null = null; panel.root.addEventListener("specpaint", () => { const chainageM = panel.currentChainage(); const boxes = [...panel.root.querySelectorAll("fieldset[data-spec-container]")]; const before = painted?.chainageM === chainageM ? painted.keys : null; painted = { chainageM, keys: new Set(boxes.map(fieldOf)) }; const born = before ? boxes.filter((box) => !before.has(fieldOf(box))).at(-1) : undefined; if (!born || chainageM === null) return; revealAt(chainageM, null); pickBox(born); }); const origFord = stationControls.ford.select; stationControls.ford.select = (chainageM, role) => { const nested = wallSelecting; if (role) markWallSelecting(); origFord(chainageM, role); if (role) remember?.(chainageM, role); if (role) panel.showAtChainage(chainageM); else if (!nested) { remember?.(chainageM, ""); panel.releasePipe(); } // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다 // (배관 기슭막이와 같은 순서, 2026-08-30 사용자 지시 1). refreshAdjustSlots(); }; const origBox = stationControls.box.select; stationControls.box.select = (chainageM, role) => { const nested = wallSelecting; if (role) markWallSelecting(); origBox(chainageM, role); if (role) remember?.(chainageM, role); if (role) panel.showAtChainage(chainageM); else if (!nested) { remember?.(chainageM, ""); panel.releasePipe(); } // 폼이 이 시설로 갈아 끼워진 뒤에 이식을 다시 돌린다 — 날개벽 칸이 그제야 선다. refreshAdjustSlots(); }; return { syncInletStructure }; }