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}
|---|