/* ============================================================================= * B05_Profile_UI_Panel_Route.ts * 좌측 패널의 **노선 쪽 칸 재료** — 계획노선 편집 단추 · 경로 계산 설정(포인트 팔레트 · 알고리즘 · * 조건) · 선택 포인트 상세. 칸 틀은 `B05_Profile_UI_Panel_Sections.ts` 가 씌운다(등록부). * `B05_Profile_UI_Panel.ts` 에서 떼어냈다(계획서 24-2b). 본문 로직과 수치는 그대로다. * ========================================================================== */ import { createSelectField } from "@ui/ui_template_elements"; import type { PlacedRoutePoint, RoutePointKind } from "./B05_Profile_UI_Markers"; import { button, checkbox, numberField } from "./B05_Profile_UI_Panel_Fields"; export interface RoutePartCallbacks { onSolve: () => void; /** [계획노선 편집] — 큰 모달을 열어 노선을 고친다(계산은 모달 [확인]에서만 돈다). */ onEditPlannedRoute: () => void; onMovePoint: () => void; onDeletePoint: () => void; onRadiusChange: (radius: number) => void; } export function createRoutePart(callbacks: RoutePartCallbacks) { // 계획노선 편집 — 노선을 고치는 유일한 입구(2026-09-06 사용자 확정). 자동탐색 // (「경로 계산 설정」)은 사용 중단이라 이 자리가 노선을 바꾸는 길이다. const plannedRouteBtn = document.createElement("button"); plannedRouteBtn.type = "button"; plannedRouteBtn.className = "b05-route__btn"; plannedRouteBtn.textContent = "계획노선 편집"; plannedRouteBtn.title = "예상노선 위에서 계획노선을 고칩니다. [확인] 때만 다시 계산합니다."; plannedRouteBtn.addEventListener("click", () => callbacks.onEditPlannedRoute()); const paletteGrid = document.createElement("div"); paletteGrid.className = "b05-route__palette"; const pointLabels: Record = { bp: "BP 시작점", ep: "EP 종료점", cp: "CP 경유점", ap: "AP 회피구역", fp: "FP 금지구역", }; (Object.keys(pointLabels) as RoutePointKind[]).forEach((kind) => { const chip = document.createElement("div"); chip.className = `b05-route__chip is-${kind}`; chip.draggable = true; chip.textContent = pointLabels[kind]; chip.addEventListener("dragstart", (event) => event.dataTransfer?.setData("pointType", kind)); paletteGrid.append(chip); }); // 드롭다운은 공통 컴포넌트(createSelectField) 재사용. `.select`로 받아 이하 로직 불변. const algorithmField = createSelectField({ label: "알고리즘", options: [ { value: "dijkstra", text: "Dijkstra" }, { value: "ridge_valley", text: "능선·계곡" }, ], }); const algorithm = algorithmField.select; const minCurveRadius = numberField("최소 곡선반경 (m)"); const maxUphillGrade = numberField("오르막 경사 상한 (%)"); const maxDownhillGrade = numberField("내리막 경사 상한 (%)"); const minUphillGrade = numberField("오르막 경사 하한 (%)"); const minDownhillGrade = numberField("내리막 경사 하한 (%)"); const paved = checkbox("포장 임도", false); const avoidPass = checkbox("회피구역 통과 허용", false); const details = document.createElement("details"); const summary = document.createElement("summary"); summary.textContent = "사용한 조건"; details.append( summary, minCurveRadius.wrapper, maxUphillGrade.wrapper, maxDownhillGrade.wrapper, minUphillGrade.wrapper, minDownhillGrade.wrapper, ); // [최적 경로 계산] 입구는 2026-09-06 사용자 확정으로 화면에서 내렸다 — 평면만 보고 // 노선을 정하는 방식이라 실제 판단(평면·종단·횡단·유토곡선을 함께 봄)과 맞지 않는다. // 코드·API 는 그대로 두고 버튼만 뺀다. 노선 변경은 계획노선 편집이 대신한다. const solveButton = button("최적 경로 계산", callbacks.onSolve, "filled"); solveButton.hidden = true; const selectedName = document.createElement("strong"); const radius = numberField("회피/금지 반경 (m)", "25"); radius.addEventListener("change", () => callbacks.onRadiusChange(Number(radius.value) || 1)); const selectedActions = document.createElement("div"); selectedActions.className = "b05-route__actions"; selectedActions.append( button("위치 이동", callbacks.onMovePoint), button("삭제", callbacks.onDeletePoint, "danger"), ); // 칸 뿌리는 등록부가 만든 뒤 알려 준다 — 평소 숨김(포인트를 고를 때만 보인다). let selectedRoot: HTMLElement | null = null; return { plannedRouteButton: plannedRouteBtn, /** 「경로 계산 설정」 본문 — 화면 순서대로. */ calcNodes: [ paletteGrid, algorithmField.root, paved.wrapper, avoidPass.wrapper, details, solveButton, ] as HTMLElement[], /** 「선택 포인트 상세 설정」 본문 — 화면 순서대로. */ selectedNodes: [selectedName, radius.wrapper, selectedActions] as HTMLElement[], fields: { algorithm, paved, avoidPass, minCurveRadius, maxUphillGrade, maxDownhillGrade, minUphillGrade, minDownhillGrade, }, bindSelectedRoot(root: HTMLElement): void { selectedRoot = root; root.hidden = true; }, setSelected(point: PlacedRoutePoint | null): void { if (selectedRoot) selectedRoot.hidden = !point; if (!point) return; selectedName.textContent = `${point.type.toUpperCase()} (${point.x.toFixed(2)}, ${point.y.toFixed(2)})`; radius.wrapper.hidden = point.type !== "ap" && point.type !== "fp"; radius.value = String(point.radius_m ?? 25); }, }; } export type RoutePart = ReturnType;