Files

691 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* -*- 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;