diff --git a/B08_Quantity/B08_Quantity_UI_SummaryGrid.ts b/B08_Quantity/B08_Quantity_UI_SummaryGrid.ts new file mode 100644 index 00000000..439817e6 --- /dev/null +++ b/B08_Quantity/B08_Quantity_UI_SummaryGrid.ts @@ -0,0 +1,176 @@ +/* ============================================================================= + * B08_Quantity_UI_SummaryGrid.ts + * 토공집계표·운반거리 그리드 (PLAN 8-11·8-3). + * + * 토공집계표 열은 거창 실무 시트 그대로 — 구분·공종·규격·단위·계·비고. + * 비고에는 **설계자가 정한 값만** 남는다(반영률을 바꿨을 때·비율 합이 100 이 아닐 때). + * 기본값 그대로면 비워 둔다 — 안내가 매번 뜨면 잡음이 된다. + * + * ⚠ 무대(소운반 20m)는 집계에는 오르되 **내역 줄이 아니다**(품셈 1-2-7). 그 줄에 + * 「내역 제외」를 붙여 화면에서도 보이게 한다 — 규칙이 코드에만 있으면 잊힌다. + * ========================================================================== */ + +import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale"; + +function L(key: keyof typeof ui_locales): string { + return ui_locales[key][currentLanguageIndex]; +} + +export interface SummaryRow { + group: string; + item: string; + spec: string; + unit: string; + amount: number; + note: string; + in_bill: boolean; +} + +export interface SummaryTable { + columns: string[]; + rows: SummaryRow[]; + rock_classes: string[]; +} + +export interface HaulRow { + equipment: string; + ground: string; + volume_m3: number; + average_distance_m: number; + legs: number; + in_bill: boolean; +} + +export interface HaulTable { + rows: HaulRow[]; + legs: { equipment: string; ground: string; volume_m3: number; distance_m: number; from_m: number; to_m: number }[]; + bill_row_count: number; +} + +/** 운반수단 표기 — 서버 키를 실무 시트 문구로. */ +const HAUL_LABELS: Record = { + free_haul: "무대(종방향유용토)", + dozer: "도자운반", + dump_truck: "덤프운반", +}; + +function num(value: number | undefined, digits: number): string { + if (value === undefined || value === null || Number.isNaN(value) || value === 0) return ""; + return value.toLocaleString("ko-KR", { + minimumFractionDigits: digits, + maximumFractionDigits: digits, + }); +} + +function textCell(text: string, className?: string): HTMLTableCellElement { + const td = document.createElement("td"); + td.textContent = text; + if (className) td.className = className; + return td; +} + +/** 토공집계표 — 실무 시트와 같은 여섯 열. */ +export function renderSummaryGrid(table: SummaryTable): HTMLElement { + const wrap = document.createElement("div"); + wrap.className = "b08-grid"; + + const scroller = document.createElement("div"); + scroller.className = "b08-grid__scroll"; + const element = document.createElement("table"); + element.className = "b08-grid__table b08-grid__table--summary"; + + const head = document.createElement("thead"); + const headRow = document.createElement("tr"); + for (const label of table.columns) { + const th = document.createElement("th"); + th.textContent = label; + headRow.append(th); + } + head.append(headRow); + + const body = document.createElement("tbody"); + let lastGroup = ""; + for (const row of table.rows) { + const tr = document.createElement("tr"); + // 같은 구분이 이어지면 한 번만 적는다 — 실무 시트가 그렇게 병합해 둔다. + tr.append(textCell(row.group === lastGroup ? "" : row.group, "b08-grid__station")); + lastGroup = row.group; + tr.append(textCell(row.item)); + tr.append(textCell(row.spec)); + tr.append(textCell(row.unit, "b08-grid__unit")); + tr.append(textCell(num(row.amount, row.unit === "㎥" ? 2 : 1))); + const note = textCell(row.note, "b08-grid__note"); + if (!row.in_bill) { + const tag = document.createElement("span"); + tag.className = "b08-grid__tag"; + tag.textContent = L("B08_Quantity_Haul_Excluded"); + note.prepend(tag); + } + tr.append(note); + body.append(tr); + } + + element.append(head, body); + scroller.append(element); + wrap.append(scroller); + return wrap; +} + +/** 운반거리 — 내역 줄(가중평균)과 근거 줄을 나눠 보인다. */ +export function renderHaulGrid(table: HaulTable, available: boolean): HTMLElement { + const wrap = document.createElement("div"); + wrap.className = "b08-grid"; + + if (!available || !table.rows.length) { + const message = document.createElement("p"); + message.className = "b08-quantity__message"; + message.textContent = L("B08_Quantity_Haul_Missing"); + wrap.append(message); + return wrap; + } + + const caption = document.createElement("p"); + caption.className = "b08-grid__caption"; + caption.textContent = `내역 줄 ${table.bill_row_count}개 · 근거 구간 ${table.legs.length}개 · 토량 가중평균`; + wrap.append(caption); + + const scroller = document.createElement("div"); + scroller.className = "b08-grid__scroll"; + const element = document.createElement("table"); + element.className = "b08-grid__table b08-grid__table--summary"; + + const head = document.createElement("thead"); + const headRow = document.createElement("tr"); + for (const label of ["운반수단", "지반유형", "토량(㎥)", "평균운반거리(m)", "근거 구간", "비고"]) { + const th = document.createElement("th"); + th.textContent = label; + headRow.append(th); + } + head.append(headRow); + + const body = document.createElement("tbody"); + for (const row of table.rows) { + const tr = document.createElement("tr"); + tr.append(textCell(HAUL_LABELS[row.equipment] ?? row.equipment, "b08-grid__station")); + tr.append(textCell(row.ground)); + tr.append(textCell(num(row.volume_m3, 2))); + tr.append(textCell(num(row.average_distance_m, 2))); + tr.append(textCell(String(row.legs))); + const note = textCell("", "b08-grid__note"); + if (!row.in_bill) { + const tag = document.createElement("span"); + tag.className = "b08-grid__tag"; + tag.textContent = L("B08_Quantity_Haul_Excluded"); + note.append(tag); + // 왜 빠지는지 같이 적는다 — 「제외」만 있으면 빠뜨린 것으로 오해된다. + note.append(document.createTextNode(" 품셈 1-2-7 소운반 20m 이내는 품에 포함")); + } + tr.append(note); + body.append(tr); + } + + element.append(head, body); + scroller.append(element); + wrap.append(scroller); + return wrap; +}