- 종단 상단 표시줄을 법정 판정 3항목으로 축소: 최대 기울기/상한(별표2 Ⅰ.2.라),
절·성토 불균형(Ⅰ.1.나.(4)(다)), 종단곡선 필요 n곳(Ⅰ.2.마 — 대수차 5% 초과인데
곡선이 빠진 변화점). 변화점·종단곡선 개수·기본 곡선길이 L·중복 경고칩은 제거.
- [초기선 복원] → [편집 되돌리기] — 실제 동작은 화면 편집 델타 삭제이지 저장 지점
복귀가 아니다. 툴팁에 동작과 [초기화]와의 차이를 적었다.
- 3D 뷰포트 안내 라벨 삭제(로딩 완료 후 남던 조작법 문구).
- 배수유역 버튼 정비: 유역 다시 나누기 / 선택한 관 삭제 / 배관 배치 초기화 +
각 툴팁에 실제 동작 명시(선택 삭제는 툴팁 자체가 없었다).
- 유토곡선 요약 12칩 → 5칩(절토·성토·잉여|부족·운반·검산). 토질 3종 내역·다짐
환산·블록 수·장거리 운반·사토는 해당 칩 툴팁으로. B05·B06 공용 함수라 동시 반영.
- 설계속도 축 도입(임도는 속도를 낼 수 없는 노선 — 기본 20km/h):
· 임도 종류는 프로젝트 등록값(projects.road_type)을 읽어 B05가 읽기 전용 표시
(main→간선임도, fire→산불진화임도, 그 외→작업임도). 화면에서 고치지 않는다.
· 설계속도 선택(간선·산불진화 20/30/40, 작업 20 고정) 신설 — 종단기울기 상한과
종단곡선 반경이 등급이 아니라 이 값으로 정해진다(지식DB 설계제원_총괄 §6·§7).
· B02 임도 종류 선택지를 현행 3종으로 정리(지선 폐지·계류보전은 사방 구분).
· 백엔드: ROUTE_GRADE_CLASSES 3종+branch 호환, selectable_design_speeds 신설,
resolve_design_speed 신설, legal_grade_limit_pct·resolve_grade_options에
설계속도 인자, sections/context가 road_type 제공(B06도 저장값 승계).
· 계획선 정책은 그릴 때마다 현재 기준으로 동기화 — 저장분의 옛 상한이 남지 않는다.
- 검증: pytest 125 통과(설계속도 12건 신규), tsc, 헤드 브라우저 — 진입 시 상한 9%,
40km/h 전환 시 7% 즉시 반영, 원복 9%, 안내문 '간선임도 · 설계속도 20km/h ·
일반지형', 유토곡선 5칩, 유역 버튼 새 이름·툴팁, 3D 라벨 빈 문자열 확인.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
120 lines
6.0 KiB
TypeScript
120 lines
6.0 KiB
TypeScript
/* =============================================================================
|
||
* 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);
|
||
}
|
||
}
|