Files
Aislo/B09_Estimation/B09_Estimation_UI_Detail.ts
T
eomsangdonandClaude Opus 5 85f7c9c6b3 feat(b09): 원가계산 화면 새 틀 — 설계내역서·일위대가·단가산출근거 탭(실무 서식 · 내역 → 호표 → 산근 들어가기)
- 틀 B09_Estimation_UI_Shell: 탭 줄과 등록만 · 탭마다 파일 하나(계약 _Shell_Types)
- 설계내역서: 실무 열(합계/노무/재료/경비 단가·금액) · 머리글 접기·레벨 고르개 · 줄 누르면 제 N 호표 · 단산 N 단추 · 미확정 빨간 테두리
- 일위대가·단가산출근거: 목록표(내역에 처음 쓰인 차례) + 본표 · 줄 누르면 하위 호표·산근·중기로 · 자취 눌러 되돌아감 · Q 식 글자 그대로
- 옛 탭 여섯(원가계산서·중기·관급사급·기초자료·설계서 구성·산출기초)은 옛 코드 그대로 이어 붙임 — 새 탭 파일이 서면 등록 한 줄씩 바꿈
- 본표 합계 줄 = 호표 성분 소계 원 미만 절사 값 · 비율 줄 금액도 0.1원 절사 표시
- 사전 ui_template_locale_b3 새 벌(b2 700줄 넘음)
- 검증: ORCA 검증 프로젝트 — 본체 122,848,989 · 지장목제거 865·15,460,837 · 제 9 호표 합계 1,708 = 산근 8호표 합계 1,708 · 옛 탭 여섯 다 뜸 · 접기 53→51·레벨1 11줄

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016VBGFXB9AbJBwXP19z75Qq
2026-09-14 01:30:01 +09:00

143 lines
5.1 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)은 단가산출근거 탭, 일위대가·중기는 일위대가 탭. */
function targetTab(row: DetailRowDto): string | null {
if (!row.drillable || !row.ref_code) return null;
return row.kind === "price_basis" ? "price_basis" : "unit_price";
}
function detailRow(row: DetailRowDto, ctx: B09TabContext): HTMLElement {
const tr = el("tr");
const percent = row.unit === "%";
const unit: [string, string, string, string] | null = percent
? null
: [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));
});
}