- 지식DB REA 분석 32 실행계약 · 34 변경차수 잔여계산 작업자료(정적분석 · 원본 실행·수정 없음) - 실행: 중기 실행단가 합계·성분 비례·끝전·차액 성분 · 실행단가·수량 할증 · 계약 변환 옵션·적용제외 · 기성 공사 낙찰율 100% - 변경: 차수 목록·이어 셈 · 보험료 3모드(변경 2차 이후) · 방법 A/B 5항목 · 퇴직공제·건설기계·표준시장단가·노임 환산 갈래·JCD - 시험 693 통과 · 브라우저 3건 화면 전수 오류 0 · 총공사비 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
374 lines
15 KiB
JavaScript
374 lines
15 KiB
JavaScript
/* -*- coding: utf-8 -*- */
|
||
/**
|
||
* 실행예산 계산 (exec_calc.js) — 원본 반올림 원형(MROUNDUD·ADDBJVAL·OPTIMIZ) · 중기 실행단가 · 실행단가·실행수량 할증
|
||
* 근거: 지식DB STmate/REA 분석/32_실행계약단계_작업자료_20261114 (정적 분석 · 주소는 그 문서 표)
|
||
* · MROUNDUD(값, 자릿수, 선택) = DP_Common 0x454044 — 선택 0 = 절사(0 쪽) · 1 = 사사오입(첫 버림 자리 ≥5)
|
||
* · 자릿수 양수 = 소수 자리 · 음수 = 10^|d| 단위 · 문자열 변환 전 부동소수 잡음 미소값 더함
|
||
* · ADDBJVAL = DP_Common 0x4565f8 — 크기 비례 미소값을 부호 방향으로 더함(곱 뒤 절사 오차 막이)
|
||
* · OPTIMIZ = ST_Func_C 0x48094c — 성분 MROUNDUD · 비목별 우선이면 총액 = 성분 합 · 아니면 총액 절사 뒤
|
||
* 남는 단위를 소수부(소수 둘째 절사) 큰 성분부터 하나씩(같으면 OPTNM 차례 앞 · L/M/S 뒤 A)
|
||
* · 중기 실행단가 = ST_Boq TwM_Boq_ExecX.wNe_AutoCost 0x4da7a8
|
||
* 목표 X = MROUNDUD(1단위당 사용료 ÷ 1단위 시간, 0, 0) · 성분 = 최초단가 × X/최초합계(비례)
|
||
* → OPTIMIZ(총액 우선 · 자릿수 = 절사 기준 0~−5) → 성분 합 ≠ X 이면 차액 X − 나머지 두 성분 = 보정 성분
|
||
* · 보정 성분 = 노/재/경 고름 · 기본보정 = 설정 OPTNM 첫 글자(L→노 · M→재 · 그 밖→경, 기본 SML → 경)
|
||
* · 실행수량 = 1단위 시간 × 단위 수(0x4dab5c) · 실행금액 = OPTIMIZ(ADDBJVAL(수량 × 단가), CHNNM)
|
||
* · 실행단가·실행수량 할증 = ST_Boq TwBoq.Act_Rate_Execute 0x512a80 → 0x511fa8
|
||
* 실행수량 = MROUNDUD(최초수량 × 율/100, 자릿수 0~4, 절사/반올림) · 실행단가 = MROUNDUD(최초단가 × 율/100, …)
|
||
* · 율은 코드 머리글자 갈래(L 노무비 · M 재료비 · X 중장비 · 그 밖 W)마다 하나 · 단가 할증은 단위 % 줄 제외
|
||
* 앱 확장(원본 없음): 비목별 율(rateM·rateL·rateS) · 원 단위 절사(jeol) — 옛 화면·시험 호환
|
||
* 시험: resources/tester/test_stmate_stage_exec_rea.py (원본 규칙 손계산 대조)
|
||
*/
|
||
const StmateExec = (() => {
|
||
const Calc = (typeof window !== "undefined" && window.STMATE_CALC) || require("./calc.js");
|
||
const Change =
|
||
(typeof window !== "undefined" && window.STMATE_CHANGE) || require("./change_calc.js");
|
||
const { roundDown } = Calc;
|
||
const n0 = (v) => Number(v) || 0;
|
||
// 절사 기준 6택 — wCm_Jeol Values '0'·'-1'…'-5'(= MROUNDUD 자릿수) · 화면 1원~100,000원 미만 절사
|
||
const JEOL = [1, 10, 100, 1000, 10000, 100000];
|
||
// 차액 보정 순서 — wCm_OPT Values 0~3
|
||
const ORDERS = [
|
||
["base", "기본보정"],
|
||
["l", "노무비"],
|
||
["m", "재료비"],
|
||
["s", "경비"],
|
||
];
|
||
const cut = (v, j) => Math.trunc(n0(v) / (n0(j) || 1) + 1e-9) * (n0(j) || 1);
|
||
const jeolDigits = (j) => -Math.round(Math.log10(n0(j) || 1));
|
||
|
||
// FormatFloat('0.###…', a) 꼴 — n 자리 반올림 뒤 끝 0 지움
|
||
function fmtHash(a, n) {
|
||
const s = a.toFixed(Math.min(n, 20));
|
||
return s.includes(".") ? s.replace(/0+$/, "").replace(/\.$/, "") : s;
|
||
}
|
||
// 부동소수 잡음 미소값 — '.' 자리(1부터) 2~9 → 10^(자리−13) · 10~13 → 1e-4 · 그 밖 1e-9 (0x4541ad)
|
||
function noiseEps(pos) {
|
||
if (pos < 2) return 0;
|
||
if (pos <= 9) return Math.pow(10, pos - 13);
|
||
return pos <= 13 ? 1e-4 : 1e-9;
|
||
}
|
||
|
||
// MROUNDUD(v, d, sel) — DP_Common 0x454044
|
||
function mroundud(v, d = 0, sel = 0) {
|
||
v = n0(v);
|
||
d = Math.trunc(n0(d));
|
||
if (v === 0) return 0;
|
||
const a = Math.abs(v);
|
||
const n = d > 11 && a <= 1 ? 15 : 13;
|
||
const s0 = fmtHash(a, n);
|
||
let pos = s0.indexOf(".") + 1;
|
||
// 유효 숫자 11개 미만이면 미소값 없음(0x454154~0x4541ab)
|
||
if (pos > 0 && s0.length - pos < 11) {
|
||
const first = s0.search(/[1-9]/) + 1;
|
||
if (first > 0 && s0.length - first < 11) pos = 0;
|
||
}
|
||
if (pos === 0 && d >= 0 && !s0.includes(".")) return v;
|
||
const eps = pos === 0 ? 0 : noiseEps(pos);
|
||
let s = fmtHash(a + eps, n);
|
||
if (!s.includes(".")) s += ".0";
|
||
const p = s.indexOf(".") + 1;
|
||
const ch = (i) => s.charAt(i - 1);
|
||
let frac, body, e;
|
||
if (d === 0) ((frac = "0." + ch(p + 1)), (body = s.slice(0, p - 1)), (e = 0));
|
||
else if (d > 0) {
|
||
frac = "0." + ch(p + d + 1);
|
||
body = s.slice(0, p - 1) + "." + s.slice(p, p + d);
|
||
e = -d;
|
||
} else {
|
||
const D = -d;
|
||
if (p - 1 > D) ((frac = "0." + ch(p - D)), (body = s.slice(0, p - D - 1) + "0".repeat(D)));
|
||
else if (p - 1 === D) ((frac = "0." + ch(1)), (body = "0"));
|
||
else ((frac = "0.0"), (body = "0"));
|
||
e = D;
|
||
}
|
||
// 사사오입 = Round(StrToFloat(frac + '1')) × 10^e — '1' 덧붙여 은행가 동률 피함
|
||
const add = sel ? Math.round(Number(frac + "1")) * Math.pow(10, e) : 0;
|
||
// 원본은 확장 정밀도 합 → binary64 저장 · 여기서는 자릿수로 다시 맞춰 이진 잡음 지움
|
||
const r = d > 0 ? Number((Number(body) + add).toFixed(Math.min(d, 20))) : Number(body) + add;
|
||
return v > 0 ? r : -r;
|
||
}
|
||
|
||
// MROUNDUP(v, d) — 올림(0 반대쪽) · 이름·호출 자리만 확인(본문 미해독) → 중간 확신
|
||
function mroundup(v, d = 0) {
|
||
const t = mroundud(v, d, 0);
|
||
return t === n0(v) ? t : t + Math.sign(n0(v)) * Math.pow(10, -Math.trunc(n0(d)));
|
||
}
|
||
|
||
// ADDBJVAL(v) — DP_Common 0x4565f8 · '.' 자리 기준 미소값을 부호 방향으로 더함
|
||
function addbjval(v) {
|
||
v = n0(v);
|
||
const s = fmtHash(Math.abs(v), 14);
|
||
const pos = s.indexOf(".") + 1;
|
||
if (pos < 2) return v;
|
||
const eps = pos <= 9 ? Math.pow(10, pos - 13) : pos <= 13 ? 1e-4 : 1e-9;
|
||
return eps > Math.abs(v) ? v : v + Math.sign(v) * eps;
|
||
}
|
||
|
||
const mfrac = (x) => x - Math.trunc(x);
|
||
|
||
// OPTIMIZ(rec, o) — ST_Func_C 0x48094c · rec {T,L,M,S,A} · o {priority, digits, mode, order}
|
||
function optimiz(rec, o = {}) {
|
||
const d = Math.trunc(n0(o.digits)),
|
||
mode = Math.trunc(n0(o.mode));
|
||
const ks = String(o.order || "SML")
|
||
.toUpperCase()
|
||
.slice(0, 3)
|
||
.split("")
|
||
.filter((k) => "LMS".includes(k));
|
||
for (const k of ["S", "M", "L"]) if (!ks.includes(k)) ks.push(k);
|
||
ks.push("A");
|
||
const c = ks.map((k) => n0(rec[k]));
|
||
const R = (x) =>
|
||
mode === 1 ? mroundud(x, d, 1) : mode === 3 ? mroundup(x, d) : mroundud(x, d);
|
||
const r = c.map(R);
|
||
const T = n0(rec.T);
|
||
const sumR = () => r.reduce((a, x) => a + x, 0);
|
||
let Tout = sumR(),
|
||
moved = 0;
|
||
if (!(o.priority || mode === 1 || mode === 3 || Tout === T)) {
|
||
Tout = mroundud(T, d, 0);
|
||
const P = Math.pow(10, d);
|
||
const f = c.map((x) => Math.abs(mroundud(mfrac(x * P), 2, 0)));
|
||
const neg = c.map((x) => x < 0);
|
||
let k = Math.round((Tout - sumR()) * P);
|
||
const negRes = k < 0;
|
||
k = Math.abs(k);
|
||
for (let guard = 0; k > 0 && k <= 4 && guard < 32; guard++) {
|
||
let i = 0,
|
||
pick = -1;
|
||
while (i < 4) {
|
||
if (f[i] > 1) {
|
||
i++;
|
||
continue;
|
||
}
|
||
const j = f.findIndex((x, jj) => jj > i && x < 1 && x > f[i]);
|
||
if (j >= 0) {
|
||
i = j;
|
||
continue;
|
||
}
|
||
pick = i;
|
||
break;
|
||
}
|
||
if (pick < 0) break;
|
||
f[pick] += 1;
|
||
k = negRes === neg[pick] ? k - 1 : k + 1;
|
||
moved++;
|
||
}
|
||
// 1/P 더하기 — 원본은 확장 정밀도 뒤 binary64 저장 · 여기서는 10^−d 를 바로 곱해 이진 잡음 막음
|
||
const step = d <= 0 ? Math.pow(10, -d) : 1 / Math.pow(10, d);
|
||
for (let i = 0; i < 4; i++) {
|
||
const t = Math.trunc(neg[i] ? -f[i] : f[i]);
|
||
if (t) r[i] = d > 0 ? Number((r[i] + t * step).toFixed(d)) : r[i] + t * step;
|
||
}
|
||
}
|
||
const out = { T: Tout, moved };
|
||
ks.forEach((k, i) => (out[k] = r[i]));
|
||
return out;
|
||
}
|
||
|
||
const firstComp = (optnm) => {
|
||
const c = String(optnm || "SML").toUpperCase()[0];
|
||
return c === "L" ? "l" : c === "M" ? "m" : "s";
|
||
};
|
||
|
||
// 중기 실행단가 — wNe_AutoCost 0x4da7a8
|
||
// p {fee, hours, units?, execQty?, initQty?, init{l,m,s,t?}, jeol | jeolDigits, order, optnm, chnnm}
|
||
function machineExecUnit(p) {
|
||
const hours = n0(p.hours),
|
||
fee = n0(p.fee);
|
||
const X = hours ? mroundud(fee / hours, 0, 0) : 0;
|
||
const init = {
|
||
l: n0(p.init && p.init.l),
|
||
m: n0(p.init && p.init.m),
|
||
s: n0(p.init && p.init.s),
|
||
};
|
||
const initT = p.init && p.init.t != null ? n0(p.init.t) : init.l + init.m + init.s;
|
||
const ratio = initT ? X / initT : 0;
|
||
const digits = p.jeolDigits != null ? Math.trunc(n0(p.jeolDigits)) : jeolDigits(p.jeol);
|
||
const o1 = optimiz(
|
||
{ T: X, L: init.l * ratio, M: init.m * ratio, S: init.s * ratio, A: 0 },
|
||
{ priority: false, digits, mode: 0, order: p.optnm },
|
||
);
|
||
const comps = { l: o1.L, m: o1.M, s: o1.S };
|
||
let to = p.order || "base";
|
||
if (to === "base") to = firstComp(p.optnm);
|
||
const before = comps.l + comps.m + comps.s;
|
||
if (before !== X) comps[to] = X - (before - comps[to]);
|
||
const qty =
|
||
p.units != null && p.units !== "" ? hours * n0(p.units) : n0(p.execQty || p.initQty);
|
||
const raw = {
|
||
T: addbjval(qty * X),
|
||
L: addbjval(qty * comps.l),
|
||
M: addbjval(qty * comps.m),
|
||
S: addbjval(qty * comps.s),
|
||
A: 0,
|
||
};
|
||
const o2 = optimiz(raw, { priority: p.chnnm !== false, digits: 0, mode: 0, order: p.optnm });
|
||
return {
|
||
target: hours ? fee / hours : 0,
|
||
X,
|
||
T: X,
|
||
Tr: o1.T,
|
||
comps,
|
||
diff: X - before,
|
||
to,
|
||
digits,
|
||
initT,
|
||
execQty: qty,
|
||
qtyRatio: n0(p.initQty) ? qty / n0(p.initQty) : 1,
|
||
amounts: { t: o2.T, l: o2.L, m: o2.M, s: o2.S },
|
||
};
|
||
}
|
||
|
||
// 실행단가 = 최초단가(Act_SameFirstExecute 0x4dadd0) — 사용료 = 최초합계 × 시간 · 절사 1원
|
||
const sameFirst = (p) =>
|
||
Object.assign({}, p, {
|
||
fee: n0(p.init && (p.init.t ?? p.init.l + p.init.m + p.init.s)) * n0(p.hours),
|
||
jeol: 1,
|
||
});
|
||
// 실행목표값(대략) 찾기(0x4da7e6) — 사용료 = MROUNDUP(목표 실행금액 ÷ 단위 수, 0)
|
||
const feeFromTarget = (target, units) => (n0(units) ? mroundup(n0(target) / n0(units), 0) : 0);
|
||
|
||
// 할증 갈래 — Act_Rate Tag%10: 1 L · 2 M · 3 X · 4 그 밖(S·W·B·D·A·P·Q)
|
||
const kindOf = (code) => {
|
||
const c = String(code || "").trim()[0] || "";
|
||
return c === "L" ? "L" : c === "M" ? "M" : c === "X" ? "X" : "W";
|
||
};
|
||
// 단가 할증 제외 — L·M 은 단위 %, W 갈래는 S·W·P 머리글자 + 단위 %(0x51300e)
|
||
const pctExcluded = (code, unit) => {
|
||
if (!String(unit || "").includes("%")) return false;
|
||
const c = String(code || "").trim()[0] || "";
|
||
const k = kindOf(code);
|
||
return k === "L" || k === "M" || (k === "W" && "SWP".includes(c));
|
||
};
|
||
|
||
// 실행예산 행 — 실행단가 = 직접 입력 또는 할증(율·자릿수·절사/반올림) · 실행수량 = 입력 또는 최초수량 × 수량율
|
||
// o {digits, round, rateQ, kindRates{L,M,X,W}, rateM/rateL/rateS(앱), jeol(앱), chnnm, optnm}
|
||
function execRow(r, o) {
|
||
const ov = (o.overrides || {})[r.id] || {};
|
||
const native = o.digits != null || o.round != null;
|
||
const dg = Math.trunc(n0(o.digits));
|
||
const rnd = (v) => (native ? mroundud(v, dg, o.round ? 1 : 0) : cut(v, n0(o.jeol) || 1));
|
||
const kr = o.kindRates;
|
||
const skip = kr && pctExcluded(r.code, r.unit);
|
||
const rate = (k) =>
|
||
kr ? n0(kr[kindOf(r.code)] ?? 100) : n0(o[{ m: "rateM", l: "rateL", s: "rateS" }[k]] ?? 100);
|
||
const u = {};
|
||
["m", "l", "s"].forEach((k, i) => {
|
||
u[k] = ov.mls ? n0(ov.mls[i]) : skip ? n0(r[k]) : rnd((n0(r[k]) * rate(k)) / 100);
|
||
});
|
||
const q =
|
||
ov.qty != null
|
||
? n0(ov.qty)
|
||
: o.rateQ != null && n0(o.rateQ) !== 100
|
||
? mroundud((n0(r.qty) * n0(o.rateQ)) / 100, dg, o.round ? 1 : 0)
|
||
: n0(r.qty);
|
||
// 행 금액 — 원본 행 계산(0x456504)은 미해독 · OPTIMIZ(CHNNM, 0자리) 대입(비목별 우선 = 비목 절사 합)
|
||
const g = optimiz(
|
||
{ T: (u.m + u.l + u.s) * q, M: u.m * q, L: u.l * q, S: u.s * q, A: 0 },
|
||
{ priority: o.chnnm !== false, digits: 0, order: o.optnm },
|
||
);
|
||
const a = { mAmt: g.M, lAmt: g.L, sAmt: g.S, tAmt: g.T };
|
||
return { qty: q, mUnit: u.m, lUnit: u.l, sUnit: u.s, tUnit: u.m + u.l + u.s, ...a };
|
||
}
|
||
|
||
// 평면 행 실행예산 — rows [{id, code?, unit?, qty, m, l, s, gov}]
|
||
function budget(rows, opts = {}) {
|
||
const out = rows.map((r) => {
|
||
const x = execRow(r, opts);
|
||
const d = {
|
||
mAmt: roundDown(n0(r.m) * n0(r.qty), 0),
|
||
lAmt: roundDown(n0(r.l) * n0(r.qty), 0),
|
||
sAmt: roundDown(n0(r.s) * n0(r.qty), 0),
|
||
};
|
||
d.tAmt = d.mAmt + d.lAmt + d.sAmt;
|
||
return Object.assign({}, r, { design: d, exec: x });
|
||
});
|
||
const tot = (key) =>
|
||
out
|
||
.filter((r) => !r.gov)
|
||
.reduce(
|
||
(a, r) => ({
|
||
mAmt: a.mAmt + r[key].mAmt,
|
||
lAmt: a.lAmt + r[key].lAmt,
|
||
sAmt: a.sAmt + r[key].sAmt,
|
||
tAmt: a.tAmt + r[key].tAmt,
|
||
}),
|
||
{ mAmt: 0, lAmt: 0, sAmt: 0, tAmt: 0 },
|
||
);
|
||
const design = tot("design"),
|
||
exec = tot("exec");
|
||
return {
|
||
rows: out,
|
||
design,
|
||
exec,
|
||
diff: exec.tAmt - design.tAmt,
|
||
rate: design.tAmt ? Math.round((exec.tAmt / design.tAmt) * 1e5) / 1e3 : 0,
|
||
};
|
||
}
|
||
|
||
// 내역 구조 위 실행예산 — 설계(계획) 칸 = 최초(3칸 복사본 · TwPrjList.MkBoq 0x55c956) · 실행 칸 = 1칸
|
||
function budgetFromTree(tree, stage, opts = {}) {
|
||
const nodes = tree.nodes.map((nd) => {
|
||
const o = Object.assign({}, nd, { st: {} });
|
||
if (nd.kind !== "leaf") return o;
|
||
const s = nd.st[stage] || {};
|
||
o.st.d = {
|
||
qty: n0(s.qty),
|
||
mUnit: n0(s.mUnit),
|
||
lUnit: n0(s.lUnit),
|
||
sUnit: n0(s.sUnit),
|
||
tUnit: n0(s.tUnit),
|
||
mAmt: n0(s.mAmt),
|
||
lAmt: n0(s.lAmt),
|
||
sAmt: n0(s.sAmt),
|
||
tAmt: n0(s.tAmt),
|
||
};
|
||
const row = { id: nd.id, code: nd.newCode || nd.code, unit: nd.unit, qty: s.qty };
|
||
Object.assign(row, { m: s.mUnit, l: s.lUnit, s: s.sUnit });
|
||
const flat = { rateM: 100, rateL: 100, rateS: 100, kindRates: null, rateQ: null };
|
||
o.st.x = execRow(row, nd.gov ? Object.assign({}, opts, flat) : opts);
|
||
return o;
|
||
});
|
||
Change.sumHeads(nodes, ["d", "x"]);
|
||
const tops = nodes.filter((nd) => nd.kind === "head" && !nd.zone);
|
||
const top = tops.reduce((a, h) => (!a || h.level > a.level ? h : a), null);
|
||
return {
|
||
nodes,
|
||
design: top ? top.st.d.tAmt : 0,
|
||
exec: top ? top.st.x.tAmt : 0,
|
||
};
|
||
}
|
||
|
||
// 실행예산서 Ⅰ·Ⅱ(0673·0674) 열 — 설치본 SET_REP.DBF 행(FLDCNT 5 · 열 이름·폭)
|
||
const REPORT_COLS = [
|
||
["공종", 25],
|
||
["설계(계획)", 20],
|
||
["실행예산", 25],
|
||
["작업명", 15],
|
||
["구분", 25],
|
||
];
|
||
|
||
return {
|
||
JEOL,
|
||
ORDERS,
|
||
REPORT_COLS,
|
||
mroundud,
|
||
mroundup,
|
||
addbjval,
|
||
optimiz,
|
||
jeolDigits,
|
||
kindOf,
|
||
pctExcluded,
|
||
machineExecUnit,
|
||
sameFirst,
|
||
feeFromTarget,
|
||
execRow,
|
||
budget,
|
||
budgetFromTree,
|
||
};
|
||
})();
|
||
|
||
if (typeof window !== "undefined") window.STMATE_EXEC = StmateExec;
|
||
if (typeof module !== "undefined" && module.exports) module.exports = StmateExec;
|