feat(B05): 유토곡선 총괄값을 종단 상단줄로 — 곡선을 접어도 남게

2026-09-06 사용자 지시. 요약 막대가 유토곡선 오버레이 안에 있어 패널을 접으면
같이 사라졌는데, 계획선을 만지며 봐야 하는 값이라 상단줄로 올림.

- 상단줄 「최대 기울기」 오른쪽에 「누가토량」 추가. 값은 유토곡선 계산 결과의
  마지막 누계이며 토량분배 켜짐과 무관하다. 음수(부족)면 경고색.
- 세부(절토 자연·다짐, 성토, 잉여/부족)는 그 항목 툴팁으로 내림 — 예전 요약
  막대가 세부를 툴팁에 두던 방식 그대로.
- 유토곡선 오버레이의 요약 막대 행 삭제. 비면 접히게 해 곡선이 그만큼 넓게 씀.
- 총괄값은 패널이 접혀 있어도 계산해 넘김(draw 앞단에서 산출).
- 상단줄 높이 24 → 28px.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 11:37:59 +09:00
co-authored by Claude Opus 5
parent fdcd075396
commit 9f36b5fab4
5 changed files with 96 additions and 7 deletions
@@ -16,6 +16,12 @@
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment"; import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
import type { MinCoverViolation } from "./B05_Profile_UI_Profile_MinCover"; import type { MinCoverViolation } from "./B05_Profile_UI_Profile_MinCover";
import type { MassHaulSummaryValues } from "./B05_Profile_UI_Profile_MassHaul";
/** 누가토량 표기 — 천 단위 구분 + 소수점 1자리(유토곡선 요약과 같은 규칙). */
function volume(value: number): string {
return `${value.toLocaleString("ko-KR", { minimumFractionDigits: 1, maximumFractionDigits: 1 })}㎥`;
}
export interface BalanceBarParams { export interface BalanceBarParams {
/** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */ /** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */
@@ -36,6 +42,9 @@ export interface BalanceBarParams {
minCoverViolations?: MinCoverViolation[]; minCoverViolations?: MinCoverViolation[];
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */ /** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
trailing?: HTMLElement; trailing?: HTMLElement;
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
massHaul?: MassHaulSummaryValues | null;
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */ /** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
onResetAll: () => void; onResetAll: () => void;
} }
@@ -77,6 +86,13 @@ export function renderBalanceBar(params: BalanceBarParams): void {
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 — // 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는 // 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다. // 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
// 유토곡선 총괄값 — 「최대 기울기」 **바로 오른쪽**(2026-09-06 사용자 지시). 곡선을 접어도
// 남는다. 세부 내역(절토·성토·잉여/부족)은 이 항목의 툴팁으로 내렸다 — 예전 요약 막대가
// 세부를 툴팁에 두던 방식 그대로다.
const mass = params.massHaul;
if (mass) {
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
}
// 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다. // 필요한 곳이 없으면 적지 않는다 — "0곳"은 화면 폭만 먹는다.
if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded}`, "over"]); if (curvesNeeded) entries.push(["종단곡선 필요", `${curvesNeeded}`, "over"]);
// 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은 // 횡단배수 최소고 표시는 2026-09-02 사용자 지시로 삭제했다. 편집 차단(강제)은
@@ -91,6 +107,13 @@ export function renderBalanceBar(params: BalanceBarParams): void {
item.append(caption, document.createTextNode(value)); item.append(caption, document.createTextNode(value));
// 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 — // 상한 초과 구간의 내역은 별도 경고 칩 대신 이 항목의 툴팁으로 붙인다 —
// 같은 사실을 두 번 적지 않는다(2026-08-19 재편). // 같은 사실을 두 번 적지 않는다(2026-08-19 재편).
if (label === "누가토량" && mass) {
item.title = [
`절토(자연) ${volume(mass.cutNaturalM3)} · 절토(다짐) ${volume(mass.cutCompactedM3)}`,
`성토(다짐) ${volume(mass.fillCompactedM3)}`,
mass.shortageM3 > 0 ? `부족 ${volume(mass.shortageM3)}` : `잉여 ${volume(mass.surplusM3)}`,
].join("\n");
}
if (label === "최대 기울기" && violations.length) { if (label === "최대 기울기" && violations.length) {
item.title = violations item.title = violations
.map( .map(
+54 -5
View File
@@ -44,7 +44,6 @@ import { resetBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
import { import {
createMassHaulChart, createMassHaulChart,
createMassHaulLegend, createMassHaulLegend,
createMassHaulSummary,
createMassHaulWindowState, createMassHaulWindowState,
MASS_HAUL_MIN_HEIGHT, MASS_HAUL_MIN_HEIGHT,
scheduleMassHaulSettle, scheduleMassHaulSettle,
@@ -180,7 +179,28 @@ function readVisible(): Set<string> {
/** /**
* @param onChanged 펼침·범례 토글·높이 조절로 다시 그려야 할 때 호출된다(패널 전체 redraw). * @param onChanged 펼침·범례 토글·높이 조절로 다시 그려야 할 때 호출된다(패널 전체 redraw).
*/ */
export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPanel { /**
* 종단 상단줄이 **접힌 상태에서도** 보여 주는 유토곡선 총괄값(2026-09-06 사용자 지시).
* 곡선 자체는 접으면 사라져도 이 값은 계획선을 만지며 계속 봐야 한다.
*/
export interface MassHaulSummaryValues {
/** 노선 끝 누가토량(㎥) — 양수 잉여, 음수 부족. */
finalM3: number;
/** 절토(자연상태) 합계(㎥). */
cutNaturalM3: number;
/** 절토를 다짐으로 환산한 값(㎥). */
cutCompactedM3: number;
/** 성토(다짐) 합계(㎥). */
fillCompactedM3: number;
surplusM3: number;
shortageM3: number;
}
export function createRouteMassHaulPanel(
onChanged: () => void,
/** 총괄값이 새로 나올 때마다 부른다 — 종단 상단줄이 받아 적는다. */
onSummary?: (summary: MassHaulSummaryValues | null) => void,
): RouteMassHaulPanel {
// 손잡이는 다른 패널과 **같은 양식**의 표준 삼각형 손잡이 하나만 쓴다(2026-08-04 사용자 // 손잡이는 다른 패널과 **같은 양식**의 표준 삼각형 손잡이 하나만 쓴다(2026-08-04 사용자
// 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는 // 지시 — 예전 풀폭 바 + "유토곡선" 캡션은 다른 패널들과 모양이 달랐다). 무엇의 손잡이인지는
// 툴팁으로 밝힌다. // 툴팁으로 밝힌다.
@@ -190,7 +210,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
handle.append(handleControl.root); handle.append(handleControl.root);
handleControl.root.setAttribute("aria-label", "유토곡선 패널"); handleControl.root.setAttribute("aria-label", "유토곡선 패널");
// 오버레이 뼈대 — 위 경계 리사이저 + 요약 막대 + 가로 스크롤러 + 범례. // 오버레이 뼈대 — 위 경계 리사이저 + 안내 문구 자리 + 가로 스크롤러 + 범례.
// (요약 막대는 2026-09-06 종단 상단줄로 올라갔다.)
const overlay = document.createElement("div"); const overlay = document.createElement("div");
overlay.className = "b05-profile__masshaul-overlay"; overlay.className = "b05-profile__masshaul-overlay";
const bar = document.createElement("div"); const bar = document.createElement("div");
@@ -316,11 +337,39 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
/** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */ /** 마지막으로 그린 입력 — 이동이 아직 안 끝났으면 다음 프레임에 이걸로 한 번 더 그린다. */
let lastParams: RouteMassHaulDrawParams | null = null; let lastParams: RouteMassHaulDrawParams | null = null;
/** 총괄값만 따로 낸다 — 패널을 접어도 상단줄이 값을 잃지 않게(2026-09-06). */
function reportSummary(params: RouteMassHaulDrawParams): void {
if (!onSummary) return;
if (!context || params.pendingRecalc) return;
const series = computeMassHaulSeries(
params.longitudinal,
params.crossSections,
context.conversion,
context.naturalSpoilMinSlope,
);
if (!series.length) return onSummary(null);
const visible = readVisible();
const picked = series.find((entry) => visible.has(entry.key)) ?? series[0];
const result = picked.result;
const cut = result.cut_natural_m3;
const points = result.points;
onSummary({
finalM3: points.length ? points[points.length - 1].cumulative_volume_m3 : 0,
cutNaturalM3: cut.soil + cut.ripping_rock + cut.blasting_rock,
cutCompactedM3: result.cut_compacted_m3,
fillCompactedM3: result.fill_compacted_m3,
surplusM3: result.surplus_m3,
shortageM3: result.shortage_m3,
});
}
function draw(params: RouteMassHaulDrawParams): void { function draw(params: RouteMassHaulDrawParams): void {
lastParams = params; lastParams = params;
clearSelection = params.onClearSelection; clearSelection = params.onClearSelection;
overlay.hidden = !open; overlay.hidden = !open;
syncHandlePosition(); syncHandlePosition();
// 곡선을 그리기 전에 총괄값부터 낸다 — 접혀 있어도 상단줄에는 값이 서야 한다.
reportSummary(params);
if (!open) return; if (!open) return;
if (!context) { if (!context) {
note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다."); note("토량환산계수를 불러오지 못해 유토곡선을 계산할 수 없습니다.");
@@ -361,8 +410,8 @@ export function createRouteMassHaulPanel(onChanged: () => void): RouteMassHaulPa
banded && visible.has(MASS_HAUL_BALANCE_KEY) banded && visible.has(MASS_HAUL_BALANCE_KEY)
? computeHaulPlan(banded.result, context.haulLimits) ? computeHaulPlan(banded.result, context.haulLimits)
: null; : null;
const summarySeries = banded ?? series[0]; // 요약 막대는 2026-09-06 사용자 지시로 뺐다 — 총괄값은 종단 상단줄이 늘 보여 주고,
bar.append(createMassHaulSummary(summarySeries, haulPlan)); // 이 자리는 곡선이 넓게 쓴다. `bar` 는 안내 문구 자리로만 남는다.
// 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체 // 곡선 몫 = 스크롤러의 **안쪽 높이**(clientHeight = 가로 스크롤바 제외). 오버레이 전체
// 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고). // 높이로 잡으면 스크롤바가 곡선 바닥(Y축 -200 라벨)을 덮는다(2026-08-04 사용자 보고).
+10 -1
View File
@@ -46,6 +46,7 @@ import { structureGroupMenuItems } from "./B05_Profile_UI_Profile_Structures";
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view"; import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
import { import {
createRouteMassHaulPanel, createRouteMassHaulPanel,
type MassHaulSummaryValues,
MASSHAUL_MIN_HEIGHT, MASSHAUL_MIN_HEIGHT,
type RouteMassHaulContext, type RouteMassHaulContext,
} from "./B05_Profile_UI_Profile_MassHaul"; } from "./B05_Profile_UI_Profile_MassHaul";
@@ -144,7 +145,14 @@ export function createRouteProfilePanel(
if (subPanelDragging) scheduleLightSync(); if (subPanelDragging) scheduleLightSync();
else draw(); else draw();
}; };
const massHaul = createRouteMassHaulPanel(subPanelChanged); /** 유토곡선 총괄값 — 상단줄이 쓴다. 패널을 접어도 값이 남게 여기에 들고 있는다. */
let massHaulSummary: MassHaulSummaryValues | null = null;
const massHaul = createRouteMassHaulPanel(subPanelChanged, (summary) => {
const before = massHaulSummary;
massHaulSummary = summary;
// 값이 그대로면 상단줄을 다시 그리지 않는다 — 계획고를 연속으로 누를 때 헛일이 된다.
if (before?.finalM3 !== summary?.finalM3) renderBalance();
});
bodyWrap.append(massHaul.overlay, massHaul.handle); bodyWrap.append(massHaul.overlay, massHaul.handle);
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지. // 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
massHaul.attachScrollSync(body); massHaul.attachScrollSync(body);
@@ -329,6 +337,7 @@ export function createRouteProfilePanel(
tools: tools.render(), tools: tools.render(),
trailing: profileZoom.bar, trailing: profileZoom.bar,
alignment, alignment,
massHaul: massHaulSummary,
legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal), legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal),
edited: store.edited(), edited: store.edited(),
hasIrregularStations: irregularStations.length > 0, hasIrregularStations: irregularStations.length > 0,
+3 -1
View File
@@ -509,7 +509,9 @@
flex-wrap: nowrap; flex-wrap: nowrap;
gap: var(--spacing-16); gap: var(--spacing-16);
align-items: center; align-items: center;
height: 24px; /* 유토곡선 총괄값이 이 줄로 올라와(2026-09-06) 항목이 늘었다 — 두 픽셀 높여 글자가
위아래로 눌리지 않게 한다. */
height: 28px;
padding: 0 var(--spacing-8); padding: 0 var(--spacing-8);
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@@ -70,6 +70,12 @@
top: -3px; top: -3px;
} }
/* 요약 막대는 2026-09-06 종단 상단줄로 올라갔다 — 이 자리는 안내 문구가 있을 때만 쓴다.
비어 있으면 테두리 한 줄도 남기지 않고 접어 곡선이 그만큼 넓게 쓴다. */
.b05-profile__masshaul-bar:empty {
display: none;
}
.b05-profile__masshaul-bar { .b05-profile__masshaul-bar {
display: flex; display: flex;
flex: none; flex: none;