feat(stmate-test): 적용기준 행자부(I_SET_BASE 1) 요율 조회 화면 연결 — 간접노무비·일반관리비·이윤 = RATE_M

- 요율 조회(scr_kanjub_rate.lookup) — 적용기준 1 이면 rateMLookup(ST_KanJub 0x47a1bc) 값으로 KANNO·ILBAN·IYUN · 그 밖 RATE_R 그대로
- 적용기준 콤보를 바꾸면 제비율표 다시 조회(요율 축에 I_SET_BASE)
- 브라우저 확인(봉화2024): 행자부 → 간접노무비 14.5 → 14.6(RATE_M 11줄 RATE1) · 지방계약법 되돌림 14.5

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-14 15:50:01 +09:00
co-authored by Claude Opus 5.5
parent 4ff96a6771
commit 7eac0d93b0
3 changed files with 75 additions and 6 deletions
+7 -2
View File
@@ -285,6 +285,7 @@
}
const RATE_AXES = [
"I_SET_BASE",
"I_KONG_TYPE",
"I_KONG_KIKAN",
"I_ANJUN_TYPE",
@@ -310,11 +311,15 @@
);
const res = R.applyWithLoopCheck(cfg, R.matrixFromRows(rows), calc, {
withTbugum: true,
extra: { rateMRows: rateMRows(ctx) },
});
u.kjMsg = res.loop ? res.msg : res.manual ? "요율 수동 확정 상태 — 요율 그대로" : "";
return res;
}
// 적용기준 행자부(I_SET_BASE 1)용 RATE_M 행 — 그 STC 의 표
const rateMRows = (ctx) => ((ctx.dbfData && ctx.dbfData.rate_tables) || {})["RATE_M.DBF"] || [];
function bind(ctx, api) {
const re = () => api.renderWorkspace();
const { cfg, st } = stageCfg(ctx);
@@ -329,7 +334,7 @@
switchScheme(cfg, schemeOf(k2));
}
setKey(cfg, key, val);
// 요율 축(공사종류·기간·안전공종·고용등급·환경·건설기계·퇴직공제·일반관리비) — 제비율표 다시 조회 후 다시 셈
// 요율 축(적용기준·공사종류·기간·안전공종·고용등급·환경·건설기계·퇴직공제·일반관리비) — 제비율표 다시 조회 후 다시 셈
if (RATE_AXES.includes(key)) reRate(ctx, api, cfg);
re();
});
@@ -395,7 +400,7 @@
api.recompute(),
(ctx.computed.stages[n] || {}).kanjub || ctx.computed.kanjubComputed
);
const res = R.applyWithLoopCheck(cfg, m, calc);
const res = R.applyWithLoopCheck(cfg, m, calc, { extra: { rateMRows: rateMRows(ctx) } });
u.kjMsg = res.loop
? res.msg
: res.manual
+36 -4
View File
@@ -65,6 +65,17 @@ const StmateRate = (() => {
return BANDS[BANDS.length - 1];
}
// 원본 차수·조회 규칙 묶음(kanjub_parts.js) — 브라우저 = 전역 · node = require
function kanjubNative() {
const g = typeof globalThis !== "undefined" ? globalThis : {};
if (g.STMATE_KANJUB_PARTS) return g.STMATE_KANJUB_PARTS.NATIVE;
try {
return require("./kanjub_parts.js").NATIVE;
} catch {
return null;
}
}
// 요율 조회 — opts: kongType · kikan · scale(직접공사비) · anjunType · anjunScale · koboType · hwan · eqip · tbugum
function lookup(m, opts = {}) {
const rates = {},
@@ -132,6 +143,20 @@ const StmateRate = (() => {
)
notes.push(`${c} — 표 칸 [${cells[c]}] 이 비어 있음(직접 입력 요율 유지)`);
const zeroed = short ? ["KUNBO", "YUNBO", "SLVBO"] : [];
// 적용기준 행자부(I_SET_BASE 1) — 간접노무비·일반관리비·이윤만 RATE_M 에서(ST_KanJub 0x47a1bc · REA 36 §4.2)
const NATIVE = kanjubNative();
if (n0(opts.setBase) === 1 && NATIVE && (opts.rateMRows || []).length) {
const rm = NATIVE.rateMLookup(opts.rateMRows, {
scale: opts.scale,
kongType: opts.kongType,
ilban: opts.ilbanType,
kikan: opts.kikan,
});
for (const [c, v] of Object.entries(rm.rates)) {
rates[c] = v;
cells[c] = ["RATE_M", ...rm.cells[c]];
}
}
return { rates, cells, amounts, band, anjunBand: ab, notes, zeroed };
}
@@ -149,6 +174,7 @@ const StmateRate = (() => {
eqip: k.I_EQIP == null ? 0 : n0(k.I_EQIP),
tbugum: n0(k.I_TBUGUM),
ilbanType: n0(k.I_ILBAN),
setBase: n0(k.I_SET_BASE),
};
}
@@ -175,14 +201,14 @@ const StmateRate = (() => {
cfg,
m,
calcFn,
{ scaleOf = (kj) => kj.dT, maxIter = 6, withTbugum = false } = {},
{ scaleOf = (kj) => kj.dT, maxIter = 6, withTbugum = false, extra = {} } = {},
) {
let kj = calcFn(cfg);
if (cfg.rateConfirm && cfg.rateConfirm.manual) return { cfg, kj, loop: false, manual: true };
const seen = [],
found = {};
for (let i = 0; i < maxIter; i++) {
const res = lookup(m, Object.assign(optsFromCfg(cfg, kj), { scale: scaleOf(kj) }));
const res = lookup(m, Object.assign(optsFromCfg(cfg, kj), extra, { scale: scaleOf(kj) }));
const sig = `${res.band.row}/${res.anjunBand.row}`;
if (seen.includes(sig) && seen[seen.length - 1] !== sig) {
const choices = seen.slice(seen.indexOf(sig)).map((g) => found[g]);
@@ -333,6 +359,12 @@ if (typeof module !== "undefined" && module.exports) module.exports = StmateRate
return `<table class="dbgrid"><thead><tr><th>행</th>${head}</tr></thead><tbody>${body.join("")}</tbody></table>`;
}
// 조회 입력 + 행자부기준용 RATE_M 행(그 STC 의 표)
const optsOf = (ctx, cfg, kj) =>
Object.assign(R.optsFromCfg(cfg, kj), {
rateMRows: ((ctx.dbfData && ctx.dbfData.rate_tables) || {})["RATE_M.DBF"] || [],
});
function lookupPanel(ctx, m) {
const S = global.STMATE_KANJUB_SCREEN;
if (!S || !m.length) return "";
@@ -340,7 +372,7 @@ if (typeof module !== "undefined" && module.exports) module.exports = StmateRate
const kj =
(ctx.computed.stages && ctx.computed.stages[n] && ctx.computed.stages[n].kanjub) ||
ctx.computed.kanjubComputed;
const res = R.lookup(m, R.optsFromCfg(cfg, kj));
const res = R.lookup(m, optsOf(ctx, cfg, kj));
const rows = Object.entries(res.rates)
.map(([c, v]) => {
const cur = cfg.rates[c];
@@ -451,7 +483,7 @@ if (typeof module !== "undefined" && module.exports) module.exports = StmateRate
{ n, cfg } = S.stageCfg(ctx);
api.recompute();
const kj = (ctx.computed.stages[n] || {}).kanjub || ctx.computed.kanjubComputed;
const res = R.lookup(m, R.optsFromCfg(cfg, kj));
const res = R.lookup(m, optsOf(ctx, cfg, kj));
R.applyToCfg(cfg, res, { withTbugum: !!(api.$("kr-tbugum") || {}).checked });
u.kjMsg = res.notes.join(" · ");
re();