From 8c519c0ab2755d5a73ef60963568930e8a5995dc Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 1 Nov 2026 16:32:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(714-1b):=20=EC=9E=A5=EB=B9=84=20=EC=A4=84?= =?UTF-8?q?=20=ED=8E=BC=EC=B9=A8=20=EB=8D=A7=EC=B9=B8=20=E2=80=94=20?= =?UTF-8?q?=EB=B6=80=EB=A5=B8=20=EA=B8=B0=EA=B3=84=EA=B2=BD=EB=B9=84=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B3=84=EC=82=B0=EC=84=9C=20=EC=A4=84=20?= =?UTF-8?q?=C2=B7=20=EC=8B=9C=EA=B0=84=EB=8B=B9=20=EC=86=8C=EA=B3=84=20?= =?UTF-8?q?=C2=B7=20=EA=B3=84=20=C2=B7=20=ED=82=A4=20=C2=B7=20=EC=9D=B8?= =?UTF-8?q?=EC=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - master_sheet_line 장비(기계경비) 줄에 「펼침」 = {키, 이름, 단위, 입력, 줄[비목 · 이름 · 글 · 금액], 소계, 계} · 못 셈 = 까닭 - 셈 결과(master_text.lines)를 그대로 실음 — 조종원 환산 같은 규칙을 여기 두지 않음 - annotate 가 files 를 받음(펼침 계산서) · M01 /text · M02 /logic-lib/sheet 같은 KEYS - 시험 test_714_1_sheet_line 펼침 · 두 길 같은 수(JSON 수 규칙) Co-Authored-By: Claude Opus 5.5 --- M01_MasterData/M01_MasterData_Store_Sheet.py | 6 +-- M02_MasterTemplete/M02_Logic_Standard.py | 2 +- .../master_data/scripts/master_sheet_line.py | 37 ++++++++++++++++++- resources/tester/test_714_1_sheet_line.py | 15 +++++++- 4 files changed, 53 insertions(+), 7 deletions(-) diff --git a/M01_MasterData/M01_MasterData_Store_Sheet.py b/M01_MasterData/M01_MasterData_Store_Sheet.py index 7d893f62b..57181e401 100644 --- a/M01_MasterData/M01_MasterData_Store_Sheet.py +++ b/M01_MasterData/M01_MasterData_Store_Sheet.py @@ -17,11 +17,11 @@ def text(key: str, inputs: dict, row: dict | None = None, file: str | None = Non given = {k: store._dec(v) for k, v in inputs.items()} try: if row is None: - master = store.loaded()[1] + files, whole = store.loaded() else: # 저장 전 시험 — `store.text` 와 같은 메모리 사본 files = store.cm.load(folder=store.FOLDER) key = store._swap(files, str(row.get("키") or key), row, file) - master = mf.Master(files) - return msl.annotate(master, key, mf.fill_empty(master.logic(key), given), got) + whole = mf.Master(files) + return msl.annotate(files, key, mf.fill_empty(whole.logic(key), given), got) except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError): return got diff --git a/M02_MasterTemplete/M02_Logic_Standard.py b/M02_MasterTemplete/M02_Logic_Standard.py index fd9959833..5ba2fc45a 100644 --- a/M02_MasterTemplete/M02_Logic_Standard.py +++ b/M02_MasterTemplete/M02_Logic_Standard.py @@ -334,7 +334,7 @@ def sheet(key: str, change: dict) -> dict: raise m01.StoreError(404, str(e)) from e given, ax = _given(whole, logic, change) filled = m01.cm.mf.fill_empty(logic, given) - got = msl.annotate(whole, key, filled, m01.mtx.lines(files, key, filled)) # 줄 덧칸(714-1) + got = msl.annotate(files, key, filled, m01.mtx.lines(files, key, filled)) # 줄 덧칸(714-1) done = settle.settle(whole, key, filled) if "결과" not in done: # 돈 로직 — 줄마다 버림(B09 산근 규칙) settle.apply(got.get("groups") or [], m01.cm.mf.run(whole, key, filled), done) diff --git a/resources/master_data/scripts/master_sheet_line.py b/resources/master_data/scripts/master_sheet_line.py index 1fe93a425..d567b5554 100644 --- a/resources/master_data/scripts/master_sheet_line.py +++ b/resources/master_data/scripts/master_sheet_line.py @@ -6,6 +6,8 @@ - `쓰는입력` = 그 줄 값(수량 식 · 요소 · 덧줄 식)에 쓰인 로직 입력 이름 — 중간값을 거슬러 감 · 덧줄이 부르는 비목 합 · 줄 금액은 거스르지 않음(율 식에 직접 쓴 입력만). - `묶음` · `장비` = 712-1 `master_bundle`(엔진 줄 덧칸 그대로). +- `펼침`(장비 줄만 · 714-1b) = 부른 기계경비 로직의 계산서 줄(조종원 · 주연료 · 잡재료비 · 기계손료 …) · + 시간당 비목 소계 · 계 · 부른 키 · 인자 — 셈 결과를 그대로 실음(환산 규칙을 여기 두지 않음). - `기준단위` = 그 줄 수량 식이 찾는 표의 기준(「10㎡」 · 712-3 `master_basis.basis`) · 답 머리 `기준단위` = 로직 결과단위 분모(「10㎡」 · 여럿이면 「㎡ · ㎥」). - `환산근거` = 표 기준배 ↔ 결과단위 배(「10㎡당 → 1㎡당 ÷10」) · 재료 품목 단위 ↔ 줄 단위 @@ -24,7 +26,7 @@ import master_formula as mf import master_text as mtx import master_unit as mu -KEYS = ("쓰는입력", "묶음", "장비", "기준단위", "환산근거") +KEYS = ("쓰는입력", "묶음", "장비", "기준단위", "환산근거", "펼침") _HOLE = re.compile(r"\{([^{}]+)\}") _ERRORS = (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) @@ -117,10 +119,39 @@ def _fields(master, row, exprs, env, per, line: dict, bundle: tuple[str, bool] | return out -def annotate(master, key: str, given: dict, answer: dict) -> dict: +def _spread(files: dict, master, item: dict, env: dict) -> dict: + """장비 줄이 부른 기계경비 로직 한 장 — `{키, 이름, 단위, 입력, 줄, 소계, 계}` · 못 셈 = 까닭.""" + node = mf.parse(item["요소"]) + key = node[1] + try: + key = mf.fill(key, env) if "{" in key else key + given = {k: mf.evaluate(v, env, master, 1) for k, v in node[2]} + got = mtx.lines(files, key, given) + except (*_ERRORS, mf.EmptyInput) as e: + return {"키": key, "까닭": str(e) or type(e).__name__} + if not got.get("ok"): + return {"키": key, "입력": given, "까닭": got.get("reason")} + groups = got.get("groups") or [] + return { + "키": key, + "이름": master.logic(key).get("이름") or "", + "단위": _per_text(mbs.result_per(master.logic(key).get("결과단위"))) or None, + "입력": given, + "줄": [ + {"비목": g["비목"], **{k: one.get(k) for k in ("이름", "글", "금액")}} + for g in groups + for one in g["줄"] + ], + "소계": {g["비목"]: g.get("소계") for g in groups}, + "계": got.get("계"), + } + + +def annotate(files: dict, key: str, given: dict, answer: dict) -> dict: """`master_text.lines` 답에 줄마다 `KEYS` 덧칸 · 머리 `기준단위` — 못 셈이면 답 그대로.""" if not answer.get("ok") or "groups" not in answer: return answer + master = mf.Master(files) row = master.logic(key) per = mbs.result_per(row.get("결과단위")) answer["기준단위"] = _per_text(per) or None @@ -148,6 +179,8 @@ def annotate(master, key: str, given: dict, answer: dict) -> dict: parts = [item.get("비목")] exprs = [item.get("수량"), item.get("요소")] got = _fields(master, row, exprs, env, per, line, mb.bundle(master, item, env)) + if got.get("장비"): + got["펼침"] = _spread(files, master, item, env) for part in parts: target = next(slots.get(part, iter(())), None) if target is not None: diff --git a/resources/tester/test_714_1_sheet_line.py b/resources/tester/test_714_1_sheet_line.py index f2266a8e5..a317f93e4 100644 --- a/resources/tester/test_714_1_sheet_line.py +++ b/resources/tester/test_714_1_sheet_line.py @@ -14,6 +14,7 @@ from M01_MasterData import M01_MasterData_Store_Sheet as sheet # noqa: E402 from M02_MasterTemplete import M02_Logic_Standard as standard # noqa: E402 msl = sheet.msl +num = standard.labor_inputs._num # Decimal → JSON 수(두 길 같은 규칙) def _text(key: str) -> dict: @@ -36,6 +37,18 @@ def test_line_keys_and_bundle(): assert any(line["묶음"] == "인력" for line in lines) +def test_machine_spread(): + """714-1b — 장비 줄 「펼침」 = 부른 기계경비 로직 계산서(조종원 · 연료 · 손료) · 시간당 계.""" + lines = _lines(_text("GF000103")) + spread = next(line for line in lines if line.get("장비"))["펼침"] + assert spread["키"].startswith("GC0002") and spread["단위"] == "시간" + names = " ".join(one["이름"] for one in spread["줄"]) + assert "조종원" in names and "손료" in names + sums = [v for v in spread["소계"].values() if v is not None] + assert spread["계"] == sum(sums) and spread["계"] > 0 + assert not any(line.get("펼침") for line in lines if not line.get("장비")) + + def test_uses_follow_middle_values(): """중간값을 거슬러 입력까지 — 돌쌓기 석공 품은 높이 · 쌓기 · 돌 입력을 씀.""" lines = _lines(_text("GF000219")) @@ -59,4 +72,4 @@ def test_two_routes_same_shape(): for a, b in zip(m01, m02): assert set(b) == {"이름", "글", "금액", *msl.KEYS} assert a["이름"] == b["이름"] - assert [a.get(k) for k in msl.KEYS] == [b.get(k) for k in msl.KEYS] + assert num([a.get(k) for k in msl.KEYS]) == [b.get(k) for k in msl.KEYS]