/* ============================================================================= * B09_Estimation_UI_Tabs_Base.ts * B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 · * 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`). * * 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈 * 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표 * ========================================================================== */ import type { DataRow } from "@ui/ui_template_data_table"; import { SOURCES, type Row } from "./B09_Estimation_Calc_Base"; import { money, parseNumber, qty, text, type TabBuild, type TabId, type TabTable, } from "./B09_Estimation_UI_Tabs"; const LINK_HINT = "줄을 누르면 단가를 낸 탭 · 줄로 이동"; /** 단가 줄 표(노무비 · 경비목록표) — 호표 · 명칭 · 규격 · 단위 · 단가 · 비고(출처) */ function priceList(rows: Row[], unitPrice: string, hint: string): TabTable { return { columns: [ qty("no", "호표", 0), text("name", "명칭", 200), text("spec", "규격", 140), text("unit", "단위", 56), money("price", unitPrice), text("source", "비고", 220), ], rows: rows.map((r) => ({ ...r })), frozen: 2, hint, }; } export const buildLabor: TabBuild = ({ sheets, payload }) => priceList( sheets.labor || [], "단가(원/인)", `노임 = 굳힌 단가(${madeText(payload)}) · M01 인력 · 바꾸려면 [단가 갱신]`, ); export const buildExpense: TabBuild = ({ sheets, payload }) => priceList( sheets.expense || [], "기준가(천원)", `기계 손료 기준가 = 굳힌 단가(${madeText(payload)}) · M01 기계 가격 · 바꾸려면 [단가 갱신]`, ); function madeText(payload: Row): string { return payload.book?.made ? `${String(payload.book.made).slice(0, 10)} 굳힘` : "아직 안 굳힘"; } export const buildBasis: TabBuild = ({ sheets }): TabTable => { const basis: Row = sheets.basis || { fx: [], drivers: [] }; const rows: DataRow[] = [ ...basis.fx.map((f: Row) => ({ part: "1. 환율", key: f.key, name: f.name, spec: f.date, unit: f.unit, day: null, factor: "", value: f.value, })), ...basis.drivers.map((d: Row) => ({ part: "2. 인건비", key: d.key, name: d.name, spec: `직종 ${d.key}`, unit: "원/시간", day: d.day, factor: d.factor, value: d.hour, labor_key: d.labor_key, })), ]; return { columns: [ text("part", "구분", 90), text("name", "명칭", 160), text("spec", "규격 · 기준일", 120), text("unit", "단위", 80), money("day", "일당(원)"), text("factor", "시간 환산", 160), money("value", "값", undefined, 2), ], rows, frozen: 2, hint: "운전사 시간 노임 = ROUNDDOWN(일당 × 1/8 × 16/12 × 25/20, 0)(M01 CC000336 · 건협기술-2230) · 환율 = M01 한국은행 · 인건비 줄 누름 = 노무비목록표", link: (i) => { const row = rows[i]; return row?.labor_key ? { tab: "labor", key: String(row.labor_key) } : null; }, }; }; const SUPPLY_NEXT: Record = { "": "관급", 관급: "별도", 별도: null }; export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => { const rows: Row[] = sheets.compare || []; const out: DataRow[] = rows.map((r) => { const row: DataRow = { no: r.no, key: r.key, name: r.name, spec: r.spec, unit: r.unit, lowest: r.lowest, applied: r.applied, applied_from: r.applied_from === "사용자" ? "사용자" : (r.applied_from ?? ""), supply: r.supply ?? "", }; for (const s of SOURCES) { row[`p:${s}`] = r.prices[s]; row[`g:${s}`] = r.pages[s] ?? ""; } return row; }); const mine = (key: string): Row => ({ ...((user.materials || {})[key] || {}) }); const put = (key: string, cell: Row): void => { const materials = { ...(user.materials || {}) }; if (Object.keys(cell).length) materials[key] = cell; else delete materials[key]; setUser({ ...user, materials }); }; return { columns: [ qty("no", "번호", 0), text("name", "품명", 180), text("spec", "규격", 140), text("unit", "단위", 56), ...SOURCES.flatMap((s) => [money(`p:${s}`, "단가", [s]), text(`g:${s}`, "면", 64, [s])]), money("lowest", "최저"), money("applied", "적용 단가", ["적용"]), text("applied_from", "출처", 80, ["적용"]), text("supply", "관급 · 별도", 80), ], rows: out, frozen: 2, hint: "적용 단가 = 출처 중 최저(0 · 빈칸 뺌) 자동 · 칸에 넣으면 사용자 값 · 지우면 다시 최저 · 「관급 · 별도」 칸 누름 = 없음 → 관급 → 별도", editable: (_i, key) => key === "applied", onEdit: (i, key, value) => { const row = out[i]; if (!row || key !== "applied") return; const v = parseNumber(value); if (v === undefined) return; const cell = mine(String(row.key)); if (v === null) delete cell.applied; else cell.applied = v; put(String(row.key), cell); }, onCell: (i, key) => { const row = out[i]; if (!row || key !== "supply") return; const cell = mine(String(row.key)); const next = SUPPLY_NEXT[String(row.supply ?? "")] ?? null; if (next) cell.supply = next; else delete cell.supply; put(String(row.key), cell); }, }; }; export const buildMaterial: TabBuild = ({ sheets }): TabTable => { const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({ ...r, supply: r.supply ?? "", })); return { columns: [ qty("no", "호표", 0), text("name", "명칭", 200), text("spec", "규격", 160), text("unit", "단위", 56), money("price", "단가"), text("supply", "관급 · 별도", 80), text("origin", "비고", 90), ], rows, frozen: 2, hint: `단가 = 자재단가대비표 적용 단가 · ${LINK_HINT}`, link: (i) => (rows[i] ? { tab: "compare", key: String(rows[i].key) } : null), }; }; export const buildMachine: TabBuild = ({ sheets }): TabTable => { const rows: DataRow[] = (sheets.machine || []).map((r: Row) => ({ ...r })); return { columns: [ qty("no", "호표", 0), text("name", "명칭", 200), text("spec", "규격", 120), text("unit", "단위", 56), money("sum", "합계"), money("labor", "노무비"), money("material", "재료비"), money("expense", "경비"), ], rows, frozen: 2, hint: `시간당 = 중기사용료 블록 합계 · ${LINK_HINT}`, link: (i) => (rows[i] ? { tab: "machine_blocks", key: String(rows[i].key) } : null), }; }; const KIND_TAB: Record = { expense: "expense", labor: "basis", material: "compare", }; export const buildMachineBlocks: TabBuild = ({ sheets }): TabTable => { const rows: DataRow[] = []; const links: Array<{ tab: TabId; key: string } | null> = []; (sheets.machine_blocks || []).forEach((b: Row, n: number) => { rows.push({ key: b.key, name: `제 ${n + 1} 호표 ${b.name}`, spec: b.spec, unit: b.unit }); links.push(null); for (const ln of b.lines) { const at = (part: string): number | null => (ln.kind === part ? ln.price : null); const amt = (part: string): number | null => (ln.kind === part ? ln.amount : null); rows.push({ key: `${b.key}|${ln.name}`, name: ` ${ln.name}`, spec: ln.spec, qty: ln.qty, unit: ln.unit, sum_price: ln.price, sum: ln.amount, labor_price: at("labor"), labor: amt("labor"), material_price: at("material"), material: amt("material"), expense_price: at("expense"), expense: amt("expense"), }); links.push(ln.ref?.key ? { tab: KIND_TAB[ln.kind] ?? "compare", key: ln.ref.key } : null); } rows.push({ key: `${b.key}|합계`, name: " 합 계", sum: b.sum, labor: b.labor, material: b.material, expense: b.expense, }); links.push(null); }); const pair = (key: string, label: string) => [ money(`${key}_price`, "단가", [label], 1), money(key, "금액", [label], 1), ]; return { columns: [ text("name", "명칭", 220), text("spec", "규격", 120), qty("qty", "수량"), text("unit", "단위", 56), ...pair("sum", "합계"), ...pair("labor", "노무비"), ...pair("material", "재료비"), ...pair("expense", "경비"), ], rows, frozen: 1, hint: "손료 = 기준가(천원) × 손료계수 ÷ 10,000 · 운전사 = 인 × 시간 노임 · 연료 = ℓ/hr × 적용 단가 · 잡품 = 연료 금액 × % · 줄 ROUNDDOWN 1 · 비목 합 ROUNDDOWN 0 · 줄 누름 = 단가를 낸 탭", link: (i) => links[i] ?? null, }; };