/* ============================================================================= * B05_Profile_UI_Panel_Fields.ts * 좌측 패널이 함께 쓰는 **값 · 상수 · 입력 칸 만들기** — `B05_Profile_UI_Panel.ts` 가 714줄이라 * 떼어낸 조각이다(계획서 24-2b). 본문 로직과 수치는 그대로다. * ========================================================================== */ import { type ButtonVariant, createButton } from "@ui/ui_template_elements"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; // 법정 기준표 = logic(511-B B-3 · X5) — 부르는 쪽은 그대로 여기서 받음 export { PROFILE_CRITERIA, SPEED_CHOICES, gradeLimitOf } from "./logic/B05_Profile_Grade_Criteria"; export interface RoutePanelValues { contourInterval: number; /** 임도 종류 — 현행 3종(간선·산불진화·작업). branch(지선)는 폐지됐으나 기존 * 저장분 호환을 위해 값으로는 받는다(2026-08-19). 값의 원천은 프로젝트 등록 * 정보(projects.road_type)이며 화면에서는 고르지 않는다. */ gradeClass: "trunk" | "fire" | "branch" | "work"; /** 설계속도(km/h) — 임도는 속도를 낼 수 없는 노선이라 기본 20이다. 간선·산불진화만 * 20~40에서 고를 수 있고 작업임도는 20 고정(별표2, 2026-08-19 사용자 확정). */ designSpeed: 20 | 30 | 40; stationInterval: number | null; crossSampleInterval: number | null; longSampleInterval: number | null; terrainType: "normal" | "special"; maxGradePct: number | null; minVerticalRadius: number | null; minTangentLength: number | null; startElevationOffset: number | null; endElevationOffset: number | null; /** 횡단배수 최소 계획고를 계획선·편집에 강제할지 — 기본 해제(2026-09-01 사용자 지시). * 해제해도 부족분 경고는 그대로 뜬다. 강제만 풀고 판단은 사용자에게 맡긴다. */ enforcePipeClearance: boolean; /** 시작 측점 · 누가거리 표시(448 저장) — B05 라벨만 · 측점 열쇠 · 다른 페이지에 안 씀. */ stationDisplay: { station: number; cumulative: number }; } /** 측점 간격(m) — 20m 고정(2026-08-18 사용자 확정, 입력 UI 삭제). */ export const STATION_INTERVAL_M = 20; /** 임도 종류별 표시 이름 — 프로젝트 등록(B02)과 같은 말을 쓴다(2026-08-19 확정). */ export const ROAD_TYPE_LABELS: Record = { trunk: "간선임도", fire: "산불진화임도", work: "작업임도", branch: "작업임도(구 지선)", }; export type WrappedInput = HTMLInputElement & { wrapper: HTMLLabelElement }; export function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } export function button( label: string, onClick: () => void, variant: ButtonVariant = "ghost", ): HTMLButtonElement { return createButton({ label, variant, onClick: () => onClick() }); } export function numberField(label: string, value = ""): WrappedInput { const wrapper = document.createElement("label"); wrapper.className = "b05-route__field"; const caption = document.createElement("span"); caption.textContent = label; const input = document.createElement("input"); input.type = "number"; input.step = "0.01"; input.value = value; wrapper.append(caption, input); return Object.assign(input, { wrapper }); } export function checkbox(label: string, checked: boolean): WrappedInput { const wrapper = document.createElement("label"); wrapper.className = "b05-route__check"; const input = document.createElement("input"); input.type = "checkbox"; input.checked = checked; wrapper.append(input, document.createTextNode(label)); return Object.assign(input, { wrapper }); } export function toggleButton( label: string, checked: boolean, onChange: (checked: boolean) => void, ): HTMLButtonElement { const element = button( label, () => { const active = !element.classList.contains("is-active"); element.classList.toggle("is-active", active); element.setAttribute("aria-pressed", String(active)); onChange(active); }, "glass", ); element.classList.toggle("is-active", checked); element.setAttribute("aria-pressed", String(checked)); return element; } export function parseOptional(input: HTMLInputElement): number | null { if (!input.value.trim()) return null; const value = Number(input.value); return Number.isFinite(value) ? value : null; }