/* ============================================================================= * B09_Estimation_UI_Store.ts * B09 내역서·일위대가·단가산출근거 탭이 **한 벌로** 쓰는 서버 자료 (PLAN 12장) * * - 설계내역서 응답 한 벌에 호표 목록(`unit_price_sheet`)·단산 목록(`price_basis`)이 함께 옴 — * 세 탭이 같은 응답을 봄(번호가 탭끼리 갈리지 않게). 번호는 서버가 낼 때마다 매김(저장 안 함). * - 캐시는 메모리 한 벌(프로젝트별) — [다시 불러오기] 가 비움. 값을 저장하지 않음(보기 전용 1차). * ========================================================================== */ import { API_BASE_URL } from "@config/config_frontend"; import type { ProvenancePayload } from "@ui/ui_template_provenance"; export interface BillNoteDto { column: string; text: string; } export interface BillRowDto { item_no: string; level: number; code: string | null; name: string; spec: string; unit: string; quantity: string | null; quantity_shown: string | null; quantity_digits: number | null; unit_price_krw: string | null; amount_krw: string | null; material_krw: string; labor_krw: string; expense_krw: string; unit_material_krw: string | null; unit_labor_krw: string | null; unit_expense_krw: string | null; price_code: string; unconfirmed: number; price_basis_label: string; is_group: boolean; in_bill: boolean; note: string; notes: BillNoteDto[]; } export interface SheetEntryDto { number: number; label: string; code: string; name: string; spec: string; unit: string; material_krw: string; labor_krw: string; expense_krw: string; total_krw?: string; unit_price_krw?: string; source?: string; unconfirmed?: number; ref_code?: string; unit_price_codes?: string[]; } export interface MissingDto { name: string; unit?: string; quantity?: string; reason: string; code?: string; blocked_kind?: string; } /** 집계표·목록표 한 줄 — 서버 `resource_summary`·`bill_lists` 그대로. */ export interface ResourceRowDto { number: number; code: string; name: string; spec: string; unit: string; quantity?: string; unit_price_krw: string | null; amount_krw?: string | null; labor_krw?: string; material_krw?: string; expense_krw?: string; unit_labor_krw?: string; unit_material_krw?: string; unit_expense_krw?: string; note: string; } 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[]; note: string; }; lists: Record; rows: BillRowDto[]; excluded: BillRowDto[]; summary: { body_total_krw: string; detail_rows: number; missing: MissingDto[]; unconfirmed_count: number; notes: string[]; material_sheet: MaterialSheetDto | null; }; price_basis: { entries: SheetEntryDto[] }; unit_price_sheet: { entries: SheetEntryDto[] }; } export interface DetailRowDto { ref_code?: string; code?: string; name: string; spec: string; unit: string; kind?: string; source_label?: string; source?: string; drillable: boolean; quantity: string; unit_material?: string; unit_labor?: string; unit_expense?: string; unit_total?: string; material: string; labor: string; expense: string; total: string; note: string; } export interface DetailDto { code: string; name: string; spec: string; unit: string; kind: string; material: string; labor: string; expense: string; total: string; rows: DetailRowDto[]; unattached_note: string; known_gap_note: string; } const bills = new Map>(); async function getJson(path: string): Promise { const response = await fetch(`${API_BASE_URL}${path}`, { credentials: "include" }); const body = await response.json().catch(() => ({})); if (!response.ok) { throw new Error(String((body as { message?: string }).message || response.status)); } return body as T; } /** 산출 조건을 저장한 뒤 — 단가가 다시 서므로 다음에 고르는 탭이 새로 받게 비움. */ export function forgetBill(projectId: string): void { bills.delete(projectId); } /** 설계내역서 한 벌 — 세 탭이 같은 응답을 봄. `force` 면 다시 받음. */ export function loadBill(projectId: string, force = false): Promise { if (force || !bills.has(projectId)) { const request = getJson(`/projects/${encodeURIComponent(projectId)}/estimation/bill`); request.catch(() => bills.delete(projectId)); bills.set(projectId, request); } return bills.get(projectId) as Promise; } export interface PriceSlotDto { name: string; price_krw: string | null; source_note: string; adopted: boolean; } export interface PriceCompareDto { material_comparison: { slot_names: string[]; rows: Array<{ code: string; name: string; spec: string; unit: string; slots: PriceSlotDto[]; min_slot: number | null; adopted_slot: number; adopted_price_krw: string | null; note: string; }>; notes: string[]; }; } /** 자재단가대비표 — 슬롯 여섯 · 채택 · 최소단가(서버 `material_price_comparison`). */ export function loadPriceCompare(projectId: string): Promise { return getJson( `/projects/${encodeURIComponent(projectId)}/estimation/price-sources`, ); } /** 호표 본표 — 일위대가(B)·시간당 중기(X)는 `unit-prices`, 단가산출(D)은 `price-basis`. */ export function loadDetail(projectId: string, code: string): Promise { const kind = code.startsWith("D-") ? "price-basis" : "unit-prices"; return getJson( `/projects/${encodeURIComponent(projectId)}/estimation/${kind}/${encodeURIComponent(code)}`, ); }