/* ============================================================================= * B05_Profile_UI_Profile_Balance.ts * 종단 패널 상단 균형 표시줄 — 절·성토 면적, 불균형률, 변화점·종단곡선 개수, * 종단기울기 위반 경고, [초기선 복원] 버튼, 미저장 배지. * * 패널 본체(B05_Profile_UI_Profile_Panel)가 700줄 한계에 닿아 분리했다. * 표시줄은 상태를 갖지 않는다 — 그릴 때마다 현재 선형·편집 상태를 인자로 받는다. * ========================================================================== */ import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment"; export interface BalanceBarParams { /** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */ balanceBar: HTMLElement; /** 현재 계획선(없으면 안내만 띄운다). */ alignment: ProfileAlignment | null; /** 계획선이 없고 저장분이 구버전 형식일 때 재계산을 안내한다. */ legacyAlignment: boolean; /** 편집이 있었는지(초기선 복원 버튼 노출 조건). */ edited: boolean; /** 비정규 측점이 있는지(초기선 복원 버튼 노출 조건). */ hasIrregularStations: boolean; /** 저장되지 않은 편집이 있는지. */ dirty: boolean; /** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */ onResetAll: () => void; } export function renderBalanceBar(params: BalanceBarParams): void { params.balanceBar.replaceChildren(); if (!params.alignment) { if (params.legacyAlignment) { const note = document.createElement("span"); note.className = "b05-route-profile__balance-warning"; note.textContent = "⚠ 계획선 데이터가 구버전 형식입니다 — [최적 경로 계산]을 다시 실행하세요."; params.balanceBar.append(note); } return; } const { alignment } = params; const { balance, policy, violations } = alignment; const entries: Array<[string, string, string?]> = [ ["절토", `${balance.cut_area_m2.toFixed(1)} m²`, "cut"], ["성토", `${balance.fill_area_m2.toFixed(1)} m²`, "fill"], [ "불균형", `${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`, balance.within_tolerance ? undefined : "over", ], ["변화점", `${alignment.pvi.length} 개`], ["종단곡선", `${alignment.curves.filter((curve) => !curve.omitted).length} 개`], // 기준은 길이 L이다. 옛 저장분(L 없음)만 그 시절 기준인 R을 그대로 밝혀 적는다. Number.isFinite(policy.default_curve_length_m) ? ["기본 곡선길이 L", `${(policy.default_curve_length_m as number).toFixed(1)} m`] : ["기본 R(옛 저장분)", `${policy.default_curve_radius_m.toFixed(1)} m`], ]; const editedCount = Object.keys(alignment.edits.station_offsets).length; if (editedCount) entries.push(["편집 측점", `${editedCount} 개`, "edited"]); entries.forEach(([label, value, tone]) => { const item = document.createElement("span"); item.className = `b05-route-profile__balance-item${tone ? ` is-${tone}` : ""}`; const caption = document.createElement("em"); caption.textContent = label; item.append(caption, document.createTextNode(value)); params.balanceBar.append(item); }); if (violations.length) { const warning = document.createElement("span"); warning.className = "b05-route-profile__balance-warning"; warning.textContent = `⚠ 종단기울기 초과 ${violations.length}개 구간`; warning.title = violations .map((item) => `구간 ${item.segment_index + 1}: ${item.value.toFixed(2)}% > ${item.limit}%`) .join("\n"); params.balanceBar.append(warning); } if (params.edited || params.hasIrregularStations) { const reset = document.createElement("button"); reset.type = "button"; reset.className = "b05-route-profile__balance-reset"; reset.textContent = "초기선 복원"; reset.title = "모든 편집과 추가한 비정규 측점을 지우고 자동 산출된 계획선으로 되돌립니다."; reset.addEventListener("click", () => { params.onResetAll(); }); params.balanceBar.append(reset); } if (params.dirty) { const badge = document.createElement("span"); badge.className = "b05-route-profile__balance-item is-unsaved"; badge.textContent = "미저장 (확정 시 반영)"; params.balanceBar.append(badge); } }