- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움) - 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만 - 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차 - 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움 - 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음) - 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
135 lines
5.0 KiB
Python
135 lines
5.0 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_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 = mmf.table(DATA, exact=False)[0]["줄"] # 기계 = 폴더(826-2)
|
||
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["로직"])
|