feat(B05): 토량분배 시 유토곡선 전체 맞춤 + 상단 여백·테이블 높이 정리

- 토량분배를 켜면 유토곡선이 종단과의 X 맞춤·세로 창을 버리고 스크롤러 안쪽
  폭·전 구간 Y 로 그려 곡선 전체가 한 화면에 들어옴(평형선·운반 블록을 노선
  전체로 읽는 그림이라). 끄면 예전대로 종단과 맞물린 가로 스크롤로 복귀.
- 요약 막대가 상단줄로 올라간 뒤 남던 상단 여백 축소 — 범례 top 34px → 4px,
  차트 padTop 40 → 30(범례 높이 24 + 여유).
- 종단 테이블 오버레이 높이 상한 신설 — 6행 × 행 30px + 24px(=204px).
  12행에서 6행으로 줄인 뒤 같은 높이를 나눠 가지며 행이 부풀던 것을 막음.
  레이아웃 제어(리사이저·패널 비례 연동)는 그대로 두고 상한만 걸음.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 17:07:01 +09:00
co-authored by Claude Opus 5
parent 43f6325d30
commit 94266757cd
4 changed files with 38 additions and 17 deletions
+16 -5
View File
@@ -422,17 +422,26 @@ export function createRouteMassHaulPanel(
(overlay.clientHeight || OVERLAY_DEFAULT_HEIGHT) - bar.offsetHeight - 12;
const chartHeight = Math.max(scrollHeight, MASS_HAUL_MIN_HEIGHT);
// 토량 분배를 켜면 **곡선 전체가 한 화면에** 들어와야 한다(2026-09-06 사용자 지시).
// 평형선·운반 블록은 노선 전체를 놓고 읽는 그림이라, 종단 그래프와의 X 맞춤(가로
// 스크롤)과 보이는 구간만 보는 Y 창을 둘 다 버리고 스크롤러 안쪽 폭·전 구간 Y로 그린다.
const fitAll = haulPlan !== null;
const widthPx = fitAll ? scroll.clientWidth || params.widthPx : params.widthPx;
const chartAxis = fitAll
? { ...params.axis, viewRange: undefined, window: undefined }
: { ...params.axis, window: windowState };
let massAxis: { padLeft: number; ticks: Array<{ y: number; label: string }> } | null = null;
const chart = createMassHaulChart(
series,
visible,
params.stationSource,
{ ...params.axis, window: windowState },
chartAxis,
params.selectedStationId,
params.stationInterval,
params.widthPx,
widthPx,
chartHeight,
params.widthPx,
widthPx,
params.onSelectStation,
haulPlan,
(axis) => {
@@ -442,14 +451,16 @@ export function createRouteMassHaulPanel(
// 종단 캔버스와 같은 폭의 래퍼 — sticky Y축 앵커의 기준(position: relative)이 된다.
const wrap = document.createElement("div");
wrap.className = "b05-profile__chart b05-profile__masshaul-chart";
wrap.style.width = `${params.widthPx}px`;
wrap.style.width = `${widthPx}px`;
wrap.style.height = `${chartHeight}px`;
wrap.append(chart);
// 앵커는 첫 자식 — SVG 뒤면 흐름 위치가 밀려 스크롤 시 축이 안 보인다(B06과 같은 규칙).
if (massAxis) wrap.prepend(buildStickyYAxis(massAxis, chartHeight));
scroll.replaceChildren(wrap);
// 종단 스크롤러가 이미 옆으로 가 있으면 새로 그린 곡선도 같은 자리에서 시작한다.
if (syncedMain) scroll.scrollLeft = syncedMain.scrollLeft;
// 전체 맞춤일 때는 스크롤할 것이 없으므로 왼쪽 끝에 둔다.
if (fitAll) scroll.scrollLeft = 0;
else if (syncedMain) scroll.scrollLeft = syncedMain.scrollLeft;
legendLayer.append(
createMassHaulLegend(series, visible, toggleSeries, () => {
+3 -3
View File
@@ -432,9 +432,9 @@ export function renderProfile(ctx: ProfileRenderContext): void {
padRight: LONG_PAD.right + originOffset,
// 축 선·눈금은 위 종단 그래프의 축과 같은 자리에 — 축이 두 개로 보이지 않게.
axisX: LONG_PAD.left,
// 범례·기준 버튼 오버레이(top 34px)가 곡선 위에 떠서 그만큼 상단 여유를 준다
// (2026-08-05 사용자 보고: 버튼과 커브 겹침).
padTop: 40,
// 범례·기준 버튼 오버레이(top 4px)가 곡선 위에 떠서 그 높이만큼만 여유를 준다
// (2026-08-05: 버튼과 커브 겹침 / 2026-09-06: 요약 막대가 빠져 상단이 너무 비었음).
padTop: 30,
// 유토곡선 Y 도 종단과 같은 창을 본다 — 전 구간 최대 토량으로 고정하면 확대해도
// 곡선이 납작하게 눌린다(2026-09-04 사용자 지시).
viewRange: { fromM: viewFromM, toM: viewToM },
@@ -13,6 +13,7 @@
import { readStateRaw, stateKey, writeStateRaw } from "../A00_Common/b_page_state";
import { createWorkflowPanelHandle } from "@ui/ui_template_overlay";
import { createPanelResizer } from "@ui/ui_template_resizer";
import { TABLE_ROW_COUNT } from "./B05_Profile_UI_Profile_Layout";
import { attachWheelHorizontalScroll } from "./B05_Profile_UI_Profile_Wheel";
/* 펼침·높이는 화면 취향(①) — 키는 등록표(`b_page_state`)가 만든다. */
@@ -21,8 +22,16 @@ const OVERLAY_MIN_HEIGHT = 140;
/** 테이블 오버레이 최소 높이 — 메인 패널 하한 계산(Profile_Panel)이 함께 쓴다. */
export const TABLE_OVERLAY_MIN_HEIGHT = OVERLAY_MIN_HEIGHT;
const OVERLAY_MAX_RATIO = 0.75;
/** CSS 기본 높이 — `var(--b05-table-height, 300px)`(_Style_MassHaul.css)와 같아야 한다. */
const OVERLAY_DEFAULT_HEIGHT = 300;
/** 행 하나의 높이 상한(px). 행이 이보다 커지면 글자만 커지고 읽을 내용은 그대로다 —
* 12행에서 6행으로 줄인 뒤 같은 높이를 나눠 가지며 행이 두 배로 부푼 것을 막는다
* (2026-09-06 사용자 지시: 「최대값 제한, 레이아웃 제어는 현재 유지」). */
const MAX_ROW_HEIGHT = 30;
/** 오버레이 높이 상한 = 6행 × 행 상한 + 가로 스크롤바·경계 몫. */
const OVERLAY_MAX_HEIGHT = TABLE_ROW_COUNT * MAX_ROW_HEIGHT + 24;
/** CSS 기본 높이 — `var(--b05-table-height, 204px)`(_Style_MassHaul.css)와 같아야 한다. */
const OVERLAY_DEFAULT_HEIGHT = OVERLAY_MAX_HEIGHT;
/** 어느 경로로 들어온 높이든 상한을 넘지 않게 한다(리사이저·패널 비례 연동 공용). */
const clampHeight = (px: number): number => Math.min(px, OVERLAY_MAX_HEIGHT);
/** 리사이저가 높이를 담는 CSS 변수. */
const HEIGHT_VAR = "--b05-table-height";
@@ -71,7 +80,8 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
cssVar: HEIGHT_VAR,
direction: -1,
min: OVERLAY_MIN_HEIGHT,
max: () => (overlay.parentElement?.clientHeight ?? window.innerHeight) * OVERLAY_MAX_RATIO,
max: () =>
clampHeight((overlay.parentElement?.clientHeight ?? window.innerHeight) * OVERLAY_MAX_RATIO),
storageKey: HEIGHT_KEY,
onResize: () => {
syncHandlePosition();
@@ -132,10 +142,10 @@ export function createProfileTableOverlay(onChanged: () => void): RouteProfileTa
isOpen: () => open,
desiredHeight: () => {
const raw = parseFloat(overlay.style.getPropertyValue(HEIGHT_VAR));
return Number.isFinite(raw) && raw > 0 ? raw : OVERLAY_DEFAULT_HEIGHT;
return clampHeight(Number.isFinite(raw) && raw > 0 ? raw : OVERLAY_DEFAULT_HEIGHT);
},
commitHeight: (px) => {
const next = Math.round(px);
const next = Math.round(clampHeight(px));
overlay.style.setProperty(HEIGHT_VAR, `${next}px`);
writeStateRaw("table-height", String(next));
syncHandlePosition();
@@ -101,7 +101,7 @@
공용 범례(.b06-masshaul__legend)가 이미 absolute + right 배치라 top만 이 래퍼가 정한다. */
.b05-profile__masshaul-legend {
position: absolute;
top: 34px;
top: 4px;
right: 0;
left: 0;
z-index: 3;
@@ -120,10 +120,10 @@
/* 재계산 대기 표시 — 곡선을 지우지 않고(2026-09-03 깜빡임 제거) 곡선 영역 우측 상단에
**떠 있는** 알림으로 둔다. 요약 막대에 붙이면 막대가 줄바꿈되며 곡선을 밀어냈다
(2026-09-04 사용자 보고). 범례(top: 34px) 아래 줄에 앉혀 서로 가리지 않는다. */
(2026-09-04 사용자 보고). 범례(top: 4px) 아래 줄에 앉혀 서로 가리지 않는다. */
.b05-profile__masshaul-pending {
position: absolute;
top: 62px;
top: 32px;
right: var(--spacing-8);
z-index: 4;
padding: 2px var(--spacing-8);
@@ -173,7 +173,7 @@
z-index: 5;
display: flex;
flex-direction: column;
height: var(--b05-table-height, 300px);
height: var(--b05-table-height, 204px);
border-top: 1px solid var(--color-border);
background: var(--color-surface-raised);
box-shadow: 0 -4px 12px rgb(0 0 0 / 18%);