일감 4·5 화면 마무리. 앞 `auto:` 커밋(4756f0fe)은 토적표 그리드·로케일 배선분임.
우측 — 실무 산출서 시트를 탭으로
토적표 · 토공집계 · 운반거리. 탭을 눌러 갈아 끼움.
토공집계 열은 거창 실무 시트 그대로(구분·공종·규격·단위·계·비고).
운반계획이 아직 없으면 빈 표 대신 「종단설계에서 확정하면 생김」을 보임.
좌측 — 산출 조건 입력
⚠ 암 갈래 칸 수를 코드에 안 박음 — 프로젝트 세트가 정함(지금 거창5 = 5칸).
반영률 4칸, 기본 100 %. 실무 관측 80/50/80 은 기본값 아님(PLAN 8-11 · 법대로).
반영률 이름을 사람 말로 보임 — 서버 키(fill_slope_compaction)를 그대로 띄우면
설계자가 못 읽음. 로케일 키 4개 추가.
[저장] 신설 (CLAUDE.md 5장 데이터 3층)
조작은 캐시에만 쌓이고 [저장]에서만 정본(project_settings.json)으로 감.
자동저장 안 만듦. quantity 구획만 감 — estimation 은 B09 것이고 서버가 막고 있음.
저장 뒤 표를 다시 받아 그림 — 조건이 바뀌면 집계·운반 값이 달라짐.
저장 안 한 변경이 있으면 나갈 때 알림(beforeunload).
⚠ [초기화]는 안 달았음 — 5장의 [초기화]는 초기값을 작업본에 덮어쓰는 것인데
설정에는 대응 초기값이 없어 재계산 단추로 오해될 자리임. TODO(미결) 주석으로 남김.
검증 — 공용 브라우저 실화면, 타입검사 0.
탭 3장 · 입력칸 9개(암 5 + 반영률 4) · 단추 [저장][확정].
토공집계 10행, 값 일치 — 흙깎기 토사 2,526.99 · 측구 90.51 · 보정량계 6,511.06.
[저장] 실제로 눌러 확인 — 연암 60·보통암 40 을 넣으니 암 3,512.09 가
2,107.25 / 1,404.83 으로 갈리고(60:40 안분), 성토면다짐 80 % 를 넣으니
13,518.6 → 10,814.9 로 바뀌며 비고에 「성토면 80 % 반영」이 남음.
⚠ 넣었던 값은 전부 원래대로(0·0·100) 되돌려 놓았고 화면도 사용자가 보던 주소로 복귀.
⚠ 개발 중 발견 — vite 가 옛 모듈을 물고 있어 첫 확인이 옛 화면이었음.
쿼리를 붙여 새로 물린 뒤 재확인(계획서 7-2 의 그 함정).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
191 lines
6.1 KiB
TypeScript
191 lines
6.1 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;
|
|
}
|