- 빌더 build_기계_인력줄_842_3.py(규칙 = master_machine_crew.from_old_row · 다시 돌려도 같음) - 인력 줄 301 대(1 인 282 · 2 인 18 · 7 인 1 · 직종 빈 줄 42 = 빈 키 = 식 입력 직종 고르기 그대로) - 잡품 줄 328 대(잡재료율 > 0 = 주연료 · 전력 줄마다 그 율 · 「73.7+」 글 연료도 줄) · 범위 글 「13∼18」 2 대 = 잡품 없음(값원문 그대로) - 선원류 칸 79 대 · 비활성 표지 79 대 그대로 · 옛 칸 남음 0 - 바탕 덮음 = 식에 그 줄이 있을 때만(손료만 식을 일부러 부른 단가산출 GC000324 → GC000273 크롤러드릴 = 조종원 안 더함) - 식 고름 = 그 줄 인력도 조종원 있음으로 봄 · 로직 검사 · 값 모으기 = 기계 칸을 목록 읽기로(master_formula_check) - 시험 고침: 821_2 · 822_2 미리보기 = 목록 읽은 값으로 · 822_1 노임 키 = 인력 줄 · 822_2 범위 까닭 글 = 「잡재료율 없음」(범위 글은 값원문) - 전후: 655 대 시간당(hourly · brief · 문서 셈) 653 같음 · 다름 2 = 펌프 준설선 EQ000552 · 553(비활성 · 범위 글) 까닭 글만 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
5.2 KiB
Python
138 lines
5.2 KiB
Python
"""PLAN 821-2 기계 계수 계산 짝 — 지킴 시험.
|
||
|
||
① 계수 전수 대조: M01 기계표에서 품셈 입력 다섯 · 계수 넷이 다 있는 줄 = 파이썬 계수 셈과 같음
|
||
(다른 줄 = 원문 표 값 그대로인 점보드릴 2 줄만 · 원문 777 · 셈 777.8).
|
||
② 파이썬 · TS 거울: 같은 입력 → 같은 계수 · 같은 시간당 미리보기.
|
||
③ 미리보기 = 서버 기계경비식(`M02_Machine_Hourly.hourly`) 견본 스무 대 · 비목 셋 같음.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
import pytest
|
||
|
||
ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(ROOT))
|
||
sys.path.insert(0, str(ROOT / "resources" / "master_data" / "scripts"))
|
||
|
||
import master_machine_coef as mc # noqa: E402
|
||
import master_machine_crew as mcrew # noqa: E402
|
||
import master_machine_files as mmf # noqa: E402
|
||
|
||
DATA = ROOT / "resources" / "master_data"
|
||
TS = ROOT / "common_util" / "common_util_machine_coef.ts"
|
||
# 원문 표 값 ≠ 식(정비비계수 0.7 ÷ 9,000 × 10^7 = 777.8 → 원문 777 · 계 2,515) — 식을 비틀지 않음
|
||
ORIGINAL_ODD = {"EQ000347", "EQ000348"}
|
||
SAMPLES = (
|
||
"EQ000001 EQ000006 EQ000022 EQ000026 EQ000040 EQ000051 EQ000073 EQ000076 EQ000126 EQ000151 "
|
||
"EQ000176 EQ000201 EQ000226 EQ000229 EQ000246 EQ000326 EQ000401 EQ000434 EQ000455 EQ000544"
|
||
).split()
|
||
|
||
|
||
def _load(name: str) -> list[dict]:
|
||
return json.loads((DATA / name).read_text(encoding="utf-8"))["줄"]
|
||
|
||
|
||
MACHINES = [ # 기계 = 폴더(826-2) · 인력 · 잡품 목록 → 미리보기가 읽는 조종원 수 · 직종 · 잡재료율(842-3)
|
||
{**r, "값": mcrew.view(r["값"])} for r in mmf.table(DATA, exact=False)[0]["줄"]
|
||
]
|
||
FUEL = {r["원문번호"]: r["값"] for r in _load("재료_유가전력.json")}
|
||
WAGE = {r["원문번호"]: r["값"] for r in _load("인력.json")}
|
||
|
||
|
||
def _full(row: dict) -> bool:
|
||
v = row["값"]
|
||
return all(isinstance(v.get(k), (int, float)) for k in (*mc.INPUTS, *mc.COEFS))
|
||
|
||
|
||
def _prices(v: dict) -> tuple[float, float]:
|
||
"""전국평균 유가 · 노임(직종 비면 기계경비식 조종원직종 첫 고르기 1048)."""
|
||
return FUEL.get(f"{v.get('주연료종류')}:전국평균", 0), WAGE.get(
|
||
str(v.get("조종원직종") or "1048"), 0
|
||
)
|
||
|
||
|
||
def test_coef_matches_master_table():
|
||
rows = [r for r in MACHINES if _full(r)]
|
||
assert len(rows) >= 590
|
||
odd = {r["키"] for r in rows if mc.coef_of(r["값"]) != {k: r["값"][k] for k in mc.COEFS}}
|
||
assert odd == ORIGINAL_ODD
|
||
|
||
|
||
def test_coef_excavator_example():
|
||
got = mc.coef(10000, 1250, 0.9, 0.7, 0.1)
|
||
assert got == {"상각비계수": 900, "정비비계수": 700, "관리비계수": 485, "손료계수": 2085}
|
||
assert mc.coef(0, 1250, 0.9, 0.7, 0.1) is None
|
||
assert mc.coef_of({"내용시간": 10000}) is None
|
||
|
||
|
||
@pytest.mark.skipif(
|
||
not (ROOT / "config" / "node_modules" / "typescript").is_dir(),
|
||
reason="프론트엔드 의존성 미설치",
|
||
)
|
||
def test_ts_mirror(tmp_path):
|
||
coef_cases = [[r["값"][k] for k in mc.INPUTS] for r in MACHINES if _full(r)]
|
||
coef_cases += [[10000, 1250, 0.9, 0.7, 0.1], [0, 1, 1, 1, 1], [3600, 540, 0.9, 1.15, 0.1]]
|
||
hour_cases = []
|
||
for r in MACHINES:
|
||
fuel, wage = _prices(r["값"])
|
||
hour_cases.append([r["값"], fuel, wage, 114.4, 0.125])
|
||
hour_cases.append(
|
||
[
|
||
{"가격": 2680, "손료계수": 5313, "주연료": 1.0, "잡재료율": 10, "전력": 3.73},
|
||
2000,
|
||
0,
|
||
114.4,
|
||
0.125,
|
||
]
|
||
)
|
||
src = tmp_path / "in.json"
|
||
src.write_text(
|
||
json.dumps({"coef": coef_cases, "hour": hour_cases}, ensure_ascii=False), encoding="utf-8"
|
||
)
|
||
script = f"""
|
||
const fs = require("fs");
|
||
const ts = require({json.dumps(str(ROOT / "config" / "node_modules" / "typescript"))});
|
||
const opt = {{ module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 }};
|
||
const src = fs.readFileSync({json.dumps(str(TS))}, "utf8");
|
||
const js = ts.transpileModule(src, {{ compilerOptions: opt }}).outputText;
|
||
const m = {{ exports: {{}} }};
|
||
new Function("exports", "module", js)(m.exports, m);
|
||
const inp = JSON.parse(fs.readFileSync({json.dumps(str(src))}, "utf8"));
|
||
process.stdout.write(JSON.stringify({{
|
||
coef: inp.coef.map((c) => m.exports.machineCoef(...c)),
|
||
hour: inp.hour.map((h) => m.exports.machineHourly(...h)),
|
||
}}));
|
||
"""
|
||
out = subprocess.run(
|
||
["node", "-e", script], capture_output=True, text=True, encoding="utf-8", check=True
|
||
)
|
||
got = json.loads(out.stdout)
|
||
want_coef = [mc.coef(*c) for c in coef_cases]
|
||
want_hour = [mc.preview(*h) for h in hour_cases]
|
||
assert got["coef"] == want_coef
|
||
assert len(got["hour"]) == len(want_hour)
|
||
for a, b in zip(got["hour"], want_hour):
|
||
assert a == pytest.approx(b, abs=1e-9)
|
||
|
||
|
||
def test_preview_equals_server_hourly():
|
||
from M02_MasterTemplete import M02_Machine_Hourly as mh
|
||
|
||
rows = {r["키"]: r for r in MACHINES}
|
||
for key in SAMPLES:
|
||
h = mh.hourly(key)
|
||
assert h["ok"], (key, h)
|
||
v = rows[key]["값"]
|
||
p = mc.preview(v, *_prices(v))
|
||
assert (p["노무비"], p["재료비"], p["경비"], p["계"]) == (
|
||
h["노무비"],
|
||
h["재료비"],
|
||
h["경비"],
|
||
h["계"],
|
||
), (key, h["로직"])
|