260719_11
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
/* =============================================================================
|
||||
* 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user