Files
Aislo/B07_wf4_DesignDetail/B07_wf4_DesignDetail_UI_QuantityTable.ts
T
2026-07-19 19:48:46 +09:00

202 lines
7.2 KiB
TypeScript

/* =============================================================================
* 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<string, HTMLInputElement>,
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<string, HTMLInputElement>();
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 };
}