From 1789b2656346e63ae9c99a18c96d0d51388b2fb6 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Mon, 7 Sep 2026 23:12:32 +0900 Subject: [PATCH] =?UTF-8?q?feat(B08):=20=ED=86=A0=EA=B3=B5=EC=A7=91?= =?UTF-8?q?=EA=B3=84=ED=91=9C=C2=B7=EC=9A=B4=EB=B0=98=EA=B1=B0=EB=A6=AC=20?= =?UTF-8?q?=EA=B7=B8=EB=A6=AC=EB=93=9C=20=EB=AA=A8=EB=93=88=20=EC=8B=A0?= =?UTF-8?q?=EC=84=A4=20(=EC=95=84=EC=A7=81=20=EB=AF=B8=EB=93=B1=EB=A1=9D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 일감 4·5 화면의 표 부품. 이 커밋 시점에는 **아무 데서도 import 하지 않음** — 「신규 파일 먼저, 등록 줄 나중」 두 걸음 규칙(공용 파일 동시 편집 사고 예방). 토공집계표 — 열은 거창 실무 시트 그대로(구분·공종·규격·단위·계·비고). 같은 구분이 이어지면 한 번만 적음(실무 시트가 병합해 두는 자리). 비고에는 설계자가 정한 값만 남음 — 반영률을 바꿨을 때, 비율 합이 100 이 아닐 때. 기본값 그대로면 비움. 안내가 매번 뜨면 잡음이 됨. 운반거리 — 내역 줄(가중평균)과 근거 구간 수를 함께 보임. ⚠ 무대 줄에 「내역 제외」 표시 + 「품셈 1-2-7 소운반 20m 이내는 품에 포함」을 적음. 「제외」만 있으면 빠뜨린 것으로 오해됨. 규칙이 코드에만 있으면 잊히므로 화면에 남김. 운반계획이 아직 없으면(=[확정] 전) 빈 표 대신 「종단설계에서 확정하면 생김」을 보임. Co-Authored-By: Claude Opus 5 (1M context) --- B08_Quantity/B08_Quantity_UI_SummaryGrid.ts | 176 ++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 B08_Quantity/B08_Quantity_UI_SummaryGrid.ts 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; +}