⑪ 준비공·사방공 — 자리를 만들되 없는 값을 지어내지 않음. - ⚠ 벌목은 값을 안 냄. 토공집계의 「지장목제거」로 이미 서 있어 또 세우면 같은 나무를 두 번 벰. 참고 면적만 보이고 「다른 표에서 이미 섬」으로 가리킴. - 못 서는 줄에 사유를 적음 — 표토제거(두께·구간 미정) · 제근(입목 본수 없음) · 규준틀(개소 기준 미정). 공종코드는 미리 적어 둠. - 사방공은 레지스트리의 실제 type_id 로 봄. 없으면 「해당 없음」 — 0 을 적지 않음. 이름을 지어내면 영영 안 걸리므로 레지스트리와 대조하는 시험을 둠. ⑫ 최종 점검 — 탭 6장 전수를 실화면에서 돌려 값으로 서는 것 확인 (토적표 65측점 · 토공집계 10줄 · 운반거리 안내 · 준비공 5줄 · 구조물 원단위 10줄 · 자재총괄 4줄). 전체 566 passed, tsc 오류 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
262 lines
8.5 KiB
TypeScript
262 lines
8.5 KiB
TypeScript
/* =============================================================================
|
||
* 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;
|
||
}
|