Merge remote-tracking branches 'origin/main_laptop_1' and 'origin/안티그래비티' into sub_laptop_3
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
// STMATE 재현 앱 연쇄 재계산(계산완료) — stdin = /api/load 결과 · 재계산 뒤 COSTN 단가와 원가계산서
|
||||
globalThis.window = globalThis;
|
||||
require("../../tools/stmate-test/calc.js");
|
||||
require("../../tools/stmate-test/smt_eval.js");
|
||||
require("../../tools/stmate-test/smt_engine.js");
|
||||
require("../../tools/stmate-test/view_prj_boq.js");
|
||||
let buf = "";
|
||||
process.stdin.on("data", (d) => (buf += d));
|
||||
process.stdin.on("end", () => {
|
||||
const d = JSON.parse(buf);
|
||||
const ctx = { dbfData: d, state: d.sim_state, uiState: {} };
|
||||
const count = window.STMATE_ENGINE.cascadeRecalculateAll(ctx);
|
||||
const prices = {};
|
||||
for (const r of d.costn_rows)
|
||||
prices[r.CODE.trim()] = ["MAMT1", "LAMT1", "SAMT1", "MAMT2", "LAMT2", "SAMT2"].map((k) =>
|
||||
Number(r[k]),
|
||||
);
|
||||
const k = window.STMATE_CALC.calculateAll(ctx.state).kanjubOrig;
|
||||
process.stdout.write(JSON.stringify({ count, prices, grand: k.grandTotal, dT: k.dT }));
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
// STMATE 재현 앱 일위대가(B)·중기(X) 조립 — stdin {costn_rows, bdqty_rows, start_ini}
|
||||
const E = require("../../tools/stmate-test/smt_eval.js");
|
||||
let buf = "";
|
||||
process.stdin.on("data", (d) => (buf += d));
|
||||
process.stdin.on("end", () => {
|
||||
const d = JSON.parse(buf);
|
||||
const ini = d.start_ini || {};
|
||||
const by = {};
|
||||
for (const r of d.costn_rows) by[r.CODE.trim()] = r;
|
||||
const slot = (r) => (Number(r.NAGCD) > 0 ? 2 : 1);
|
||||
const lookup = (c) => {
|
||||
const r = by[c];
|
||||
if (!r) return null;
|
||||
const s = slot(r);
|
||||
return { M: +r[`MAMT${s}`], L: +r[`LAMT${s}`], S: +r[`SAMT${s}`], unit: r.UNIT };
|
||||
};
|
||||
const conld = String(ini.CONLD || "");
|
||||
const labor = new Set(conld.match(/.{1,6}/g) || []);
|
||||
const groups = {};
|
||||
for (const r of d.bdqty_rows) (groups[r.BDCODE] = groups[r.BDCODE] || []).push(r);
|
||||
const out = {};
|
||||
for (const [parent, rows] of Object.entries(groups)) {
|
||||
if (!by[parent] || !/^[BX]/.test(parent)) continue;
|
||||
const ev = E.evaluateComposition(rows, lookup, {
|
||||
totalFirst: parent[0] === "B" && String(ini.CHBNM ?? "1") === "0",
|
||||
order: ini.OPTNM || "SML",
|
||||
sbmok: String(by[parent].SBMOK || "").trim(),
|
||||
labor: parent[0] === "X" ? labor : null,
|
||||
rx: ini.RXXNM,
|
||||
usdRate: ini.EXLNM1,
|
||||
});
|
||||
out[parent] = [ev.M, ev.L, ev.S];
|
||||
}
|
||||
process.stdout.write(JSON.stringify(out));
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
"""STMATE 재현 앱 일위대가(B)·중기(X) 재계산과 연쇄 재계산(계산완료) — 실무 STC 저장 단가와 1원 대조.
|
||||
|
||||
구성 = BDQTY.DBF 복호(tools/stmate-test/bdqty.py · 사용자 승인) · 정답 = STC 의 COSTN 저장 단가.
|
||||
연쇄 시험은 기초단가(L·M·S)만 두고 중기 → 산근 → 일위대가 → 내역 → 원가계산서를 다시 셈.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
sys.path.insert(0, str(ROOT / "tools/stmate-test"))
|
||||
import server # noqa: E402
|
||||
|
||||
HERE = Path(__file__).parent
|
||||
DOCS = ROOT / "resources/knowledge/original/실무문서"
|
||||
ORIGINAL = {
|
||||
"봉화2024": "04.산불 봉화/01. 내역서/13-2024년 산불진화임도 실시설계(기번41_현동)_ L=3.46km.STC",
|
||||
"울진2025": "25년 산불진화임도(기번8)(울진소광)/2025년 산불진화임도 신설사업(기번8 울진.금강송.소광.산29외).STC",
|
||||
"영월2024": "05. 동부청 영월/내역서/2024년 임도시설사업2차(산불진화_기번6)-영월.STC",
|
||||
"대흥2024": "2024년 간선임도(기번3-울진.대흥)(공통)/1. 설계원본/01 내역자료/2024년 간선임도사업(기번3).STC",
|
||||
"소광원설계": "25년 산불진화임도(기번8)(울진소광)/1. 설계원본/03-내역/22-2025년 산불진화임도사업(울진 금강송 소광).STC",
|
||||
}
|
||||
CHANGED_OK = {
|
||||
"영덕변경": "20250527 2025년 계류보전사업(기번1-영덕.병곡.영리.산214-1)(변경)/01.내역자료/2025년 계류보전사업(기번1 영덕.병곡.영.산214)(변경).STC",
|
||||
}
|
||||
|
||||
|
||||
def load(rel):
|
||||
return server.api_load(server.LoadReq(source_path=str(DOCS / rel)))
|
||||
|
||||
|
||||
def node(helper, payload):
|
||||
out = subprocess.check_output(
|
||||
["node", str(HERE / helper)],
|
||||
input=json.dumps(payload, ensure_ascii=False),
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return json.loads(out)
|
||||
|
||||
|
||||
def stored(row, slot):
|
||||
return [float(row[f"{k}AMT{slot}"] or 0) for k in "MLS"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(ORIGINAL) + list(CHANGED_OK))
|
||||
def test_b_and_x_compositions_match_stored(name):
|
||||
d = load({**ORIGINAL, **CHANGED_OK}[name])
|
||||
out = node(
|
||||
"helper_stmate_ilwi.cjs", {k: d[k] for k in ("costn_rows", "bdqty_rows", "start_ini")}
|
||||
)
|
||||
rows = {r["CODE"].strip(): r for r in d["costn_rows"]}
|
||||
assert len(out) > 20, f"{name} 구성 호표 {len(out)}"
|
||||
diffs = {}
|
||||
for code, got in out.items():
|
||||
slot = 2 if float(rows[code].get("NAGCD") or 0) > 0 else 1
|
||||
if got != stored(rows[code], slot):
|
||||
diffs[code] = (got, stored(rows[code], slot))
|
||||
assert not diffs, f"{name} 불일치 {len(diffs)}/{len(out)} {list(diffs.items())[:4]}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", list(ORIGINAL))
|
||||
def test_full_cascade_keeps_every_unit_price_and_total(name):
|
||||
d = load(ORIGINAL[name])
|
||||
before = {r["CODE"].strip(): stored(r, 1) for r in d["costn_rows"]}
|
||||
out = node("helper_stmate_cascade.cjs", d)
|
||||
assert out["count"]["xCount"] > 0 and out["count"]["dCount"] > 0 and out["count"]["bCount"] > 0
|
||||
moved = {c: (v[:3], before[c]) for c, v in out["prices"].items() if v[:3] != before[c]}
|
||||
assert not moved, f"{name} 재계산 뒤 바뀐 단가 {list(moved.items())[:4]}"
|
||||
assert abs(out["grand"] - float(d["prjlst"]["COSTW1"])) < 0.5
|
||||
Reference in New Issue
Block a user