/* ============================================================================= * B08_Quantity_UI_Earthwork.ts * 3탭 토적표(PLAN 61-3) — 줄 = 측점 · 열 = 옛 토적 엔진 출력 칸 그대로 · 맨 아래 합계 줄. * * GET …/quantity/earthwork (서버 단독 · 읽기만) — 평균단면적법 · 보정량 = 입적 × 다짐 환산계수 * 절토 암은 그 측점이 센 갈래(`cut_rock_kind`)대로 리핑암 · 발파암 열에 놓음. * 표 모양은 1탭 집계표 것(`b08-summary__*`)을 다시 씀. * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import { stationLabel } from "../common_util/common_util_station"; import { stationInterval } from "./B08_Quantity_UI_Summary"; 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; cut_rock_kind: string; 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; notes: string[]; } interface EarthworkTable { rows: EarthworkRow[]; totals: Record; conversion_factors: Record; station_count: number; route_id: number; confirmed: boolean; } const ROCKS = ["ripping_rock", "blasting_rock"] as const; const GROUPS: Array<[string, number]> = [ ["절토 토사", 3], ["절토 리핑암", 3], ["절토 발파암", 3], ["측구터파기 토사", 3], ["측구터파기 암", 3], ]; const TRIPLE = ["단면적", "입적", "보정량"]; const num = (value: number | undefined | null): string => value == null || !Number.isFinite(value) ? "" : value.toFixed(2); function cell(tag: "th" | "td", text: string, span?: { row?: number; col?: number }) { const node = document.createElement(tag); node.textContent = text; if (span?.row) node.rowSpan = span.row; if (span?.col) node.colSpan = span.col; return node; } function note(text: string): HTMLElement { const p = document.createElement("p"); p.className = "ui-general-block__empty"; p.textContent = text; return p; } function head(): HTMLTableSectionElement { const top = document.createElement("tr"); top.append(cell("th", "측점", { row: 2 }), cell("th", "거리", { row: 2 })); for (const [label, span] of GROUPS) top.append(cell("th", label, { col: span })); top.append( cell("th", "보정량계", { row: 2 }), cell("th", "성토", { col: 2 }), cell("th", "유용토", { row: 2 }), cell("th", "차인토량", { row: 2 }), cell("th", "누가토량", { row: 2 }), ); top.firstElementChild?.classList.add("b08-summary__station"); const sub = document.createElement("tr"); for (const _ of GROUPS) sub.append(...TRIPLE.map((t) => cell("th", t))); sub.append(cell("th", "단면적"), cell("th", "입적")); const thead = document.createElement("thead"); thead.append(top, sub); return thead; } /** 절토 암 세 칸 — 그 측점 갈래 열에만 · 다른 갈래 열은 빈 칸 */ function rockCells(row: EarthworkRow, kind: string): string[] { return row.cut_rock_kind === kind ? [num(row.cut_rock_area_m2), num(row.cut_rock_volume_m3), num(row.cut_rock_adjusted_m3)] : ["", "", ""]; } function bodyRow(row: EarthworkRow, interval: number): HTMLTableRowElement { const tr = document.createElement("tr"); tr.dataset.b08Earthwork = String(row.chainage_m); const station = cell( "th", stationLabel(row.chainage_m, interval) + (row.notes.length ? " *" : ""), ); station.className = "b08-summary__station"; if (row.notes.length) station.title = row.notes.join("\n"); const values = [ num(row.distance_m), num(row.cut_soil_area_m2), num(row.cut_soil_volume_m3), num(row.cut_soil_adjusted_m3), ...ROCKS.flatMap((kind) => rockCells(row, kind)), num(row.ditch_soil_area_m2), num(row.ditch_soil_volume_m3), num(row.ditch_soil_adjusted_m3), num(row.ditch_rock_area_m2), num(row.ditch_rock_volume_m3), num(row.ditch_rock_adjusted_m3), num(row.adjusted_total_m3), num(row.fill_area_m2), num(row.fill_volume_m3), num(row.diverted_m3), num(row.balance_m3), num(row.cumulative_m3), ]; tr.append(station, ...values.map((v) => cell("td", v))); return tr; } /** 합계 줄 — 단면적은 합이 뜻이 없어 비움(옛 엔진 `totals` 와 같음) · 누가토량 = 마지막 줄 */ function totalRow(table: EarthworkTable): HTMLTableRowElement { const t = table.totals; const last = table.rows[table.rows.length - 1]; const values = [ num(t.distance_m), "", num(t.cut_soil_volume_m3), num(t.cut_soil_adjusted_m3), ...ROCKS.flatMap((kind) => [ "", num(t[`cut_${kind}_volume_m3`]), num(t[`cut_${kind}_adjusted_m3`]), ]), "", num(t.ditch_soil_volume_m3), num(t.ditch_soil_adjusted_m3), "", num(t.ditch_rock_volume_m3), num(t.ditch_rock_adjusted_m3), num(t.adjusted_total_m3), "", num(t.fill_volume_m3), num(t.diverted_m3), num(t.balance_m3), num(last?.cumulative_m3), ]; const tr = document.createElement("tr"); tr.className = "b08-earthwork__total"; tr.dataset.b08EarthworkTotal = ""; const label = cell("th", "합계"); label.className = "b08-summary__station"; tr.append(label, ...values.map((v) => cell("td", v))); return tr; } export async function renderEarthwork(host: HTMLElement, projectId: string): Promise { host.replaceChildren(note("토적표를 만드는 중…")); let table: EarthworkTable; let interval = 20; try { const [response, gap] = await Promise.all([ fetch(`${API_BASE_URL}/projects/${projectId}/quantity/earthwork`, { credentials: "include" }), stationInterval(projectId).catch(() => 20), ]); const body = (await response.json().catch(() => null)) as (EarthworkTable & { detail?: unknown }) | null; if (!response.ok || !body) { const detail = body?.detail; throw new Error(typeof detail === "string" ? detail : `HTTP ${response.status}`); } table = body; interval = gap; } catch (error) { host.replaceChildren(note(`토적표를 불러오지 못했습니다 — ${String(error)}`)); return; } if (!table.rows.length) { host.replaceChildren(note("측점 단면적이 아직 없습니다 — 횡단 설계를 먼저 마치세요.")); return; } const c = (kind: string): string => num(table.conversion_factors[kind]?.compacted); const info = note( `평균단면적법 · 노선 ${table.route_id}${table.confirmed ? "(확정)" : "(확정 전 최신 노선)"} · ` + `측점 ${table.station_count} · 다짐 환산계수 토사 ${c("soil")} · 리핑암 ${c("ripping_rock")} · ` + `발파암 ${c("blasting_rock")} · 단위 ㎡ · ㎥ · * = 사유 있음(마우스를 올림)`, ); const tbody = document.createElement("tbody"); tbody.append(...table.rows.map((row) => bodyRow(row, interval))); const tfoot = document.createElement("tfoot"); tfoot.append(totalRow(table)); const el = document.createElement("table"); el.className = "ui-general-block__table b08-summary__table b08-earthwork__table"; el.dataset.b08Earthwork = "table"; el.append(head(), tbody, tfoot); const wrap = document.createElement("div"); wrap.className = "ui-general-block__table-wrap b08-summary__wrap"; wrap.append(el); host.replaceChildren(info, wrap); }