/* ============================================================================= * B07_wf4_DesignDetail_UI_QuantityTable.ts * 횡단도 수량 산출표 (편집 가능). 첨부 양식의 병합셀 구조를 12열 표로 재현한다. * * 값은 백엔드 `_quantity_table`가 내려준 초기값으로 채우되, 사용자가 각 칸을 * 직접 수정할 수 있다. 절토고/성토고는 지반고·계획고에서 파생되어 읽기 전용이며 * 입력 변경 시 즉시 재계산된다. * ========================================================================== */ import type { QuantityTable } from "./B07_wf4_DesignDetail_Api_Fetch"; /** 편집 입력이 있는 항목 키 (cut/fill 제외 — 파생 읽기전용). */ const EDITABLE_KEYS = [ "ground", "planned", "cut_soil", "cut_soft_rock", "cut_rock", "tree_removal", "fill_slope_protection", "cut_slope_protection", "ditch_soil", "ditch_soft_rock", "ditch_rock", "embankment", "grubbing", "surface_grading", ] as const; export interface QuantityTableController { element: HTMLElement; /** 새 도면 선택 시 값·측점명을 갱신한다. */ update(stationTitle: string, values: QuantityTable | null | undefined): void; /** 현재 입력값(파생 cut/fill 포함)을 수집한다. */ getValues(): QuantityTable; } function round2(value: number): number { return Math.round(value * 100) / 100; } function formatValue(value: number | null | undefined): string { return typeof value === "number" && Number.isFinite(value) ? String(round2(value)) : ""; } function parseValue(raw: string): number | null { const text = raw.trim(); if (!text) return null; const parsed = Number(text); return Number.isFinite(parsed) ? parsed : null; } /** 편집 가능한 값 입력 칸을 만든다. */ function valueCell( key: string, colSpan: number, inputs: Map, options: { readonly?: boolean } = {}, ): HTMLTableCellElement { const cell = document.createElement("td"); cell.colSpan = colSpan; cell.className = "b07-qtable__value"; const input = document.createElement("input"); input.type = "text"; input.inputMode = "decimal"; input.autocomplete = "off"; input.dataset.key = key; if (options.readonly) { input.readOnly = true; cell.classList.add("b07-qtable__value--derived"); } inputs.set(key, input); cell.append(input); return cell; } /** 라벨(헤더) 셀을 만든다. */ function labelCell( text: string, colSpan: number, options: { rowSpan?: number; vertical?: boolean } = {}, ): HTMLTableCellElement { const cell = document.createElement("th"); cell.scope = "row"; cell.colSpan = colSpan; if (options.rowSpan) cell.rowSpan = options.rowSpan; cell.className = "b07-qtable__label"; if (options.vertical) cell.classList.add("b07-qtable__label--vertical"); cell.textContent = text; return cell; } /** * 첨부 양식과 동일한 편집 가능 수량 산출표를 생성한다. * @param onEdit 사용자가 값을 바꿀 때마다 호출 (확정 상태 롤백 연동용) */ export function buildQuantityTable(onEdit: () => void): QuantityTableController { const inputs = new Map(); const container = document.createElement("section"); container.className = "b07-qtable"; container.hidden = true; const table = document.createElement("table"); const body = document.createElement("tbody"); // 1행: 측 점 | (측점명) const titleRow = document.createElement("tr"); titleRow.append(labelCell("측 점", 2)); const titleCell = document.createElement("td"); titleCell.colSpan = 10; titleCell.className = "b07-qtable__station"; titleRow.append(titleCell); body.append(titleRow); // 2행: 지반고 | 계획고 | 절토고 | 성토고 const baseRow = document.createElement("tr"); baseRow.append(labelCell("지반고", 2), valueCell("ground", 1, inputs)); baseRow.append(labelCell("계획고", 2), valueCell("planned", 1, inputs)); baseRow.append(labelCell("절토고", 2), valueCell("cut", 1, inputs, { readonly: true })); baseRow.append(labelCell("성토고", 2), valueCell("fill", 1, inputs, { readonly: true })); body.append(baseRow); // 3행: 흙깎기 토사 | 지장목제거 | 옆도랑파기 토사 const row3 = document.createElement("tr"); row3.append(labelCell("흙깎기", 1, { rowSpan: 3, vertical: true })); row3.append(labelCell("토사", 1), valueCell("cut_soil", 2, inputs)); row3.append(labelCell("지장목제거", 2), valueCell("tree_removal", 2, inputs)); row3.append(labelCell("옆도랑파기", 1, { rowSpan: 3, vertical: true })); row3.append(labelCell("토사", 1), valueCell("ditch_soil", 2, inputs)); body.append(row3); // 4행: 연암 | 비탈보호공 성토면 | 연암 const row4 = document.createElement("tr"); row4.append(labelCell("연암", 1), valueCell("cut_soft_rock", 2, inputs)); row4.append(labelCell("비탈보호공", 1, { rowSpan: 2, vertical: true })); row4.append(labelCell("성토면", 1), valueCell("fill_slope_protection", 2, inputs)); row4.append(labelCell("연암", 1), valueCell("ditch_soft_rock", 2, inputs)); body.append(row4); // 5행: 보통암 | 절토면 | 보통암 const row5 = document.createElement("tr"); row5.append(labelCell("보통암", 1), valueCell("cut_rock", 2, inputs)); row5.append(labelCell("절토면", 1), valueCell("cut_slope_protection", 2, inputs)); row5.append(labelCell("보통암", 1), valueCell("ditch_rock", 2, inputs)); body.append(row5); // 6행: 흙쌓기 | 제근 | 노면고르기 const row6 = document.createElement("tr"); row6.append(labelCell("흙쌓기", 2), valueCell("embankment", 2, inputs)); row6.append(labelCell("제근", 2), valueCell("grubbing", 2, inputs)); row6.append(labelCell("노면고르기", 2), valueCell("surface_grading", 2, inputs)); body.append(row6); table.append(body); container.append(table); const groundInput = inputs.get("ground"); const plannedInput = inputs.get("planned"); const cutInput = inputs.get("cut"); const fillInput = inputs.get("fill"); const recomputeCutFill = () => { const ground = parseValue(groundInput?.value ?? ""); const planned = parseValue(plannedInput?.value ?? ""); if (ground !== null && planned !== null) { if (cutInput) cutInput.value = formatValue(Math.max(ground - planned, 0)); if (fillInput) fillInput.value = formatValue(Math.max(planned - ground, 0)); } else { if (cutInput) cutInput.value = ""; if (fillInput) fillInput.value = ""; } }; for (const key of EDITABLE_KEYS) { const input = inputs.get(key); input?.addEventListener("input", () => { if (key === "ground" || key === "planned") recomputeCutFill(); onEdit(); }); } const update: QuantityTableController["update"] = (stationTitle, values) => { titleCell.textContent = stationTitle; for (const [key, input] of inputs) { input.value = formatValue(values?.[key]); } recomputeCutFill(); container.hidden = false; }; const getValues: QuantityTableController["getValues"] = () => { const result: QuantityTable = {}; for (const [key, input] of inputs) { result[key] = parseValue(input.value); } return result; }; return { element: container, update, getValues }; }