675 lines
22 KiB
JavaScript
675 lines
22 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* STMATE 계산 사슬 및 제어 옵션 시뮬레이터 엔진 (calc.js)
|
|
* 근거: resources/knowledge/original/경쟁사 프로그램/STmate 분석/ (REA 정적분석 및 골든셋 회귀)
|
|
*/
|
|
const StmateCalc = (() => {
|
|
const KINDS = ["재료비", "노무비", "경비"];
|
|
const KIND_KEYS = { 재료비: "M", 노무비: "L", 경비: "S" };
|
|
|
|
// MROUNDUD(selector=0): 0 방향 절사 (ROUNDDOWN)
|
|
function roundDown(val, digits = 0) {
|
|
const n = Number(val) || 0;
|
|
const f = Math.pow(10, digits);
|
|
const s = n * f;
|
|
return Math.trunc(s + (s >= 0 ? 1e-9 : -1e-9)) / f;
|
|
}
|
|
|
|
// MROUNDUP: 0에서 먼 방향 올림 (ROUNDUP)
|
|
function roundUp(val, digits = 0) {
|
|
const n = Number(val) || 0;
|
|
if (n === 0) return 0;
|
|
const f = Math.pow(10, digits);
|
|
const s = n * f;
|
|
return (n > 0 ? Math.ceil(s - 1e-9) : Math.floor(s + 1e-9)) / f;
|
|
}
|
|
|
|
// 사사오입 (Excel ROUND / 세전 끝전조정 / 집계표)
|
|
function roundHalfUp(val, digits = 0) {
|
|
const n = Number(val) || 0;
|
|
const f = Math.pow(10, digits);
|
|
const r = Math.floor(Math.abs(n) * f + 0.5 + 1e-9) / f;
|
|
return n < 0 ? -r : r;
|
|
}
|
|
|
|
// 은행가 반올림 (Delphi System.Round 최근접 짝수)
|
|
function roundBanker(val, digits = 0) {
|
|
const n = Number(val) || 0;
|
|
const f = Math.pow(10, digits);
|
|
const s = n * f;
|
|
const 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) {
|
|
if (mode === "up") return roundUp(val, digits);
|
|
if (mode === "half") return roundHalfUp(val, digits);
|
|
if (mode === "banker") return roundBanker(val, digits);
|
|
return roundDown(val, digits);
|
|
}
|
|
|
|
// 1. 노임 시간당 환산식 좌→우 순차 평가 (RXNM_ = "1/8*16/12*25/20", 18번 문서 §2.1)
|
|
function evalWageFormulaLeftToRight(dailyWage, exprStr) {
|
|
const base = Number(dailyWage) || 0;
|
|
const clean = String(exprStr || "").replace(/\s+/g, "");
|
|
if (!clean) return { seqResult: base, preCalcResult: base, steps: [] };
|
|
const tokens = clean.match(/([*/])?(\d+(?:\.\d+)?)/g) || [];
|
|
let acc = base,
|
|
coeff = 1;
|
|
const steps = [];
|
|
for (const tok of tokens) {
|
|
const op = tok[0] === "*" || tok[0] === "/" ? tok[0] : "*";
|
|
const num = parseFloat(op === tok[0] ? tok.slice(1) : tok);
|
|
if (!Number.isFinite(num) || num === 0) continue;
|
|
acc = op === "*" ? acc * num : acc / num;
|
|
coeff = op === "*" ? coeff * num : coeff / num;
|
|
steps.push(`${op}${num} → ${roundHalfUp(acc, 4)}`);
|
|
}
|
|
return { seqResult: roundDown(acc, 0), preCalcResult: Math.trunc(base * coeff), coeff, steps };
|
|
}
|
|
|
|
// 2. 기초자원 후보단가 채택 및 노임 환산 처리
|
|
function calcResources(data) {
|
|
const sys = data.sysConfig || {};
|
|
const 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;
|
|
const 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 = 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} 고정`;
|
|
} else {
|
|
basePrice = Number(res.manualPrice) || 0;
|
|
chosenLabel = "수동입력";
|
|
}
|
|
const krwPrice = res.isUsd ? roundDown(basePrice * usdRate, ecdDigits) : basePrice;
|
|
let adoptedPrice = krwPrice,
|
|
wageInfo = null;
|
|
if (res.kind === "노무비" && (res.convertHourly || condCodes.has(res.code))) {
|
|
wageInfo = evalWageFormulaLeftToRight(krwPrice, rxFormula);
|
|
adoptedPrice = wageInfo.seqResult;
|
|
}
|
|
const item = { ...res, cands, basePrice, adoptedPrice, chosenLabel, wageInfo };
|
|
map[res.id] = item;
|
|
map[res.code] = item;
|
|
return item;
|
|
});
|
|
return { list, map };
|
|
}
|
|
|
|
// 3. 중기 시간당 사용료(X 코드) 계산 (17번 문서 §3)
|
|
function calcMachines(data, resMap) {
|
|
const map = {};
|
|
const list = (data.machines || []).map((m) => {
|
|
const opHourly = resMap[m.operatorId]?.adoptedPrice || 0;
|
|
const fuelPrice = resMap[m.fuelId]?.adoptedPrice || 0;
|
|
const rawFuel = (Number(m.fuelConsumption) || 0) * fuelPrice;
|
|
const M = roundDown(rawFuel * (1 + (Number(m.miscRatePercent) || 0) / 100), 0);
|
|
const L = roundDown(opHourly * (Number(m.operatorCount) || 1), 0);
|
|
const S = roundDown((Number(m.acquisitionPrice) || 0) * (Number(m.hourlyRateFactor) || 0), 0);
|
|
const item = { ...m, M, L, S, T: M + L + S, opHourly, fuelPrice };
|
|
map[m.id] = item;
|
|
map[m.code] = item;
|
|
return item;
|
|
});
|
|
return { list, map };
|
|
}
|
|
|
|
// 4. 단가산출근거(D 코드, Q식) 계산 (18번 문서 §2.2)
|
|
function evalQFormula(dItem) {
|
|
const q = Number(dItem.q) || 0,
|
|
K = Number(dItem.K) || 0,
|
|
f = Number(dItem.f) || 0;
|
|
const E = Number(dItem.E) || 0,
|
|
Cm = Number(dItem.Cm) || 1;
|
|
const 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];
|
|
const { rawQ, Q } = evalQFormula(d);
|
|
const M = mach && Q > 0 ? roundDown(mach.M / Q, 1) : 0;
|
|
const L = mach && Q > 0 ? roundDown(mach.L / Q, 1) : 0;
|
|
const S = mach && Q > 0 ? roundDown(mach.S / Q, 1) : 0;
|
|
const intM = roundDown(M, 0),
|
|
intL = roundDown(L, 0),
|
|
intS = roundDown(S, 0);
|
|
const item = {
|
|
...d,
|
|
mach,
|
|
rawQ,
|
|
Q,
|
|
M,
|
|
L,
|
|
S,
|
|
T: roundHalfUp(M + L + S, 1),
|
|
intM,
|
|
intL,
|
|
intS,
|
|
intT: intM + intL + intS,
|
|
};
|
|
map[d.id] = item;
|
|
map[d.code] = item;
|
|
return item;
|
|
});
|
|
return { list, map };
|
|
}
|
|
|
|
// 5. 일위대가(B 코드) 조립 (17번 문서 §2, 30번 문서 DEVD1·X_HAL)
|
|
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];
|
|
const 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);
|
|
let uM = 0,
|
|
uL = 0,
|
|
uS = 0;
|
|
if (ref) {
|
|
if ("adoptedPrice" in ref) {
|
|
if (ref.kind === "재료비") uM = ref.adoptedPrice;
|
|
else if (ref.kind === "노무비") uL = ref.adoptedPrice;
|
|
else uS = ref.adoptedPrice;
|
|
} else {
|
|
uM = ref.M || 0;
|
|
uL = ref.L || 0;
|
|
uS = ref.S || 0;
|
|
}
|
|
}
|
|
const lineM = roundDown(uM * appliedQty, 2),
|
|
lineL = roundDown(uL * appliedQty, 2),
|
|
lineS = roundDown(uS * appliedQty, 2);
|
|
sumM += lineM;
|
|
sumL += lineL;
|
|
sumS += lineS;
|
|
return {
|
|
...ln,
|
|
refName: ref?.name || "미연결",
|
|
refCode: ref?.code || "-",
|
|
appliedQty,
|
|
uM,
|
|
uL,
|
|
uS,
|
|
lineM,
|
|
lineL,
|
|
lineS,
|
|
lineT: roundHalfUp(lineM + lineL + lineS, 2),
|
|
};
|
|
});
|
|
const M = roundDown(sumM, 0),
|
|
L = roundDown(sumL, 0),
|
|
S = roundDown(sumS, 0);
|
|
const item = { ...ilwi, lines, M, L, S, T: M + L + S };
|
|
map[ilwi.id] = item;
|
|
map[ilwi.code] = item;
|
|
return item;
|
|
});
|
|
return { list, map };
|
|
}
|
|
|
|
// 6. 내역서(BOQ) 및 ST_Func_C::OPTIMIZ 차액보정 계산 (15단계 메모, 16번 문서)
|
|
function calcBoq(data, resMap, machMap, sanMap, ilwiMap, useChangedQty = false) {
|
|
const sys = data.sysConfig || {};
|
|
const chnMode = sys.chnMode || "bimok";
|
|
const 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: "-" };
|
|
if ("adoptedPrice" in r) {
|
|
const M = r.kind === "재료비" ? r.adoptedPrice : 0,
|
|
L = r.kind === "노무비" ? r.adoptedPrice : 0,
|
|
S = r.kind === "경비" ? r.adoptedPrice : 0;
|
|
return { M, L, S, T: M + L + S, name: r.name, code: r.code, unit: r.unit };
|
|
}
|
|
if ("intT" in r)
|
|
return {
|
|
M: r.intM,
|
|
L: r.intL,
|
|
S: r.intS,
|
|
T: r.intT,
|
|
name: r.name,
|
|
code: r.code,
|
|
unit: r.unit,
|
|
};
|
|
return { M: r.M, L: r.L, S: r.S, T: r.T, name: r.name, code: r.code, unit: r.unit };
|
|
};
|
|
|
|
let directM = 0,
|
|
directL = 0,
|
|
directS = 0,
|
|
govRawTotal = 0,
|
|
govFeeTotal = 0;
|
|
const rows = (data.boqItems || []).map((row) => {
|
|
const u = lookupUnit(row.refId);
|
|
const 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;
|
|
}
|
|
return {
|
|
...row,
|
|
unitInfo: u,
|
|
appliedQty: qty,
|
|
M,
|
|
L,
|
|
S,
|
|
T,
|
|
optimizDiff,
|
|
optimizTarget,
|
|
aggM,
|
|
aggL,
|
|
aggS,
|
|
aggT: aggM + aggL + aggS,
|
|
};
|
|
});
|
|
return {
|
|
rows,
|
|
directM,
|
|
directL,
|
|
directS,
|
|
directTotal: directM + directL + directS,
|
|
govRawTotal,
|
|
govFeeTotal,
|
|
govCombinedRaw: govRawTotal + govFeeTotal,
|
|
};
|
|
}
|
|
|
|
// 7. 제잡비 및 원가계산서 (ST_KanJub.CPM :: Calc_Kanjub, 06·12·16번 문서)
|
|
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);
|
|
if (!origKanjubRes)
|
|
return { code, label, baseDesc, baseAmt, ratePct, amount: truncAmt, modeUsed: "원설계" };
|
|
const origRow = origKanjubRes.itemMap[code];
|
|
const origAmt = origRow ? origRow.amount : 0,
|
|
origBase = origRow ? origRow.baseAmt : 0;
|
|
const mode = revModes[code] || "recalculated";
|
|
let finalAmt = truncAmt;
|
|
if (mode === "carry_original") finalAmt = origAmt;
|
|
else if (mode === "proportional")
|
|
finalAmt =
|
|
origAmt + truncInd(origAmt * (origBase > 0 ? (baseAmt - origBase) / origBase : 0));
|
|
else if (mode === "delta_rate")
|
|
finalAmt = origAmt + truncInd(((baseAmt - origBase) * ratePct) / 100);
|
|
return {
|
|
code,
|
|
label,
|
|
baseDesc,
|
|
baseAmt,
|
|
ratePct,
|
|
amount: finalAmt,
|
|
origAmt,
|
|
deltaAmt: finalAmt - origAmt,
|
|
modeUsed: mode,
|
|
};
|
|
}
|
|
|
|
const items = [],
|
|
itemMap = {};
|
|
const pushItem = (row) => {
|
|
items.push(row);
|
|
itemMap[row.code] = row;
|
|
return row.amount;
|
|
};
|
|
|
|
const rKanno = Number(rates.KANNO) ?? 14.5;
|
|
const kanno = pushItem(
|
|
resolveRow("KANNO", "간접노무비", "직접노무비", dL, rKanno, (dL * rKanno) / 100),
|
|
);
|
|
const totalLabor = dL + kanno;
|
|
|
|
pushItem(
|
|
resolveRow(
|
|
"SANBO",
|
|
"산재보험료",
|
|
"노무비(직노+간노)",
|
|
totalLabor,
|
|
Number(rates.SANBO) ?? 3.56,
|
|
(totalLabor * (Number(rates.SANBO) ?? 3.56)) / 100,
|
|
),
|
|
);
|
|
pushItem(
|
|
resolveRow(
|
|
"KOBO",
|
|
"고용보험료",
|
|
"노무비(직노+간노)",
|
|
totalLabor,
|
|
Number(rates.KOBO) ?? 1.01,
|
|
(totalLabor * (Number(rates.KOBO) ?? 1.01)) / 100,
|
|
),
|
|
);
|
|
const kunbo = pushItem(
|
|
resolveRow(
|
|
"KUNBO",
|
|
"건강보험료",
|
|
"직접노무비",
|
|
dL,
|
|
Number(rates.KUNBO) ?? 3.545,
|
|
(dL * (Number(rates.KUNBO) ?? 3.545)) / 100,
|
|
),
|
|
);
|
|
pushItem(
|
|
resolveRow(
|
|
"SLVBO",
|
|
"노인장기요양보험료",
|
|
"건강보험료(확정액)",
|
|
kunbo,
|
|
Number(rates.SLVBO) ?? 12.95,
|
|
(kunbo * (Number(rates.SLVBO) ?? 12.95)) / 100,
|
|
),
|
|
);
|
|
pushItem(
|
|
resolveRow(
|
|
"YUNBO",
|
|
"연금보험료",
|
|
"직접노무비",
|
|
dL,
|
|
Number(rates.YUNBO) ?? 4.5,
|
|
(dL * (Number(rates.YUNBO) ?? 4.5)) / 100,
|
|
),
|
|
);
|
|
pushItem(
|
|
resolveRow(
|
|
"TBUGUM",
|
|
"퇴직공제부금비",
|
|
"직접노무비",
|
|
dL,
|
|
Number(rates.TBUGUM) ?? 0,
|
|
(dL * (Number(rates.TBUGUM) ?? 0)) / 100,
|
|
),
|
|
);
|
|
|
|
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;
|
|
const anjunBaseWithGov = anjunBaseNoGov + anjunGovDiv;
|
|
const anjunNormal = truncInd((anjunBaseWithGov * rAnjun) / 100) + anjunBaseAdd;
|
|
const anjunCap12 = truncInd(((anjunBaseNoGov * rAnjun) / 100 + anjunBaseAdd) * 1.2);
|
|
const anjunRaw = cfg.anjunCap12 !== false ? Math.min(anjunNormal, anjunCap12) : anjunNormal;
|
|
pushItem(
|
|
resolveRow(
|
|
"ANJUN",
|
|
"산업안전보건관리비",
|
|
scheme === "TA" || scheme === "SA"
|
|
? "직접공사비+관급원자재/1.1 (+기초액)"
|
|
: "직재+직노+관급원자재/1.1 (+기초액)",
|
|
roundHalfUp(anjunBaseWithGov, 0),
|
|
rAnjun,
|
|
anjunRaw,
|
|
),
|
|
);
|
|
|
|
if (scheme === "S" || scheme === "SA") {
|
|
const suBase = dM + totalLabor;
|
|
for (const [sc, sl, sr] of [
|
|
["S_SUDO", "수도광열비", Number(rates.S_SUDO) ?? 0.45],
|
|
["S_BOKRI", "복리후생비", Number(rates.S_BOKRI) ?? 1.15],
|
|
["S_SOMO", "소모품·사무용품비", Number(rates.S_SOMO) ?? 0.65],
|
|
["S_TONGSIN", "여비·교통·통신비", Number(rates.S_TONGSIN) ?? 0.85],
|
|
["S_SEGUM", "세금·공과금", Number(rates.S_SEGUM) ?? 0.55],
|
|
["S_PEGI", "폐기물처리비", Number(rates.S_PEGI) ?? 0.35],
|
|
["S_DOSEO", "도서인쇄비", Number(rates.S_DOSEO) ?? 0.25],
|
|
["S_JIGUP", "지급수수료", Number(rates.S_JIGUP) ?? 0.45],
|
|
])
|
|
pushItem(
|
|
resolveRow(
|
|
sc,
|
|
`[수공] ${sl}`,
|
|
"재료비+노무비(직노+간노)",
|
|
suBase,
|
|
sr,
|
|
(suBase * sr) / 100,
|
|
),
|
|
);
|
|
} else {
|
|
const rKita = Number(rates.KITA) ?? 6.0,
|
|
kitaBase = scheme === "TA" ? dT : dM + totalLabor;
|
|
pushItem(
|
|
resolveRow(
|
|
"KITA",
|
|
"기타경비",
|
|
scheme === "TA" ? "직접공사비(실적일반)" : "재료비+노무비(직노+간노)",
|
|
kitaBase,
|
|
rKita,
|
|
(kitaBase * rKita) / 100,
|
|
),
|
|
);
|
|
}
|
|
|
|
const hwanBase = scheme === "TA" ? dT : dM + dL + dS,
|
|
hwanDesc = scheme === "TA" ? "직접공사비(실적일반)" : "재료비+직접노무비+산출경비";
|
|
pushItem(
|
|
resolveRow(
|
|
"HWAN",
|
|
"환경보전비",
|
|
hwanDesc,
|
|
hwanBase,
|
|
Number(rates.HWAN) ?? 0.8,
|
|
(hwanBase * (Number(rates.HWAN) ?? 0.8)) / 100,
|
|
),
|
|
);
|
|
pushItem(
|
|
resolveRow(
|
|
"EQIP",
|
|
"건설기계대여대금 지급보증",
|
|
hwanDesc,
|
|
hwanBase,
|
|
Number(rates.EQIP) ?? 0.4,
|
|
(hwanBase * (Number(rates.EQIP) ?? 0.4)) / 100,
|
|
),
|
|
);
|
|
|
|
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" ? "직접공사비+간접노무비(실적수공)" : "순공사원가(재+노+경)";
|
|
const 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;
|
|
|
|
return {
|
|
scheme,
|
|
dM,
|
|
dL,
|
|
dS,
|
|
dT,
|
|
kanno,
|
|
totalLabor,
|
|
indirectExpenseSum,
|
|
totalExpense,
|
|
pureConstructionCost,
|
|
ilbanBase,
|
|
ilbanDesc,
|
|
ilbanAmt,
|
|
iyunBase,
|
|
iyunDesc,
|
|
iyunRaw,
|
|
pretaxProfitAdj,
|
|
iyunFinal,
|
|
supplyBeforeAdj,
|
|
vatBeforeAdj,
|
|
contractBeforeAdj,
|
|
diffTotal,
|
|
supplyFinal,
|
|
vatFinal,
|
|
contractFinal,
|
|
govRawTotal: boqRes.govRawTotal,
|
|
govFeeTotal: boqRes.govFeeTotal,
|
|
govFinal,
|
|
grandTotal: contractFinal + govFinal,
|
|
items,
|
|
itemMap,
|
|
};
|
|
}
|
|
|
|
// 8. K_ORD 등록·차수내리기 전이 규칙 시뮬레이션 (STMATE_REA_최종보고서 §4.1)
|
|
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 calculateAll(data) {
|
|
const resources = calcResources(data);
|
|
const machines = calcMachines(data, resources.map);
|
|
const 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);
|
|
const kanjubOrig = calcKanjub(data, boqOrig, null);
|
|
const boqRev = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, true);
|
|
const kanjubRev = calcKanjub(data, boqRev, kanjubOrig);
|
|
return { resources, machines, sanCal, ilwi, boqOrig, kanjubOrig, boqRev, kanjubRev };
|
|
}
|
|
|
|
return {
|
|
KINDS,
|
|
KIND_KEYS,
|
|
roundDown,
|
|
roundUp,
|
|
roundHalfUp,
|
|
roundBanker,
|
|
evalWageFormulaLeftToRight,
|
|
evalQFormula,
|
|
evalKOrdTransition,
|
|
calculateAll,
|
|
};
|
|
})();
|
|
|
|
if (typeof module !== "undefined" && module.exports) module.exports = StmateCalc;
|