From 7eac0d93b00e1b2ec22014eea04c712d4e3d93b4 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sat, 14 Nov 2026 15:50:01 +0900 Subject: [PATCH] =?UTF-8?q?feat(stmate-test):=20=EC=A0=81=EC=9A=A9?= =?UTF-8?q?=EA=B8=B0=EC=A4=80=20=ED=96=89=EC=9E=90=EB=B6=80(I=5FSET=5FBASE?= =?UTF-8?q?=201)=20=EC=9A=94=EC=9C=A8=20=EC=A1=B0=ED=9A=8C=20=ED=99=94?= =?UTF-8?q?=EB=A9=B4=20=EC=97=B0=EA=B2=B0=20=E2=80=94=20=EA=B0=84=EC=A0=91?= =?UTF-8?q?=EB=85=B8=EB=AC=B4=EB=B9=84=C2=B7=EC=9D=BC=EB=B0=98=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EB=B9=84=C2=B7=EC=9D=B4=EC=9C=A4=20=3D=20RATE=5FM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 요율 조회(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 --- resources/tester/test_stmate_std_market.py | 32 +++++++++++++++++ tools/stmate-test/scr_kanjub.js | 9 +++-- tools/stmate-test/scr_kanjub_rate.js | 40 +++++++++++++++++++--- 3 files changed, 75 insertions(+), 6 deletions(-) diff --git a/resources/tester/test_stmate_std_market.py b/resources/tester/test_stmate_std_market.py index 9637a08cb..53552b61e 100644 --- a/resources/tester/test_stmate_std_market.py +++ b/resources/tester/test_stmate_std_market.py @@ -166,6 +166,38 @@ def test_rate_m_lookup_against_stc_table(): assert [g["band"] for g in res] == [0, 1, 2, 0] +def test_rate_screen_lookup_uses_rate_m_for_set_base_1(): + """요율 조회 화면(scr_kanjub_rate.lookup) — 적용기준 행자부(1)면 간접노무비·일반관리비·이윤만 RATE_M · 그 밖은 RATE_R.""" + files = server.read_project_files(str(STC)) + rate_r = server.parse_dbf_bytes(files["RATE_R.DBF"])[1] + rows = _rate_m_rows() + opts = {"scale": 100e8, "kongType": 3, "ilbanType": 0, "kikan": 2, "rateMRows": rows} + req = [{"matrix": rate_r, "opts": {**opts, "setBase": b}} for b in (1, 0, 2)] + helper = HELPER.with_name("helper_stmate_kanjub_rate.cjs") + out = subprocess.run( + ["node", str(helper)], + input=json.dumps(req), + capture_output=True, + text=True, + encoding="utf-8", + check=True, + ) + mo, local, nation = json.loads(out.stdout) + want = run( + { + "mode": "ratem", + "rows": rows, + "cases": [{"scale": 100e8, "kongType": 3, "ilban": 0, "kikan": 2}], + } + ) + for c in ("KANNO", "ILBAN", "IYUN"): + assert mo["rates"][c] == want["out"][0]["rates"][c] + assert mo["cells"][c][0] == "RATE_M" + assert local["rates"] == nation["rates"] # 지방·국가는 RATE_R 조회 그대로 + for c in ("SANBO", "KUNBO", "ANJUN"): + assert mo["rates"][c] == local["rates"][c] + + def test_set_base_combo_pairs_from_dfm(): nodes = json.loads(DFM.read_text(encoding="utf-8"))["nodes"] combo = next(o for o in _walk(nodes) if o.get("name") == "cmBx_Set_Base") diff --git a/tools/stmate-test/scr_kanjub.js b/tools/stmate-test/scr_kanjub.js index 5ab6d2b02..3085a49b4 100644 --- a/tools/stmate-test/scr_kanjub.js +++ b/tools/stmate-test/scr_kanjub.js @@ -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 diff --git a/tools/stmate-test/scr_kanjub_rate.js b/tools/stmate-test/scr_kanjub_rate.js index 424d0e230..45eb1cfc1 100644 --- a/tools/stmate-test/scr_kanjub_rate.js +++ b/tools/stmate-test/scr_kanjub_rate.js @@ -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 `${head}${body.join("")}
행
`; } + // 조회 입력 + 행자부기준용 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();