Files
Aislo/resources/tester/test_stmate_kanjub_rea.py
T
eomsangdonandClaude Opus 5.5 c5fc1fd7aa feat(stmate-test): REA MCP(Ghidra) 변경 차수·잔여 재검증 — 변경 4차 = 계열 5 · 할증 계수 4바이트 실수 · 손료 계수 칸 걷음 · 강제금액 = JDL 변환 · JDL 고정폭 형식
- 변경 차수 상한 4차(K_ORD>4 거부) · 4차 = 5번째 칸(MCOST5·XX5NM·QTY5)
- 할증 계수 float32 — 70%·115% 1원 적음
- 손료 E0~E3 = 조달청 XML 구분값뿐 · 계수 곱 없음
- jdl_format.py — 13종 자르는 자리 · 단가 목록 8종 열 이름 · 줄 만들기·읽기·검사
- 지식DB 34번 §17 · REA_MCP_보강 근거

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 13:24:24 +09:00

212 lines
7.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""STMATE 재현 앱 — 원본 정적 규칙(지식DB REA 34) 지킴 시험 · 제잡비 차수 콤보·요율 조회·표준시장단가·노임 환산·할증율.
기대값은 원본 바이트에서 읽은 규칙을 이 파일에서 따로 손셈한 것(실무 표본이 없는 갈래).
차수 콤보 = ST_KanJub 0x46c779(PCLASS 갈래표 0x46c790) · 처음 계열 0x46c679 · 기본값 0x47a6ec·0x47b633
건설기계 = 0x47fa24 · 퇴직공제 0x47ae79 · 30일 미만 0x47b376 · 표준시장단가 Main 0x45a468 · OPTIMIZ ST_Func_C 0x48094c
노임 환산 갈래 Main 0x43894c(RXX 1·10~14) · 할증율 ST_Boq 0x507cc8
"""
import json
import math
import subprocess
from pathlib import Path
HELPER = Path(__file__).with_name("helper_stmate_kanjub_rea.cjs")
def run(req):
return json.loads(
subprocess.check_output(
["node", str(HELPER)], input=json.dumps(req), text=True, encoding="utf-8"
)
)
def test_chasu_combo_by_pclass():
"""cBx_Chasu 목록 = PCLASS 갈래 · 7 = 계약(당초) + K_ORD 개 변경 · 계열 = 순번+1."""
cases = [
[0, 0, 0],
[1, 0, 0],
[2, 0, 0],
[3, 0, 0],
[4, 0, 1],
[4, 0, 0],
[6, 0, 0],
[7, 3, 0],
[8, 0, 0],
[9, 0, 0],
]
out = run({"mode": "native", "cases": cases})
series = [[s for _, s in o["list"]] for o in out]
assert series == [
[1],
[1, 2],
[1, 2],
[1, 2],
[2, 1],
[1],
[1, 3, 4],
[1, 2, 3, 4],
[1, 2, 3, 4],
[1],
]
assert [lb.strip() for lb, _ in out[7]["list"]] == [
"계약(당초) 내역",
"1차 변경내역",
"2차 변경내역",
"3차 변경내역",
]
assert [lb.strip() for lb, _ in out[8]["list"]][2] == "변경내역-시비"
over = run({"mode": "native", "cases": [[7, 4, 0], [7, 5, 0]]})
assert [s for _, s in over[0]["list"]] == [1, 2, 3, 4, 5] and over[1][
"list"
] == [] # 4차 = 계열 5 · 5차 거부
def test_first_series_and_change_defaults():
"""처음 계열(0x46c690) · 변경 계열 기본값 = 안전 2 · 보험료 1(상시근로자 밑수면 0)."""
out = run(
{
"mode": "native",
"cases": [[7, 2, 0], [7, 0, 0], [7, 6, 0], [8, 0, 0], [3, 0, 0], [0, 0, 0]],
}
)
assert [o["first"] for o in out] == [3, 2, 2, 4, 2, 1]
seven = out[0]
assert [d["I_ANJUNAUTO"] for d in seven["dft"]] == [1, 2, 2]
assert [d["I_BF_CH_BO"] for d in seven["dft"]] == [0, 1, 1]
assert seven["dftSangsi"]["I_BF_CH_BO"] == 0
assert seven["gate"] == [False, True]
assert out[4]["gate"] == [False, False] # PCLASS 3(통합형) — I_BF_CH_BO 를 0 으로 지움
assert set(seven["items"]) == {"KUNBO", "YUNBO", "SLVBO", "TBUGUM", "ANJUN"}
def _matrix():
"""50×35 — 칸 값 = 행×100 + 열(1부터) · 0 칸 없음."""
return [[r * 100 + c for c in range(1, 36)] for r in range(50)]
def test_rate_eqip_tbugum_short_period():
base = {"kongType": 0, "kikan": 1, "scale": 1e9, "anjunType": 1, "anjunScale": 1e9}
cases = [
base | {"eqip": 0, "tbugum": 0},
base | {"eqip": 5, "tbugum": 2},
base | {"eqip": 1, "tbugum": 3},
base | {"eqip": 0, "kongType": 3, "tbugum": 1},
base | {"eqip": 0, "kongType": 2},
base | {"eqip": 2, "kongType": 5},
base | {"eqip": -1},
base | {"kikan": -1},
]
out = run({"mode": "rate", "matrix": _matrix(), "cases": cases})
cell = lambda r, c: r * 100 + c # noqa: E731
eq = [o["rates"].get("EQIP") for o in out]
# 구분 0 = 공사종류(0 RATE_01 · 3 RATE_03 · 2 RATE_02) · 1~5 = RATE_06~10 · 국가유산(5)·−1 = 0
assert eq[:7] == [cell(49, 1), cell(49, 10), cell(49, 6), cell(49, 3), cell(49, 2), 0, 0]
tb = [o["rates"].get("TBUGUM") for o in out[:4]]
assert tb == [cell(46, 16), cell(46, 18), None, cell(46, 17)]
assert out[2]["keys"]["R_TBUGUM"] == 7 # 기타(3) = 그대로
short = out[7]
assert [short["rates"][c] for c in ("KUNBO", "YUNBO", "SLVBO")] == [0, 0, 0]
assert short["keys"]["R_KUNBO"] == 0
assert (
short["rates"]["KANNO"] == out[0]["rates"]["KANNO"] - 100
) # 30일 미만 = 6개월 이하 칸(기간 0)
def _optimiz(T, parts):
fl = [math.trunc(v) for v in parts]
fr = [v - f for v, f in zip(parts, fl)]
left = round(math.trunc(T) - sum(fl))
while left > 0:
k = max(range(len(fr)), key=lambda i: (fr[i], -i))
if fr[k] <= 0:
break
fl[k] += 1
fr[k] = -1
left -= 1
return fl
def _split(T, R, pre, ty, op):
if R == 0 and pre:
L = pre
else:
L0, X = T * R / 100, T * (100 - R) / 100
L = (
_optimiz(T, [L0, 0, 0, X])[0]
if op == 1
else _optimiz(T, [X, 0, 0, L0])[3]
if op == 2
else math.trunc(L0 + 1e-9)
)
rem = T - L
return {
"L": L,
"M": rem if ty == 1 else 0,
"S": rem if ty == 2 else 0,
"N": 0 if ty in (1, 2) else rem,
}
def test_std_market_split_modes():
"""ACTTYP 0·1·2 × ACTOPT 0·1·2 — 노무 끝전(버림 · 노무 우선 · 타금액 우선) · 나머지 자리."""
cases = [
[T, R, 0, ty, op]
for T, R in ((1001, 37.5), (1000, 45.05), (999, 50), (12345, 63.3))
for ty in (0, 1, 2)
for op in (0, 1, 2)
]
cases.append([5000, 0, 1234, 1, 1]) # 율 0 · 노무 미리값 = 그대로
out = run({"mode": "split", "cases": cases})
for c, o in zip(cases, out):
want = _split(*c)
assert {k: o[k] for k in "LMSN"} == want, c
assert o["T"] == c[0]
# 0.5 동률 — 노무 우선이면 노무가 1원 · 타금액 우선이면 다른 금액이 1원
assert out[cases.index([999, 50, 0, 1, 1])]["L"] == 500
assert out[cases.index([999, 50, 0, 1, 2])]["L"] == 499
def test_labor_branch_by_rxx():
"""RXX 1·10~14 = 식 하나로 모든 L 행 · 0·2~9 = 직종 목록 CONLD→CONTD→CON1D~ 첫 일치."""
ini = {
"RXXNM": "A",
"RXTNM": "T",
"RX1NM": "R1",
"RX4NM": "R4",
"CONLD": "L00001",
"CON1D": "L00002L00001",
}
out = run(
{
"mode": "labor",
"ini": ini,
"cases": [
[0, ["L00001", "L00002", "L00003"]],
[1, ["L00009"]],
[10, ["L00001"]],
[14, ["L00003", "M00001"]],
[3, ["L00002"]],
],
}
)
assert out == [["A", "R1", None], ["A"], ["T"], ["R4", None], ["R1"]]
def test_hal_rate_rows():
"""할증율 여러 행 — 보호(UC=T) 행 건너뜀(비목 무관) · B·D·L·M·S·W·X 밖 코드는 율 0."""
rows = [
{"BDCODE1": c, "UC": uc, "_recno": i, "SORTCODE": str(i + 1), "ENDCODE": "0"}
for i, (c, uc) in enumerate(
[("M00001", ""), ("B00002", "T"), ("A00003", ""), ("W00004", "")]
)
]
out = run({"mode": "hal", "rows": rows, "rates": {"M": 110, "L": 120, "S": 130}, "how": "each"})
got = {r[0]: r[1:] for r in out["rows"]}
assert out["r"]["kept"] == 1
assert got["M00001"] == ["110", "120", "130"]
assert got["A00003"] == ["0", "0", "0"]
assert got["W00004"] == ["110", "120", "130"]
assert got["B00002"] == [None, None, None]