feat(B05,B06): 최종 누가토량 배지 + B06 유토곡선 그래프 제거
사용자 지시(2026-09-06): 유토곡선은 자리를 많이 먹는데 정작 늘 봐야 하는 값은 마지막 지점 누가토량 하나임. 기준은 횡단으로 고정(종단 기준 곡선은 뒤에 삭제 예정). - 총괄값 계열을 범례 선택과 무관하게 **횡단 기준**으로 뽑음 - 공용 배지 `common_util_mass_haul_badge.ts` — 종단 그래프 좌측 상단 오버레이, 세부 내역은 툴팁 - B05: 상단줄의 「누가토량」 항목을 배지로 옮김(유토곡선 패널은 그대로 유지) - B06: 유토곡선 그래프·범례·요약줄 삭제, 그 높이를 종단면도가 전부 사용 검증: B06 배지 −5,595.5㎥·범례 없음·종단 228px, B05 배지 −6,842.6㎥·상단줄에서 제거. 테스트 390 통과. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,12 +16,6 @@
|
||||
|
||||
import type { ProfileAlignment } from "./B05_Profile_UI_Profile_Alignment";
|
||||
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 {
|
||||
/** 표시줄 컨테이너. 그릴 때마다 통째로 갈아 끼운다. */
|
||||
@@ -42,11 +36,11 @@ export interface BalanceBarParams {
|
||||
minCoverViolations?: MinCoverViolation[];
|
||||
/** 요약줄 **오른쪽 끝**에 붙이는 묶음(줌·Y레인지 조작구) — 2026-09-04. */
|
||||
trailing?: HTMLElement;
|
||||
/** 유토곡선 총괄값 — 「최대 기울기」 오른쪽에 적는다(2026-09-06 사용자 지시).
|
||||
* 유토곡선 패널을 접어도 이 값은 남아야 계획선을 만지며 볼 수 있다. */
|
||||
massHaul?: MassHaulSummaryValues | null;
|
||||
/** 법정 평면 최소곡선반지름 위반(2026-09-06) — 자동 보정 없이 **경고만** 낸다. */
|
||||
planCurve?: { count: number; worstRadiusM: number; limitM: number; hairpin: number } | null;
|
||||
// 유토곡선 총괄값은 이 줄에서 **뺐다**(2026-09-06 사용자 지시) — 그래프 좌측 상단에
|
||||
// 겹쳐 뜨는 배지(`common_util_mass_haul_badge`)가 맡는다. B06 은 곡선을 아예 빼므로
|
||||
// 같은 배지를 써야 두 화면이 같은 자리에서 같은 값을 보인다.
|
||||
/** [초기선 복원] — 편집·비정규 측점을 모두 지운다. */
|
||||
onResetAll: () => void;
|
||||
}
|
||||
@@ -88,13 +82,9 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
||||
// 절·성토 불균형은 2026-09-03 사용자 지시로 뺐다. 이 값은 **종단 기준**이었다 —
|
||||
// 계획선과 지반선 사이 세로 면적(㎡)의 절·성토 비이지 실제 토량이 아니다. 의미 있는
|
||||
// 균형은 횡단 단면적을 쌓아 부피로 내는 하단 유토곡선 요약(㎥)이고, 그쪽이 이미 있다.
|
||||
// 유토곡선 총괄값 — 「최대 기울기」 **바로 오른쪽**(2026-09-06 사용자 지시). 곡선을 접어도
|
||||
// 남는다. 세부 내역(절토·성토·잉여/부족)은 이 항목의 툴팁으로 내렸다 — 예전 요약 막대가
|
||||
// 세부를 툴팁에 두던 방식 그대로다.
|
||||
const mass = params.massHaul;
|
||||
if (mass) {
|
||||
entries.push(["누가토량", volume(mass.finalM3), mass.finalM3 < 0 ? "over" : undefined]);
|
||||
}
|
||||
// 유토곡선 총괄값은 2026-09-06 사용자 지시로 이 줄에서 **뺐다** — 그래프 좌측 상단에
|
||||
// 겹쳐 뜨는 배지(`common_util_mass_haul_badge`)가 맡는다. B06 은 곡선을 아예 빼므로
|
||||
// 같은 배지를 써야 두 화면이 같은 자리에서 같은 값을 보인다.
|
||||
// 평면 곡선반경 — 법정 하한을 밑도는 측점이 있을 때만 적는다(별표2 Ⅰ.2.다.(1)).
|
||||
// 프로그램은 값만 드러내고 고치지 않는다: 노선을 바꿀지는 사용자 판단이다.
|
||||
const plan = params.planCurve;
|
||||
@@ -128,13 +118,6 @@ export function renderBalanceBar(params: BalanceBarParams): void {
|
||||
: "배향곡선 하한 미만은 없음",
|
||||
].join("\n");
|
||||
}
|
||||
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) {
|
||||
item.title = violations
|
||||
.map(
|
||||
|
||||
@@ -350,8 +350,9 @@ export function createRouteMassHaulPanel(
|
||||
context.naturalSpoilMinSlope,
|
||||
);
|
||||
if (!series.length) return onSummary(null);
|
||||
const visible = readVisible();
|
||||
const picked = series.find((entry) => visible.has(entry.key)) ?? series[0];
|
||||
// 총괄값 기준은 **횡단 고정**이다(2026-09-06 사용자: 종단 기준 곡선은 뒤에 없앨 예정).
|
||||
// 범례에서 무엇을 켜 두었든 배지·상단줄이 흔들리지 않는다.
|
||||
const picked = series.find((entry) => entry.basis === "cross") ?? series[0];
|
||||
const result = picked.result;
|
||||
const cut = result.cut_natural_m3;
|
||||
const points = result.points;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_stat
|
||||
import type { SectionDetailResponse } from "../B06_Section/B06_Section_Api_Fetch";
|
||||
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
|
||||
import { createPanelResizer } from "@ui/ui_template_resizer";
|
||||
import { createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import { createDrainagePanel } from "./B05_Profile_UI_Drainage_Panel";
|
||||
import type { StructureInstance, StructureType } from "./B05_Profile_Api_Structures";
|
||||
import {
|
||||
@@ -145,13 +146,17 @@ export function createRouteProfilePanel(
|
||||
if (subPanelDragging) scheduleLightSync();
|
||||
else draw();
|
||||
};
|
||||
/** 유토곡선 총괄값 — 상단줄이 쓴다. 패널을 접어도 값이 남게 여기에 들고 있는다. */
|
||||
/** 유토곡선 총괄값 — **그래프 좌측 상단 배지**가 보인다(2026-09-06 사용자 지시).
|
||||
* 유토곡선을 접어도, B06 처럼 곡선을 아예 안 그려도 이 값은 남아야 한다. */
|
||||
const massBadge = createMassHaulBadge();
|
||||
// B05 는 그래프 위에 도구줄(상단줄)이 한 줄 있다 — 그 아래로 내려 겹치지 않게 한다.
|
||||
massBadge.root.style.setProperty("--mass-haul-badge-top", "34px");
|
||||
bodyWrap.append(massBadge.root);
|
||||
let massHaulSummary: MassHaulSummaryValues | null = null;
|
||||
const massHaul = createRouteMassHaulPanel(subPanelChanged, (summary) => {
|
||||
const before = massHaulSummary;
|
||||
massHaulSummary = summary;
|
||||
// 값이 그대로면 상단줄을 다시 그리지 않는다 — 계획고를 연속으로 누를 때 헛일이 된다.
|
||||
if (before?.finalM3 !== summary?.finalM3) renderBalance();
|
||||
if (before?.finalM3 !== summary?.finalM3) massBadge.set(summary);
|
||||
});
|
||||
bodyWrap.append(massHaul.overlay, massHaul.handle);
|
||||
// 오버레이의 가로 스크롤을 종단 스크롤러와 양방향 동기화 — 측점 세로선 정렬 유지.
|
||||
@@ -366,7 +371,6 @@ export function createRouteProfilePanel(
|
||||
tools: tools.render(),
|
||||
trailing: profileZoom.bar,
|
||||
alignment,
|
||||
massHaul: massHaulSummary,
|
||||
legacyAlignment: !!detail && hasLegacyAlignment(detail.longitudinal),
|
||||
edited: store.edited(),
|
||||
hasIrregularStations: irregularStations.length > 0,
|
||||
|
||||
@@ -47,10 +47,9 @@ import {
|
||||
visibleChainageRange,
|
||||
visibleElevationRange,
|
||||
} from "./B06_Section_UI_Section_View_Chart";
|
||||
import { applyLegendToggle } from "@util/common_util_mass_haul";
|
||||
import { configureBalloonOffsets } from "@util/common_util_mass_haul_balance_view";
|
||||
import { MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
||||
import { createMassHaulRenderer } from "./B06_Section_UI_Section_View_MassHaul";
|
||||
import { computeMassHaulSeries } from "@util/common_util_mass_haul";
|
||||
import { badgeValuesFrom, createMassHaulBadge } from "@util/common_util_mass_haul_badge";
|
||||
import {
|
||||
applyElevationWindow,
|
||||
needsFullRedraw,
|
||||
@@ -65,7 +64,6 @@ import {
|
||||
MIN_PANEL_HEIGHT,
|
||||
PANEL_CHROME_PX,
|
||||
PANEL_HEIGHT_KEY,
|
||||
readVisibleSeries,
|
||||
} from "./B06_Section_UI_Section_View_Panel";
|
||||
import {
|
||||
CROSS_GRID_GAP,
|
||||
@@ -138,13 +136,10 @@ export function createSectionView(
|
||||
let currentCrossHalfWidth: number | undefined;
|
||||
let currentStationInterval: number | undefined;
|
||||
let currentConversion: EarthworkConversion | undefined;
|
||||
let currentHaulLimits: HaulEquipmentLimit[] | undefined;
|
||||
let currentNaturalSpoilSlope: number | undefined;
|
||||
let renderWidth = 0;
|
||||
let resizeTimer = 0;
|
||||
let panelResizeTimer = 0;
|
||||
// 유토곡선 표시 곡선 집합. 토글해도 Y 스케일은 전체 곡선 기준이라 축이 움직이지 않는다.
|
||||
const visibleSeries = readVisibleSeries();
|
||||
// 그래프 상자의 마지막 실측 높이와 보정 예약 플래그(빈 공간·세로 스크롤 둘 다 막는다).
|
||||
let lastChartAvailable = 0;
|
||||
let chartFitScheduled = false;
|
||||
@@ -451,18 +446,10 @@ export function createSectionView(
|
||||
);
|
||||
};
|
||||
|
||||
/** 범례 버튼 — 곡선 하나를 켜고 끈다. 축은 전체 곡선 기준이라 여기서 움직이지 않는다. */
|
||||
const toggleSeries = (key: string): void => {
|
||||
// 곡선 기준(횡단/종단)은 라디오 — 하나를 고르면 그 기준으로 계산된 그래프만 보인다.
|
||||
const next = applyLegendToggle(visibleSeries, key);
|
||||
visibleSeries.clear();
|
||||
next.forEach((entry) => visibleSeries.add(entry));
|
||||
writeStateRaw("masshaul-visible", JSON.stringify([...visibleSeries]));
|
||||
drawPanel();
|
||||
};
|
||||
|
||||
/** 유토곡선만 새 창으로 다시 그리는 함수 — 그릴 때마다 새로 만든다(붙인 노드는 그 안). */
|
||||
let renderMassHaul: ((fromM: number, toM: number) => void) | null = null;
|
||||
/** 최종 누가토량 배지 — 종단 그래프 좌측 상단(2026-09-06 사용자 지시). */
|
||||
const massBadge = createMassHaulBadge();
|
||||
panelBody.append(massBadge.root);
|
||||
/** 세로 창 갱신기 — 종단은 변환, 유토곡선은 곡선만. */
|
||||
let updateChartWindow: (() => ElevationWindowResult | null) | null = null;
|
||||
|
||||
@@ -478,9 +465,7 @@ export function createSectionView(
|
||||
// 화면에 붙기 전(detached)에는 잴 수 없으니 그때만 `PANEL_CHROME_PX` 추정치로 시작한다.
|
||||
const measured = chartWrap.clientHeight;
|
||||
lastChartAvailable =
|
||||
measured > 0
|
||||
? measured
|
||||
: Math.max(panelHeight() - PANEL_CHROME_PX, MIN_LONG_HEIGHT + MASS_HAUL_MIN_HEIGHT);
|
||||
measured > 0 ? measured : Math.max(panelHeight() - PANEL_CHROME_PX, MIN_LONG_HEIGHT);
|
||||
const heights = chartHeights(lastChartAvailable);
|
||||
const minWidth = longitudinalMinimumWidth(detail.longitudinal, cachedStationInterval);
|
||||
const chartWidth = Math.max(renderWidth, minWidth);
|
||||
@@ -498,32 +483,23 @@ export function createSectionView(
|
||||
});
|
||||
const longAxis = chart.axis;
|
||||
const nodes: Element[] = [chart.node];
|
||||
const { viewFromM, viewToM } = chart;
|
||||
|
||||
chartWrap.replaceChildren(...nodes);
|
||||
// 유토곡선 몫은 전용 모듈이 만들어 붙인다(2026-09-03 · 700줄 제한). 가로로 스크롤할
|
||||
// 때는 **이 함수만** 다시 불러 곡선의 세로 창을 따라오게 한다 — 종단면도는 건드리지
|
||||
// 않는다(2026-09-04: 상단 패널을 통째로 다시 그리면 화면이 한 번 끊긴다).
|
||||
renderMassHaul = createMassHaulRenderer(
|
||||
{
|
||||
detail,
|
||||
conversion: currentConversion,
|
||||
haulLimits: currentHaulLimits,
|
||||
naturalSpoilSlope: currentNaturalSpoilSlope,
|
||||
visibleSeries,
|
||||
selectedStationId,
|
||||
stationInterval: cachedStationInterval,
|
||||
chartWidth,
|
||||
minWidth,
|
||||
massHeight: heights.mass,
|
||||
longHeight: heights.long,
|
||||
selectStation: (stationId) => selectStation(stationId, true),
|
||||
toggleSeries,
|
||||
redraw: drawPanel,
|
||||
},
|
||||
{ chartWrap, panel, panelBody, statusNode: panelCount },
|
||||
);
|
||||
renderMassHaul(viewFromM, viewToM);
|
||||
// 유토곡선 그래프는 B06 에서 **그리지 않는다**(2026-09-06 사용자 지시) — 자리를 많이
|
||||
// 먹는데 정작 필요한 값은 마지막 지점 누가토량 하나다. 곡선은 B05 유토곡선 패널에서
|
||||
// 펼쳐 본다. 여기서는 같은 계산으로 값만 내 좌측 상단 배지에 올린다(기준: 횡단).
|
||||
if (currentConversion) {
|
||||
const series = computeMassHaulSeries(
|
||||
detail.longitudinal,
|
||||
detail.cross_sections,
|
||||
currentConversion,
|
||||
currentNaturalSpoilSlope,
|
||||
);
|
||||
const cross = series.find((entry) => entry.basis === "cross") ?? series[0];
|
||||
massBadge.set(cross ? badgeValuesFrom(cross.result) : null);
|
||||
} else {
|
||||
massBadge.set(null);
|
||||
}
|
||||
// 종단 고정 Y축 — 0크기 sticky 앵커라 **첫 자식**으로 넣어야 세로 기준이 컨테이너
|
||||
// 상단이 된다(SVG 뒤에 넣으면 앵커가 차트 아래로 밀린다).
|
||||
if (longAxis) {
|
||||
@@ -541,7 +517,6 @@ export function createSectionView(
|
||||
chartWrap.scrollLeft,
|
||||
chartWrap.clientWidth || chartWidth,
|
||||
);
|
||||
renderMassHaul?.(fromM, toM);
|
||||
return applyElevationWindow(
|
||||
chartWrap,
|
||||
visibleElevationRange(detail, fromM, toM) ?? undefined,
|
||||
@@ -673,7 +648,9 @@ export function createSectionView(
|
||||
crossHalfWidth,
|
||||
stationInterval,
|
||||
earthworkConversion,
|
||||
haulEquipmentLimits,
|
||||
// 운반 장비 한계는 곡선(운반 블록)에만 쓰던 값이라 B06 에서는 더 받지 않는다
|
||||
// (2026-09-06 유토곡선 그래프 제거). 인자 자리는 호출부 호환을 위해 남긴다.
|
||||
_haulEquipmentLimits,
|
||||
balloonScope,
|
||||
naturalSpoilMinSlope,
|
||||
) {
|
||||
@@ -684,7 +661,6 @@ export function createSectionView(
|
||||
currentStationInterval =
|
||||
stationInterval !== undefined && stationInterval > 0 ? stationInterval : undefined;
|
||||
if (earthworkConversion) currentConversion = earthworkConversion;
|
||||
if (haulEquipmentLimits?.length) currentHaulLimits = haulEquipmentLimits;
|
||||
if (Number.isFinite(naturalSpoilMinSlope)) currentNaturalSpoilSlope = naturalSpoilMinSlope;
|
||||
// balloon 위치는 **영구저장소 값이 이긴다** — 다른 브라우저에서 옮긴 자리를 그대로 받는다.
|
||||
configureBalloonOffsets(balloonScope ?? "default", detail.balloon_offsets ?? undefined);
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { MASS_HAUL_DEFAULT_VISIBLE, normalizeVisibleBasis } from "@util/common_util_mass_haul";
|
||||
import { readStateRaw, stateKey } from "../A00_Common/b_page_state";
|
||||
import { MASS_HAUL_HEIGHT, MASS_HAUL_MIN_HEIGHT } from "@util/common_util_mass_haul_view";
|
||||
import { LONG_HEIGHT } from "./B06_Section_UI_Section_Common";
|
||||
|
||||
export const PANEL_CHROME_PX = 102;
|
||||
export const BASE_PANEL_HEIGHT = LONG_HEIGHT + MASS_HAUL_HEIGHT + PANEL_CHROME_PX;
|
||||
/* 유토곡선 그래프를 B06 에서 뺐다(2026-09-06 사용자 지시) — 패널 높이도 종단면도 몫만
|
||||
잡는다. 곡선은 B05 유토곡선 패널에서 보고, 여기서는 좌측 상단 배지가 총괄값을 보인다. */
|
||||
export const BASE_PANEL_HEIGHT = LONG_HEIGHT + PANEL_CHROME_PX;
|
||||
export const MIN_LONG_HEIGHT = 110;
|
||||
export const MIN_PANEL_HEIGHT = MIN_LONG_HEIGHT + MASS_HAUL_MIN_HEIGHT + PANEL_CHROME_PX;
|
||||
export const MIN_PANEL_HEIGHT = MIN_LONG_HEIGHT + PANEL_CHROME_PX;
|
||||
export const MAX_PANEL_HEIGHT_RATIO = 0.8;
|
||||
/* 패널 접힘·높이·유토곡선 범례는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다.
|
||||
범례 키는 B05 와 **같은 값**이어야 한다(두 화면이 같은 그림의 두 창). */
|
||||
@@ -29,20 +30,12 @@ export function readVisibleSeries(): Set<string> {
|
||||
}
|
||||
}
|
||||
|
||||
/** 종단면도 높이 — 유토곡선을 빼면서 남는 자리를 **전부** 종단이 쓴다(2026-09-06). */
|
||||
export function chartHeights(availableHeightPx: number): { long: number; mass: number } {
|
||||
if (!Number.isFinite(availableHeightPx) || availableHeightPx <= 0) {
|
||||
return { long: LONG_HEIGHT, mass: MASS_HAUL_HEIGHT };
|
||||
return { long: LONG_HEIGHT, mass: 0 };
|
||||
}
|
||||
const available = Math.max(availableHeightPx, MIN_LONG_HEIGHT + MASS_HAUL_MIN_HEIGHT);
|
||||
if (available >= LONG_HEIGHT + MASS_HAUL_HEIGHT) {
|
||||
return { long: LONG_HEIGHT, mass: available - LONG_HEIGHT };
|
||||
}
|
||||
const ratio = available / (LONG_HEIGHT + MASS_HAUL_HEIGHT);
|
||||
const mass = Math.min(
|
||||
Math.max(MASS_HAUL_MIN_HEIGHT, Math.round(MASS_HAUL_HEIGHT * ratio)),
|
||||
available - MIN_LONG_HEIGHT,
|
||||
);
|
||||
return { long: available - mass, mass };
|
||||
return { long: Math.max(MIN_LONG_HEIGHT, availableHeightPx), mass: 0 };
|
||||
}
|
||||
|
||||
export function unwrapChart(node: HTMLElement): Element {
|
||||
|
||||
@@ -371,3 +371,41 @@
|
||||
border-top-color: var(--color-warning);
|
||||
border-top-style: dashed;
|
||||
}
|
||||
|
||||
/* ── 최종 누가토량 배지 (2026-09-06 사용자 지시) ──────────────────────────
|
||||
종단 그래프 좌측 상단에 겹쳐 띄운다. 유토곡선을 접거나(B05) 아예 빼도(B06)
|
||||
이 값은 늘 보여야 한다. 그래프 조작을 막지 않도록 이벤트는 통과시키되,
|
||||
툴팁은 읽혀야 하므로 배지 자신만 이벤트를 받는다. */
|
||||
.mass-haul-badge {
|
||||
position: absolute;
|
||||
top: var(--mass-haul-badge-top, 6px);
|
||||
/* Y축 띠(고정 축) 오른쪽에 놓는다 — 8px 로 두면 축 라벨 위에 겹쳐 값이 잘린다
|
||||
(2026-09-06 실측). 축 폭이 다른 화면은 이 변수를 덮어쓴다. */
|
||||
left: var(--mass-haul-badge-left, 86px);
|
||||
z-index: 4;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-4, 4px);
|
||||
padding: 2px var(--spacing-8, 8px);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-pills, 999px);
|
||||
background: color-mix(in srgb, var(--color-surface-raised) 88%, transparent);
|
||||
color: var(--color-text-body);
|
||||
font-size: var(--text-caption);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.mass-haul-badge em {
|
||||
color: var(--color-text-secondary);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.mass-haul-badge strong {
|
||||
font-weight: var(--font-weight-bold, 700);
|
||||
}
|
||||
|
||||
.mass-haul-badge.is-over strong {
|
||||
color: var(--color-danger, #f43f5e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
/* =============================================================================
|
||||
* common_util_mass_haul_badge.ts
|
||||
* 「최종 누가토량」 배지 — 종단 그래프 **좌측 상단에 겹쳐** 띄우는 한 줄.
|
||||
*
|
||||
* 왜 배지인가(2026-09-06 사용자 지시) — 유토곡선 그래프는 자리를 많이 먹는데, 계획선을
|
||||
* 만지며 늘 봐야 하는 값은 **마지막 지점의 누가토량 하나**다. 그래서 그 값만 그래프 위에
|
||||
* 겹쳐 두고, 곡선 자체는 B05 의 유토곡선 패널에서만 펼쳐 본다(B06 에서는 곡선을 뺐다).
|
||||
*
|
||||
* 기준은 **횡단**이다 — 종단 기준 곡선은 계획선만 보는 개략값이라 뒤에 없앨 예정이다
|
||||
* (2026-09-06 사용자). 값을 넣는 쪽에서 횡단 기준 계열을 골라 넘긴다.
|
||||
* ========================================================================== */
|
||||
|
||||
/** 배지에 실리는 값 한 벌 — 유토곡선 계산 결과에서 뽑는다. */
|
||||
export interface MassHaulBadgeValues {
|
||||
/** 마지막 측점의 누계 토량(㎥). 음수면 부족(토취). */
|
||||
finalM3: number;
|
||||
cutNaturalM3: number;
|
||||
cutCompactedM3: number;
|
||||
fillCompactedM3: number;
|
||||
surplusM3: number;
|
||||
shortageM3: number;
|
||||
}
|
||||
|
||||
/** 누가토량 표기 — 천 단위 구분 + 소수점 1자리(유토곡선 요약과 같은 규칙). */
|
||||
function volume(value: number): string {
|
||||
return `${value.toLocaleString("ko-KR", { maximumFractionDigits: 1 })}㎥`;
|
||||
}
|
||||
|
||||
export interface MassHaulBadge {
|
||||
/** 그래프 컨테이너(`position: relative`)에 붙일 요소. */
|
||||
root: HTMLElement;
|
||||
/** 값을 갈아 끼운다. null 이면 배지를 숨긴다(아직 계산 못 한 상태). */
|
||||
set: (values: MassHaulBadgeValues | null) => void;
|
||||
}
|
||||
|
||||
/** 유토곡선 계산 결과 한 벌에서 배지 값을 뽑는다 — B05·B06 이 같은 규칙을 쓴다. */
|
||||
export function badgeValuesFrom(result: {
|
||||
points: ReadonlyArray<{ cumulative_volume_m3: number }>;
|
||||
cut_natural_m3: { soil: number; ripping_rock: number; blasting_rock: number };
|
||||
cut_compacted_m3: number;
|
||||
fill_compacted_m3: number;
|
||||
surplus_m3: number;
|
||||
shortage_m3: number;
|
||||
}): MassHaulBadgeValues {
|
||||
const cut = result.cut_natural_m3;
|
||||
const points = result.points;
|
||||
return {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/** 배지를 만든다. 붙이는 것은 부르는 쪽 몫이다(그래프 컨테이너 첫 자식). */
|
||||
export function createMassHaulBadge(): MassHaulBadge {
|
||||
const root = document.createElement("span");
|
||||
root.className = "mass-haul-badge";
|
||||
root.hidden = true;
|
||||
const caption = document.createElement("em");
|
||||
caption.textContent = "누가토량";
|
||||
const value = document.createElement("strong");
|
||||
root.append(caption, value);
|
||||
return {
|
||||
root,
|
||||
set(values) {
|
||||
if (!values) {
|
||||
root.hidden = true;
|
||||
return;
|
||||
}
|
||||
root.hidden = false;
|
||||
root.classList.toggle("is-over", values.finalM3 < 0);
|
||||
value.textContent = volume(values.finalM3);
|
||||
root.title = [
|
||||
`절토(자연) ${volume(values.cutNaturalM3)} · 절토(다짐) ${volume(values.cutCompactedM3)}`,
|
||||
`성토(다짐) ${volume(values.fillCompactedM3)}`,
|
||||
values.shortageM3 > 0
|
||||
? `부족 ${volume(values.shortageM3)}`
|
||||
: `잉여 ${volume(values.surplusM3)}`,
|
||||
"기준: 횡단",
|
||||
].join("\n");
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user