Files
Aislo/B08_Quantity/B08_Quantity_UI_SummaryGrid.ts
T
eomsangdonandClaude Opus 5 564cb11103 feat(B08): 수량산출 화면 — 탭 3장·산출 조건 입력·[저장]
일감 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>
2026-09-07 23:18:10 +09:00

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;
}