Files
Aislo/B09_Estimation/B09_Estimation_UI_Detail.ts
T
eomsangdonandClaude Opus 5 3ce997fa69 feat(b09): 표 화면 넷 — 중기(목록표·중기사용료) · 자재단가대비표 · 집계표 넷 · 목록표 일곱
- 서버: 내역 응답에 집계표 넷(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
2026-09-14 01:56:02 +09:00

147 lines
5.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =============================================================================
* 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));
});
}