567 lines
22 KiB
JavaScript
567 lines
22 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
|
/**
|
|
* STMATE 계산 사슬 및 제어 옵션 시뮬레이터 엔진 (calc.js)
|
|
* 근거: resources/knowledge/original/경쟁사 프로그램/STmate/ (old/20_분석 · 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));
|
|
}
|
|
|
|
// 자리 맞춤 — 음수 자리(십·백·천원)는 나눗셈으로 정수 결과를 냄(10^-n 곱셈 오차 방지)
|
|
const scaleOf = (digits) =>
|
|
digits >= 0
|
|
? { mul: (v) => v * Math.pow(10, digits), back: (s) => s / Math.pow(10, digits) }
|
|
: { mul: (v) => v / Math.pow(10, -digits), back: (s) => s * Math.pow(10, -digits) };
|
|
|
|
function roundDown(val, digits = 0) {
|
|
const n = Number(val) || 0,
|
|
f = scaleOf(digits),
|
|
s = f.mul(n);
|
|
return f.back(Math.trunc(s + (s >= 0 ? 1e-9 : -1e-9)));
|
|
}
|
|
|
|
function roundUp(val, digits = 0) {
|
|
const n = Number(val) || 0;
|
|
if (n === 0) return 0;
|
|
const f = scaleOf(digits),
|
|
s = f.mul(n);
|
|
return f.back(n > 0 ? Math.ceil(s - 1e-9) : Math.floor(s + 1e-9));
|
|
}
|
|
|
|
function roundHalfUp(val, digits = 0) {
|
|
const n = Number(val) || 0,
|
|
f = scaleOf(digits);
|
|
const r = f.back(Math.floor(f.mul(Math.abs(n)) + 0.5 + 1e-9));
|
|
return n < 0 ? -r : r;
|
|
}
|
|
|
|
// 은행가 반올림(Delphi RoundTo · FPU 최근접-짝수 — 11번 §4.2)
|
|
function roundBanker(val, digits = 0) {
|
|
const n = Number(val) || 0,
|
|
f = scaleOf(digits),
|
|
s = f.mul(n),
|
|
fl = Math.floor(s);
|
|
if (Math.abs(s - fl - 0.5) < 1e-9) return f.back(fl % 2 === 0 ? fl : fl + 1);
|
|
return f.back(Math.round(s));
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// 원가계산서(제잡비) 계산 — kanjub_calc.js
|
|
const calcKanjub = (globalThis.STMATE_KANJUB_FACTORY || require("./kanjub_calc.js"))({
|
|
num,
|
|
fmt,
|
|
roundDown,
|
|
roundUp,
|
|
roundHalfUp,
|
|
roundBanker,
|
|
applyRoundMode,
|
|
});
|
|
|
|
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 };
|
|
}
|
|
|
|
// 총액우선 끝전 배분(OPTIMIZ) — 성분 절사 · 총액 절사와의 차 1원씩 소수부 큰 성분부터(동률 = 순서 앞)
|
|
function optimize(raw, rawT, order) {
|
|
const out = { M: roundDown(raw.M, 0), L: roundDown(raw.L, 0), S: roundDown(raw.S, 0) };
|
|
const keys = String(order || "SML")
|
|
.toUpperCase()
|
|
.split("")
|
|
.filter((k) => "MLS".includes(k));
|
|
const frac = Object.fromEntries(keys.map((k) => [k, roundHalfUp(raw[k] - out[k], 6)]));
|
|
let rest = roundDown(rawT, 0) - out.M - out.L - out.S;
|
|
while (rest > 0) {
|
|
let pick = null;
|
|
for (const k of keys) if (frac[k] < 1 && (pick === null || frac[k] > frac[pick])) pick = k;
|
|
if (pick === null) break;
|
|
((out[pick] += 1), (frac[pick] += 1), (rest -= 1));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// 내역 비율 줄(PR · 조달수수료 등) — 밑수 비목(PRIN) 누적 × 율(QTY, %) → 대상 비목(PROUT) 원 미만 절사
|
|
// 단가 칸 = 밑수 · 수량 칸 = 율 (원본 BOQ MAMT1 · QTY1 · M_AMT1 저장 꼴) · PRIN 없는 줄은 일반 줄
|
|
const PR_COMP = { 1: "M", 2: "L", 3: "S", 4: "T" };
|
|
function prRow(row, slot, base) {
|
|
if (String(row.code || "").trim() !== "PR") return null;
|
|
const bk = PR_COMP[String(row.PRIN ?? "").trim()],
|
|
ok = PR_COMP[String(row.PROUT ?? "").trim()];
|
|
if (!bk || !ok) return null;
|
|
const pct = num((row.prQtys || [])[slot - 1]);
|
|
const bv = bk === "T" ? base.M + base.L + base.S : base[bk];
|
|
const to = ok === "T" ? "M" : ok;
|
|
const a = { M: 0, L: 0, S: 0 },
|
|
u = { M: 0, L: 0, S: 0 };
|
|
((a[to] = roundDown((bv * pct) / 100, 0)), (u[to] = bv));
|
|
const T = a.M + a.L + a.S;
|
|
const unitInfo = {
|
|
M: u.M,
|
|
L: u.L,
|
|
S: u.S,
|
|
T: bv,
|
|
name: row.name,
|
|
code: "PR",
|
|
spec: "",
|
|
unit: "%",
|
|
};
|
|
const out = {
|
|
unitInfo,
|
|
appliedQty: pct,
|
|
qty: pct,
|
|
mUnit: u.M,
|
|
lUnit: u.L,
|
|
sUnit: u.S,
|
|
tUnit: bv,
|
|
};
|
|
Object.assign(out, { mAmt: a.M, lAmt: a.L, sAmt: a.S, tAmt: T, M: a.M, L: a.L, S: a.S, T });
|
|
Object.assign(out, {
|
|
optimizDiff: 0,
|
|
optimizTarget: "",
|
|
aggM: a.M,
|
|
aggL: a.L,
|
|
aggS: a.S,
|
|
aggT: T,
|
|
});
|
|
return { M: a.M, L: a.L, S: a.S, T, out };
|
|
}
|
|
|
|
function calcBoq(data, resMap, machMap, sanMap, ilwiMap, useChangedQty = false) {
|
|
const slot = typeof useChangedQty === "number" ? useChangedQty : useChangedQty ? 2 : 1;
|
|
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;
|
|
// 내역 비율 줄(PR) 밑수 — 표시(PRPT) 줄 금액 누적 · PR 줄을 지나면 비움(영월 조달수수료 4줄 대조)
|
|
let prBase = { M: 0, L: 0, S: 0 };
|
|
const rows = (data.boqItems || []).map((row) => {
|
|
const pr = prRow(row, slot, prBase);
|
|
if (pr) {
|
|
prBase = { M: 0, L: 0, S: 0 };
|
|
if (row.rowType === "gov_mat") govRawTotal += pr.T;
|
|
else if (row.rowType === "gov_fee") govFeeTotal += pr.T;
|
|
else ((directM += pr.M), (directL += pr.L), (directS += pr.S));
|
|
return Object.assign({}, row, pr.out);
|
|
}
|
|
const su = slot > 1 && row.units && row.units[slot];
|
|
const u = su
|
|
? Object.assign({}, lookupUnit(row.refId), { M: su[0], L: su[1], S: su[2] })
|
|
: lookupUnit(row.refId);
|
|
// 행 고정 단가(fixedUnit) · 내역 할증율(wBoqRate RATE/RM·RL·RS = 비목별 단가 가산, 원 단위 절사)
|
|
// — 수량 할증(BDQTY X_HAL)과 다른 금액 가산(07번 §4 · 34번 wBoqRate) · 저장 차수 단가는 이미 반영된 값이라 건너뜀
|
|
if (!su && row.fixedUnit)
|
|
["M", "L", "S"].forEach((c) => row.fixedUnit[c] != null && (u[c] = num(row.fixedUnit[c])));
|
|
const hr = row.halRate;
|
|
if (!su && hr != null) {
|
|
const pct = (c) => num(typeof hr === "object" ? hr[c] : hr);
|
|
["M", "L", "S"].forEach((c) => pct(c) && (u[c] = roundDown(u[c] * (1 + pct(c) / 100), 0)));
|
|
}
|
|
u.T = u.M + u.L + u.S;
|
|
const qty =
|
|
Number(
|
|
slot > 1 && row.qtys
|
|
? row.qtys[slot - 1]
|
|
: slot > 1 && 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") {
|
|
// 총액우선(CHNNM=0) — REA 15 OPTIMIZ: 총액 절사 후 남는 1원을 소수부 큰 성분부터 하나씩
|
|
// (동률은 OPTNM 순서 앞쪽) · 산근·일위대가(smt_eval finish)와 같은 규칙
|
|
const got = optimize({ M: rawM, L: rawL, S: rawS }, rawT, optOrder);
|
|
optimizDiff = got.M + got.L + got.S - (M + L + S);
|
|
if (optimizDiff) {
|
|
optimizTarget = "MLS"
|
|
.split("")
|
|
.filter((k) => got[k] !== { M, L, S }[k])
|
|
.map((k) => `${k}(+${got[k] - { M, L, S }[k]})`)
|
|
.join("");
|
|
((M = got.M), (L = got.L), (S = got.S), (T = M + L + S));
|
|
}
|
|
}
|
|
const aggM = roundHalfUp(rawM, 0),
|
|
aggL = roundHalfUp(rawL, 0),
|
|
aggS = roundHalfUp(rawS, 0);
|
|
if (String(row.PRPT ?? "0").trim() && String(row.PRPT ?? "0").trim() !== "0")
|
|
((prBase.M += M), (prBase.L += L), (prBase.S += S));
|
|
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 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) };
|
|
}
|
|
|
|
// 새 공사 = 빈 상태 · 요율만 초기값(지식DB 06 · 2024 기준표) — 특정 공사 금액을 담지 않음
|
|
function createDefaultState() {
|
|
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,ANJUN1:1.86,ANJUN2:1.86,KITA:6,HWAN:0.8,EQIP:0.4,ILBAN:6,IYUN:15,S_SUDO:0,S_BOKRI:0,S_SOMO:0,S_TONGSIN:0,S_SEGUM:0,S_PEGI:0,S_DOSEO:0,S_JIGUP:0";
|
|
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: "새 공사", pnum: "", pcode: "11", pclass: 0, kOrd: 0 };
|
|
const sysConfig = {
|
|
rxFormula: "1/8*16/12*25/20",
|
|
condCodes: "",
|
|
usdRate1: 0,
|
|
usdRate2: 0,
|
|
useChangedUsd: false,
|
|
ecdDigits: 0,
|
|
chnMode: "bimok",
|
|
optOrder: "SML",
|
|
};
|
|
sysConfig.kOrdSim = { pclass: 0, regMode: 7, flag: 0, currentK: 1 };
|
|
// 원본 키 초기값 — 일반형식 · 안전비 관급포함 자동 · 고용 7등급 · 환경 기타토목 · 사급 재료비 포함 · 관급 천원 절상
|
|
const keys = Object.fromEntries(
|
|
"JAB_TYPE:0,CAL_ACTL:0,I_KONG_TYPE:0,I_SET_BASE:0,I_KONG_KIKAN:0,I_KOBO_TYPE:7,I_TBUGUM:3,I_ANJUN_TYPE:1,I_HWAN:14,I_EQIP:0,I_POS_SA:3,I_KN_SA_TYPE:0,I_ANJUN_K:1,I_ANJUN_B:0,I_ANJUNAUTO:1,I_BUGA:0,I_TOT_JEOL:3,I_KWN_JEOLSA:3,I_KWN_K_JEOLSA:3,I_KWN_UPDN:0,I_KWN_K_UPDN:0,I_KAN_JEOLSA:0,I_JEOLSA:0,I_DOG_JEOL:0,C_ANJUN_G:0,C_ANJUN1_G:0,C_ANJUN2_G:0,C_KWANGUP_D:0,C_KWANGUP_K:0,C_T_JODAL:0,C_IYUN_M:0"
|
|
.split(",")
|
|
.map((p) => [p.split(":")[0], Number(p.split(":")[1])]),
|
|
);
|
|
const kanjubConfig = {
|
|
slot: 1,
|
|
scheme: "T",
|
|
keys,
|
|
addRows: [],
|
|
addAmounts: [],
|
|
fixed: {},
|
|
anjunAuto: 1,
|
|
anjunBaseAdd: 0,
|
|
};
|
|
Object.assign(kanjubConfig, { rates, revModes });
|
|
return Object.assign(
|
|
{ projectMeta, sysConfig, kanjubConfig },
|
|
{ resources: [], machines: [], sanItems: [], ilwiItems: [], boqItems: [] },
|
|
);
|
|
}
|
|
|
|
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 st = data.kanjubStages || {};
|
|
const boqRev = calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, true),
|
|
kanjubRev = st[2]
|
|
? calcKanjub(data, boqRev, kanjubOrig, st[2])
|
|
: calcKanjub(data, boqRev, kanjubOrig);
|
|
// 차수 n 원가계산서 — 변경 계열은 바로 앞 차수(n−1)를 당초로 씀(REA 18 직전 계열 = 현재−1)
|
|
const stages = {};
|
|
for (const n of Object.keys(st)
|
|
.map(Number)
|
|
.sort((a, b) => a - b)) {
|
|
const boq =
|
|
n === 1
|
|
? boqOrig
|
|
: n === 2
|
|
? boqRev
|
|
: calcBoq(data, resources.map, machines.map, sanCal.map, ilwi.map, n);
|
|
const prev = (stages[n - 1] && stages[n - 1].kanjub) || kanjubOrig;
|
|
stages[n] = {
|
|
boq,
|
|
kanjub: n === 1 ? kanjubOrig : n === 2 ? kanjubRev : calcKanjub(data, boq, prev, st[n]),
|
|
};
|
|
}
|
|
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 },
|
|
stages,
|
|
});
|
|
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;
|