From 6d5b78f83eecbbd2df814356c0f5a43b23484c86 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 16:02:13 +0900 Subject: [PATCH] =?UTF-8?q?feat(712-1):=20=EC=9D=B8=EB=A0=A5=20=C2=B7=20?= =?UTF-8?q?=EC=9E=AC=EB=A3=8C=20=C2=B7=20=EA=B2=BD=EB=B9=84=20=EB=AC=B6?= =?UTF-8?q?=EC=9D=8C=20=EC=85=88=20=EB=95=8C=20=ED=8C=8C=EC=83=9D=20=C2=B7?= =?UTF-8?q?=20=EA=B8=B0=EA=B3=84=EA=B2=BD=EB=B9=84=20=ED=8C=90=EB=B3=84=20?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 새 master_bundle — 호표 · 덧줄 = 비목 따라 · 비목 없는 로직 줄 = 기계경비면 경비 + 장비 · 아니면 하위 - 기계경비 판별 = 결과단위 원/시간(is_machine) — M02 엑셀 맞춤(이름 「시간당」) · B09 산근 · 산근 텍스트(입력 「기계」) 갈아 끼움 - 엔진 run 줄 덧칸 「묶음」 · 기계경비 줄 「장비」(출력 꼬리표만 · 노 · 재 · 경 몫은 줄 비목 그대로) - 정본 칸 안 더함 · 5,911 줄 중 하위 31 · 기계경비 줄 1,044 - 지킴 시험 test_712_1_bundle Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_Sangeun.py | 2 +- B09_Estimation/B09_Estimation_Sangeun_Text.py | 2 +- M02_MasterTemplete/M02_Hopyo_Ours.py | 3 +- .../master_data/scripts/master_bundle.py | 60 +++++++++++++++++ .../master_data/scripts/master_formula.py | 2 + resources/tester/test_712_1_bundle.py | 64 +++++++++++++++++++ 6 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 resources/master_data/scripts/master_bundle.py create mode 100644 resources/tester/test_712_1_bundle.py diff --git a/B09_Estimation/B09_Estimation_Sangeun.py b/B09_Estimation/B09_Estimation_Sangeun.py index 02549d389..6364407a4 100644 --- a/B09_Estimation/B09_Estimation_Sangeun.py +++ b/B09_Estimation/B09_Estimation_Sangeun.py @@ -242,7 +242,7 @@ def call_of(whole: Any, item: dict, env: dict) -> dict[str, Any] | None: target = None if not target: return None - if any(s.get("이름") == "기계" for s in target.get("입력", [])): + if mf.mb.is_machine(target): # 기계경비 판별 한 곳(712-1) keys = _captured(whole, node, env) return {"machine": keys[0] if keys else None} given = {name: mf.evaluate(expr, env, whole, 1) for name, expr in node[2]} diff --git a/B09_Estimation/B09_Estimation_Sangeun_Text.py b/B09_Estimation/B09_Estimation_Sangeun_Text.py index 84c005b79..53d112e4d 100644 --- a/B09_Estimation/B09_Estimation_Sangeun_Text.py +++ b/B09_Estimation/B09_Estimation_Sangeun_Text.py @@ -276,7 +276,7 @@ def _logic_lines(whole, item, env, qty_text, q, price_of) -> list[tuple[str, dic except mf.FormulaError: target = None name = item.get("이름", "") - if target and any(s.get("이름") == "기계" for s in target.get("입력", [])): + if mf.mb.is_machine(target): # 기계경비 판별 한 곳(712-1) keys = sg._captured(whole, node, env) got = price_of(keys[0]) if keys else None if not got: diff --git a/M02_MasterTemplete/M02_Hopyo_Ours.py b/M02_MasterTemplete/M02_Hopyo_Ours.py index 92272c4ad..90c6619c1 100644 --- a/M02_MasterTemplete/M02_Hopyo_Ours.py +++ b/M02_MasterTemplete/M02_Hopyo_Ours.py @@ -32,6 +32,7 @@ from M01_MasterData import M01_MasterData_Store_Cache as m01_cache from M02_MasterTemplete import M02_Hopyo_Match as hopyo_match from M02_MasterTemplete.M02_Hopyo_Words import conds, mtypes, specnums +mb = m01.cm.mf.mb # 묶음 · 기계경비 판별(712-1) FP_VERSION = 1 # 지문 꼴이 바뀌면 올림 — 옛 캐시 버림 ROOT = Path(__file__).resolve().parents[1] _PER = re.compile(r"원\s*/\s*(\d[\d,]*\.?\d*|\.\d+)?") @@ -84,7 +85,7 @@ def whole() -> Any: def is_mach_logic(row: dict[str, Any]) -> bool: - return "시간당" in str(row.get("이름") or "") + return mb.is_machine(row) # 기계경비 판별 한 곳(712-1 · 결과단위 원/시간) def eq_of(code: Any) -> tuple[set[str], set[float]]: diff --git a/resources/master_data/scripts/master_bundle.py b/resources/master_data/scripts/master_bundle.py new file mode 100644 index 000000000..282e813cc --- /dev/null +++ b/resources/master_data/scripts/master_bundle.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +"""인력 · 재료 · 경비 묶음 — **셈 때 파생**(정본에 칸 없음 · PLAN 712-1 · 요구서 원칙 5). + +규칙 한 줄: +- 호표 · 덧줄 = `비목` 따라(노무비 → 인력 · 재료비 → 재료 · 경비 → 경비). +- 비목 없는 로직 줄 = 부르는 로직이 기계경비(`is_machine`)면 경비 + `장비` · 아니면 하위. + 두 줄 모두 엔진 줄 `비목` 에 노무비 · 재료비 · 경비 세 몫이 그대로 남음(한 줄 · 흩어 넣기 둘 다). + +기계경비 판별 = `is_machine` 한 곳 — M01 엔진 · M02 엑셀 맞춤(`M02_Hopyo_Ours`) · +B09 산근(`B09_Estimation_Sangeun` · `_Text`)이 같이 씀. +엔진 줄 덧칸 = `묶음`(출력 꼬리표만 · 입력 값 덧칸 `값묶음` 은 715-1b 몫) · 기계경비 줄 `장비` true. +""" + +from __future__ import annotations + +import re + +import master_formula as mf + +BUNDLES = ("인력", "재료", "경비", "하위") +BY_ITEM = {"노무비": "인력", "재료비": "재료", "경비": "경비"} +BY_KIND = {"인력": "인력", "재료": "재료", "기계": "경비"} # 비목 빈 옛 줄 받침 + + +def is_machine(row: dict | None) -> bool: + """기계경비 로직 — 돈 로직이고 결과단위 「원/시간」(작업량 「㎥/시간」 · 이름 「시간당」 아님).""" + if not row or "결과" in row or isinstance(row.get("흐름"), dict): + return False + return re.sub(r"\s", "", str(row.get("결과단위") or "")) == "원/시간" + + +def called(master, item: dict, env: dict | None = None) -> dict | None: + """로직 줄이 부르는 로직 줄(정본) — 못 찾으면 None.""" + try: + node = mf.parse(item.get("요소") or "") + if not node or node[0] != "logic": + return None + key = mf.fill(node[1], env) if env is not None and "{" in node[1] else node[1] + return master.logic(key) + except (mf.FormulaError, mf.EmptyInput, KeyError, TypeError): + return None + + +def bundle(master, item: dict, env: dict | None = None) -> tuple[str, bool]: + """줄 하나(호표 · 덧줄 정본 줄) → (묶음, 장비인가).""" + if item.get("비목") in BY_ITEM: + return BY_ITEM[item["비목"]], False + if item.get("종류") == "로직": + return ("경비", True) if is_machine(called(master, item, env)) else ("하위", False) + return BY_KIND.get(str(item.get("종류")), "하위"), False + + +def tag(master, row: dict, lines: list[dict], env: dict | None = None) -> list[dict]: + """엔진 `run` 줄에 `묶음`(+ 기계경비 줄 `장비`) — 줄 차례 = 호표 줄 하나씩 뒤 덧줄 하나씩.""" + for item, line in zip([*row.get("호표", []), *row.get("덧줄", [])], lines): + name, machine = bundle(master, item, env) + line["묶음"] = name + if machine: + line["장비"] = True + return lines diff --git a/resources/master_data/scripts/master_formula.py b/resources/master_data/scripts/master_formula.py index 4fdda195a..29c4665bb 100644 --- a/resources/master_data/scripts/master_formula.py +++ b/resources/master_data/scripts/master_formula.py @@ -657,6 +657,7 @@ def run(master: Master, ref: str, given: dict, depth: int = 0) -> dict: } ) sums = {k: cut(row, v, "계") for k, v in sums.items()} # 비목 합마다 끊고 계는 그 합 + mb.tag(master, row, lines, env) # 줄마다 인력 · 재료 · 경비 · 하위 묶음(712-1) return {"줄": lines, **sums, "계": sum(sums.values()), "중간": _shown(env)} @@ -676,6 +677,7 @@ from master_material import ( # noqa: E402, F401 pick, ) import master_pick as mp # noqa: E402 — 검색어 · 고름 · 단위 +import master_bundle as mb # noqa: E402 — 인력 · 재료 · 경비 묶음 from master_formula_check import ( # noqa: E402, F401 — 검사는 나눈 파일 · 이름은 여기서 그대로 _check_cut, _check_node, diff --git a/resources/tester/test_712_1_bundle.py b/resources/tester/test_712_1_bundle.py new file mode 100644 index 000000000..272a93b0c --- /dev/null +++ b/resources/tester/test_712_1_bundle.py @@ -0,0 +1,64 @@ +"""712-1 지킴 — 인력 · 재료 · 경비 묶음 셈 때 파생 · 기계경비 판별 한 곳(`master_bundle`).""" + +from __future__ import annotations + +import sys +from decimal import Decimal +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / "resources/master_data/scripts")) +sys.path.insert(0, str(ROOT)) + +import check_master as cm # noqa: E402 +import master_auto as mau # noqa: E402 +import master_bundle as mb # noqa: E402 +import master_formula as mf # noqa: E402 + +FILES = cm.load() +MASTER = mf.Master(FILES) +LOGICS = {r["키"]: r for d in FILES.values() if d.get("그룹") == "로직" for r in d.get("줄") or []} + + +def test_every_line_bundled(): + """호표 · 덧줄 전수 — 하위(못 가름) ≤ 31 · 나머지는 인력 · 재료 · 경비 · 기계경비 줄 = 경비.""" + count = dict.fromkeys(mb.BUNDLES, 0) + machine = 0 + for row in LOGICS.values(): + for item in [*row.get("호표", []), *row.get("덧줄", [])]: + name, is_mach = mb.bundle(MASTER, item) + count[name] += 1 + machine += is_mach + if is_mach: + assert name == "경비" + assert sum(count.values()) > 5_000 + assert count["하위"] <= 31, count + assert machine >= 1_000, machine + + +def test_machine_rule_one_place(): + """기계경비 = 결과단위 원/시간 돈 로직 — 작업량(㎥/시간) · 이름 「시간당」 은 아님.""" + machines = {k for k, r in LOGICS.items() if mb.is_machine(r)} + assert {"GC000267", "GC000268", "GF000145"} <= machines + assert not machines & {"GF000102", "GC000422", "GC000260"} # 작업량 · ㎡/시간 · 원/㎡ + sys.path.insert(0, str(ROOT)) + from M02_MasterTemplete import M02_Hopyo_Ours as ours + + assert all(ours.is_mach_logic(r) == mb.is_machine(r) for r in LOGICS.values()) + + +def test_run_lines_tagged(): + """엔진 `run` 줄마다 `묶음` · 기계경비 줄은 `장비` + 비목 노 · 재 · 경 세 몫.""" + seen = set() + for key in ("GF000169", "GF000103"): + row = LOGICS[key] + given = {k: v for k, v in mau.values(MASTER, row).items() if v is not None} + got = mf.run(MASTER, key, given) + for line in got["줄"]: + assert line["묶음"] in mb.BUNDLES + seen.add(line["묶음"]) + if line.get("장비"): + assert set(line["비목"]) == set(mf.COST_ITEMS) + total = sum((line["비목"].get(k) or Decimal(0)) for line in got["줄"] for k in line["비목"]) + assert abs(total - got["계"]) < 1 + assert {"인력", "재료", "하위"} <= seen