feat(M01): PLAN 7-5 — 안 쓰는 마스터 old/ 로 마저 옮기고 죽은 코드 셋 끊음

- 아무도 안 읽는 최상위·ref 파일 7개를 old/ 로(지우지 않음) — _원문목록·_열목록_화면표·
  수량_돌쌓기찰라이브러리_소광리·공종_수량연결_실무·수량_구조물원단위_울진·
  품셈_산림_재료할증_울진·품셈_산림_돌종류계수_울진(뒤 넷은 old_code 만 읽어 이제 0)
- 공종_자원별칭_실무도 old/ 로 — common_util_aliases.py 를 읽던 시험 둘이 이미 깨져 있어(
  B08·B09 모듈 없음) 실제로 부르는 살아 있는 코드가 없었음
- 살아 있는 common_util/ 에서 dead 모듈 셋(work_item_key·work_item_link·aliases)을
  old_code/common_util/ 로 옮김 — old_code 만 부르던 것이라 옮겨도 부르는 자리 그대로(
  old_code 는 old_code 를 스스로 루트로 읽는 관례) · 상대경로 계산만 한 단 보정
- 깨져 있던 시험 둘(test_b09_fuel_kind_vibrator·test_b08_root_removal_excavator)도
  resources/tester/ 에서 old_code/resources/tester/ 로 — 7-3 이 놓친 것 마저 정리
- UnitPrice.py 의 load_basis_missing() — old/ 스냅샷 대신 새 최상위 마스터
  `기준수량없음_산림품셈_2026-01-01.json` 으로(브레인 판정: 마스터 쪽) · old_code 로
  옮겨진 뒤 root 계산이 한 단 부족해 조용히 빈 딕셔너리를 돌려주던 것도 같이 바로잡음
  (79건 견본 대조 통과) · check_master.py 틀 검사에 특수 파일 면제 한 줄 추가
- 검증 — typecheck · check_master 틀 0건 · M01·자재품목 시험 215 + masterdata·m01_api 98 통과 ·
  전체 모으기 1475 그대로

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JgUhN55Z3BCYhUJTjwTNfj
This commit is contained in:
2026-09-22 14:35:46 +09:00
co-authored by Claude Sonnet 5
parent 04784a808a
commit e937978116
16 changed files with 15 additions and 7 deletions
@@ -0,0 +1,70 @@
"""9-21 제근 굴착기 크기 칸 — 2026-09-14 브레인 판정(산출 조건 칸 · 제안 0.7㎥ · 비면 사유).
9-21 품은 굴착기 0.2·0.7 × 임목축적 등급(소·중·밀) 여섯 갈래인데 B08 은 등급만 보내 B09 가 못 고름.
⇒ 매핑 `variant_template` 이 크기·등급 두 입력을 한 값(「0.7·소림」)으로 엮고, 범위 별칭(FP-09-21)이
그 값을 B09 갈래(「굴착기(무한궤도) 0.7 · 소」)로 잇는다 — 갈래 키 표기는 별칭표 한 곳만 앎.
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from B08_Quantity.B08_Quantity_Engine_EarthworkSummary import ( # noqa: E402
ROOT_REMOVAL_EXCAVATOR_SIZES,
ROOT_REMOVAL_EXCAVATOR_SUGGESTED,
)
from B08_Quantity.B08_Quantity_Engine_Handoff import build_handoff # noqa: E402
from B08_Quantity.B08_Quantity_Engine_Preparation import STAND_VOLUME_CLASSES # noqa: E402
def _row(**inputs) -> dict:
summary = {"rows": [{"group": "지장목제거", "item": "뿌리뽑기", "spec": "", "unit": "",
"amount": 1000.0, "application_ratio_pct": 100.0}]} # fmt: skip
rows = build_handoff(summary_table=summary, **inputs)["work_items"]
return next(r for r in rows if r["work_item_code"] == "FP-09-21")
def test_크기와_등급을_한_갈래_값으로_엮어_내역_금액이_섬() -> None:
from B09_Estimation.B09_Estimation_BillOfQuantities import build_bill
inputs = {"stand_volume_class": "소림", "root_removal_excavator_m3": "0.7"}
row = _row(**inputs)
assert row["variant_value"] == "0.7·소림" and not row["blocked_kind"], row
summary = {"rows": [{"group": "지장목제거", "item": "뿌리뽑기", "spec": "", "unit": "",
"amount": 1000.0}]} # fmt: skip
bill = build_bill(build_handoff(summary_table=summary, **inputs))
line = next(r for r in bill.rows if r.code == "FP-09-21")
assert line.price_code == "B-FP-09-21#굴착기(무한궤도)0.7·소", line.note
assert line.amount_krw and int(line.amount_krw) > 0, line.note
def test_크기가_비면_금액_없이_입력_사유_등급은_그대로_실음() -> None:
row = _row(stand_volume_class="중림")
assert row["variant_value"] == "중림" # 판정 Ⓑ — 등급 갈래를 잃지 않음
assert row["blocked_kind"] == "input_missing" and "굴착기" in row["blocked_reason"], row
assert _row()["blocked_kind"] == "input_missing"
def test_선택지는_마스터_갈래_키의_크기_제안은_0_7_근거_10_12_1() -> None:
from B09_Estimation.B09_Estimation_ResourceAxis import load_work_item_master
node = next(
n for n in load_work_item_master()["work_items"] if n["work_item_code"] == "FP-09-21"
)
sizes = {key.split(")")[1].split("·")[0].strip() for key in node["variant_keys"]}
assert sizes == set(ROOT_REMOVAL_EXCAVATOR_SIZES)
value, basis = ROOT_REMOVAL_EXCAVATOR_SUGGESTED
assert value == "0.7" and "10-12-1" in basis
# 크기 × 등급 여섯이 모두 별칭으로 B09 갈래에 닿음
from common_util.common_util_aliases import alias_target, load_aliases
rows = load_aliases("variant")
for size in ROOT_REMOVAL_EXCAVATOR_SIZES:
for grade in STAND_VOLUME_CLASSES:
target = alias_target(rows, f"{size}·{grade}", "FP-09-21", "2026-01-01")
assert target in node["variant_keys"], (size, grade, target)
@@ -0,0 +1,103 @@
"""연료 종류(휘발유) · 콘크리트 진동기 — 2026-09-14 브레인 661 뒤 ①②.
② 운전경비표 `fuel_kind` 를 조립이 안 읽어 **휘발유 기계에 경유값**이 붙고 있었음(플레이트 콤팩터 ·
진동기 · 믹서 · 래머 · 커터) — 종류대로 그 유가(전국·시도 둘 다)로 섬.
① 콘크리트 진동기 — 원문 8-3 (4611) 두 줄이 한 칸에 뭉쳐 규격·손료계수가 비었음 → 원문값 되살림
(전기식 플렉시블형 ø45(0.75㎾) 4,935 · 엔진식 플렉시블형 ø45(2.6㎾) 5,101). 산림 12-15 「봉상후렉시블(45mm)
Q=5.4㎥/hr」 은 전기·엔진을 안 적음 → 12-34-1 「콘크리트 진동기(3.5HP)」(= 2.6㎾) · 운전경비표에 엔진식만
있는 것을 근거로 엔진식 4611-0350. 집수정 구체콘크리트 갈래가 이제 섬(㉯ ⑴ 조건이 풀림).
"""
from __future__ import annotations
import json
from decimal import Decimal
from pathlib import Path
from B09_Estimation.B09_Estimation_UnitPrice import cached_build, detail_of
ROOT = Path(__file__).resolve().parents[2]
DATA = ROOT / "resources" / "data_cost_input_value"
def _oil(file: str, key: str) -> dict:
return json.loads((DATA / file).read_text(encoding="utf-8"))["variables"][key]
def test_휘발유_기계는_휘발유값_경유_기계는_경유값() -> None:
book = cached_build().book
fuels = {r.ref_code for r in book.details["X-1730-0015"] if r.ref_code.startswith("M-FUEL-")}
assert fuels == {"M-FUEL-휘발유"}, fuels
gasoline = Decimal(str(_oil("oil_2026-08-14.json", "oil_gasoline")["value"]))
assert (
book.titles["M-FUEL-휘발유"].slots[-1] == gasoline
or gasoline in book.titles["M-FUEL-휘발유"].slots
)
diesel_rows = {
r.ref_code for r in book.details["X-0201-0070"] if r.ref_code.startswith("M-FUEL-")
}
assert diesel_rows == {"M-FUEL-경유"}
def test_시도를_고르면_휘발유도_그_시도값() -> None:
records = _oil("oil_regional_2026-09-09.json", "oil_gasoline")["records"]
seoul = next(Decimal(str(r["value"])) for r in records if r["sido_code"] == "01")
book = cached_build(fuel_region="01").book
assert seoul in book.titles["M-FUEL-휘발유"].slots
assert "서울" in book.titles["M-FUEL-휘발유"].spec
def test_기계_하나_시간당_사용료도_연료_종류대로() -> None:
from B09_Estimation.B09_Estimation_MachineOperating import hourly_cost_of, load_fuel_price
gasoline, _ = load_fuel_price(kind="휘발유")
diesel, _ = load_fuel_price()
assert gasoline != diesel
cost = hourly_cost_of("1730-0015") # 휘발유 1.0 L/hr · 잡품 20%
assert cost.money.material == Decimal("1.0") * Decimal("1.2") * gasoline, cost
def test_진동기_원문값_되살림() -> None:
from B09_Estimation.B09_Estimation_MachineCost import load_machine_catalog
machines = load_machine_catalog().machines
assert machines["4611-0350"].specification == "엔진식 플렉시블형 ø45(2.6㎾)"
assert machines["4611-0350"].loss_coefficient_per_hour == Decimal("0.0005101")
assert machines["4611-0075"].specification == "전기식 플렉시블형 ø45(0.75㎾)"
assert machines["4611-0075"].loss_coefficient_per_hour == Decimal("0.0004935")
def test_집수정_구체콘크리트_갈래가_엔진식_진동기로_섬() -> None:
build = cached_build()
rows = {
r["ref_code"]: Decimal(r["quantity"])
for r in detail_of(build, "B-FP-12-15#구체콘크리트")["rows"]
if r.get("ref_code")
}
assert rows["X-4611-0350"] == Decimal(1) / Decimal("5.4"), rows
left = " ".join(build.unattached.get("FP-12-15", []))
assert "봉상후렉시블" not in left and "구체콘크리트" not in left, left
def test_진동기_별칭_사유에_12_15_는_전기_엔진을_안_적음() -> None:
from common_util.common_util_aliases import load_aliases
row = next(r for r in load_aliases("resource") if r["to"] == "4611-0350")
assert row["scope"] == "FP-12-15"
assert "전기·엔진을 안 적음" in row["basis"] and "12-34-1" in row["basis"], row
def test_중기경비_장은_갈래대로_잡품과_손료계수() -> None:
"""661 뒤처리 — `#암석` 장이 조합 16%·비암석 계수로 보이던 자리."""
from B09_Estimation.B09_Estimation_MachineExpenseSheet import machine_expense_sheets
build = cached_build(dump_haul_m=("164.23",))
sheets = {s["code"]: s for s in machine_expense_sheets(build)}
rock, combined = sheets["X-0201-0070#암석"], sheets["X-0201-0070#조합"]
assert rock["misc_material_percent"] == sheets["X-0201-0070"]["misc_material_percent"]
assert combined["misc_material_percent"] == "16"
assert rock["loss_coefficient"] == 2405.0 and sheets["X-0201-0070"]["loss_coefficient"] == 2085
gasoline = next(s for s in sheets.values() if s["machine_code"] == "1730-0015")
assert Decimal(gasoline["fuel_price_per_liter"]) == Decimal(
str(_oil("oil_2026-08-14.json", "oil_gasoline")["value"])
)