Files
Aislo/B09_Estimation/B09_Estimation_UI_Tab_UnitPrice.ts
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

127 lines
4.4 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tab_UnitPrice.ts
* B09 일위대가 탭 — 목록표 + 일위대가표 (STmate `일위대가목록표` · `wM_Edit_iLWi` 보기 · PLAN 12장 1차)
*
* - 목록표 = 내역에 쓰인 호표만, **내역에 처음 쓰인 차례**로 「제 N 호표」(번호는 서버가 낼 때마다 매김).
* - 줄을 누르면 아래에 그 호표의 일위대가표 — 호표 안 줄(하위 호표·단산·중기)을 누르면 더 들어감.
* - 구조물도 호표는 B08 구조물도 탭의 일위대가 표가 본표 — 여기선 금액·안내만.
* - 편집(구성행 수정)은 2차 — B09 에 수정 저장 자리가 아직 없음(브레인 판정).
* ========================================================================== */
import type { B09Tab, B09TabContext } from "./B09_Estimation_UI_Shell_Types";
import { renderDetail } from "./B09_Estimation_UI_Detail";
import {
L,
el,
hint,
numberCell,
sheetTable,
unconfirmedBadge,
won,
} from "./B09_Estimation_UI_Sheet";
import { loadBill, type BillDto, type SheetEntryDto } from "./B09_Estimation_UI_Store";
let selected = "";
/** 목록표 머리 — 호표 · 명칭 · 규격 · 단위 · 합계 · 노무비 · 재료비 · 경비 · 비고(실무 목록표). */
export function listHead(first: string): HTMLElement {
const thead = el("thead");
const tr = el("tr");
for (const label of [
first,
L("B09_Sheet_Col_Name"),
L("B09_Sheet_Col_Spec"),
L("B09_Sheet_Col_Unit"),
L("B09_Sheet_Col_Total"),
L("B09_Sheet_Col_Labor"),
L("B09_Sheet_Col_Material"),
L("B09_Sheet_Col_Expense"),
L("B09_Sheet_Col_Note"),
]) {
tr.append(el("th", "", label));
}
thead.append(tr);
return thead;
}
export function listRow(entry: SheetEntryDto, total: string, isSelected: boolean): HTMLElement {
const tr = el("tr", `is-clickable${isSelected ? " is-selected" : ""}`);
tr.append(
el("td", "", entry.label),
el("td", "", entry.name),
el("td", "", entry.spec),
el("td", "", entry.unit),
numberCell(won(total)),
numberCell(won(entry.labor_krw)),
numberCell(won(entry.material_krw)),
numberCell(won(entry.expense_krw)),
);
const note = el("td", "b09s-note");
if (entry.unconfirmed) {
note.append(unconfirmedBadge(entry.unconfirmed));
tr.classList.add("is-manual");
}
tr.append(note);
return tr;
}
function labelFor(bill: BillDto, code: string): string {
const entry = bill.unit_price_sheet.entries.find((item) => item.code === code);
if (entry) return entry.label;
return code.startsWith("X-") ? `${L("B09_Sheet_Machine")} ${code.slice(2)}` : code;
}
function draw(ctx: B09TabContext, bill: BillDto): void {
const entries = bill.unit_price_sheet.entries;
const page = el("div", "b09s-split");
const { wrap, tbody } = sheetTable(listHead(L("B09_Sheet_Col_Sheet")));
const detail = el("div", "b09s-split");
const select = (code: string): void => {
selected = code;
ctx.body.replaceChildren();
draw(ctx, bill);
};
for (const entry of entries) {
const tr = listRow(entry, entry.total_krw ?? "", entry.code === selected);
tr.addEventListener("click", () => select(entry.code));
tbody.append(tr);
}
page.append(el("div", "b09s-title", L("B09_Sheet_UnitPriceList")), wrap);
if (entries.length === 0) page.append(hint(L("B09_Sheet_EmptyList")));
page.append(detail);
ctx.body.append(page);
if (!selected) {
detail.append(hint(L("B09_Sheet_PickSheet")));
return;
}
const picked = entries.find((entry) => entry.code === selected);
if (picked?.source === "structure") {
detail.append(el("div", "b09s-title", `${picked.label} ${picked.name} · ${picked.spec}`));
detail.append(hint(L("B09_Sheet_StructureSheet")));
return;
}
renderDetail(ctx, detail, "unit_price", selected, labelFor(bill, selected));
}
export const unitPriceTab: B09Tab = {
key: "unit_price",
label: () => L("B09_Estimation_Tab_UnitPrice"),
render(ctx, arg) {
if (!ctx.projectId) {
ctx.body.append(hint(L("B09_Sheet_NoProject")));
return;
}
if (arg) selected = arg;
ctx.body.append(hint(L("B09_Sheet_Loading")));
loadBill(ctx.projectId)
.then((bill) => {
ctx.body.replaceChildren();
draw(ctx, bill);
})
.catch((error: Error) => {
ctx.body.replaceChildren(hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true));
});
},
};