691 lines
28 KiB
JavaScript
691 lines
28 KiB
JavaScript
/* -*- 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;
|