202 lines
7.2 KiB
TypeScript
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 };
|
|
}
|