Files
Aislo/resources/tester/test_791_1_engine_semantics.py
eomsangdonandClaude Sonnet 5.5 43e061ba2a test(791-1): 마스터 지문 해시가 줄바꿈(CRLF/LF) 무관 + 지문 fixture 마스터 칸 재저장(결과 칸 그대로)
폴더마다 체크아웃 줄바꿈이 달라 메인 폴더에서 test_794_2b 셈 지문 시험이 깨짐.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 20:09:38 +09:00

519 lines
21 KiB
Python
Raw Permalink 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.
"""791-1 엔진 셈 의미 지킴 시험 — 금액번지 · 소계 단계 · 제외 · 줄 자리 · 블록 끝전 · 결과 조정 ·
특수 % · 변수 표시.
① 전후 같음 — M01 로직 전부(입력 = 자동값 + 기본값)의 날 셈 · 끝수 셈 지문 = 고치기 전 뜬 지문
(`fixtures/m01_791_1_logic_fingerprint.json` · 마스터 데이터가 바뀌면 건너뜀 → 이 파일을
바로 돌려 다시 뜸).
② 새 의미 단위 시험 · 검사.
③ 실무 손 옮김 견본 넷 — 원문 글(STmate 숨은 G 열 · EST 보이는 글)을 사람이 로직 꼴로 옮김 ·
단가 = 그 엑셀 목록표 값 · 노 · 재 · 경 · 계 = 엑셀(`helper_m02_hopyo_golden.FILES` · 649-G2).
"""
import hashlib
import importlib
import json
import sys
from decimal import Decimal
from pathlib import Path
import pytest
from M01_MasterData import M01_MasterData_Store as m01 # scripts 길을 둠
mcut = importlib.import_module("master_cut")
mf = importlib.import_module("master_formula")
ms = importlib.import_module("master_formula_sum")
ROOT = Path(__file__).resolve().parents[2]
FINGER = Path(__file__).resolve().parent / "fixtures" / "m01_791_1_logic_fingerprint.json"
COSTS = ("노무비", "재료비", "경비", "계")
D = Decimal
# ── ① 전후 같음 ────────────────────────────────────────────────────────
def _master_hash() -> str:
h = hashlib.sha256()
for path in sorted((ROOT / "resources" / "master_data").glob("*.json")):
if not path.name.startswith("_"):
raw = path.read_bytes().replace(b"\r\n", b"\n") # 줄바꿈(CRLF/LF) 무관
h.update(path.name.encode() + raw)
return h.hexdigest()[:16]
def _given(whole, row) -> dict:
import master_auto as ma
given = ma.values(whole, row)
given.update({k: v for k, v in ma.defaults(whole, row).items() if v is not None})
return given
def _print(whole, row) -> str:
key = str(row["키"])
try:
raw = mf.run(whole, key, _given(whole, row))
text = json.dumps(raw, default=str, sort_keys=True, ensure_ascii=False)
cut = mcut.engine(whole, key, raw)
text += json.dumps(cut, default=str, sort_keys=True, ensure_ascii=False)
except Exception as e: # 멈춤도 지문 — 멈춤 까닭이 바뀌어도 잡음
text = f"ERR {type(e).__name__} {e}"
return hashlib.sha256(text.encode()).hexdigest()[:16]
def fingerprints() -> dict:
files, whole = m01.loaded()
rows = [r for d in files.values() if d.get("그룹") == "로직" for r in d.get("줄", [])]
return {str(r["키"]): _print(whole, r) for r in rows}
def test_M01_로직_전부_전후_같음():
saved = json.loads(FINGER.read_text(encoding="utf-8"))
if saved["마스터"] != _master_hash():
pytest.skip("마스터 데이터 바뀜 — 지문 다시 뜸: python resources/tester/test_791_1_…py")
now = fingerprints()
assert len(now) == len(saved["결과"]) >= 1515
diff = sorted(k for k in now if now[k] != saved["결과"].get(k))
assert not diff, f"셈 결과 바뀐 로직 {len(diff)} 개: {diff[:10]}"
# ── 시험 마스터 ───────────────────────────────────────────────────────
def _master(logics: list[dict], labor: dict, mats: dict | None = None) -> mf.Master:
"""시험 전용 색인 — 인력 LB99 · 자재품목 MT99(물가자료) · 로직 GX99."""
files = {
"인력.json": {
"그룹": "인력",
"줄": [
{"키": k, "이름": n, "단위": "원/일", "값": D(v)} for k, (n, v) in labor.items()
],
},
"재료_자재품목.json": {
"그룹": "재료",
"줄": [
{"키": k, "이름": n, "단위": u, "물가자료": D(v)}
for k, (n, u, v) in (mats or {}).items()
],
},
"로직_시험.json": {"그룹": "로직", "줄": logics},
}
return mf.Master(files)
def _calc(master, key: str, given: dict | None = None) -> dict:
return mcut.engine(master, key, mf.run(master, key, given or {}))
def _money(got: dict) -> dict:
return {k: got[k] for k in COSTS}
def _line(got: dict, name: str) -> dict:
return next(x for x in got["줄"] if x["이름"] == name)
def _step(got: dict, name: str) -> dict:
return next(x for x in got["단계"] if x["이름"] == name)
def _man(key, qty, name, **more):
return {
"종류": "인력",
"요소": key,
"이름": name,
"단위": "인",
"수량": qty,
"비목": "노무비",
**more,
}
def _mat(key, qty, name, **more):
return {
"종류": "재료",
"요소": key,
"이름": name,
"단위": "식",
"수량": qty,
"비목": "재료비",
**more,
}
def _machine(qty, name, bundle):
"""기계 시간당 노 · 재 · 경 = 엑셀 중기목록표 값 그대로(M02 `값묶음` 길)."""
return {
"종류": "로직",
"요소": "로직(GX990009)",
"이름": name,
"단위": "hr",
"수량": qty,
"값묶음": dict(zip(("노무비", "재료비", "경비"), map(D, bundle))),
}
_HOUR = {
"키": "GX990009",
"이름": "시간당 기계경비(시험)",
"결과단위": "원/시간",
"입력": [],
"호표": [],
}
def _logic(key: str, **body) -> dict:
return {"키": key, "이름": key, "결과단위": "원/식", "입력": [], **body}
# ── ② 새 의미 단위 시험 ────────────────────────────────────────────────
LAB = {"LB990001": ("보통인부", 100000), "LB990002": ("특별인부", 200000)}
MAT = {"MT990001": ("자재 갑", "식", 1000), "MT990002": ("자재 을", "식", 3000)}
def _base(**body) -> mf.Master:
row = _logic(
"GX990001",
호표=[
_mat("MT990001", "1.23", "갑", 번지="A01"),
_mat("MT990002", "0.5", "을", 번지="A01"),
_man("LB990001", "0.0123", "보통", 번지="A02"),
_man("LB990002", "0.01", "특별", 번지="A02"),
],
**body,
)
return _master([row], LAB, MAT)
def test_옛_로직은_새_칸_없음():
master = _base()
for item in master.logic("GX990001")["호표"]:
del item["번지"]
got = _calc(master, "GX990001")
assert "단계" not in got and not ms.uses(master.logic("GX990001"))
assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960}
def test_번지_율_밑():
master = _base(덧줄=[{"이름": "잡재료", "식": "번지.A01.재료비 * 10 / 100", "비목": "재료비"}])
got = _calc(master, "GX990001")
assert _line(got, "잡재료")["금액"] == D("273.0")
assert got["재료비"] == 3003
def test_단계_누계_제외_합밖():
master = _base(
덧줄=[{"이름": "공구손료", "식": "단계.'인부 소계'.노무비 * 3 / 100", "비목": "경비"}],
단계=[
{"이름": "자재 소계", "단": 1, "줄": ["갑", "을"]},
{"이름": "인부 소계", "단": 1, "줄": ["보통", "특별"]},
{"이름": "경비 소계", "단": 1, "줄": ["공구손료"], "제외": True},
{"이름": "계", "단": 2},
],
)
got = _calc(master, "GX990001")
assert _step(got, "인부 소계")["계"] == D("3230.0")
assert _line(got, "공구손료")["금액"] == D("96.9")
assert _step(got, "경비 소계") == {
"이름": "경비 소계",
"단": 1,
"노무비": 0,
"재료비": 0,
"경비": D("96.9"),
"계": D("96.9"),
"제외": True,
}
assert _step(got, "계")["계"] == D("5960.0") # 제외 단계는 위로 안 올라감
assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960}
# 합밖 = 번지엔 쌓되 합에서 뺌
row = master.logic("GX990001")
row["호표"][1]["합밖"] = True
got = _calc(master, "GX990001")
assert got["재료비"] == 1230 and _step(got, "자재 소계")["재료비"] == D("2730.0")
def test_줄_자리_셋():
for way, want in (("반올림", 124), ("올림", 124), ("버림", 123)): # 123.5
master = _base(
덧줄=[{"이름": "덧", "식": "123.5", "비목": "경비", "자리": {"방법": way, "자리": 0}}]
)
assert _line(_calc(master, "GX990001"), "덧")["금액"] == want
master = _base(
덧줄=[
{"이름": "덧", "식": "123.456", "비목": "경비", "자리": {"방법": "반올림", "자리": 2}}
]
)
assert _line(_calc(master, "GX990001"), "덧")["금액"] == D("123.46")
def test_블록_끝전_둘():
body = {"덧줄": [{"이름": "경", "식": "0.6", "비목": "경비"}]} # 노 3230.0 · 재 2730.0 · 경 0.6
assert _calc(_base(**body), "GX990001")["계"] == 5960
got = _calc(_base(끝전="큰나머지", **body), "GX990001")
assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 0, "계": 5960}
# 원 미만이 모여 1 원이 되면 큰 나머지 비목에 · 같으면 경 › 재 › 노
body = {
"덧줄": [
{"이름": "노", "식": "0.6", "비목": "노무비"},
{"이름": "경", "식": "0.6", "비목": "경비"},
]
}
assert _calc(_base(**body), "GX990001")["계"] == 5960
got = _calc(_base(끝전="큰나머지", **body), "GX990001")
assert _money(got) == {"노무비": 3230, "재료비": 2730, "경비": 1, "계": 5961}
def test_결과금액_조정():
got = _calc(_base(조정={"옮김": "경비"}), "GX990001")
assert _money(got) == {"노무비": 0, "재료비": 0, "경비": 5960, "계": 5960}
got = _calc(_base(조정={"배율": {"노무비": D("0.5")}}), "GX990001")
assert _money(got) == {"노무비": 1615, "재료비": 2730, "경비": 0, "계": 4345}
assert mf.run(_base(조정={"옮김": "재료비"}), "GX990001", {})["재료비"] == D("5960.0")
def test_특수_퍼센트_누계와_앞_소계():
master = _base(
덧줄=[
{"이름": "L%", "식": "노무비 * 2 / 100", "비목": "경비"}, # 처음부터 노무비 누계
{"이름": "T%", "식": "단계.'소계'.계 * 0.8", "비목": "경비"}, # 바로 앞 소계 × 수량
],
단계=[{"이름": "소계", "단": 1, "줄": ["갑", "을"]}],
)
got = _calc(master, "GX990001")
assert _line(got, "L%")["금액"] == D("64.6")
assert _line(got, "T%")["금액"] == D("2184.0")
def test_변수_표시_반올림():
row = _logic(
"GX990002",
중간=[{"이름": "f", "식": "1 / 1.3", "표시": 2}, {"이름": "g", "식": "1 / 1.3"}],
결과={"식": "f * 100"},
)
got = mf.run(_master([row], LAB), "GX990002", {})
assert got["결과"] == D("77.00") and got["중간"]["g"] != D("0.77")
def test_검사():
def problems(**body) -> list[str]:
master = _base(**body)
return mf.check_logic(master, master.logic("GX990001"))[0]
assert problems(덧줄=[{"이름": "덧", "식": "번지.A01.재료비 * 1", "비목": "경비"}]) == []
assert any(
"없는 번지" in x
for x in problems(덧줄=[{"이름": "덧", "식": "번지.B09.계", "비목": "경비"}])
)
order = problems(
덧줄=[{"이름": "덧", "식": "단계.'소'.계", "비목": "경비"}],
단계=[{"이름": "소", "단": 1, "줄": ["갑", "덧"]}],
)
assert any("단계 순서" in x for x in order)
assert any(
"자리" in x
for x in problems(
덧줄=[{"이름": "덧", "식": "1", "비목": "경비", "자리": {"방법": "버림", "자리": 4}}]
)
)
assert any("끝전" in x for x in problems(끝전="합버림"))
assert any("단 「6」" in x for x in problems(단계=[{"이름": "소", "단": 6, "줄": ["갑"]}]))
assert any("없는 줄" in x for x in problems(단계=[{"이름": "소", "단": 1, "줄": ["없음"]}]))
assert any("조정 옮김" in x for x in problems(조정={"옮김": "계"}))
assert any(
"이 단계" in x
for x in problems(
단계=[{"이름": "가", "단": 1, "줄": ["갑"]}, {"이름": "나", "단": 1, "줄": ["갑"]}]
)
)
# ── ③ 실무 손 옮김 견본 넷 ─────────────────────────────────────────────
def 봉화():
"""봉화 단가산출근거 2909~2943 행 — `&M00081& * 1.03 * 0.327 :=A01` · `&PA01M:M& * 11'%'` ·
`&PA02L:S& * 1'%'` · `(=)` · `(==)` · 총계 45,652 · 34,119 · 11,192 · 341."""
row = _logic(
"GX990101",
호표=[
_mat("MT990101", "1.03 * 0.327", "합판(내수)", 번지="A01"),
_mat("MT990102", "0.038 * 0.327", "각재", 번지="A01"),
_man("LB990101", "5 / 50", "형틀목공", 번지="A02"),
_man("LB990102", "2 / 50", "보통인부", 번지="A02"),
],
덧줄=[
{"이름": "소모자재(박리재 등)", "식": "번지.A01.재료비 * 11 / 100", "비목": "재료비"},
{
"이름": "공구손료 및 경장비 기계경비",
"식": "번지.A02.노무비 * 1 / 100",
"비목": "경비",
},
],
단계=[
{"이름": "자재비 소계", "단": 1, "줄": ["합판(내수)", "각재", "소모자재(박리재 등)"]},
{
"이름": "설치 소계",
"단": 1,
"줄": ["형틀목공", "보통인부", "공구손료 및 경장비 기계경비"],
},
{"이름": "계", "단": 2},
],
)
master = _master(
[row],
{"LB990101": ("형틀목공", 274978), "LB990102": ("보통인부", 165545)},
{"MT990101": ("합판(내수)", "㎡", 11468), "MT990102": ("각재", "㎥", 500661)},
)
return master, row, {"노무비": 34119, "재료비": 11192, "경비": 341, "계": 45652}
def test_봉화_D00102_합판거푸집_STmate_번지_율():
master, row, want = 봉화()
assert mf.check_logic(master, row)[0] == []
got = _calc(master, "GX990101")
assert _money(got) == want
assert [x["금액"] for x in got["줄"]] == [
D(x) for x in ("3862.5", "6221.2", "27497.8", "6621.8", "1109.2", "341.1")
]
assert [x["계"] for x in got["단계"]] == [D("11192.9"), D("34460.7"), D("45653.6")]
def 소광():
"""울진소광 32 호표(D01489) — `'공구손료 :'&S01045& * 2'%'`(S01045 단위 L% = 처음부터
노무비 누계) · `L = Q/10 =` 표시 2 · 계 줄 420,734 · 182,223 · 230,359 · 8,152
(계약단가 87.745 % 는 B09 몫)."""
row = _logic(
"GX990102",
중간=[
{"이름": "N", "식": "40"},
{"이름": "Q", "식": "200"},
{"이름": "L", "식": "Q / 10", "표시": 2},
{"이름": "Q1", "식": "360"},
{"이름": "A", "식": "5.2"},
],
호표=[
_mat("MT990103", "1", "낙석방지책"),
_man("LB990103", "1 / N", "용접공"),
_man("LB990104", "3 / N", "특별인부(지주)"),
_man("LB990105", "2 / N", "보통인부(지주)"),
_machine("1 / N * 8", "트럭탑재형 크레인 10톤", (49479, 16797, 22539)),
_man("LB990104", "4 / Q * L", "특별인부(와이어)"),
_man("LB990105", "2 / Q * L", "보통인부(와이어)"),
_man("LB990104", "4 / Q1 * A", "특별인부(철망)"),
_man("LB990105", "2 / Q1 * A", "보통인부(철망)"),
],
덧줄=[{"이름": "공구손료", "식": "노무비 * 2 / 100", "비목": "경비"}],
단계=[
{"이름": "재료비 계", "단": 2, "줄": ["낙석방지책"]},
{"이름": "인건비 소계", "단": 1, "줄": ["용접공", "특별인부(지주)", "보통인부(지주)"]},
{"이름": "크레인 소계", "단": 1, "줄": ["트럭탑재형 크레인 10톤"]},
{"이름": "지주 계", "단": 2},
{"이름": "와이어 계", "단": 2, "줄": ["특별인부(와이어)", "보통인부(와이어)"]},
{"이름": "철망 계", "단": 2, "줄": ["특별인부(철망)", "보통인부(철망)"]},
{"이름": "공구손료 계", "단": 2, "줄": ["공구손료"]},
{"이름": "합계", "단": 3},
],
)
master = _master(
[row, _HOUR],
{
"LB990103": ("용접공", 278326),
"LB990104": ("특별인부", 221506),
"LB990105": ("보통인부", 169804),
},
{"MT990103": ("낙석방지책", "경간", 227000)},
)
return master, row, {"노무비": 182223, "재료비": 230359, "경비": 8152, "계": 420734}
def test_울진소광_D01489_낙석방지책_STmate_L퍼센트():
master, row, want = 소광()
assert mf.check_logic(master, row)[0] == []
got = _calc(master, "GX990102")
assert _money(got) == want
assert _line(got, "공구손료")["금액"] == D("3644.4")
assert _step(got, "지주 계")["계"] == D("49824.2")
assert _step(got, "합계")["계"] == D("420735.3")
def 영월():
"""동부청영월 단가산출근거 54~103 행(D00003) — `f = 1/1.3 =` · `E=…=` ·
`Q = 3600*q*K*f*E/Cm =`(표시 2) · `&X00205L/M/S& / Q * 0.9` · 계 원 버림 + 큰 나머지
5,471 · 4,764 · 310 · 397."""
row = _logic(
"GX990103",
중간=[
{"이름": "q", "식": "0.7"},
{"이름": "f", "식": "1 / 1.3", "표시": 2},
{"이름": "K", "식": "0.9"},
{"이름": "Cm", "식": "20"},
{"이름": "E", "식": "(0.7 + 0.6) / 2 - 0.05", "표시": 2},
{"이름": "Q", "식": "3600 * q * K * f * E / Cm", "표시": 2},
],
호표=[
_man("LB990106", "(0.2 + 0.26) / 2 * 0.1", "인부"),
_machine("1 / Q * 0.9", "유압식 백호우 0.7㎥", (55700, 18015, 23128)),
],
단계=[
{"이름": "인력 소계", "단": 1, "줄": ["인부"]},
{"이름": "기계 소계", "단": 1, "줄": ["유압식 백호우 0.7㎥"]},
{"이름": "계", "단": 2},
],
끝전="큰나머지",
)
master = _master([row, _HOUR], {"LB990106": ("보통인부", 165545)})
return master, row, {"노무비": 4764, "재료비": 310, "경비": 397, "계": 5471}
def test_동부청영월_D00003_터파기_STmate_큰나머지_끝전():
master, row, want = 영월()
assert mf.check_logic(master, row)[0] == []
got = _calc(master, "GX990103")
assert got["중간"]["Q"] == D("52.39")
assert _money(got) == want
assert _step(got, "계")["계"] == D("5471.0")
row["끝전"] = "비목버림" # 비목 원 버림이면 1 원 모자람
assert _calc(master, "GX990103")["계"] == 5470
def 임도():
"""북부청임도 단가산출 18 번(문양거푸집) — `5,786.1 × 2% = 116`(그 줄만 반올림 0) ·
`인부소계 : 24,467.3` · 노무비 24,467 · 재료비 5,902 · 계 30,369."""
row = _logic(
"GX990104",
호표=[
_mat("MT990104", "1 * (1 / 0.72)", "문양거푸집(ABS코팅폼)"),
_man("LB990107", "0.07", "형틀목공"),
_man("LB990108", "0.03", "보통인부"),
],
덧줄=[
{
"이름": "잡재료비",
"식": "재료비 * 2 / 100",
"비목": "재료비",
"자리": {"방법": "반올림", "자리": 0},
}
],
단계=[{"이름": "인부소계", "단": 1, "줄": ["형틀목공", "보통인부"]}],
)
master = _master(
[row],
{"LB990107": ("형틀목공", 275790), "LB990108": ("보통인부", 172068)},
{"MT990104": ("문양거푸집(ABS코팅폼)", "㎡", 4166)},
)
return master, row, {"노무비": 24467, "재료비": 5902, "경비": 0, "계": 30369}
def test_북부청임도_18_문양거푸집_EST_줄_자리():
master, row, want = 임도()
assert mf.check_logic(master, row)[0] == []
got = _calc(master, "GX990104")
assert _line(got, "잡재료비")["금액"] == 116
assert _step(got, "인부소계")["계"] == D("24467.3")
assert _money(got) == want
del row["덧줄"][0]["자리"] # 기본 0.1 버림이면 엑셀과 1 원 다름
assert _calc(master, "GX990104")["계"] == 30368
if __name__ == "__main__": # 지문 다시 뜸 — 마스터 데이터가 바뀐 뒤 · 엔진 고치기 전 커밋에서만
data = {"마스터": _master_hash(), "결과": fingerprints()}
FINGER.write_text(
json.dumps(data, ensure_ascii=False, indent=1, sort_keys=True) + "\n", encoding="utf-8"
)
print(len(data["결과"]), "개 지문", FINGER, file=sys.stderr)