/* ============================================================================= * B09_Estimation_UI_Tabs_Base.ts * B09 바탕 단가 탭 일곱(PLAN 144-2~4) — 노무비 · 경비목록표 · 환율및기초자료 · 자재단가대비표 · * 재료비목록표 · 중기사용료 · 중기목록표(실무 엑셀 꼴 · 값 = `B09_Estimation_Calc_Base`). * 재료비목록표 = 엑셀 꼴 + 수량 · 총액 열(166-3) · 줄 = 대가 · 산근 · 중기가 쓰는 자재 + B08 줄 · * 관급 · 별도 줄(183-1 · 서버가 거른 입력) · 수량 = 역전개 총수량 · 비고 = 쓰는 곳(`usesOf`) * * 고칠 칸 = 자재단가대비표 적용 단가(빈칸으로 지우면 최저 자동) · 관급 / 별도 칸 누름 = 표시 바꿈 * · 견적 단가 손입력 · 적용 「출처」 칸 누름 = 고를 출처 돌림(값 있는 출처만 · 견적이 높아도) * · 「견적서」 칸(견적 묶음 아래 · 183-2) 누름 = 파일 모달(`B09_Estimation_UI_Tabs_Base_Quote`)(166-4) * · 출처 묶음 둘째 칸 = 「출처」(M01 면수 · 유가 기준일 값 그대로 · 183-2 · 견적엔 없음) * 이동 = 재료비목록표 줄 → 대비표 · 중기목록표 줄 → 중기사용료 블록 · 블록 줄 → 경비 · 기초 · 대비표 * ========================================================================== */ import type { DataRow } from "@ui/ui_template_data_table"; import { SOURCES, type Row } from "./B09_Estimation_Calc_Base"; import { openQuotes } from "./B09_Estimation_UI_Tabs_Base_Quote"; 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 }; /** 고를 출처 돌림 — 자동 → 값 있는 출처 차례 → 자동 */ function nextPick(r: Row): string | null { const have = SOURCES.filter((s) => (r.prices[s] ?? 0) > 0); return have[have.indexOf(r.pick) + 1] ?? null; } export const buildCompare: TabBuild = ({ sheets, user, setUser }): TabTable => { const rows: Row[] = sheets.compare || []; const out: DataRow[] = rows.map((r) => { const files: string[] = (user.materials || {})[r.key]?.files || []; 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.pick ? `${r.pick} 고름` : (r.applied_from ?? ""), supply: r.supply ?? "", files: files.length ? `${files.length} 파일` : "첨부", }; 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), // 183-2 — 견적 = 단가 · 견적서 · 그 밖 = 단가 · 출처(면수 값) ...SOURCES.flatMap((s) => [ money(`p:${s}`, "단가", [s]), s === "견적" ? text("files", "견적서", 72, [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" || key === "p:견적", onEdit: (i, key, value) => { const row = out[i]; if (!row) return; const cell = mine(String(row.key)); const v = parseNumber(value); if (v === undefined) return; const field = key === "applied" ? "applied" : key === "p:견적" ? "quote" : null; if (!field) return; if (v === null) delete cell[field]; else cell[field] = v; if (field === "applied") delete cell.pick; // 손 수 = 고른 출처를 풂 put(String(row.key), cell); }, onCell: (i, key) => { const row = out[i]; const r = rows[i]; if (!row || !r) return; const cell = mine(String(row.key)); if (key === "files") { const label = `${r.name} ${r.spec}`.trim(); return openQuotes(r.key, label, (names) => { const now = mine(r.key); if (JSON.stringify(now.files || []) === JSON.stringify(names)) return; if (names.length) now.files = names; else delete now.files; put(r.key, now); }); } if (key === "applied_from") { const pick = nextPick(r); if (pick) cell.pick = pick; else delete cell.pick; return put(String(row.key), cell); } if (key !== "supply") return; const next = SUPPLY_NEXT[String(row.supply ?? "")] ?? null; if (next) cell.supply = next; else delete cell.supply; put(String(row.key), cell); }, }; }; /** 쓰는 곳(183-1) — 자재 키 → 「대가 n호표」 · 「산근 n호표」 · 「중기 n호표」 · 「내역 자재대」 * (블록 재료 줄 · 설계내역서 자재 항목 · 표시 잇기만 · 셈 아님) */ function usesOf(sheets: Row): Map { const out = new Map(); const add = (key: unknown, label: string): void => { if (!key) return; const list = out.get(String(key)) ?? []; if (!list.includes(label)) out.set(String(key), [...list, label]); }; const scan = (blocks: Row[] | undefined, label: (b: Row, n: number) => string): void => (blocks || []).forEach((b, n) => (b.lines || []).forEach((ln: Row) => ln.kind === "material" && add(ln.ref?.key, label(b, n))), ); scan(sheets.daega_blocks, (b) => `대가 ${b.no}호표`); scan(sheets.sg, (b) => `산근 ${b.no}호표`); scan(sheets.machine_blocks, (_b, n) => `중기 ${n + 1}호표`); for (const r of sheets.bill?.rows || []) if (r.kind === "item" && r.ref?.tab === "material") add(r.ref.key, "내역 자재대"); return out; } export const buildMaterial: TabBuild = ({ sheets }): TabTable => { const uses = usesOf(sheets); const rows: DataRow[] = (sheets.material || []).map((r: Row) => ({ ...r, supply: r.supply ?? "", uses: (uses.get(r.key) ?? []).join(" · ") || (r.origin === "B08" ? "B08 총괄" : ""), })); return { columns: [ qty("no", "호표", 0), text("name", "명칭", 200), text("spec", "규격", 160), text("unit", "단위", 56), qty("qty", "수량", 3), money("price", "단가"), money("amount", "총액"), text("supply", "관급 · 별도", 80), text("uses", "비고(쓰는 곳)", 240), ], rows, total: rows.length ? { name: "계", amount: rows.reduce((sum, r) => sum + (Number(r.amount) || 0), 0) } : null, frozen: 2, hint: `줄 = 연결된 일위대가 · 산근 · 중기가 쓰는 자재 + B08 총괄 줄 · 관급 · 별도 줄 · 수량 = 설계내역서 × 호표 역전개 총수량(재료비수량금액집계표와 같음 · B08 할증 값과 다를 수 있음) · 단가 = 자재단가대비표 적용 단가 · 총액 = ROUND(수량 × 단가, 0) · 계 = 빈칸 뺀 합 · 비고 = 쓰는 곳 · ${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}`, no: n + 1, title: b.name, // 171-3 묶음 카드 머리 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, }; };