/* -*- coding: utf-8 -*- */ /** * STMATE 계산 사슬 및 제어 옵션 시뮬레이터 엔진 (calc.js) * 근거: resources/knowledge/original/경쟁사 프로그램/STmate 분석/ (REA 정적분석 및 골든셋 회귀) */ const StmateCalc = (() => { const KINDS = ["재료비", "노무비", "경비"]; const KIND_KEYS = { 재료비: "M", 노무비: "L", 경비: "S" }; function num(val, def = 0) { if (val === null || val === undefined || val === "") return def; const n = Number(String(val).replace(/,/g, "").trim()); return Number.isFinite(n) ? n : def; } function fmt(val, maxDec = 0) { return new Intl.NumberFormat("ko-KR", { minimumFractionDigits: 0, maximumFractionDigits: maxDec, }).format(num(val, 0)); } function roundDown(val, digits = 0) { const n = Number(val) || 0, f = Math.pow(10, digits), s = n * f; return Math.trunc(s + (s >= 0 ? 1e-9 : -1e-9)) / f; } function roundUp(val, digits = 0) { const n = Number(val) || 0; if (n === 0) return 0; const f = Math.pow(10, digits), s = n * f; return (n > 0 ? Math.ceil(s - 1e-9) : Math.floor(s + 1e-9)) / f; } function roundHalfUp(val, digits = 0) { const n = Number(val) || 0, f = Math.pow(10, digits); const r = Math.floor(Math.abs(n) * f + 0.5 + 1e-9) / f; return n < 0 ? -r : r; } function roundBanker(val, digits = 0) { const n = Number(val) || 0, f = Math.pow(10, digits), s = n * f, fl = Math.floor(s); if (Math.abs(s - fl - 0.5) < 1e-9) return (fl % 2 === 0 ? fl : fl + 1) / f; return Math.round(s) / f; } function applyRoundMode(val, mode, digits = 0) { return mode === "up" ? roundUp(val, digits) : mode === "half" ? roundHalfUp(val, digits) : mode === "banker" ? roundBanker(val, digits) : roundDown(val, digits); } function evalFormula(exprStr, vars = {}) { let s = String(exprStr || "") .replace(/'[^']*'/g, "") .trim(); if (!s) return 0; Object.keys(vars) .sort((a, b) => b.length - a.length) .forEach((k) => { s = s.replace(new RegExp("\\b" + k + "\\b", "g"), String(vars[k])); }); s = s.replace(/[^0-9.+\-*/()]/g, ""); if (!s) return 0; try { const v = Function('"use strict"; return (' + s + ");")(); return Number.isFinite(v) ? v : 0; } catch (_e) { return 0; } } function evalWageFormulaLeftToRight(dailyWage, exprStr) { const base = Number(dailyWage) || 0, clean = String(exprStr || "").replace(/\s+/g, ""); if (!clean) return { seqResult: base, preCalcResult: base, steps: [] }; let acc = base, coeff = 1; const steps = []; for (const tok of clean.match(/([*/])?(\d+(?:\.\d+)?)/g) || []) { const op = tok[0] === "*" || tok[0] === "/" ? tok[0] : "*"; const n = parseFloat(op === tok[0] ? tok.slice(1) : tok); if (!Number.isFinite(n) || n === 0) continue; acc = op === "*" ? acc * n : acc / n; coeff = op === "*" ? coeff * n : coeff / n; steps.push(`${op}${n} → ${roundHalfUp(acc, 4)}`); } return { seqResult: roundDown(acc, 0), preCalcResult: Math.trunc(base * coeff), coeff, steps }; } function calcResources(data) { const sys = data.sysConfig || {}, rxFormula = sys.rxFormula || "1/8*16/12*25/20"; const condCodes = new Set( String(sys.condCodes || "") .split(/[,\s]+/) .map((s) => s.trim()) .filter(Boolean), ); const usdRate = Number(sys.useChangedUsd ? sys.usdRate2 : sys.usdRate1) || 1000, ecdDigits = Number(sys.ecdDigits) || 0; const map = {}; const list = (data.resources || []).map((res) => { const cands = Array.isArray(res.candidates) ? res.candidates.map(Number) : [0, 0, 0, 0, 0]; const nonZero = cands.filter((v) => v > 0); let basePrice = Number(res.manualPrice) || 0, chosenLabel = "수동입력"; if (res.adoptMode === "min") { basePrice = nonZero.length ? Math.min(...nonZero) : 0; chosenLabel = nonZero.length ? `최저가(후보${cands.indexOf(basePrice) + 1})` : "0원"; } else if (res.adoptMode && res.adoptMode.startsWith("slot")) { const sIdx = parseInt(res.adoptMode.replace("slot", ""), 10) - 1; basePrice = cands[sIdx] || 0; chosenLabel = `후보${sIdx + 1} 고정`; } const krwPrice = res.isUsd ? roundDown(basePrice * usdRate, ecdDigits) : basePrice; const wageInfo = res.kind === "노무비" && (res.convertHourly || condCodes.has(res.code)) ? evalWageFormulaLeftToRight(krwPrice, rxFormula) : null; const adoptedPrice = wageInfo ? wageInfo.seqResult : krwPrice; const item = Object.assign( {}, res, { cands, basePrice, adoptedPrice }, { appliedPrice: adoptedPrice, chosenLabel, wageInfo }, ); return ((map[res.id] = item), (map[res.code] = item), item); }); return { list, map }; } function calcMachines(data, resMap) { const map = {}; const list = (data.machines || []).map((m) => { let M = num(m.hourlyM), L = num(m.hourlyL), S = num(m.hourlyS), opHourly = 0, fuelPrice = 0; if (m.hourlyT === undefined || m.operatorId !== undefined) { opHourly = resMap[m.operatorId]?.adoptedPrice || 0; fuelPrice = resMap[m.fuelId]?.adoptedPrice || 0; M = roundDown( (Number(m.fuelConsumption) || 0) * fuelPrice * (1 + (Number(m.miscRatePercent) || 0) / 100), 0, ); L = roundDown(opHourly * (Number(m.operatorCount) || 1), 0); S = roundDown((Number(m.acquisitionPrice) || 0) * (Number(m.hourlyRateFactor) || 0), 0); } const T = num(m.hourlyT) || M + L + S; const item = Object.assign({}, m, { M, L, S, T, opHourly, fuelPrice }); return ((map[m.id] = item), (map[m.code] = item), item); }); return { list, map }; } function evalQFormula(dItem) { const q = Number(dItem.q) || 0, K = Number(dItem.K) || 0; const f = dItem.f !== undefined ? Number(dItem.f) : evalFormula(dItem.fExpr || "1", { q, K }); const E = dItem.E !== undefined ? Number(dItem.E) : evalFormula(dItem.EExpr || "0.55", { q, K, f }); const Cm = Number(dItem.Cm) || 1, mult = dItem.formulaType === "60_qKfE_Cm" ? 60 : 3600; const rawQ = Cm !== 0 ? (mult * q * K * f * E) / Cm : 0; return { rawQ, Q: roundHalfUp(rawQ, 2) }; } function calcSanCal(data, machineMap) { const map = {}; const list = (data.sanItems || []).map((d) => { const mach = machineMap[d.machineId], { rawQ, Q } = evalQFormula(d); const M = d.fixedM !== undefined ? num(d.fixedM) : mach && Q > 0 ? roundDown(mach.M / Q, 1) : 0; const L = d.fixedL !== undefined ? num(d.fixedL) : mach && Q > 0 ? roundDown(mach.L / Q, 1) : 0; const S = d.fixedS !== undefined ? num(d.fixedS) : mach && Q > 0 ? roundDown(mach.S / Q, 1) : 0; const intM = roundDown(M, 0), intL = roundDown(L, 0), intS = roundDown(S, 0), intT = intM + intL + intS; const item = Object.assign({}, d, { mach, rawQ, Q, M, L, S, T: roundHalfUp(M + L + S, 1) }); Object.assign(item, { intM, intL, intS, intT }); return ((map[d.id] = item), (map[d.code] = item), item); }); return { list, map }; } function calcIlwi(data, resMap, machMap, sanMap) { const map = {}; const list = (data.ilwiItems || []).map((ilwi) => { let sumM = 0, sumL = 0, sumS = 0; const lines = (ilwi.lines || []).map((ln) => { const ref = resMap[ln.refId] || machMap[ln.refId] || sanMap[ln.refId], rawQty = Number(ln.qty) || 0; const baseQty = ln.devd && rawQty !== 0 ? roundHalfUp(1 / rawQty, 7) : rawQty; const appliedQty = roundHalfUp(baseQty * (1 + (Number(ln.halPercent) || 0) / 100), 7); const pickU = (k, prop) => !ref ? 0 : "adoptedPrice" in ref ? ref.kind === k ? ref.adoptedPrice : 0 : ref[prop] || 0; const uM = pickU("재료비", "M"), uL = pickU("노무비", "L"), uS = pickU("경비", "S"); const lineM = roundDown(uM * appliedQty, 2), lineL = roundDown(uL * appliedQty, 2), lineS = roundDown(uS * appliedQty, 2); sumM += lineM; sumL += lineL; sumS += lineS; const lineT = roundHalfUp(lineM + lineL + lineS, 2); const out = Object.assign({}, ln, { refName: ref?.name || "미연결", refCode: ref?.code || "-", }); return Object.assign(out, { appliedQty, uM, uL, uS, lineM, lineL, lineS, lineT }); }); const M = roundDown(sumM, 0), L = roundDown(sumL, 0), S = roundDown(sumS, 0); const item = Object.assign({}, ilwi, { lines, M, L, S, T: M + L + S }); return ((map[ilwi.id] = item), (map[ilwi.code] = item), item); }); return { list, map }; } function calcBoq(data, resMap, machMap, sanMap, ilwiMap, useChangedQty = false) { const sys = data.sysConfig || {}, chnMode = sys.chnMode || "bimok", optOrder = (sys.optOrder || "SML").toUpperCase(); const lookupUnit = (refId) => { const r = ilwiMap[refId] || sanMap[refId] || machMap[refId] || resMap[refId]; if (!r) return { M: 0, L: 0, S: 0, T: 0, name: "미연결", code: "-", spec: "", unit: "" }; const pick = (k, iProp, prop) => "adoptedPrice" in r ? r.kind === k ? r.adoptedPrice : 0 : "intT" in r ? r[iProp] : r[prop]; const M = pick("재료비", "intM", "M"), L = pick("노무비", "intL", "L"), S = pick("경비", "intS", "S"); const meta = { name: r.name, code: r.code, spec: r.spec || "", unit: r.unit || "" }; return Object.assign({ M, L, S, T: M + L + S }, meta); }; let directM = 0, directL = 0, directS = 0, govRawTotal = 0, govFeeTotal = 0; const rows = (data.boqItems || []).map((row) => { const u = lookupUnit(row.refId), qty = Number(useChangedQty && row.changedQty !== undefined ? row.changedQty : row.qty) || 0; const rawM = u.M * qty, rawL = u.L * qty, rawS = u.S * qty, rawT = u.T * qty; let M = roundDown(rawM, 0), L = roundDown(rawL, 0), S = roundDown(rawS, 0), T = M + L + S; let optimizDiff = 0, optimizTarget = ""; if (chnMode === "total") { const targetT = roundDown(rawT, 0); optimizDiff = targetT - (M + L + S); if (optimizDiff !== 0) { const fracMap = { S: rawS - S, M: rawM - M, L: rawL - L }; let bestKey = "S", bestFrac = -1; for (const k of optOrder.split("").filter((c) => ["S", "M", "L"].includes(c))) { if (fracMap[k] > bestFrac) ((bestFrac = fracMap[k]), (bestKey = k)); } if (bestKey === "S") S += optimizDiff; else if (bestKey === "M") M += optimizDiff; else L += optimizDiff; T = targetT; optimizTarget = `${bestKey}(+${optimizDiff})`; } } const aggM = roundHalfUp(rawM, 0), aggL = roundHalfUp(rawL, 0), aggS = roundHalfUp(rawS, 0); if (row.rowType === "gov_mat") govRawTotal += T; else if (row.rowType === "gov_fee") govFeeTotal += T; else ((directM += M), (directL += L), (directS += S)); const out = Object.assign({}, row, { spec: row.spec || u.spec || "", unit: row.unit || u.unit || "", }); const u1 = { unitInfo: u, appliedQty: qty, qty, mUnit: u.M, lUnit: u.L, sUnit: u.S, tUnit: u.T, }; const u2 = { mAmt: M, lAmt: L, sAmt: S, tAmt: T, M, L, S, T, optimizDiff, optimizTarget }; return Object.assign(out, u1, u2, { aggM, aggL, aggS, aggT: aggM + aggL + aggS }); }); const directT = directM + directL + directS, govCombinedRaw = govRawTotal + govFeeTotal; const totals = Object.assign( { directM, directL, directS, directT }, { govRawTotal, govMatTotal: govRawTotal, govFeeTotal, govCombinedRaw }, ); return Object.assign({ rows, directTotal: directT, totals }, totals); } function calcKanjub(data, boqRes, origKanjubRes = null) { const cfg = data.kanjubConfig || {}, scheme = cfg.scheme || "T", rates = cfg.rates || {}, revModes = cfg.revModes || {}; const indTruncDigits = Number(cfg.indirectTruncDigits) || 0; const dM = boqRes.directM, dL = boqRes.directL, dS = boqRes.directS, dT = boqRes.directTotal, govPureMat = boqRes.govRawTotal; const truncInd = (v) => roundDown(v, indTruncDigits); function resolveRow(code, label, baseDesc, baseAmt, ratePct, rawCalcAmt) { const truncAmt = truncInd(rawCalcAmt); const base = Object.assign( { key: code, code, label, formula: baseDesc }, { baseDesc, baseAmt, base: baseAmt, rate: ratePct, ratePct }, ); if (!origKanjubRes) return Object.assign(base, { amount: truncAmt, modeUsed: "원설계" }); const origRow = origKanjubRes.itemMap[code], origAmt = origRow ? origRow.amount : 0, origBase = origRow ? origRow.baseAmt : 0; const mode = revModes[code] || "recalculated"; const finalAmt = mode === "carry_original" ? origAmt : mode === "proportional" ? origAmt + truncInd(origAmt * (origBase > 0 ? (baseAmt - origBase) / origBase : 0)) : mode === "delta_rate" ? origAmt + truncInd(((baseAmt - origBase) * ratePct) / 100) : truncAmt; return Object.assign(base, { amount: finalAmt, origAmt, deltaAmt: finalAmt - origAmt, modeUsed: mode, }); } const items = [], itemMap = {}; const pushItem = (row) => (items.push(row), (itemMap[row.code] = row), row.amount); const pushRate = (code, label, desc, base, defR) => { const r = Number(rates[code]) ?? defR; return pushItem(resolveRow(code, label, desc, base, r, (base * r) / 100)); }; const kanno = pushRate("KANNO", "간접노무비", "직접노무비", dL, 14.5); const totalLabor = dL + kanno; pushRate("SANBO", "산재보험료", "노무비(직노+간노)", totalLabor, 3.56); pushRate("KOBO", "고용보험료", "노무비(직노+간노)", totalLabor, 1.01); const kunbo = pushRate("KUNBO", "건강보험료", "직접노무비", dL, 3.545); pushRate("SLVBO", "노인장기요양보험료", "건강보험료(확정액)", kunbo, 12.95); pushRate("YUNBO", "연금보험료", "직접노무비", dL, 4.5); pushRate("TBUGUM", "퇴직공제부금비", "직접노무비", dL, 0); const rAnjun = Number(rates.ANJUN) ?? 1.86, anjunBaseAdd = Number(cfg.anjunBaseAdd) || 5349000; const anjunGovDiv = cfg.anjunGovVatExcluded !== false ? govPureMat / 1.1 : govPureMat; const anjunBaseNoGov = scheme === "TA" || scheme === "SA" ? dT : dM + dL, anjunBaseWithGov = anjunBaseNoGov + anjunGovDiv; const anjunNormal = truncInd((anjunBaseWithGov * rAnjun) / 100) + anjunBaseAdd; const anjunCap12 = truncInd(((anjunBaseNoGov * rAnjun) / 100 + anjunBaseAdd) * 1.2); const anjunDesc = scheme === "TA" || scheme === "SA" ? "직접공사비+관급원자재/1.1 (+기초액)" : "직재+직노+관급원자재/1.1 (+기초액)"; pushItem( resolveRow( "ANJUN", "산업안전보건관리비", anjunDesc, roundHalfUp(anjunBaseWithGov, 0), rAnjun, cfg.anjunCap12 !== false ? Math.min(anjunNormal, anjunCap12) : anjunNormal, ), ); if (scheme === "S" || scheme === "SA") { const suBase = dM + totalLabor; [ ["S_SUDO", "수도광열비", 0.45], ["S_BOKRI", "복리후생비", 1.15], ["S_SOMO", "소모품·사무용품비", 0.65], ["S_TONGSIN", "여비·교통·통신비", 0.85], ["S_SEGUM", "세금·공과금", 0.55], ["S_PEGI", "폐기물처리비", 0.35], ["S_DOSEO", "도서인쇄비", 0.25], ["S_JIGUP", "지급수수료", 0.45], ].forEach(([sc, sl, defR]) => pushRate(sc, `[수공] ${sl}`, "재료비+노무비(직노+간노)", suBase, defR), ); } else { pushRate( "KITA", "기타경비", scheme === "TA" ? "직접공사비(실적일반)" : "재료비+노무비(직노+간노)", scheme === "TA" ? dT : dM + totalLabor, 6.0, ); } const hwanBase = scheme === "TA" ? dT : dM + dL + dS, hwanDesc = scheme === "TA" ? "직접공사비(실적일반)" : "재료비+직접노무비+산출경비"; pushRate("HWAN", "환경보전비", hwanDesc, hwanBase, 0.8); pushRate("EQIP", "건설기계대여대금 지급보증", hwanDesc, hwanBase, 0.4); const indirectExpenseSum = items .filter((x) => x.code !== "KANNO") .reduce((acc, x) => acc + x.amount, 0); const totalExpense = dS + indirectExpenseSum, pureConstructionCost = dM + totalLabor + totalExpense; const rIlban = Number(rates.ILBAN) ?? 6.0, ilbanBase = scheme === "SA" ? dT + kanno : pureConstructionCost; const ilbanDesc = scheme === "SA" ? "직접공사비+간접노무비(실적수공)" : "순공사원가(재+노+경)", ilbanAmt = truncInd((ilbanBase * rIlban) / 100); const rIyun = Number(rates.IYUN) ?? 15.0; const iyunBase = scheme === "TA" || scheme === "SA" ? pureConstructionCost + ilbanAmt : totalLabor + totalExpense + ilbanAmt; const iyunDesc = scheme === "TA" || scheme === "SA" ? "직접비+간접비+일반관리비(실적형식)" : "노무비+경비+일반관리비(재료비 제외)"; const iyunRaw = truncInd((iyunBase * rIyun) / 100); const vatMode = cfg.vatMode || "supply_10", totJeolDigits = Number(cfg.totJeolDigits) ?? -3; const calcVat = (sVal) => vatMode === "zero" ? 0 : vatMode === "mat_10" ? roundDown(dM * 0.1, 0) : vatMode === "mat_exp_10" ? roundDown((dM + dS) * 0.1, 0) : roundDown(sVal * 0.1, 0); const supplyBeforeAdj = pureConstructionCost + ilbanAmt + iyunRaw, vatBeforeAdj = calcVat(supplyBeforeAdj), contractBeforeAdj = supplyBeforeAdj + vatBeforeAdj; let contractTarget = contractBeforeAdj, diffTotal = 0, pretaxProfitAdj = 0, iyunFinal = iyunRaw, supplyFinal = supplyBeforeAdj, vatFinal = vatBeforeAdj; if (totJeolDigits < 0 && cfg.enableProfitEndAdj !== false) { contractTarget = roundDown(contractBeforeAdj, totJeolDigits); diffTotal = contractBeforeAdj - contractTarget; pretaxProfitAdj = vatMode === "supply_10" ? roundHalfUp(diffTotal / 1.1, 0) : diffTotal; iyunFinal = iyunRaw - pretaxProfitAdj; supplyFinal = pureConstructionCost + ilbanAmt + iyunFinal; vatFinal = calcVat(supplyFinal); if (supplyFinal + vatFinal !== contractTarget) vatFinal = contractTarget - supplyFinal; } const contractFinal = supplyFinal + vatFinal; const govFinal = boqRes.govCombinedRaw > 0 ? applyRoundMode( boqRes.govCombinedRaw, cfg.govRoundMode || "up", Number(cfg.govRoundDigits) ?? -3, ) : 0; const grandTotal = contractFinal + govFinal; const mkLine = (key, label, formula, baseAmt, rate, amount) => Object.fromEntries([ ["key", key], ["label", label], ["formula", formula], ["baseAmt", baseAmt], ["base", baseAmt], ["rate", rate], ["amount", amount], ]); const totStr = "재료비 + 노무비 + 경비", iyunStr = `${iyunDesc} (보정 -${fmt(pretaxProfitAdj)})`, supStr = "순공사원가 + 일반관리비 + 이윤", finStr = "공급가액 + 부가가치세 (천원절사)", govStr = `관급원자재(${fmt(boqRes.govRawTotal)}) + 수수료(${fmt(boqRes.govFeeTotal)})`; const pCost = pureConstructionCost; const lines = [ mkLine("DIRECT_M", "직접재료비", "내역서 재료비 합계", dM, null, dM), mkLine("DIRECT_L", "직접노무비", "내역서 직접노무비 합계", dL, null, dL), mkLine("DIRECT_S", "산출경비", "내역서 산출경비 합계", dS, null, dS), mkLine("DIRECT_COST", "직접공사비 계", "직재 + 직노 + 산출경비", dT, null, dT), ...items.map((it) => mkLine(it.key, it.label, it.formula, it.baseAmt, it.rate, it.amount)), mkLine("TOTAL_COST", "순공사원가 계", totStr, pCost, null, pCost), mkLine("ILBAN", "일반관리비", ilbanDesc, ilbanBase, rIlban, ilbanAmt), mkLine("IYUN", "이윤 (끝전보정 후)", iyunStr, iyunBase, rIyun, iyunFinal), mkLine("SUPPLY_VAL", "총원가 (공급가액)", supStr, supplyFinal, null, supplyFinal), mkLine("VAT", "부가가치세", "공급가액 × 10%", supplyFinal, 10, vatFinal), mkLine("FINAL_SUPPLY", "도급공사비", finStr, contractBeforeAdj, null, contractFinal), mkLine("GOV_MAT", "관급자재대", govStr, boqRes.govCombinedRaw, null, govFinal), mkLine("GRAND_TOTAL", "총공사비 (COSTW1)", "도급 + 관급", grandTotal, null, grandTotal), ]; const out = { scheme, dM, dL, dS, dT, kanno, totalLabor }; Object.assign(out, { indirectExpenseSum, totalExpense, pureConstructionCost: pCost }); Object.assign(out, { ilbanBase, ilbanDesc, ilbanAmt, iyunBase, iyunDesc, iyunRaw }); Object.assign(out, { pretaxProfitAdj, iyunFinal, supplyBeforeAdj, vatBeforeAdj }); Object.assign(out, { contractBeforeAdj, diffTotal, supplyFinal, vatFinal, contractFinal }); Object.assign(out, { govRawTotal: boqRes.govRawTotal, govFeeTotal: boqRes.govFeeTotal }); Object.assign(out, { govFinal, grandTotal, items, itemMap, lines }); Object.assign(out, { DIRECT_COST: dT, KANNO: kanno, IND_EXP: kanno + indirectExpenseSum }); Object.assign(out, { TOTAL_COST: pCost, ILBAN: ilbanAmt, IYUN_INIT: iyunRaw }); Object.assign(out, { PROFIT_ADJ: -pretaxProfitAdj, IYUN: iyunFinal, SUPPLY_VAL: supplyFinal }); return Object.assign(out, { VAT: vatFinal, FINAL_SUPPLY: contractFinal, GOV_MAT: govFinal, GRAND_TOTAL: grandTotal, }); } function evalKOrdTransition(pclass, regMode, flag, currentK) { const K = Number(currentK) || 1; const nextK = Number(regMode) === 6 ? Number(pclass) === 6 ? K + 1 : 1 : Number(regMode) === 7 && Number(pclass) === 7 && Number(flag) === 0 ? K + 1 : 1; return { nextK, downK: K - (K > 5 ? K - 4 : 1) }; } function createDefaultState() { const mkRes = (id, name, spec, unit, kind, val) => { const r = { id, code: id, name, spec, unit, kind }; Object.assign(r, { candidates: [val, 0, 0, 0, 0], adoptMode: "manual", manualPrice: val }); return Object.assign(r, { convertHourly: false, isUsd: false }); }; const mkBoq = (id, code, name, spec, unit, refId, rowType, _recno) => { const b = { id, code, name, spec, unit, refId }; return Object.assign(b, { rowType, qty: 1, changedQty: 1, _recno }); }; const ratePairs = "KANNO:14.5,SANBO:3.56,KOBO:1.01,KUNBO:3.545,SLVBO:12.95,YUNBO:4.5,TBUGUM:0,ANJUN:1.86,KITA:6,HWAN:0.8,EQIP:0.4,ILBAN:6,IYUN:15,S_SUDO:0.45,S_BOKRI:1.15,S_SOMO:0.65,S_TONGSIN:0.85,S_SEGUM:0.55,S_PEGI:0.35,S_DOSEO:0.25,S_JIGUP:0.45"; const rates = Object.fromEntries( ratePairs.split(",").map((p) => [p.split(":")[0], Number(p.split(":")[1])]), ); const revKeys = "KANNO,SANBO,KOBO,KUNBO,SLVBO,YUNBO,TBUGUM,ANJUN,KITA,HWAN,EQIP".split(","); const revModes = Object.fromEntries( revKeys.map((k) => [ k, ["KUNBO", "SLVBO", "YUNBO"].includes(k) ? "carry_original" : "recalculated", ]), ); const projectMeta = { pname: "2024년 산림유역관리사업(기번41, 국비)", pnum: "2024년 단가", pcode: "11", }; Object.assign(projectMeta, { pclass: 0, kOrd: 0, tcost1: 781283415, costw1: 1414283000 }); const sysConfig = { rxFormula: "1/8*16/12*25/20", condCodes: "L00008, L00009, L00010", usdRate1: 1350, usdRate2: 1400, }; Object.assign(sysConfig, { useChangedUsd: false, ecdDigits: 0, chnMode: "bimok", optOrder: "SML", }); sysConfig.kOrdSim = { pclass: 7, regMode: 7, flag: 0, currentK: 2 }; const kanjubConfig = { scheme: "T", indirectTruncDigits: 0, anjunBaseAdd: 5349000, anjunGovVatExcluded: true, }; Object.assign(kanjubConfig, { anjunCap12: true, vatMode: "supply_10", totJeolDigits: -3, enableProfitEndAdj: true, }); Object.assign(kanjubConfig, { govRoundMode: "up", govRoundDigits: -3, rates, revModes }); const dName = "2024년 산림유역관리 본공사 종합"; const dLines = ["B_MAT", "B_LAB", "B_EXP"].map((refId) => ({ refId, qty: 1, halPercent: 0, devd: false, })); const d00014 = { id: "D00014", code: "D00014", name: dName, rawName: dName }; Object.assign(d00014, { prefix: "D", spec: "산림토목", unit: "식", lines: dLines }); const b1Name = "2024년 산림유역관리 본공사 일체"; return { projectMeta, sysConfig, resources: [ mkRes("B_MAT", "봉화군 산림유역 직접재료비", "식", "식", "재료비", 191867113), mkRes("B_LAB", "봉화군 산림유역 직접노무비", "식", "식", "노무비", 426802539), mkRes("B_EXP", "봉화군 산림유역 산출경비", "식", "식", "경비", 162613763), mkRes("G_MAT", "봉화군 관급자재 원자재 합계", "C4 제외구역", "식", "재료비", 142783280), mkRes("G_FEE", "봉화군 관급 조달수수료", "C4 제외구역", "식", "경비", 771029), ], machines: [], sanItems: [], ilwiItems: [d00014], boqItems: [ mkBoq("BOQ_1", "D00014", b1Name, "산림토목", "식", "D00014", "normal", 1), mkBoq("BOQ_2", "G_MAT", "[제외 C4] 관급자재 원자재", "관급", "식", "G_MAT", "gov_mat", 2), mkBoq("BOQ_3", "G_FEE", "[제외 C4] 관급 조달수수료", "관급", "식", "G_FEE", "gov_fee", 3), ], kanjubConfig, }; } function calculateAll(data) { const resources = calcResources(data), machines = calcMachines(data, resources.map), sanCal = calcSanCal(data, machines.map); const ilwi = calcIlwi(data, resources.map, machines.map, sanCal.map); const boqOrig = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, false), kanjubOrig = calcKanjub(data, boqOrig, null); const boqRev = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, true), kanjubRev = calcKanjub(data, boqRev, kanjubOrig); const revMap = Object.fromEntries((boqRev.rows || []).map((r) => [r.id, r])); (boqOrig.rows || []).forEach((r) => { const rv = revMap[r.id]; r.changedTAmt = rv ? rv.tAmt : r.tAmt; r.diffTAmt = r.changedTAmt - r.tAmt; }); const base = { resources, machines, sanCal, ilwi, boqOrig, kanjubOrig }; Object.assign(base, { boqRev, kanjubRev, resList: resources.list, revComputed: { revBoq: boqRev, revKj: kanjubRev }, }); return Object.assign(base, { boqComputed: boqOrig, kanjubComputed: kanjubOrig }); } const evalLeftToRight = (exprStr) => evalWageFormulaLeftToRight(1, exprStr).coeff || 0; const api = { KINDS, KIND_KEYS, num, fmt, evalFormula, evalLeftToRight }; Object.assign(api, { roundDown, roundUp, roundHalfUp, roundBanker }); Object.assign(api, { evalWageFormulaLeftToRight, evalQFormula, evalKOrdTransition }); return Object.assign(api, { createDefaultState, calculateAll, computeAll: calculateAll }); })(); if (typeof window !== "undefined") window.STMATE_CALC = StmateCalc; if (typeof globalThis !== "undefined") globalThis.STMATE_CALC = StmateCalc; if (typeof module !== "undefined" && module.exports) module.exports = StmateCalc;