fix(ui): 유토곡선 상단 여유·사이드 컨테이너 외곽선·패널 리사이즈 부드럽게

- 유토곡선 SVG 상단 여백 padTop 옵션 신설(공용 렌더러), B05는 40px —
  범례·기준 버튼 오버레이(top 34px)와 커브 겹침 해소. B06은 기존 10px 유지
- 사이드 패널 컨테이너 공통 클래스(ui-sidebar-section): 외곽선을 본문 보조색
  45% 혼합으로 진하게 — 두 테마 모두 흐려 안 보이던 문제. B04/B05/B06 적용
- 하단 패널 리사이즈 끊김 해소: 끌리는 동안 프레임당 경량 높이 동기화
  (캐스케이드 계산 + 캔버스/차트 높이만), 무거운 차트·테이블 재구성은
  손 뗀 뒤 120ms 디바운스로 1회. 추가 리소스 없음 — 프레임당 비용 감소

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 19:51:10 +09:00
co-authored by Claude Fable 5
parent 69aed9d26c
commit 81825413ef
6 changed files with 108 additions and 60 deletions
+13 -9
View File
@@ -41,6 +41,8 @@ export interface MassHaulAxis {
* (2026-08-03 사용자 보고). 축은 여기(종단과 같은 자리)에, 데이터는 padLeft에 둔다.
*/
axisX?: number;
/** 그래프 위 여백(px). 생략하면 기본(10). B05는 범례 오버레이만큼 크게 준다. */
padTop?: number;
}
/** 축 눈금이 읽히는 최소 높이. 이보다 낮아지면 그래프가 뭉개진다. */
@@ -313,7 +315,9 @@ export function createMassHaulChart(
const maxChainage = Math.max(axis.maxChainageM, 1);
const axisX = axis.axisX ?? axis.padLeft;
const plotWidth = widthPx - axis.padLeft - axis.padRight;
const plotHeight = heightPx - MASS_PAD_TOP - MASS_PAD_BOTTOM;
// B05 오버레이는 범례 버튼이 그래프 위(top 34px)에 떠서 상단 여유를 더 받는다.
const padTop = axis.padTop ?? MASS_PAD_TOP;
const plotHeight = heightPx - padTop - MASS_PAD_BOTTOM;
const x = (chainage: number) => axis.padLeft + (chainage / maxChainage) * plotWidth;
// 기준 버튼이 라디오가 되면서(택1 표시) Y 범위도 **표시 중인 곡선**으로 잡는다 —
@@ -321,13 +325,13 @@ export function createMassHaulChart(
const rangeSource = series.filter((entry) => visibleKeys.has(entry.key));
const { min, max } = volumeRange(rangeSource.length ? rangeSource : series);
const span = Math.max(max - min, 1e-6);
const y = (volume: number) => MASS_PAD_TOP + ((max - volume) / span) * plotHeight;
const y = (volume: number) => padTop + ((max - volume) / span) * plotHeight;
// 패널을 줄이면 그래프 몫이 60px대까지 내려간다 — 눈금 5개를 그대로 두면 라벨이 서로 겹친다.
const tickRatios = plotHeight < 110 ? [0, 0.5, 1] : [0, 0.25, 0.5, 0.75, 1];
const axisTicks: Array<{ y: number; label: string }> = [];
for (const ratio of tickRatios) {
const gridY = MASS_PAD_TOP + ratio * plotHeight;
const gridY = padTop + ratio * plotHeight;
const value = max - ratio * span;
// 고정 축 라벨에는 단위를 붙인다 — 바로 위 종단 그래프의 표고축(`…m`)과 같은 자리에
// 서므로, 단위가 없으면 어느 축인지 읽히지 않는다(2026-08-03 사용자 지적).
@@ -404,14 +408,14 @@ export function createMassHaulChart(
marker.append(
svgElement("line", {
x1: stationX,
y1: MASS_PAD_TOP,
y1: padTop,
x2: stationX,
y2: heightPx - MASS_PAD_BOTTOM,
class: "b06-chart__station-hit",
}),
svgElement("line", {
x1: stationX,
y1: MASS_PAD_TOP,
y1: padTop,
x2: stationX,
y2: heightPx - MASS_PAD_BOTTOM,
class: `b06-chart__station-line b06-chart__station-line--${selected ? "selected" : station.kind}`,
@@ -458,7 +462,7 @@ export function createMassHaulChart(
const labelWidth = textWidth(text) + 10;
// 종점은 오른쪽 끝이라 라벨을 왼쪽으로 눕히고, 위아래는 플롯 안으로 클램프한다.
const labelX = Math.max(epX - 8, axis.padLeft + labelWidth);
const labelY = Math.min(Math.max(epY - 8, MASS_PAD_TOP + 12), heightPx - MASS_PAD_BOTTOM - 4);
const labelY = Math.min(Math.max(epY - 8, padTop + 12), heightPx - MASS_PAD_BOTTOM - 4);
svg.append(
svgElement("circle", { cx: epX, cy: epY, r: 3.5, class: "b06-masshaul__ep-dot" }),
svgElement("rect", {
@@ -487,7 +491,7 @@ export function createMassHaulChart(
y,
left: axis.padLeft,
right: widthPx - axis.padRight,
top: MASS_PAD_TOP,
top: padTop,
bottom: heightPx - MASS_PAD_BOTTOM,
curveYAt,
stationInterval,
@@ -511,7 +515,7 @@ export function createMassHaulChart(
anchorY: y(focus.cumulative_volume_m3),
left: axis.padLeft,
right: widthPx - axis.padRight,
top: MASS_PAD_TOP,
top: padTop,
bottom: heightPx - MASS_PAD_BOTTOM,
curveYAt,
});
@@ -520,7 +524,7 @@ export function createMassHaulChart(
svg.append(
svgElement("line", {
x1: axisX,
y1: MASS_PAD_TOP,
y1: padTop,
x2: axisX,
y2: heightPx - MASS_PAD_BOTTOM,
class: "b06-chart__axis",