/* ============================================================================= * B05_Profile_UI_Profile_Balance.ts * 종단 패널 상단 표시줄 — **법정 판정에 쓰이는 값만** 남긴다(2026-08-19 재편). * * 표시 항목(별표2 근거): * 최대 기울기 x.x% (상한 y%) — Ⅰ.2.라: 설계속도·지형별 상한 준수 판정 * 불균형 z% / 허용 w% — Ⅰ.1.나.(4)(다): 시공계획고 절·성토 균형 * 곡선 필요 n곳 — Ⅰ.2.마: 대수차 5% 초과 변화점의 종단곡선 삽입 * 절·성토 면적은 하단 유토곡선이 부피로 더 정확히 보여 주고, 변화점·종단곡선 개수와 * 기본 곡선길이 L은 판단 기준이 없어 뺐다(2026-08-19 사용자 지적 + 지식DB 대조). * [저장선 복원]은 상단에서 내려 그래프 조작부로 옮겼다. * * 패널 본체(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; // 최대 종단기울기 — 법정 상한 대비가 이 화면의 첫 판정 항목이다(별표2 Ⅰ.2.라). const steepest = alignment.segments.reduce( (worst, segment) => Math.max(worst, Math.abs(segment.grade_percent)), 0, ); // 종단곡선이 필요한데 빠진 변화점 — 생략은 비포장 & 대수차 5% 이하만 허용된다 // (별표2 Ⅰ.2.마 / 지식DB 종단선형 §3). skip_allowed가 그 판정 결과다. const curvesNeeded = alignment.curves.filter( (curve) => curve.omitted && !curve.skip_allowed, ).length; const entries: Array<[string, string, string?]> = [ [ "최대 기울기", `${steepest.toFixed(1)} % / 상한 ${policy.max_grade_pct.toFixed(1)} %`, violations.length ? "over" : undefined, ], [ "절·성토 불균형", `${balance.imbalance_percent.toFixed(1)} % / 허용 ${balance.tolerance_percent.toFixed(0)} %`, balance.within_tolerance ? undefined : "over", ], ]; // 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다. if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded} 곳`, "over"]); 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)); // 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 — // 같은 사실을 두 번 적지 않는다(2026-08-19 재편). if (label === "최대 기울기" && violations.length) { item.title = violations .map( (entry) => `구간 ${entry.segment_index + 1}: ${entry.value.toFixed(2)}% > ${entry.limit}%`, ) .join("\n"); } params.balanceBar.append(item); }); if (params.edited || params.hasIrregularStations) { const reset = document.createElement("button"); reset.type = "button"; reset.className = "b05-route-profile__balance-reset"; // 이 버튼이 지우는 것은 **화면에 쌓인 편집 델타**다(계획선 변화점 이동·비정규 // 측점). 임시저장을 거치면 그 편집이 정본에 반영되므로 결과적으로 "마지막 저장 // 상태"가 되는 것뿐, 저장 지점으로 되돌아가는 기능이 아니다. 최초 자동 계산 // 상태로의 복귀는 좌측 하단 [초기화](B05·B06 재계산)가 맡는다(2026-08-19 정정). // 자리도 값 칩들 뒤 — 편집 상태(미저장 배지) 옆이 맥락에 맞다. reset.textContent = "편집 되돌리기"; reset.title = "화면에서 수정한 계획선 변화점과 추가한 비정규 측점을 지웁니다.\n" + "이미 임시저장한 내용은 정본에 반영돼 있어 그대로 남습니다.\n" + "최초 자동 계산 상태로 되돌리려면 좌측 하단 [초기화]를 쓰세요."; 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); } }