"""기계 원천 벌 뽑기(`_build_mach.py`) 시험 — 2026-09-18 브레인 일감(랩탑 메인). 못박는 것 - 기계 자료가 **지금 8장 md 와 같음** — md 가 고쳐졌는데 안 뽑았으면 빨강(뽑기를 돌리라는 뜻). - ⚠ **줄이 밀려도 값이 그대로** — 자리는 줄 번호가 아니라 **기계 코드**로 잡음. - 한 칸에 뭉친 줄(`0101-0007 0010 …`)과 줄마다 푼 판이 **같은 값**을 냄(코덱스 8-4 손질이 그 꼴). - 토막 수가 안 맞으면 **지어내지 않고** `parse_audit` 에 적음. - 사람이 맞춘 줄(`parse_method`)·손질 사유(`note`)는 **옛 벌에서 이어받음** — ⚠ 원문에 줄이 있는 것만. 금액 불변 — 뽑은 뒤 `test_work_item_key_gate.py` 원가계산서 시험(직접공사비 5,983,724)이 잼. """ from __future__ import annotations import json import sys from pathlib import Path ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT / "resources" / "data_cost_input_value")) import _build_mach as build # noqa: E402 GLUED = """ 8-3 기계손료(0101) 불도저(무한궤도) | 분류 번호 | 규격 (ton) | 내용 시간 | 연간표준 가동시간 | 상각 비율 | 정비 비율 | 연간 관리 비율 | 시 간 당(10-7) | | | | |---|---|---|---|---|---|---|---|---|---|---| | | | | | | | | 상각비 계수 | 정비비 계수 | 관리비 계수 | 계 | | 0101-0007 | 7 | 12,000 | 1,250 | 0.9 | 0.7 | 0.1 | 750 | 583 | 478 | 1,811 | | 0010 | 10 | 12,000 | 1,250 | 0.9 | 0.7 | 0.1 | 750 | 583 | 478 | 1,811 | 8-4 운전경비 산정8-4-1 [00]토공기계 | 분류번호 | 기계명 | 규격 | 주연료 (ℓ/hr) | 잡재료 (주연료의%) | 조종원 (인/일) | |---|---|---|---|---|---| | 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 | 8-5 기계가격8-5-1 [00]토공기계 | 기 종 | 분류번호 | 가격(₩) | |---|---|---| | 불 도 저 ( 무 한 궤 도 ) | 0101-0007 0010 | 76,547 168,668 | """ UNGLUED = GLUED.replace( "| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |", "| 0101-0007 | 불도저(무한궤도) | 7ton | 9.0 | 16% | 1 |\n| 0010 | | 10 | 12.5 | 16 | 1 |", ) BARE = { "sources": [{"path": "x.md", "sha256": "", "role": "primary"}], "variables": { "mach_price": {"unit": "thousand_KRW", "key": "machine_code", "records": []}, "mach_loss_coef": {"unit": "1/hour", "key": "machine_code", "records": []}, "mach_fuel_rate": {"unit": "L/hour", "key": "machine_code", "parsed_records": [], "source_tables": []}, }, } # fmt: skip def _of(doc: dict, key: str, field: str = "records") -> dict: return {r["machine_code"]: r for r in doc["variables"][key][field]} def _extract(text: str, old: dict | None = None) -> dict: return build.extract(json.loads(json.dumps(old or BARE)), text.encode("utf-8"))[0] def test_기계_자료가_지금_md_와_같음() -> None: """md 가 고쳐졌으면 `_build_mach.py` 를 돌릴 것.""" old = json.loads(build.TARGET.read_text(encoding="utf-8")) doc, report = build.extract(old, (ROOT / old["sources"][0]["path"]).read_bytes()) diff = build.diff_report(old, doc) stale = {k: {n: len(v) for n, v in item.items() if v} for k, item in diff.items() if any(item.values())} # fmt: skip assert not stale, f"기계 자료가 md 와 다름 — _build_mach.py 돌릴 것: {stale}" def test_줄이_밀려도_값이_그대로() -> None: doc = _extract(GLUED) shifted = _extract(GLUED.replace("8-4 운전경비", "보탠 줄\n\n보탠 줄\n\n8-4 운전경비")) for key, field in (("mach_price", "records"), ("mach_loss_coef", "records"), ("mach_fuel_rate", "parsed_records")): # fmt: skip assert _of(doc, key, field) == _of(shifted, key, field) def test_뭉친_줄과_풀린_줄이_같은_값() -> None: """코덱스가 8-4 를 줄마다 풀어 놓아도 값이 같아야 함 — 이름만 원문 표기를 따름.""" glued, unglued = _of(_extract(GLUED), "mach_fuel_rate", "parsed_records"), _of(_extract(UNGLUED), "mach_fuel_rate", "parsed_records") # fmt: skip assert set(glued) == set(unglued) == {"0101-0007", "0101-0010"} for code in glued: assert {k: v for k, v in glued[code].items() if k != "machine_name"} == { k: v for k, v in unglued[code].items() if k != "machine_name" } assert glued["0101-0010"]["fuel_rate_l_per_hour"] == 12.5 def test_손료_2단_머리를_읽고_계수를_시간당으로() -> None: loss = _of(_extract(GLUED), "mach_loss_coef")["0101-0007"] assert loss["source_coefficient_1e_minus_7"] == 1811 assert loss["loss_coefficient_per_hour"] == 1811 * 1e-7 assert (loss["economic_life_hours"], loss["depreciation_ratio"], loss["specification"]) == ( 12000, 0.9, "7", ) assert loss["machine_name"] == "불도저(무한궤도)" def test_토막_수가_안_맞으면_지어내지_않고_적어_둠() -> None: broken = GLUED.replace("| 9.0 12.5 |", "| 9.0 |") doc, report = build.extract(json.loads(json.dumps(BARE)), broken.encode("utf-8")) assert "0101-0007" not in _of(doc, "mach_fuel_rate", "parsed_records") assert doc["parse_audit"]["unparsed_fuel_rows"] == 1 # 자료엔 셈만 assert report["unparsed_rows"]["fuel"] == [ {"what": "0101-0007 0010", "codes": 2} ] # 자세한 것은 알림에 def test_사람이_맞춘_줄과_사유는_이어받음() -> None: """원문에 줄은 있으나 연료를 못 읽은 자리(`1.49㎾` — 칸 밀림)만 이어받음. ⚠ 원문에 아예 없는 기종은 안 이어받음 — 옛 벌에만 있는 줄이 자료에 남으면 안 됨. """ md = GLUED.replace( "| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |", "| 0101-0007 0010 | 불 도 저 | 7ton 10 | 9.0 12.5 | 16% 16 | 1 1 |\n" "| 7993-0020 | 양수기 | 1.49㎾ | 1.49㎾ | - | - |", ) old = json.loads(json.dumps(BARE)) old["variables"]["mach_fuel_rate"]["parsed_records"] = [ { "machine_code": "7993-0020", "machine_name": "양수기", "fuel_type": "gasoline", "fuel_rate_l_per_hour": 1.3, "parse_method": "verified_source_row_alignment", }, { "machine_code": "9999-0001", "machine_name": "원문에 없는 기계", "fuel_type": "diesel", "fuel_rate_l_per_hour": 4.3, "parse_method": "verified_source_row_alignment", }, ] # fmt: skip old["variables"]["mach_loss_coef"]["records"] = [ { "machine_code": "0101-0007", "machine_name": "불도저(무한궤도)", "note": "칸 밀림 바로잡음", } ] doc = _extract(md, old) fuel = _of(doc, "mach_fuel_rate", "parsed_records") assert fuel["7993-0020"]["parse_method"] == "verified_source_row_alignment" assert fuel["7993-0020"]["fuel_rate_l_per_hour"] == 1.3 assert "9999-0001" not in fuel assert _of(doc, "mach_loss_coef")["0101-0007"]["note"] == "칸 밀림 바로잡음" def test_사라진_코드는_알림에() -> None: old = json.loads(json.dumps(BARE)) old["variables"]["mach_price"]["records"] = [ {"machine_code": "9999-0001", "machine_name": "옛 기계", "price_thousand_krw": 1} ] doc = _extract(GLUED, old) assert build.diff_report(old, doc)["mach_price"]["사라진 코드"] == ["9999-0001"]