From 2bd1c874ca6eb0c95afbcb460079100a69f7d4ee Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 2 Aug 2026 13:57:45 +0900 Subject: [PATCH] =?UTF-8?q?fix(B06):=20=ED=9A=A1=EB=8B=A8=20=EB=A9=B4?= =?UTF-8?q?=EC=A0=81=EC=9D=84=20EA/RR/BR=20=ED=91=9C=EB=A1=9C=20=EB=B0=94?= =?UTF-8?q?=EA=BE=B8=EA=B3=A0=20=EA=B0=95=EC=A1=B0=20=ED=81=B4=EB=A6=AD=20?= =?UTF-8?q?=EB=B2=84=EB=B8=94=EB=A7=81=20=EC=B0=A8=EB=8B=A8,=200=EC=84=A0?= =?UTF-8?q?=20=EB=88=88=EA=B8=88=20=ED=91=9C=EA=B8=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 면적 하이라이트가 먹지 않던 원인: 값 칸·밴드 클릭이 카드까지 버블링돼 측점 선택이 다시 걸리고, draw()가 카드를 새로 만들면서 방금 켠 강조가 즉시 지워졌다. stopPropagation 추가. - 면적 표기를 칩 나열에서 절토/성토 2행 × EA·RR·BR·계 4열 표로 바꾼다. 이름은 도면 표기를 그대로 쓰고, 0값이어도 항목을 유지해 카드끼리 비교가 되게 한다. 성토는 완성 단면이라 지반유형으로 갈리지 않으므로 계 한 칸만 쓴다. - 암반 밴드는 하나뿐이라 그 측점의 암종에 해당하는 칸만 강조 버튼이 된다. - 유토곡선 0선에 Y축 눈금값 0을 적어 무슨 선인지 읽히게 한다(선 자체는 원래 상시 표시). - 오버레이가 1줄에서 3행 표가 되어 상단 여유를 28 → 58px로 키웠다. Co-Authored-By: Claude Opus 5 (1M context) --- .../B06_wf3_ProfileCross_UI_Cross_Areas.ts | 188 +++++++++++++----- .../B06_wf3_ProfileCross_UI_Cross_View.ts | 3 +- .../B06_wf3_ProfileCross_UI_MassHaul_View.ts | 9 +- .../B06_wf3_ProfileCross_UI_Style_Cross.css | 43 +++- ...B06_wf3_ProfileCross_UI_Style_MassHaul.css | 7 +- ui_template/ui_template_locale_b2.ts | 5 + 6 files changed, 189 insertions(+), 66 deletions(-) diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Areas.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Areas.ts index f2fb5d92..ff2c6426 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Areas.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_Areas.ts @@ -162,7 +162,11 @@ export function appendCrossAreaBands( if (!polygons.length) continue; const group = svgElement("g", { class: `b06-chart__area b06-chart__area--${band.key}` }); for (const points of polygons) group.append(svgElement("polygon", { points })); - group.addEventListener("click", () => onSelect(band.key)); + group.addEventListener("click", (event) => { + // 카드까지 올라가면 측점 선택이 다시 걸려 카드가 통째로 다시 그려진다 — 방금 켠 강조가 사라진다. + event.stopPropagation(); + onSelect(band.key); + }); groups.set(band.key, group); svg.append(group); } @@ -177,73 +181,153 @@ export function appendCrossAreaBands( }; } +/** 절토 단면적을 도면 표기(EA 토사 / RR 리핑암 / BR 발파암)로 갈라 낸다. */ +function cutBreakdown(design: CrossDesign): { + ea: number; + rr: number; + br: number; + total: number; +} { + const total = design.cut_area_m2; + // 구 데이터(분리 필드 없음)는 절토 전량을 측점 지반유형으로 돌린다 — 유토곡선 폴백과 같은 규칙. + if (design.cut_soil_area_m2 === undefined || design.cut_rock_area_m2 === undefined) { + const ground = design.ground_type; + return { + ea: ground === "soil" ? total : 0, + rr: ground === "ripping_rock" ? total : 0, + br: ground === "blasting_rock" ? total : 0, + total, + }; + } + const rock = design.cut_rock_area_m2; + return { + ea: design.cut_soil_area_m2, + rr: design.cut_rock_kind === "ripping_rock" ? rock : 0, + br: design.cut_rock_kind === "blasting_rock" ? rock : 0, + total, + }; +} + +/** 표 한 칸. `key`가 없으면 강조 대상이 아닌 칸(성토의 EA/RR/BR 자리)이다. */ +interface AreaCell { + key: CrossAreaKey | null; + value: number | null; + variant: string; +} + +function appendCell( + row: HTMLTableRowElement, + cell: AreaCell, + onSelect: ((key: CrossAreaKey) => void) | undefined, + cells: Map, +): void { + const td = document.createElement("td"); + td.className = `b06-design__area b06-design__area--${cell.variant}`; + const text = cell.value === null ? "—" : cell.value.toFixed(2); + // 값이 0이어도 칸은 남긴다 — 측점마다 항목 수가 달라지면 카드끼리 비교가 안 된다. + if (!cell.key || !onSelect || cell.value === null) { + td.textContent = text; + row.append(td); + return; + } + const button = document.createElement("button"); + button.type = "button"; + button.textContent = text; + button.title = L("B06_Design_Area_Highlight"); + button.setAttribute("aria-pressed", "false"); + button.addEventListener("click", (event) => { + // 카드까지 올라가면 측점 선택이 다시 걸려 카드가 통째로 다시 그려진다 — 강조가 즉시 사라진다. + event.stopPropagation(); + onSelect(cell.key as CrossAreaKey); + }); + cells.set(cell.key, [...(cells.get(cell.key) ?? []), button]); + td.append(button); + row.append(td); +} + /** - * 절·성토 면적값 오버레이(E-4) — 그래프 중상단에 표시한다. - * `onSelect`가 오면 칩이 강조 토글 버튼이 된다(선택된 카드에서만 넘어온다). + * 절·성토 면적값 오버레이(E-4) — 그래프 중상단에 표(절토/성토 × EA·RR·BR·계)로 표시한다. + * 항목 이름은 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암). + * `onSelect`가 오면 값 칸이 강조 토글 버튼이 된다(선택된 카드에서만 넘어온다). */ export function buildAreaReadout( design: CrossDesign | undefined, onSelect?: (key: CrossAreaKey) => void, ): { root: HTMLElement; setActive: AreaHighlightSetter } { - const readout = document.createElement("div"); - readout.className = "b06-cross-card__areas"; if (!design) { - const unset = document.createElement("span"); - unset.className = "b06-design__area b06-design__area--unset"; + const unset = document.createElement("div"); + unset.className = "b06-cross-card__areas b06-design__area--unset"; unset.textContent = L("B06_Design_Unset"); - readout.append(unset); - return { root: readout, setActive: () => undefined }; + return { root: unset, setActive: () => undefined }; } - // 토사/암반 칩은 암 측점에서만 의미가 있다(토사 측점은 절토계와 같은 값이라 중복 표기). - const entries: Array<{ key: CrossAreaKey; label: string; value: number }> = []; - if (design.cut_rock_kind) { - entries.push( - { - key: "cut_soil", - label: L("B06_Design_Cut_Soil_Area"), - value: design.cut_soil_area_m2 ?? 0, - }, - { - key: "cut_rock", - label: L("B06_Design_Cut_Rock_Area"), - value: design.cut_rock_area_m2 ?? 0, - }, - ); + const table = document.createElement("table"); + table.className = "b06-cross-card__areas"; + const head = document.createElement("tr"); + for (const [text, tip] of [ + ["", ""], + [L("B06_Design_Area_EA"), L("B06_Design_Ground_Soil")], + [L("B06_Design_Area_RR"), L("B06_Design_Ground_Ripping")], + [L("B06_Design_Area_BR"), L("B06_Design_Ground_Blasting")], + [L("B06_Design_Area_Total"), ""], + ]) { + const th = document.createElement("th"); + th.textContent = text; + if (tip) th.title = tip; + head.append(th); } - entries.push( - { key: "cut_total", label: L("B06_Design_Cut_Area"), value: design.cut_area_m2 }, - { key: "fill", label: L("B06_Design_Fill_Area"), value: design.fill_area_m2 }, - ); + const thead = document.createElement("thead"); + thead.append(head); - const chips = new Map(); - for (const entry of entries) { - const text = `${entry.label} ${entry.value.toFixed(2)}㎡`; - if (!onSelect) { - const chip = document.createElement("span"); - chip.className = `b06-design__area b06-design__area--${entry.key}`; - chip.textContent = text; - readout.append(chip); - continue; - } - const chip = document.createElement("button"); - chip.type = "button"; - chip.className = `b06-design__area b06-design__area--${entry.key}`; - chip.textContent = text; - chip.title = L("B06_Design_Area_Highlight"); - chip.setAttribute("aria-pressed", "false"); - chip.addEventListener("click", () => onSelect(entry.key)); - chips.set(entry.key, chip); - readout.append(chip); + const cells = new Map(); + const cut = cutBreakdown(design); + const rockKind = + design.cut_rock_kind ?? (design.ground_type === "soil" ? null : design.ground_type); + const tbody = document.createElement("tbody"); + const rows: Array<{ label: string; variant: string; cells: AreaCell[] }> = [ + { + label: L("B06_Design_Cut_Area"), + variant: "cut_total", + cells: [ + { key: "cut_soil", value: cut.ea, variant: "cut_soil" }, + // 암반 밴드는 하나뿐이다 — 그 측점의 암종에 해당하는 칸만 강조 버튼이 된다. + { key: rockKind === "ripping_rock" ? "cut_rock" : null, value: cut.rr, variant: "cut_rr" }, + { key: rockKind === "blasting_rock" ? "cut_rock" : null, value: cut.br, variant: "cut_br" }, + { key: "cut_total", value: cut.total, variant: "cut_total" }, + ], + }, + { + // 성토는 완성 단면(다짐 상태)이라 지반유형으로 갈리지 않는다 — 계 한 칸만 쓴다. + label: L("B06_Design_Fill_Area"), + variant: "fill", + cells: [ + { key: null, value: null, variant: "fill" }, + { key: null, value: null, variant: "fill" }, + { key: null, value: null, variant: "fill" }, + { key: "fill", value: design.fill_area_m2, variant: "fill" }, + ], + }, + ]; + for (const row of rows) { + const tr = document.createElement("tr"); + const th = document.createElement("th"); + th.className = `b06-design__area b06-design__area--${row.variant}`; + th.textContent = row.label; + tr.append(th); + for (const cell of row.cells) appendCell(tr, cell, onSelect, cells); + tbody.append(tr); } + table.append(thead, tbody); return { - root: readout, + root: table, setActive: (key) => { - for (const [chipKey, chip] of chips) { - const active = chipKey === key; - chip.classList.toggle("is-active", active); - chip.setAttribute("aria-pressed", String(active)); + for (const [cellKey, elements] of cells) { + const active = cellKey === key; + for (const element of elements) { + element.classList.toggle("is-active", active); + element.setAttribute("aria-pressed", String(active)); + } } }, }; diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts index 598a4d7c..e64fd2c7 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Cross_View.ts @@ -38,7 +38,8 @@ import { // 절·성토 값 오버레이(그래프 상단 고정)가 계획고 선을 가리는 드문 경우를 대비해, Y 플롯 // 최대값에 더하는 상단 여유(px). 오버레이·라벨은 손대지 않고 데이터를 그만큼 아래로 내린다. -const AREA_OVERLAY_HEADROOM_PX = 28; +// 오버레이가 칩 한 줄에서 표(머리글 + 절토 + 성토 3행)로 바뀌어 그만큼 키웠다. +const AREA_OVERLAY_HEADROOM_PX = 58; interface CrossPlotMetrics { sourceSamples: SectionSample[]; diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_MassHaul_View.ts b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_MassHaul_View.ts index 54e554a9..c5632aa8 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_MassHaul_View.ts +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_MassHaul_View.ts @@ -331,7 +331,8 @@ export function createMassHaulChart( svg.append(marker); } - // 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그린다. + // 0선 — 절토 우세와 성토 우세를 가르는 기준선이라 격자보다 진하게 그리고 눈금값도 따로 적는다. + // Y 범위 계산(`volumeRange`)이 0을 항상 품으므로 이 선은 어떤 노선에서도 화면에 남는다. svg.append( svgElement("line", { x1: LONG_PAD.left, @@ -340,6 +341,12 @@ export function createMassHaulChart( y2: zeroY, class: "b06-masshaul__zero", }), + svgText("0", { + x: LONG_PAD.left - 9, + y: zeroY + 4, + "text-anchor": "end", + class: "b06-chart__tick b06-masshaul__zero-tick", + }), ); // 뒤에 그린 곡선이 위로 오므로 범례 순서의 역순으로 그려 첫 곡선(정식)을 가장 위에 둔다. diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css index a23477d3..876b6f96 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_Cross.css @@ -521,37 +521,57 @@ padding: 1px 4px; } -/* 절·성토 면적 오버레이(E-4) — 그래프 중상단, 배경색으로 그래프 선과 겹쳐도 가독. */ +/* 절·성토 면적 오버레이(E-4) — 그래프 중상단, 배경색으로 그래프 선과 겹쳐도 가독. + 절토/성토 × EA·RR·BR·계 표라서 칩을 늘어놓을 때보다 가로 폭을 덜 먹는다. */ .b06-cross-card__areas { position: absolute; top: var(--spacing-8); left: 50%; transform: translateX(-50%); - display: inline-flex; - gap: var(--spacing-8); - padding: 2px 8px; + padding: 2px 6px; border-radius: var(--radius-inputs); - background: color-mix(in srgb, var(--color-surface) 82%, transparent); + background: color-mix(in srgb, var(--color-surface) 88%, transparent); font-size: 0.72rem; font-family: var(--font-mono); white-space: nowrap; + border-collapse: collapse; pointer-events: none; } +.b06-cross-card__areas th, +.b06-cross-card__areas td { + padding: 0 5px; + text-align: right; + font-weight: var(--font-weight-regular); +} + +.b06-cross-card__areas thead th { + color: var(--color-text-muted); +} + +/* 행 이름(절토/성토)은 왼쪽 정렬 — 숫자 열과 구분된다. */ +.b06-cross-card__areas tbody th { + text-align: left; +} + .b06-design__area--cut, .b06-design__area--cut_total { color: rgb(220 38 38); } -/* 절토 내역 — 토사(지표면~암반 경계선)와 암반(경계선 아래)을 색으로 갈라 둔다. */ +/* 절토 내역 — 토사(EA), 리핑암(RR), 발파암(BR)을 색으로 갈라 둔다. */ .b06-design__area--cut_soil { color: rgb(217 119 6); } -.b06-design__area--cut_rock { +.b06-design__area--cut_rr { color: rgb(71 85 105); } +.b06-design__area--cut_br { + color: rgb(15 23 42); +} + .b06-design__area--fill { color: rgb(37 99 235); } @@ -560,20 +580,21 @@ color: var(--color-text-muted); } -/* 선택된 카드에서만 칩이 버튼이 된다 — 부모가 pointer-events: none이라 여기서 되살린다. */ -button.b06-design__area { +/* 선택된 카드에서만 값 칸이 버튼이 된다 — 부모가 pointer-events: none이라 여기서 되살린다. */ +.b06-cross-card__areas button { padding: 0 2px; border: none; border-radius: var(--radius-inputs); background: none; + color: inherit; font-family: inherit; font-size: inherit; cursor: pointer; pointer-events: auto; } -/* 켠 칩은 글자색을 유지한 채 같은 색으로 옅게 칠하고 테두리를 둘러 대비를 살린다. */ -button.b06-design__area.is-active { +/* 켠 칸은 글자색을 유지한 채 같은 색으로 옅게 칠하고 테두리를 둘러 대비를 살린다. */ +.b06-cross-card__areas button.is-active { background: color-mix(in srgb, currentcolor 18%, transparent); box-shadow: inset 0 0 0 1px currentcolor; font-weight: var(--font-weight-medium); diff --git a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_MassHaul.css b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_MassHaul.css index 38ea1e3e..6a8baa39 100644 --- a/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_MassHaul.css +++ b/B06_wf3_ProfileCross/B06_wf3_ProfileCross_UI_Style_MassHaul.css @@ -86,13 +86,18 @@ fill: var(--color-success); } -/* 0선: 절토 우세와 성토 우세의 경계라 격자보다 진하게. */ +/* 0선: 절토 우세와 성토 우세의 경계라 격자보다 진하게. 어떤 노선에서도 반드시 보인다. */ .b06-masshaul__zero { stroke: var(--color-text-secondary); stroke-width: 1.2; stroke-dasharray: 4 3; } +.b06-masshaul__zero-tick { + fill: var(--color-text-secondary); + font-weight: var(--font-weight-medium); +} + .b06-masshaul__summary { display: flex; flex-wrap: wrap; diff --git a/ui_template/ui_template_locale_b2.ts b/ui_template/ui_template_locale_b2.ts index 8b6650cb..191eba00 100644 --- a/ui_template/ui_template_locale_b2.ts +++ b/ui_template/ui_template_locale_b2.ts @@ -255,6 +255,11 @@ export const ui_locales_b2 = { /* 절토 내역 — 지표면~암반 경계선이 토사, 그 아래가 암반(합=절토). */ B06_Design_Cut_Soil_Area: ["절토(토사)", "Cut (soil)"], B06_Design_Cut_Rock_Area: ["절토(암반)", "Cut (rock)"], + /* 횡단 면적표 열 이름은 유토곡선 도면 표기를 그대로 쓴다(EA 토사 / RR 리핑암 / BR 발파암). */ + B06_Design_Area_EA: ["EA", "EA"], + B06_Design_Area_RR: ["RR", "RR"], + B06_Design_Area_BR: ["BR", "BR"], + B06_Design_Area_Total: ["계 (㎡)", "Total (㎡)"], B06_Design_Area_Highlight: ["누르면 해당 면적을 강조합니다", "Click to highlight this area"], B06_Design_Fill_Area: ["성토", "Fill"], B06_Design_Unset: ["미지정", "Not set"],