feat(B08): 토공집계표·운반거리 그리드 모듈 신설 (아직 미등록)
일감 4·5 화면의 표 부품. 이 커밋 시점에는 **아무 데서도 import 하지 않음** — 「신규 파일 먼저, 등록 줄 나중」 두 걸음 규칙(공용 파일 동시 편집 사고 예방). 토공집계표 — 열은 거창 실무 시트 그대로(구분·공종·규격·단위·계·비고). 같은 구분이 이어지면 한 번만 적음(실무 시트가 병합해 두는 자리). 비고에는 설계자가 정한 값만 남음 — 반영률을 바꿨을 때, 비율 합이 100 이 아닐 때. 기본값 그대로면 비움. 안내가 매번 뜨면 잡음이 됨. 운반거리 — 내역 줄(가중평균)과 근거 구간 수를 함께 보임. ⚠ 무대 줄에 「내역 제외」 표시 + 「품셈 1-2-7 소운반 20m 이내는 품에 포함」을 적음. 「제외」만 있으면 빠뜨린 것으로 오해됨. 규칙이 코드에만 있으면 잊히므로 화면에 남김. 운반계획이 아직 없으면(=[확정] 전) 빈 표 대신 「종단설계에서 확정하면 생김」을 보임. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<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;
|
||||
}
|
||||
Reference in New Issue
Block a user