From 331be116a9be0f919add78f9d54a33b6135dd466 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Fri, 4 Sep 2026 22:31:09 +0900 Subject: [PATCH] =?UTF-8?q?fix(B05,B06):=20=EC=9C=A0=ED=86=A0=EA=B3=A1?= =?UTF-8?q?=EC=84=A0=EC=9D=B4=20=EA=B7=B8=EB=9E=98=ED=94=84=20=EC=B9=B8=20?= =?UTF-8?q?=EB=B0=96=EC=9C=BC=EB=A1=9C=20=EC=83=88=EB=8D=98=20=EA=B2=83=20?= =?UTF-8?q?=EC=B0=A8=EB=8B=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자 보고(2026-09-04) — 「0선이 갑자기 위로 넘어가고 그 영역 채움색이 눈금 상한을 넘어 표현됨」. 보이는 구간에 세로를 맞추면서 0이 창 밖으로 나가는데, 유토곡선에는 **자르는 영역(clip)이 없어** 0선과 면 채움색이 범례·토글 버튼 위까지 그대로 칠해짐. 종단 그래프는 진작 자르고 있었는데 유토곡선만 빠져 있었음. - 그래프 칸(축 오른쪽 · 상단 여백 아래)을 자르는 겹 둘을 둠 — 측점 세로선이 면과 곡선 **사이**에 서야 해서 앞뒤로 나눔(그리는 순서 유지). - 면·곡선·0선·EP 표식·선택 강조·말풍선·토량 분배를 그 겹 안으로 옮김. - 0선은 창 안에 있을 때만 그리고 눈금에도 그때만 실음 — 밖으로 나갔는데 실으면 좌측 고정 축 맨 위에 남의 값과 겹쳐 찍힘(사용자 확정: 0선은 나가도 됨). - 눈금 글자는 축 **왼쪽**이라 자르는 겹에 넣지 않음(넣으면 사라짐). 자체검증(공용 브라우저 임시 탭): 확대 4칸 + 휠 25칸으로 0을 창 밖으로 보낸 상태에서 ① 자르는 겹 2개 확인 ② 0선·0눈금 사라짐, 눈금 5개 전부 음수(겹쳐 찍힌 0 없음) ③ 영역 캡처로 면 채움색이 그래프 칸 위 경계에서 정확히 멈추는 것 확인. 전체 시험 381 통과·17 건너뜀. Co-Authored-By: Claude Opus 5 --- .../common_util_mass_haul_balance_view.ts | 11 +- common_util/common_util_mass_haul_view.ts | 110 ++++++++++++------ 2 files changed, 84 insertions(+), 37 deletions(-) diff --git a/common_util/common_util_mass_haul_balance_view.ts b/common_util/common_util_mass_haul_balance_view.ts index dca257ff..0933fb5a 100644 --- a/common_util/common_util_mass_haul_balance_view.ts +++ b/common_util/common_util_mass_haul_balance_view.ts @@ -442,7 +442,14 @@ function appendTransfer( * 띠 면을 누르면 그 balloon이 강조된다. 다른 띠나 빈 곳을 누르면 풀린다 * (2026-08-02 사용자 지시). 강조는 이 SVG 안에서만 사는 상태라 다시 그리면 초기화된다. */ -export function appendBalanceLayer(svg: SVGSVGElement, plan: HaulPlan, box: BalanceLayerBox): void { +export function appendBalanceLayer( + svg: SVGSVGElement, + plan: HaulPlan, + box: BalanceLayerBox, + /** 실제로 붙일 자리 — 그래프 칸 밖을 자르는 겹이 있으면 그쪽(2026-09-04). 재는 일은 + * `svg`(뷰박스가 필요)가 그대로 맡는다. */ + host: SVGSVGElement | SVGGElement = svg, +): void { const group = svgElement("g", { class: "b06-balance" }); appendBalanceLine(group, plan, box); @@ -482,7 +489,7 @@ export function appendBalanceLayer(svg: SVGSVGElement, plan: HaulPlan, box: Bala balloons.forEach((balloon, index) => bind(balloon, index)); // 띠 밖(그래프 빈 곳)을 누르면 강조를 푼다. svg.addEventListener("click", clearAll); - svg.append(group); + host.append(group); } /** diff --git a/common_util/common_util_mass_haul_view.ts b/common_util/common_util_mass_haul_view.ts index d06e516f..d09d8de6 100644 --- a/common_util/common_util_mass_haul_view.ts +++ b/common_util/common_util_mass_haul_view.ts @@ -241,7 +241,8 @@ function textWidth(value: string): number { * 재서 여유가 큰 쪽을 고르며, 어느 쪽에도 안 들어가면 여유가 더 큰 쪽 끝에 붙인다. */ function appendVolumeCallout( - svg: SVGSVGElement, + /** 붙일 자리 — 그래프 칸 밖을 자르는 겹이다(2026-09-04). */ + svg: SVGSVGElement | SVGGElement, point: MassHaulPoint, stationInterval: number, box: CalloutPlacement, @@ -368,6 +369,27 @@ export function createMassHaulChart( const span = Math.max(max - min, 1e-6); const y = (volume: number) => padTop + ((max - volume) / span) * plotHeight; + // 그래프 칸 밖으로 나간 것은 그리지 않는다(2026-09-04 사용자 보고) — 보이는 구간에 + // 세로를 맞추면 0선과 그 채움색이 눈금 상한을 넘어 **범례·토글 버튼 위까지** 칠해졌다. + // 종단 그래프는 진작 자르고 있었는데 유토곡선만 빠져 있었다. + const plotBottom = heightPx - MASS_PAD_BOTTOM; + const clipId = `b06-mass-clip-${Math.random().toString(36).slice(2, 9)}`; + const clip = svgElement("clipPath", { id: clipId }); + clip.append( + svgElement("rect", { + x: axisX, + y: padTop, + width: Math.max(0, widthPx - axis.padRight - axisX), + height: Math.max(0, plotHeight), + }), + ); + const defs = svgElement("defs", {}); + defs.append(clip); + // 두 겹으로 나누는 이유: 측점 세로선이 면과 곡선 **사이**에 서야 한다(그리는 순서 유지). + const plotBack = svgElement("g", { "clip-path": `url(#${clipId})` }); + const plotFront = svgElement("g", { "clip-path": `url(#${clipId})` }); + svg.append(defs); + // 패널을 줄이면 그래프 몫이 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 }> = []; @@ -395,8 +417,10 @@ export function createMassHaulChart( } const zeroY = y(0); - // 0선은 격자보다 진하게 따로 그리므로 고정 축 눈금에도 함께 실어 준다. - axisTicks.push({ y: zeroY, label: "0㎥" }); + // 0선이 창 안에 있을 때만 눈금에 싣는다 — 밖으로 나갔는데도 실으면 고정 축 맨 위에 + // 남의 값과 겹쳐 찍힌다(2026-09-04 사용자 보고). + const zeroInside = zeroY >= padTop && zeroY <= plotBottom; + if (zeroInside) axisTicks.push({ y: zeroY, label: "0㎥" }); onAxis?.({ padLeft: axisX, ticks: axisTicks }); const visible = series.filter((entry) => visibleKeys.has(entry.key)); @@ -420,7 +444,7 @@ export function createMassHaulChart( const banded = visible[0]; if (banded) { const last = banded.result.points[banded.result.points.length - 1]; - svg.append( + plotBack.append( svgElement("path", { // 곡선과 **같은 경로**로 위쪽 테두리를 그린 뒤 0선을 따라 닫는다. d: @@ -431,6 +455,8 @@ export function createMassHaulChart( ); } + svg.append(plotBack); + // 측점선 — 종단도와 같은 자리에 서고, 눌러서 측점을 고를 수 있다(3자 선택 동기화). // 라벨은 바로 위 종단면도가 이미 달고 있어 여기서는 생략한다(중복 표기 방지). for (const station of longitudinal.stations) { @@ -465,27 +491,33 @@ export function createMassHaulChart( svg.append(marker); } - // 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그리고 눈금값도 따로 적는다. - // Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다. - svg.append( - svgElement("line", { - x1: axisX, - y1: zeroY, - x2: widthPx - axis.padRight, - y2: zeroY, - class: "b06-masshaul__zero", - }), - svgText("0", { - x: axisX - 9, - y: zeroY + 4, - "text-anchor": "end", - class: "b06-chart__tick b06-masshaul__zero-tick", - }), - ); + // 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그린다. + // 보이는 구간에 세로를 맞추면 0이 창 밖으로 나갈 수 있다(2026-09-04 사용자 확정: + // 「나가도 됨」) — 그때는 아예 그리지 않는다. 글자는 축 **왼쪽**이라 자르는 겹에 넣으면 + // 사라지므로 따로 붙인다. + if (zeroInside) { + plotFront.append( + svgElement("line", { + x1: axisX, + y1: zeroY, + x2: widthPx - axis.padRight, + y2: zeroY, + class: "b06-masshaul__zero", + }), + ); + svg.append( + svgText("0", { + x: axisX - 9, + y: zeroY + 4, + "text-anchor": "end", + class: "b06-chart__tick b06-masshaul__zero-tick", + }), + ); + } // 뒤에 그린 곡선이 위로 오므로 범례 순서의 역순으로 그려 첫 곡선(정식)을 가장 위에 둔다. for (const entry of [...visible].reverse()) { - svg.append( + plotFront.append( svgElement("path", { d: curvePath(entry.result.points, x, y), class: seriesClass(entry, "b06-masshaul__curve"), @@ -504,7 +536,7 @@ export function createMassHaulChart( // 종점은 오른쪽 끝이라 라벨을 왼쪽으로 눕히고, 위아래는 플롯 안으로 클램프한다. const labelX = Math.max(epX - 8, axis.padLeft + labelWidth); const labelY = Math.min(Math.max(epY - 8, padTop + 12), heightPx - MASS_PAD_BOTTOM - 4); - svg.append( + plotFront.append( svgElement("circle", { cx: epX, cy: epY, r: 3.5, class: "b06-masshaul__ep-dot" }), svgElement("rect", { x: labelX - labelWidth, @@ -527,23 +559,29 @@ export function createMassHaulChart( // 토량 분배 레이어 — 곡선 **위**, 선택 말풍선 **아래**. 순서를 바꾸면 평형선이 곡선을 // 가리거나(위로 올리면) 선택 말풍선이 balloon에 묻힌다(아래로 내리면). if (haulPlan) { - appendBalanceLayer(svg, haulPlan, { - x, - y, - left: axis.padLeft, - right: widthPx - axis.padRight, - top: padTop, - bottom: heightPx - MASS_PAD_BOTTOM, - curveYAt, - stationInterval, - }); + appendBalanceLayer( + svg, + haulPlan, + { + x, + y, + left: axis.padLeft, + right: widthPx - axis.padRight, + top: padTop, + bottom: heightPx - MASS_PAD_BOTTOM, + curveYAt, + stationInterval, + }, + // 그리는 자리는 자르는 겹 — 창 밖으로 나간 평형선·말풍선이 눈금 위로 새지 않는다. + plotFront, + ); } // 선택 측점 강조 — 곡선 위 점 + 구간 물량 말풍선. const focus = banded ? massHaulPointAt(banded, longitudinal, selectedStationId) : null; if (focus) { const focusX = x(focus.chainage_m); - svg.append( + plotFront.append( svgElement("circle", { cx: focusX, cy: y(focus.cumulative_volume_m3), @@ -551,7 +589,7 @@ export function createMassHaulChart( class: "b06-masshaul__focus", }), ); - appendVolumeCallout(svg, focus, stationInterval, { + appendVolumeCallout(plotFront, focus, stationInterval, { anchorX: focusX, anchorY: y(focus.cumulative_volume_m3), left: axis.padLeft, @@ -562,6 +600,8 @@ export function createMassHaulChart( }); } + svg.append(plotFront); + svg.append( svgElement("line", { x1: axisX,