/* ============================================================================= * B06_Section_UI_Page_Structures_Panel.ts * B06 좌측 「구조물 배치」 — B05와 **같은 컨테이너·목록 템플릿**을 세운다 * (2026-08-29 사용자: B05/B06 인터페이스 일원화. 페이지 본체는 700줄 제한으로 * 여기서 조립만 받아 간다). * * · 폼·목록 구현 = 공용 진입점(`A00_Common/b_structures_section`) — B05 그대로. * · 구조물(B~G군) 추가·수정·삭제는 세션 미저장분(`writePendingStructures`)에 쌓고 * [임시저장]·[확정]의 `flushPendingStructures`가 정본에 쓴다(캐시→저장 원칙). * · 계곡 통과 시설(A군, pipe_points 정본)은 **표시·선택만** — 추가·이동·삭제는 * 배수유역 재분할 체인이 있는 B05 몫이라 안내만 한다(2026-08-29 1차 범위). * · 목록·폼 선택 → 해당 측점 카드 선택·스크롤. 횡단도 벽·구체 선택 → 폼에 그 시설 * 로드(소유 측점 기준). 하위 [횡단 조정] 컨테이너는 Adjust_Dock이 맡는다. * ========================================================================== */ import { createStructuresSection, type PipeFacilityItem } from "../A00_Common/b_structures_section"; import { fetchStructures, fetchStructureTypes, readPendingStructures, structureAnchorM, writePendingStructures, type StructureInstance, } from "../B05_Profile/B05_Profile_Api_Structures"; import { fetchDetailPipePoints } from "../B04_PreProcess/B04_PreProcess_Api_Fetch"; import { showToast } from "@ui/ui_template_elements"; import { adjustDockRoot, resetAdjustDock } from "./B06_Section_UI_Adjust_Dock"; import type { SectionDetailResponse } from "./B06_Section_Api_Fetch"; import type { StationControls } from "./B06_Section_UI_Page_Station_Controls"; const PIPE_GUIDE = "계곡 통과 시설의 추가·이동·삭제는 B05(종단) 화면에서 합니다."; export interface B06StructuresPanelDeps { projectId: string | null; /** 측점 간격(m) — 측점번호+잔여거리 환산용. */ stationInterval: () => number; /** 목록·폼에서 고른 시설의 측점 카드를 선택·스크롤한다. */ focusChainage: (chainageM: number) => void; } export interface B06StructuresPanel { /** 「구조물 배치」 섹션(하위 [횡단 조정] 포함) — 좌측 패널에 붙인다. */ root: HTMLElement; /** 배치된 구조물 목록 — 하단 고정 dock에 붙인다(B05와 같은 자리). */ listRoot: HTMLElement; /** 타입 레지스트리·구조물 정본(미저장분 우선)·관 지점을 받아 채운다. */ load: () => Promise; /** 횡단도에서 고른 벽·구체의 시설을 폼에 올린다(null = 해제). */ showPipeAt: (chainageM: number | null) => void; } export function createB06StructuresPanel(deps: B06StructuresPanelDeps): B06StructuresPanel { let structures: StructureInstance[] = []; /** 새 항목에 식별자를 미리 붙인다 — 저장 전에도 목록에서 고르고 지울 수 있어야 * 하고, 서버는 빈 값일 때만 새로 발급한다(B05 다리와 같은 규칙). */ const withLocalIds = (next: StructureInstance[]): StructureInstance[] => next.map((item) => item.structure_id ? item : { ...item, structure_id: crypto.randomUUID().replace(/-/g, "") }, ); const section = createStructuresSection({ onChange: (next) => { structures = withLocalIds(next); section.setStructures(structures); if (deps.projectId) writePendingStructures(deps.projectId, structures); }, onSelect: (structure) => { if (structure) deps.focusChainage(structureAnchorM(structure)); }, getInterval: deps.stationInterval, onPipeAdd: () => showToast(PIPE_GUIDE, "error"), onPipeUpdate: () => showToast(PIPE_GUIDE, "error"), onPipeRemove: () => showToast(PIPE_GUIDE, "error"), onPipeSelect: (chainageM) => { if (chainageM !== null) deps.focusChainage(chainageM); }, }); // 하위 [횡단 조정] 컨테이너 — 폼 본문 맨 아래, 기존 항목과 경계 구분(2026-08-29 // 사용자: 병합 항목은 하위 컨테이너로 담아 혼동 방지). resetAdjustDock(); section.root.querySelector(".b05-route__panel-body")?.append(adjustDockRoot()); async function load(): Promise { if (!deps.projectId) return; const projectId = deps.projectId; try { const [types, stored, pipeResponse] = await Promise.all([ fetchStructureTypes(), fetchStructures(projectId), fetchDetailPipePoints(projectId), ]); section.setTypes(types); // 저장하지 않고 나갔던 조작분이 있으면 그것으로 화면을 세운다(B05와 같은 규칙). structures = readPendingStructures(projectId) ?? stored.structures; section.setStructures(structures); const pipes: PipeFacilityItem[] = pipeResponse.pipe_points.map((pipe) => ({ chainage_m: pipe.chainage_m, facility: pipe.facility ?? "pipe", start_m: pipe.start_m, end_m: pipe.end_m, source: pipe.source, options: pipe.options, design_flow_m3s: null, })); section.setPipeFacilities(pipes); } catch (error) { showToast( error instanceof Error ? error.message : "구조물 정보를 불러오지 못했습니다.", "error", ); } } return { root: section.root, listRoot: section.listRoot, load, showPipeAt: (chainageM) => section.selectPipeByChainage(chainageM), }; } /** * 횡단도 선택 → 좌측 폼 연동 배선. 벽(기슭막이)·세월교·BOX 선택이 일어나면 그 * 구조물의 **소유 측점** 시설을 좌측 폼에 올린다(2026-08-29 일원화 3단계). * 제어 객체의 select를 감싸기만 한다 — 조작·저장 경로는 그대로다. */ export function wireStructureSelection( stationControls: StationControls, detail: () => SectionDetailResponse | null, panel: B06StructuresPanel, ): 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; }; const origRevet = stationControls.revetOffset.select; stationControls.revetOffset.select = (chainageM, key) => { origRevet(chainageM, key); panel.showPipeAt(key ? ownerChainageOf(chainageM) : null); }; const origFord = stationControls.ford.select; stationControls.ford.select = (chainageM, role) => { origFord(chainageM, role); panel.showPipeAt(role ? chainageM : null); }; const origBox = stationControls.box.select; stationControls.box.select = (chainageM, role) => { origBox(chainageM, role); panel.showPipeAt(role ? chainageM : null); }; }