diff --git a/B09_Estimation/B09_Estimation_UI_Shell.ts b/B09_Estimation/B09_Estimation_UI_Shell.ts index f64ddf0d..ff91db4d 100644 --- a/B09_Estimation/B09_Estimation_UI_Shell.ts +++ b/B09_Estimation/B09_Estimation_UI_Shell.ts @@ -25,6 +25,7 @@ import { summaryTab } from "./B09_Estimation_UI_Tab_Summary"; import { listsTab } from "./B09_Estimation_UI_Tab_Lists"; import { designDocTab } from "./B09_Estimation_UI_Tab_DesignDoc"; import { basisSheetTab } from "./B09_Estimation_UI_Tab_BasisSheet"; +import { supplyTab } from "./B09_Estimation_UI_Tab_Supply"; /** 탭 등록 — 한 줄에 탭 하나. 옛 탭(`legacyTab`)은 새 탭 파일이 서면 그 줄만 바꿈. */ const TABS: B09Tab[] = [ @@ -37,7 +38,7 @@ const TABS: B09Tab[] = [ priceCompareTab, summaryTab, listsTab, - legacyTab("supply", "B09_Estimation_Tab_Supply"), + supplyTab, legacyTab("base_data", "B09_Estimation_Tab_BaseData"), designDocTab, basisSheetTab, diff --git a/B09_Estimation/B09_Estimation_UI_Store.ts b/B09_Estimation/B09_Estimation_UI_Store.ts index c152598b..1db83951 100644 --- a/B09_Estimation/B09_Estimation_UI_Store.ts +++ b/B09_Estimation/B09_Estimation_UI_Store.ts @@ -8,6 +8,7 @@ * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; +import type { ProvenancePayload } from "@ui/ui_template_provenance"; export interface BillNoteDto { column: string; @@ -89,7 +90,28 @@ export interface ResourceRowDto { export type ResourceGroup = "labor" | "material" | "expense" | "machine" | "lumpsum"; +export interface MaterialSheetRowDto { + name: string; + spec: string; + unit: string; + total_amount: string; + unit_price_krw: string | null; + amount_krw: string | null; + note: string; +} + +export interface MaterialSheetDto { + contractor: MaterialSheetRowDto[]; + owner: MaterialSheetRowDto[]; + unknown: MaterialSheetRowDto[]; + contractor_total_krw: string; + owner_total_krw: string; + notes: string[]; +} + export interface BillDto { + /** 근거 사전 — 개발환경에서만 옴. */ + provenance?: ProvenancePayload; resources: { groups: Record; missing: string[]; @@ -104,6 +126,7 @@ export interface BillDto { missing: MissingDto[]; unconfirmed_count: number; notes: string[]; + material_sheet: MaterialSheetDto | null; }; price_basis: { entries: SheetEntryDto[] }; unit_price_sheet: { entries: SheetEntryDto[] }; diff --git a/B09_Estimation/B09_Estimation_UI_Tab_Supply.ts b/B09_Estimation/B09_Estimation_UI_Tab_Supply.ts new file mode 100644 index 00000000..01fea936 --- /dev/null +++ b/B09_Estimation/B09_Estimation_UI_Tab_Supply.ts @@ -0,0 +1,85 @@ +/* ============================================================================= + * B09_Estimation_UI_Tab_Supply.ts + * B09 관급·사급 탭 — 자재대: B08 수량·할증에 단가를 붙인 표 (PLAN 8-7 「금액은 B09」) + * + * - 옛 `B09_Estimation_UI_Page.ts` 의 자재대 탭을 그대로 옮김(PLAN 12장 옛 탭 옮기기). + * - 사급(도급 재료비) · 관급(총원가 밖 별도 표기) · 안 갈린 것(어느 합계에도 안 넣음) 세 무리. + * ⚠ 「안 갈린 것」은 못 세운 것이 아니라 **세면 안 되는 것** — 채우면 이중계상(PLAN 8-36 ㉱). + * - 자료는 설계내역서 응답 한 벌(`summary.material_sheet`) — 내역 탭들과 같은 응답을 봄. + * ========================================================================== */ + +import type { ui_locales } from "@ui/ui_template_locale"; +import type { B09Tab } from "./B09_Estimation_UI_Shell_Types"; +import { L, hint, quantity } from "./B09_Estimation_UI_Sheet"; +import { infoTable, note } from "./B09_Estimation_UI_Table"; +import { loadBill, type BillDto, type MaterialSheetRowDto } from "./B09_Estimation_UI_Store"; + +const MATERIAL_KEYS = [ + "name", + "spec", + "unit", + "total_amount", + "unit_price_krw", + "amount_krw", + "note", +]; + +function draw(body: HTMLElement, bill: BillDto): void { + const sheet = bill.summary.material_sheet; + if (!sheet) { + body.append(hint(L("B09_Estimation_Mat_Empty"))); + return; + } + // 자재가 아예 없으면 **빈 표 셋을 늘어놓지 않음** — 「없다」 한 줄이면 됨(2026-09-08 화면 전수). + if (sheet.contractor.length === 0 && sheet.owner.length === 0 && sheet.unknown.length === 0) { + body.append(hint(L("B09_Estimation_Mat_None"))); + return; + } + const groups: Array<[keyof typeof ui_locales, MaterialSheetRowDto[], string | null, string]> = [ + ["B09_Estimation_Mat_Contractor", sheet.contractor, sheet.contractor_total_krw, "material"], + ["B09_Estimation_Mat_Owner", sheet.owner, sheet.owner_total_krw, "material"], + ["B09_Estimation_Mat_Unknown", sheet.unknown, null, "material_unknown"], + ]; + for (const [labelKey, rows, total, sheetName] of groups) { + body.append(note(`${L(labelKey)} (${rows.length})` + (total === null ? "" : ` — ${total}`))); + if (rows.length === 0) continue; + body.append( + infoTable( + ["자재", "규격", "단위", "수량", "단가", "금액", "비고"], + rows.map((row) => [ + row.name, + row.spec, + row.unit, + quantity(row.total_amount, 2), + row.unit_price_krw ?? "", + row.amount_krw ?? "", + row.note, + ]), + [0, 1, 2, 6], + MATERIAL_KEYS, + bill.provenance?.sheets?.[sheetName], + ), + ); + } + for (const line of sheet.notes) body.append(note(line.replace(/\*\*/g, ""))); +} + +export const supplyTab: B09Tab = { + key: "supply", + label: () => L("B09_Estimation_Tab_Supply"), + render(ctx) { + if (!ctx.projectId) { + ctx.body.append(hint(L("B09_Sheet_NoProject"))); + return; + } + ctx.body.append(hint(L("B09_Sheet_Loading"))); + loadBill(ctx.projectId) + .then((bill) => { + ctx.body.replaceChildren(); + draw(ctx.body, bill); + }) + .catch((error: Error) => { + ctx.body.replaceChildren(hint(`${L("B09_Sheet_LoadFailed")} ${error.message}`, true)); + }); + }, +};