# -*- coding: utf-8 -*- """일위대가 조합 — 단가산출 로직을 골라 담은 묶음 (`_틀.md` 10장 · 계약 `_화면_계약.md` 9장). 조합은 **담은 로직 목록만** 지님 — 수량 · 비율 · 계산은 두지 않음(프로젝트 몫). 미리 보기(`run`)는 담은 로직마다 그 자리에서 받은 입력값으로 값을 뽑아 비목별로 갈라 합침 — 저장하지 않음. """ from __future__ import annotations from decimal import Decimal import master_formula as mf import master_keys as mk GROUP = "일위대가조합" FILE = "일위대가조합.json" TID = "UA" OWNERS = ("현장", "공용") MAX_이름 = 60 # 조합 이름 최대 글자 수 — 관리자가 짓는 짧은 이름이라 이보다 길면 저장 막음 # 줄 열 한 벌 — 차례까지 이대로(`_틀.md` 10장) COLS = ("키", "이름", "구분", "상세구분", "단위", "담은로직", "출처", "소유", "비고") ITEM_COLS = ("로직", "메모") def rows(files: dict[str, dict]): """(파일 이름, 조합 줄) — 조합 파일은 하나지만 그룹으로 찾음.""" for name, data in files.items(): if data.get("그룹") == GROUP: for row in data.get("줄") or []: yield name, row def items(row: dict) -> list[dict]: """담은 로직 줄 목록 — 차례가 곧 조합 안 차례.""" got = row.get("담은로직") return got if isinstance(got, list) else [] def logic_keys(row: dict) -> list[str]: return [str(one.get("로직") or "") for one in items(row) if isinstance(one, dict)] # ── 검사 ────────────────────────────────────────────────────────────── def check_row(whole: mf.Master, row: dict) -> list[str]: """조합 한 줄 — 빈 조합 · 없는 로직 키 · 같은 로직 두 번 · 조합을 담음(1단만) · 메모 모양.""" where = f"{row.get('키')} {row.get('이름') or ''}".strip() got = items(row) out = [] if got else [f"{where} · 빈 조합 — 담은 로직이 없음"] if row.get("담은로직") is not None and not isinstance(row.get("담은로직"), list): out.append(f"{where} · 「담은로직」 이 목록 아님") seen: set[str] = set() for at, one in enumerate(got, start=1): if not isinstance(one, dict): out.append(f"{where} 줄{at} · 담은 로직 줄이 묶음 아님 「{one}」") continue extra = set(one) - set(ITEM_COLS) if extra: out.append(f"{where} 줄{at} · 모르는 칸 {sorted(extra)}") memo = one.get("메모") if memo is not None and not isinstance(memo, str): out.append(f"{where} 줄{at} · 「메모」 가 글자 아님 「{memo}」 — 수량·계산은 두지 않음") key = str(one.get("로직") or "") if not mk.KEY.fullmatch(key): out.append(f"{where} 줄{at} · 로직 키 모양 아님 「{key}」") continue if mk.id_of(key) == TID: out.append(f"{where} 줄{at} · 조합은 조합을 담지 못함 「{key}」 — 단가산출 로직만") continue try: whole.logic(key) except mf.FormulaError as e: out.append(f"{where} 줄{at} · {e}") continue if key in seen: out.append(f"{where} 줄{at} · 같은 로직을 두 번 담음 「{key}」") seen.add(key) return out def check_all(files: dict[str, dict], whole: mf.Master) -> list[str]: """조합 전부 — 파일 이름을 앞에 붙임.""" return [f"{name} · {x}" for name, row in rows(files) for x in check_row(whole, row)] def using(files: dict[str, dict], key: str) -> list[dict]: """「이 로직을 쓰는 조합」 — 소요량 「용도」 와 결은 같되 자리가 다름. 조합은 관리자가 그때그때 만드는 것이라 로직 줄에 칸으로 박지 않고 물을 때 훑어 찾음 (박아 두면 조합을 고칠 때마다 낡음). """ out = [] for _, row in rows(files): at = [i for i, k in enumerate(logic_keys(row), start=1) if k == key] if at: out.append( { **{k: row.get(k) for k in ("키", "이름", "구분", "상세구분", "단위", "소유")}, "차례": at, } ) return out # ── 미리 보기 ───────────────────────────────────────────────────────── def run(master: mf.Master, row: dict, given: dict[str, dict] | None = None) -> dict: """담은 로직마다 그 자리의 입력값으로 값을 뽑아 비목별로 갈라 합침 — 저장하지 않음. `given` = {로직 키: {입력 이름: 값}} · 못 도는 줄은 까닭을 적고 합계에서 뺌(통째로 멈추지 않음). 돈이 아닌 로직(결과 수 하나)은 비목이 없어 합계에 얹지 않고 결과만 보임. """ given = given or {} sums = dict.fromkeys(mf.COST_ITEMS, Decimal(0)) lines, stuck = [], [] for at, one in enumerate(items(row), start=1): key = str(one.get("로직") or "") if isinstance(one, dict) else "" head = {"차례": at, "로직": key, "메모": one.get("메모") if isinstance(one, dict) else None} try: target = master.logic(key) except mf.FormulaError as e: lines.append({**head, "ok": False, "까닭": str(e)}) stuck.append(f"{key} · {e}") continue head.update( {k: target.get(k) for k in ("이름", "원문번호", "결과단위", "구분", "상세구분", "출처")} ) try: got = mf.run(master, key, dict(given.get(key) or {})) except (mf.FormulaError, ArithmeticError, KeyError, TypeError, ValueError) as e: reason = str(e) or type(e).__name__ lines.append({**head, "ok": False, "까닭": reason}) stuck.append(f"{key} {target.get('이름')} · {reason}") continue if "결과" in got: # 돈 아닌 로직 — 비목이 없어 합계에 안 얹음 lines.append({**head, "ok": True, "갈래": "결과", "결과": got["결과"]}) continue part = {k: got[k] for k in mf.COST_ITEMS} for k, v in part.items(): sums[k] += v lines.append({**head, "ok": True, "갈래": "돈", **part, "계": got["계"]}) return {"줄": lines, **sums, "계": sum(sums.values()), "멈춤": stuck}