- 서버: 내역 응답에 집계표 넷(resources)·목록표(lists) — 내역이 쓴 자원을 처음 쓰인 차례로 되모음 (묶음 줄·구조물도 호표 줄도 구성까지 풀어 셈) · 재료비 집계표에 자재대(사급·관급) 줄 이어 붙임 - 중기시간금액집계표: 단가 열 없이 합계·노무·재료·경비 — 성분마다 반올림한 뒤 합(골든셋 131/144, 합계 한 번 반올림 107/144) - 재료·노무·경비 집계표: 반올림(수량 × 단가) 골든셋 279/279 — 시험 한 벌 더함 - 중기사용료 호표: 잡품 줄은 수량 칸 율(%) · 단가 칸 밑수(주연료비) · 금액 — 실무 표 그대로. 제잡비·공구손료 줄도 밑수를 단가 칸에 - 자재단가대비표: 슬롯 1~6 단가·페이지 · 채택 칸 굵게 · 최소단가 표시(서버 min_slot) - 목록표 일곱(일위대가·단가산출근거·중기·재료비·노무비·경비·일식견적): 색인만 — 서버 번호·단가 그대로, 일위대가·산근·중기 줄은 누르면 들어감 - 옛 중기 탭 줄을 새 탭으로 바꿔 끼움(중기경비계산서는 옛 표를 아래에 이어 보임) - 검증: ORCA — 중기 목록 9 · 굴착기 0.7 호표 합계 109,704 · 잡품 22 % × 21,418.1 = 4,711.9 · 노무 집계 벌목부 9,686,529 · 중기 집계 성분 합 · 시험 1638 통과 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
147 lines
5.3 KiB
TypeScript
147 lines
5.3 KiB
TypeScript
/* =============================================================================
|
||
* B09_Estimation_UI_Detail.ts
|
||
* 호표 본표(일위대가표·단가산출근거·시간당 중기) 한 장 + **들어가기 자취** (PLAN 12장 1차)
|
||
*
|
||
* - ⭐ STmate 를 익숙하게 만드는 동작 = 타고 들어가기(브레인 판정). 내역 줄 → 제 N 호표 →
|
||
* 호표 안 줄(일위대가·단산·중기) → … 을 누를 때마다 자취가 쌓이고, 자취 글을 누르면 그 자리로 되돌아감.
|
||
* - 표 모양은 실무 `일위대가표`·`단가산출근거` 시트: 명칭·규격·수량·단위 · 합계/노무/재료/경비(단가·금액) · 비고.
|
||
* - ⚠ 합계 줄은 서버 값(호표 성분 소계 원 미만 절사) — 여기서 안 더함.
|
||
* ========================================================================== */
|
||
|
||
import type { B09TabContext } from "./B09_Estimation_UI_Shell_Types";
|
||
import {
|
||
L,
|
||
el,
|
||
hint,
|
||
linkButton,
|
||
moneyCells,
|
||
numberCell,
|
||
quantity,
|
||
sheetHead,
|
||
sheetTable,
|
||
} from "./B09_Estimation_UI_Sheet";
|
||
import { loadDetail, type DetailDto, type DetailRowDto } from "./B09_Estimation_UI_Store";
|
||
|
||
interface Crumb {
|
||
tab: string;
|
||
code: string;
|
||
label: string;
|
||
}
|
||
|
||
const crumbs: Crumb[] = [];
|
||
let drilling = false;
|
||
|
||
/** 본표 안 줄을 눌러 들어갈 때 — 자취를 이어 쌓음. 목록·내역에서 들어가면 자취가 새로 시작. */
|
||
export function drill(ctx: B09TabContext, tab: string, code: string): void {
|
||
drilling = true;
|
||
ctx.open(tab, code);
|
||
}
|
||
|
||
function visit(tab: string, code: string, label: string): void {
|
||
if (!drilling) crumbs.length = 0;
|
||
drilling = false;
|
||
const at = crumbs.findIndex((crumb) => crumb.tab === tab && crumb.code === code);
|
||
if (at >= 0) crumbs.length = at;
|
||
crumbs.push({ tab, code, label });
|
||
}
|
||
|
||
function trail(ctx: B09TabContext): HTMLElement {
|
||
const box = el("div", "b09s-trail");
|
||
crumbs.forEach((crumb, index) => {
|
||
if (index > 0) box.append(el("span", "b09s-hint", "›"));
|
||
if (index === crumbs.length - 1) {
|
||
box.append(el("span", "b09s-title", crumb.label));
|
||
return;
|
||
}
|
||
box.append(linkButton(crumb.label, () => drill(ctx, crumb.tab, crumb.code)));
|
||
});
|
||
return box;
|
||
}
|
||
|
||
/** 줄이 가리키는 곳 — 단산(D)은 단가산출근거 탭, 시간당 중기(X)는 중기 탭, 일위대가는 일위대가 탭. */
|
||
function targetTab(row: DetailRowDto): string | null {
|
||
if (!row.drillable || !row.ref_code) return null;
|
||
if (row.kind === "price_basis") return "price_basis";
|
||
return row.kind === "machine_hourly" ? "machine" : "unit_price";
|
||
}
|
||
|
||
function detailRow(row: DetailRowDto, ctx: B09TabContext): HTMLElement {
|
||
const tr = el("tr");
|
||
// 비율 줄(잡품·공구손료·제잡비)은 단가 칸에 **밑수**가 옴 — 「21 % × 주연료비 6,365」 꼴(실무 표).
|
||
const unit: [string, string, string, string] = [
|
||
row.unit_total ?? "",
|
||
row.unit_labor ?? "",
|
||
row.unit_material ?? "",
|
||
row.unit_expense ?? "",
|
||
];
|
||
tr.append(
|
||
el("td", "", row.name),
|
||
el("td", "", row.spec),
|
||
numberCell(quantity(row.quantity)),
|
||
el("td", "", row.unit),
|
||
...moneyCells(unit, [row.total, row.labor, row.material, row.expense]),
|
||
);
|
||
const note = el("td", "b09s-note");
|
||
const source = row.source_label || row.source || "";
|
||
if (source) note.append(el("span", "b09s-hint", `[${source}] `));
|
||
if (row.note) note.append(el("span", "b09s-formula", row.note));
|
||
tr.append(note);
|
||
const tab = targetTab(row);
|
||
if (tab && row.ref_code) {
|
||
const code = row.ref_code;
|
||
tr.classList.add("is-clickable");
|
||
tr.title = L("B09_Sheet_Drill");
|
||
tr.addEventListener("click", () => drill(ctx, tab, code));
|
||
}
|
||
return tr;
|
||
}
|
||
|
||
function drawDetail(ctx: B09TabContext, box: HTMLElement, detail: DetailDto, label: string): void {
|
||
const title = el(
|
||
"div",
|
||
"b09s-title",
|
||
`${label} ${detail.name}${detail.spec ? ` · ${detail.spec}` : ""}`,
|
||
);
|
||
if (detail.unit) title.append(el("span", "b09s-hint", ` (${detail.unit})`));
|
||
const { wrap, tbody } = sheetTable(
|
||
sheetHead([
|
||
L("B09_Sheet_Col_Name"),
|
||
L("B09_Sheet_Col_Spec"),
|
||
L("B09_Sheet_Col_Quantity"),
|
||
L("B09_Sheet_Col_Unit"),
|
||
]),
|
||
);
|
||
for (const row of detail.rows) tbody.append(detailRow(row, ctx));
|
||
const sum = el("tr", "is-sum");
|
||
sum.append(el("td", "", L("B09_Sheet_Sum")), el("td"), el("td"), el("td"));
|
||
sum.append(
|
||
...moneyCells(null, [detail.total, detail.labor, detail.material, detail.expense]),
|
||
el("td"),
|
||
);
|
||
tbody.append(sum);
|
||
box.append(title, wrap);
|
||
if (detail.unattached_note) box.append(hint(detail.unattached_note.replace(/\*\*/g, ""), true));
|
||
if (detail.known_gap_note) box.append(hint(detail.known_gap_note, true));
|
||
}
|
||
|
||
/** 본표 한 장을 `box` 에 — 자취를 쌓고 서버 본표를 받아 그림. */
|
||
export function renderDetail(
|
||
ctx: B09TabContext,
|
||
box: HTMLElement,
|
||
tab: string,
|
||
code: string,
|
||
label: string,
|
||
): void {
|
||
if (!ctx.projectId) return;
|
||
visit(tab, code, label);
|
||
box.replaceChildren(trail(ctx), hint(L("B09_Sheet_Loading")));
|
||
loadDetail(ctx.projectId, code)
|
||
.then((detail) => {
|
||
box.replaceChildren(trail(ctx));
|
||
drawDetail(ctx, box, detail, label);
|
||
})
|
||
.catch((error: Error) => {
|
||
box.replaceChildren(trail(ctx), hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true));
|
||
});
|
||
}
|