From fe92941d7bdc3bd6059ad7085e47347c24f3d3a3 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 20:12:02 +0900 Subject: [PATCH] auto: 2026-09-07 20:12 (EOMSANGDON-HOME) --- B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts diff --git a/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts new file mode 100644 index 00000000..36449e77 --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_EarthworkGrid.ts @@ -0,0 +1,234 @@ +/* ============================================================================= + * B08_Quantity_UI_EarthworkGrid.ts + * 토적표 그리드 — 실무 토적표(3단 머리글)를 그대로 그린다 (PLAN 8-4b). + * + * 왜 실무 서식 그대로인가 + * 이 화면의 첫 사용자는 「프로그램이 맞나」를 확인하려는 설계자다. 보기 좋게 재배치하면 + * 실무 산출서와 눈으로 대조를 못 한다. 열 순서·머리글 문구를 실무 시트에 맞춘다. + * + * ⚠ 소수 자리는 표기 규칙일 뿐이다 (PLAN 8-16) + * 서버는 전정밀 값을 준다. 자르는 것은 여기(화면)뿐이다. 실무 시트 관측 그대로 + * 단면적·체적 2자리 · 보정량계·유용토·차인·누가 1자리 · 거리 정수로 보인다. + * 원가 쪽(줄마다 원 단위 절사)과 규칙이 반대이므로 그 코드를 여기로 옮기지 말 것. + * ========================================================================== */ + +/** 서버가 주는 토적표 한 줄. 이름은 엔진(`B08_Quantity_Engine_EarthworkTable.py`)과 같다. */ +export interface EarthworkRow { + chainage_m: number; + distance_m: number; + cut_soil_area_m2: number; + cut_soil_volume_m3: number; + cut_soil_adjusted_m3: number; + cut_rock_area_m2: number; + cut_rock_volume_m3: number; + cut_rock_adjusted_m3: number; + ditch_soil_area_m2: number; + ditch_soil_volume_m3: number; + ditch_soil_adjusted_m3: number; + ditch_rock_area_m2: number; + ditch_rock_volume_m3: number; + ditch_rock_adjusted_m3: number; + adjusted_total_m3: number; + fill_area_m2: number; + fill_volume_m3: number; + diverted_m3: number; + balance_m3: number; + cumulative_m3: number; +} + +export interface EarthworkTable { + method: string; + station_count: number; + route_id?: number; + rows: EarthworkRow[]; + totals: Record; +} + +/** 열 하나. `digits` 는 **표기 자리**이며 값 자체는 자르지 않는다. */ +interface Column { + key: keyof EarthworkRow; + digits: number; + /** 합계행에 낼지 — 단면적은 합이 뜻이 없어 비운다(실무 시트도 비어 있다). */ + sum?: boolean; +} + +/** 실무 토적표 3단 머리글. 대분류 → 중분류 → 소분류 순서가 곧 열 순서다. */ +const GROUPS: { label: string; sub: { label: string; cols: Column[] }[] }[] = [ + { label: "", sub: [{ label: "측 점", cols: [{ key: "chainage_m", digits: 0 }] }] }, + { label: "", sub: [{ label: "거 리", cols: [{ key: "distance_m", digits: 0, sum: true }] }] }, + { + label: "절 토", + sub: [ + { + label: "토 사", + cols: [ + { key: "cut_soil_area_m2", digits: 2 }, + { key: "cut_soil_volume_m3", digits: 2, sum: true }, + { key: "cut_soil_adjusted_m3", digits: 2, sum: true }, + ], + }, + { + label: "암 석", + cols: [ + { key: "cut_rock_area_m2", digits: 2 }, + { key: "cut_rock_volume_m3", digits: 2, sum: true }, + { key: "cut_rock_adjusted_m3", digits: 2, sum: true }, + ], + }, + ], + }, + { + label: "측 구 터 파 기", + sub: [ + { + label: "토 사", + cols: [ + { key: "ditch_soil_area_m2", digits: 2 }, + { key: "ditch_soil_volume_m3", digits: 2, sum: true }, + { key: "ditch_soil_adjusted_m3", digits: 2, sum: true }, + ], + }, + { + label: "암 석", + cols: [ + { key: "ditch_rock_area_m2", digits: 2 }, + { key: "ditch_rock_volume_m3", digits: 2, sum: true }, + { key: "ditch_rock_adjusted_m3", digits: 2, sum: true }, + ], + }, + ], + }, + { + label: "", + sub: [{ label: "보정량계", cols: [{ key: "adjusted_total_m3", digits: 1, sum: true }] }], + }, + { + label: "성 토", + sub: [ + { + label: "", + cols: [ + { key: "fill_area_m2", digits: 2 }, + { key: "fill_volume_m3", digits: 2, sum: true }, + ], + }, + ], + }, + { label: "", sub: [{ label: "유 용 토", cols: [{ key: "diverted_m3", digits: 1, sum: true }] }] }, + { label: "", sub: [{ label: "차인토량", cols: [{ key: "balance_m3", digits: 1, sum: true }] }] }, + { label: "", sub: [{ label: "누가토량", cols: [{ key: "cumulative_m3", digits: 1 }] }] }, +]; + +/** 소분류가 「단면적·입적·보정량」 삼중조일 때 붙는 3단 머리글 문구. */ +const TRIPLE_LABELS = ["단면적", "입 적", "보정량"]; +const PAIR_LABELS = ["단면적", "입 적"]; + +const flatColumns = (): Column[] => + GROUPS.flatMap((g) => g.sub.flatMap((s) => s.cols)); + +/** 측점 표기 — `20` → `NO.1`, `25` → `NO.1+5`. 실무 토적표가 이 모양이다. */ +function stationLabel(chainage: number, interval = 20): string { + const no = Math.floor(chainage / interval); + const plus = chainage - no * interval; + const rounded = Math.round(plus * 100) / 100; + return rounded === 0 ? `NO.${no}` : `NO.${no}+${rounded}`; +} + +function cell(value: number | undefined, digits: number): string { + if (value === undefined || value === null || Number.isNaN(value)) return ""; + if (value === 0) return ""; + return value.toLocaleString("ko-KR", { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); +} + +function buildHead(): HTMLTableSectionElement { + const head = document.createElement("thead"); + const r1 = document.createElement("tr"); + const r2 = document.createElement("tr"); + const r3 = document.createElement("tr"); + + for (const group of GROUPS) { + const span = group.sub.reduce((n, s) => n + s.cols.length, 0); + if (group.label) { + const th = document.createElement("th"); + th.colSpan = span; + th.textContent = group.label; + r1.append(th); + for (const sub of group.sub) { + const th2 = document.createElement("th"); + th2.colSpan = sub.cols.length; + th2.textContent = sub.label; + r2.append(th2); + const labels = sub.cols.length === 3 ? TRIPLE_LABELS : PAIR_LABELS; + sub.cols.forEach((_, index) => { + const th3 = document.createElement("th"); + th3.textContent = labels[index] ?? ""; + r3.append(th3); + }); + } + continue; + } + // 대분류가 없는 열(측점·거리·보정량계·유용토·…)은 세 줄을 하나로 합친다. + for (const sub of group.sub) { + const th = document.createElement("th"); + th.colSpan = sub.cols.length; + th.rowSpan = 3; + th.textContent = sub.label; + r1.append(th); + } + } + head.append(r1, r2, r3); + return head; +} + +function buildBody(rows: EarthworkRow[]): HTMLTableSectionElement { + const body = document.createElement("tbody"); + const columns = flatColumns(); + for (const row of rows) { + const tr = document.createElement("tr"); + columns.forEach((column, index) => { + const td = document.createElement("td"); + td.textContent = + index === 0 ? stationLabel(row.chainage_m) : cell(row[column.key], column.digits); + if (index === 0) td.className = "b08-grid__station"; + tr.append(td); + }); + body.append(tr); + } + return body; +} + +function buildFoot(totals: Record): HTMLTableSectionElement { + const foot = document.createElement("tfoot"); + const tr = document.createElement("tr"); + flatColumns().forEach((column, index) => { + const td = document.createElement("td"); + if (index === 0) td.textContent = "계"; + else if (column.sum) td.textContent = cell(totals[column.key], column.digits); + tr.append(td); + }); + foot.append(tr); + return foot; +} + +/** 토적표 하나를 그린다. 넓은 표라 스스로 가로 스크롤한다. */ +export function renderEarthworkGrid(table: EarthworkTable): HTMLElement { + const wrap = document.createElement("div"); + wrap.className = "b08-grid"; + + const caption = document.createElement("p"); + caption.className = "b08-grid__caption"; + caption.textContent = `측점 ${table.station_count}곳 · 평균단면적법`; + wrap.append(caption); + + const scroller = document.createElement("div"); + scroller.className = "b08-grid__scroll"; + const element = document.createElement("table"); + element.className = "b08-grid__table"; + element.append(buildHead(), buildBody(table.rows), buildFoot(table.totals)); + scroller.append(element); + wrap.append(scroller); + return wrap; +}