Files
Aislo/B08_Quantity/B08_Quantity_UI_SummaryGrid.ts
eomsangdonandClaude Opus 5 e90a5703ee feat(B08): 준비공·사방공 탭 + 페이지 최종 점검
⑪ 준비공·사방공 — 자리를 만들되 없는 값을 지어내지 않음.
- ⚠ 벌목은 값을 안 냄. 토공집계의 「지장목제거」로 이미 서 있어 또 세우면 같은
  나무를 두 번 벰. 참고 면적만 보이고 「다른 표에서 이미 섬」으로 가리킴.
- 못 서는 줄에 사유를 적음 — 표토제거(두께·구간 미정) · 제근(입목 본수 없음) ·
  규준틀(개소 기준 미정). 공종코드는 미리 적어 둠.
- 사방공은 레지스트리의 실제 type_id 로 봄. 없으면 「해당 없음」 — 0 을 적지 않음.
  이름을 지어내면 영영 안 걸리므로 레지스트리와 대조하는 시험을 둠.

⑫ 최종 점검 — 탭 6장 전수를 실화면에서 돌려 값으로 서는 것 확인
(토적표 65측점 · 토공집계 10줄 · 운반거리 안내 · 준비공 5줄 · 구조물 원단위 10줄 ·
자재총괄 4줄). 전체 566 passed, tsc 오류 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 00:50:35 +09:00

262 lines
8.5 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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<string, string> = {
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;
}
export interface PreparationRow {
group: string;
item: string;
unit: string;
amount: number | null;
status: string;
reason: string;
reference_amount?: number;
work_item_code: string | null;
}
export interface PreparationTable {
columns: string[];
rows: PreparationRow[];
pending_count: number;
row_count: number;
}
/** 준비공·사방공 — **못 서는 줄도 보인다.**
*
* 빈 표를 내면 「빠뜨린 것」과 「원래 없는 것」이 구별되지 않는다. 그래서 값이 없는 줄도
* 상태와 사유를 달아 그대로 세운다.
*/
export function renderPreparationGrid(table: PreparationTable): HTMLElement {
const wrap = document.createElement("div");
wrap.className = "b08-grid";
const caption = document.createElement("p");
caption.className = "b08-grid__caption";
caption.textContent = `${table.row_count}줄 · 값을 낼 근거가 아직 없는 줄 ${table.pending_count}개`;
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 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.unit, "b08-grid__unit"));
// 값이 없으면 빈칸이 아니라 「-」 — 빈칸이면 0 으로 오해된다.
tr.append(textCell(row.amount === null ? "" : num(row.amount, 2)));
tr.append(textCell(row.status));
const note = textCell(row.reason.replace(/\*\*/g, ""), "b08-grid__note");
if (row.reference_amount) {
note.append(document.createTextNode(` (참고 면적 ${num(row.reference_amount, 1)})`));
}
tr.append(note);
body.append(tr);
}
element.append(head, body);
scroller.append(element);
wrap.append(scroller);
return wrap;
}