From f2d51252a09b55a3917e19e76bd78b00c930a570 Mon Sep 17 00:00:00 2001 From: umsangdon Date: Sun, 15 Nov 2026 15:40:47 +0900 Subject: [PATCH] =?UTF-8?q?refactor(841-1):=20B09=20=EB=8C=80=EA=B0=80=20?= =?UTF-8?q?=C2=B7=20=EC=82=B0=EA=B7=BC=20=EB=8B=A8=EA=B0=80=20=EC=9B=90?= =?UTF-8?q?=EC=B2=9C=20=3D=20M02=20=EC=97=94=EC=A7=84=20=E2=80=94=20A=20?= =?UTF-8?q?=EA=B1=B8=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 산근 Solver 단가 = M02 엔진 줄 금액 ÷ 수량(labor_doc.run_line 날 값) · B09 단가 책 · 바탕 표 안 읽음 - 대가 블록 줄 노무 · 자재 · 기계 = 엔진 단가(own) · 자재 줄 = 엔진 기준단가 · 고정 줄 단가 책 덮어쓰기 걷음 - 대가 단가 고친 칸(daega_cover 노 · 재 · 경) 걷음 · 품(수량) 고친 칸 둠 - 기계 시간당 = M02 엔진 기계경비 값(운전사 계수 16/12 · 25/20 따로 곱 0) — 견본 노체포설 4,522 → 3,548 = M02 - 운반 단가 미리보기(logic-preview) = 엔진 단가 - 낡은 시험(B09 단가 책 입력 견본)은 C 걸음에서 정리 Co-Authored-By: Claude Opus 5.5 --- B09_Estimation/B09_Estimation_Calc_Labor.py | 49 ++++---- B09_Estimation/B09_Estimation_Calc_Labor.ts | 37 +++--- B09_Estimation/B09_Estimation_Labor_Source.py | 27 ++-- B09_Estimation/B09_Estimation_Router.py | 9 +- B09_Estimation/B09_Estimation_Sangeun.py | 116 ++++++++---------- 5 files changed, 115 insertions(+), 123 deletions(-) diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.py b/B09_Estimation/B09_Estimation_Calc_Labor.py index c445a5f6f..5858c2c6d 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.py +++ b/B09_Estimation/B09_Estimation_Calc_Labor.py @@ -47,7 +47,7 @@ from B09_Estimation.B09_Estimation_Calc_Base import ( ) PARTS = ("labor", "material", "expense") -COVER_KEYS = ("qty", *PARTS) +COVER_KEYS = ("qty",) # 841-1 단가 고친 칸 걷음 — 품(수량)만 TAB = "dg" NO_LINES = "구성 줄 없음" # 줄 0 개 일위대가의 빈 줄 이름 COST = {"노무비": "labor", "재료비": "material", "경비": "expense"} @@ -55,9 +55,9 @@ WAYS = {"버림": rounddown, "올림": roundup, "반올림": round_half} COPY = "@copy" ADD = "@add" WHY = { - "labor": "노임 단가 없음", - "material": "자재 적용 단가 없음", - "machine": "중기 시간 단가 없음", + "labor": "노임 단가 없음(M02 엔진)", + "material": "자재 단가 없음(M02 엔진)", + "machine": "기계 시간당 없음(M02 엔진)", "daega": "부른 대가 합계 없음", "sangeun": "산근 단가 없음(145-1)", } @@ -96,16 +96,18 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str """줄 비목 단가 — 그 비목이 없는 줄은 0 · 원천이 없으면 None.""" kind, key = line["kind"], line.get("key") out: dict[str, float | None] = dict.fromkeys(PARTS, 0.0) - if kind == "labor": - out["labor"] = num((books["labor"].get(key) or {}).get("price")) - elif kind == "material": - row = books["material"].get(key) or {} - # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈) - out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row.get("price")) - elif kind in ("machine", "daega", "sangeun"): - row = {"machine": books["machine"], "daega": sums, "sangeun": books["sangeun"]}[kind].get( - key - ) or {} + if kind in ( + "labor", + "material", + "machine", + ): # 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음 + own = line.get("own") + out = {p: num(own.get(p)) if p in own else 0.0 for p in PARTS} if own else _none() + row = (books["material"].get(key) or {}) if kind == "material" else {} + if row.get("supply") in SUPPLY: # 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락) + out["material"] = 0.0 + elif kind in ("daega", "sangeun"): + row = {"daega": sums, "sangeun": books["sangeun"]}[kind].get(key) or {} for part in PARTS: out[part] = None if row.get("blank") else num(row.get(part)) # 빈 줄 대가 = 빈칸 elif kind == "fixed": @@ -115,21 +117,18 @@ def _prices(line: dict[str, Any], books: dict[str, Any], sums: dict) -> dict[str return out +def _none() -> dict[str, float | None]: + return dict.fromkeys(PARTS, None) + + def _fixed(line: dict[str, Any], books: dict[str, Any]) -> dict[str, float | None]: - """고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값.""" + """고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(841-1 단가 책 + 값으로 다시 안 덮음).""" own, link = line.get("own") or {}, line.get("link") or {} out = {part: num(own.get(part)) or 0.0 for part in PARTS} - labor = books["labor"].get(link.get("labor")) if link.get("labor") else None - if labor and num(labor.get("price")) is not None: - out["labor"] = num(labor["price"]) row = books["material"].get(link.get("material")) if link.get("material") else None - if row and (row.get("supply") in SUPPLY or num(row.get("price")) is not None): - out["material"] = 0.0 if row.get("supply") in SUPPLY else num(row["price"]) - machine = books["machine"].get(link.get("machine")) if link.get("machine") else None - if machine and not machine.get("blank"): - for part in PARTS: - if num(machine.get(part)) is not None: - out[part] = num(machine[part]) + if row and row.get("supply") in SUPPLY: + out["material"] = 0.0 return out diff --git a/B09_Estimation/B09_Estimation_Calc_Labor.ts b/B09_Estimation/B09_Estimation_Calc_Labor.ts index 9839ebf91..42202cd0a 100644 --- a/B09_Estimation/B09_Estimation_Calc_Labor.ts +++ b/B09_Estimation/B09_Estimation_Calc_Labor.ts @@ -39,16 +39,16 @@ import { } from "./B09_Estimation_Calc_Base"; const PARTS = ["labor", "material", "expense"] as const; -const COVER_KEYS = ["qty", ...PARTS] as const; +const COVER_KEYS = ["qty"] as const; // 841-1 단가 고친 칸 걷음 — 품(수량)만 const TAB = "dg"; const NO_LINES = "구성 줄 없음"; // 줄 0 개 일위대가의 빈 줄 이름(파이썬 짝) export const COPY = "@copy"; /** 붙인 줄 덮개 열쇠(705-2) */ export const ADD = "@add"; const WHY: Record = { - labor: "노임 단가 없음", - material: "자재 적용 단가 없음", - machine: "중기 시간 단가 없음", + labor: "노임 단가 없음(M02 엔진)", + material: "자재 단가 없음(M02 엔진)", + machine: "기계 시간당 없음(M02 엔진)", daega: "부른 대가 합계 없음", sangeun: "산근 단가 없음(145-1)", }; @@ -103,14 +103,17 @@ const blanks = (): Parts => ({ labor: null, material: null, expense: null }); function linePrices(line: Row, books: Row, sums: Record): Parts { const { kind, key } = line; - const out = zeros(); - if (kind === "labor") out.labor = num((books.labor[key] || {}).price); - else if (kind === "material") { - const row: Row = books.material[key] || {}; - // 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락에서 셈 — 두 번 안 셈) - out.material = (SUPPLY as readonly string[]).includes(row.supply) ? 0 : num(row.price); - } else if (kind === "machine" || kind === "daega" || kind === "sangeun") { - const src: Row = ({ machine: books.machine, daega: sums, sangeun: books.sangeun } as Row)[kind]; + let out = zeros(); + if (kind === "labor" || kind === "material" || kind === "machine") { + // 841-1 = M02 엔진 단가(`own`) · B09 단가 책 안 읽음 + const own: Row | undefined = line.own; + out = blanks(); + if (own) for (const part of PARTS) out[part] = part in own ? num(own[part]) : 0; + const row: Row = kind === "material" ? books.material[key] || {} : {}; + // 관급 · 별도계상 자재 = 대가 안 0원(자재대 · 관급 단락) + if ((SUPPLY as readonly string[]).includes(row.supply)) out.material = 0; + } else if (kind === "daega" || kind === "sangeun") { + const src: Row = ({ daega: sums, sangeun: books.sangeun } as Row)[kind]; const row: Row = src[key] || {}; for (const part of PARTS) out[part] = row.blank ? null : num(row[part]); // 빈 줄 대가 = 빈칸 } else if (kind === "fixed") return fixedPrices(line, books); @@ -118,20 +121,14 @@ function linePrices(line: Row, books: Row, sums: Record): Parts { return out; } -/** 고정 줄 단가(705-1) — 줄 단가 위에 이은 비목만 단가 책 값(짝 `_fixed`) */ +/** 고정 줄 단가(705-1) — 줄 단가 그대로 · 이은 자재가 관급 · 별도계상이면 재료 0(짝 `_fixed`) */ function fixedPrices(line: Row, books: Row): Parts { const own: Row = line.own || {}; const link: Row = line.link || {}; const out = zeros(); for (const part of PARTS) out[part] = num(own[part]) ?? 0; - const labor: Row | undefined = link.labor ? books.labor[link.labor] : undefined; - if (labor && num(labor.price) !== null) out.labor = num(labor.price); const row: Row | undefined = link.material ? books.material[link.material] : undefined; - const supplied = !!row && (SUPPLY as readonly string[]).includes(row.supply); - if (row && (supplied || num(row.price) !== null)) out.material = supplied ? 0 : num(row.price); - const machine: Row | undefined = link.machine ? books.machine[link.machine] : undefined; - if (machine && !machine.blank) - for (const part of PARTS) if (num(machine[part]) !== null) out[part] = num(machine[part]); + if (row && (SUPPLY as readonly string[]).includes(row.supply)) out.material = 0; return out; } diff --git a/B09_Estimation/B09_Estimation_Labor_Source.py b/B09_Estimation/B09_Estimation_Labor_Source.py index 5c8e8d096..b2bd13df0 100644 --- a/B09_Estimation/B09_Estimation_Labor_Source.py +++ b/B09_Estimation/B09_Estimation_Labor_Source.py @@ -243,18 +243,18 @@ class Builder: part = PART.get(str(add.get("비목")), "expense") return {**head, "kind": "pct", "part": part, "base": base, "spec": f"{label}의 %", "unit": "%", "qty": round(value / under * 100, 6)} # fmt: skip - kind = item.get("종류") + kind, own = item.get("종류"), _own(x, qty) if kind == "인력": key = goods.get("키") or str(item.get("요소") or "") - return {**head, "kind": "labor", "key": key, "qty": qty} + return {**head, "kind": "labor", "key": key, "qty": qty, "own": own} if kind == "재료": if not goods.get("키"): return {**head, "kind": "blank", "qty": qty, "why": x.get("까닭") or "품목 없음"} return {**head, "kind": "material", "key": goods["키"], "spec": goods.get("규격") or "", - "qty": qty} # fmt: skip + "qty": qty, "own": own} # fmt: skip # 로직 줄 — `Sangeun.call_of` 판정: 기계경비 로직 = 중기 · 그 밖 = 하위 산근 - if call and call.get("machine"): - return {**head, "kind": "machine", "key": call["machine"], "qty": qty} + if call and call.get("machine"): # 기계 시간당 = M02 엔진 값(841-1) + return {**head, "kind": "machine", "key": call["machine"], "qty": qty, "own": own} if call and call.get("code"): key = sg.sangeun_key(call["code"], call["given"]) return {**head, "kind": "sangeun", "key": key, "qty": qty} @@ -559,11 +559,24 @@ def _row_of(key: str, where: dict) -> str: return f"{where[line]}@{PART[cost]}" if cost else str(where[line]) +def _own(x: dict, qty: float | None) -> dict[str, float] | None: + """엔진 줄 비목 단가(841-1) = 엔진 금액 ÷ 엔진 수량 · 수량 0 = 0 · 못 셈 = None(빈칸).""" + money = x.get("비목") or {} + if not qty: + return dict.fromkeys(PART.values(), 0.0) if qty == 0 else None + return {PART[k]: (_f(v) or 0.0) / qty for k, v in money.items() if k in PART} + + def _material(whole: Any, mat: dict, qty: Any) -> dict[str, Any]: - """자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가.""" + """자재 줄 — 조합 사용자 자재 · 사전조합 자재(175-2) 한 꼴 · 이름 · 규격 · 단위 = 사본 단가 · + 단가 = M02 엔진 자재 기준단가(841-1 · 조합 셈 답 `기준단가` · 없으면 사본 단가 묶음으로).""" row = (whole.get(mat.get("코드")) if mat.get("코드") else None) or {} names = {k: row.get(v) or "" for k, v in (("name", "이름"), ("spec", "규격"), ("unit", "단위"))} - return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty)} + price = mat["기준단가"] if "기준단가" in mat else None + if price is None and mat.get("코드"): + price = labor_doc.material_price(whole, mat["코드"], mat.get("단위"))["기준단가"] + own = None if _f(price) is None else {"material": _f(price)} + return {"kind": "material", "key": mat.get("코드"), **names, "qty": _f(qty), "own": own} def _engine_bases(engine: list[dict], items: list[dict]) -> dict[str, float]: diff --git a/B09_Estimation/B09_Estimation_Router.py b/B09_Estimation/B09_Estimation_Router.py index 340457c5f..1260ba3a3 100644 --- a/B09_Estimation/B09_Estimation_Router.py +++ b/B09_Estimation/B09_Estimation_Router.py @@ -160,7 +160,7 @@ def compute_sheets( 화면 짝 = `B09_Estimation_UI_Shell.computeSheets`(산근만 서버 답 그대로) · memo = 두 번 셈의 앞 산근 풀이(`Sangeun.collect`).""" base = compute_base(inputs, user) - sg = sangeun.build(root, base, user, memo) # 145-1 — 굳힌 단가로 풀이(+ 166-2 손 줄 로직) + sg = sangeun.build(root, user, memo) # 145-1 — M02 엔진 값으로 풀이(841-1 · + 166-2 손 줄 로직) sg = sg_cover.cover_sangeun(sg, user.get("sg_cover")) # 719-3 이 공사 산근 고친 칸 by_key = {row["key"]: row for row in sg["sg_list"]} labor = compute_labor( @@ -423,14 +423,11 @@ class PreviewBody(BaseModel): def logic_preview(root: Path, body: PreviewBody) -> dict[str, Any]: - """로직 한 산근 미리보기 — 단가 = 굳힌 책(없는 줄 = 지금 M01) · 사용자 대비표 고름.""" + """로직 한 산근 미리보기 — 단가 = M02 엔진(841-1 · 그 로직 닫힌 묶음 · B09 단가 책 안 읽음).""" src = {"code": body.code, "given": sangeun._clean_given(body.given), "rate": body.rate, "picks": body.picks} # fmt: skip hands = [("preview", "미리보기", src)] - keys = sangeun.hand_resource_keys({"haul_refs": {"preview": {"tab": "logic", **src}}}) - user = store.read_user(root) - base = compute_base(prices.build_inputs(store.read_book(root), keys), user) - solver = sangeun.Solver(base) + solver = sangeun.Solver() block = solver.add(sangeun.hand_whole(hands), src, None) text = [ *block["head"], diff --git a/B09_Estimation/B09_Estimation_Sangeun.py b/B09_Estimation/B09_Estimation_Sangeun.py index fa224c392..c18536c4f 100644 --- a/B09_Estimation/B09_Estimation_Sangeun.py +++ b/B09_Estimation/B09_Estimation_Sangeun.py @@ -62,6 +62,7 @@ import master_sheet_line as msl # noqa: E402 — 계산서 줄 덧칸(714-1) · PARTS = ("labor", "material", "expense") COST_PART = {"노무비": "labor", "재료비": "material", "경비": "expense"} +PART_COST = {v: k for k, v in COST_PART.items()} PCT_OF = {**COST_PART, "인건비": "people"} # % 덧줄 밑수 → 덮개 다시 셈(719-3) DEPTH = 4 # 하위 산근 깊이 상한 NO_SOURCE = "조합 밖 토공 · 운반 · 사토 · 준비공은 산근 없음" @@ -165,49 +166,15 @@ def rows_of(folder: Path, whole: Any = None) -> list[dict[str, Any]]: # ── 풀이 ────────────────────────────────────────────────────────────── -class _Seen(dict): - """읽은 키를 적는 단가 표 — 두 번 셈(183-1)에서 첫 풀이가 본 값만 견줌.""" - - def __init__(self, *args: Any) -> None: - super().__init__(*args) - self.seen: set[Any] = set() - - def __contains__(self, key: object) -> bool: - self.seen.add(key) - return super().__contains__(key) - - def __getitem__(self, key: Any) -> Any: - self.seen.add(key) - return super().__getitem__(key) - - def get(self, key: Any, default: Any = None) -> Any: - self.seen.add(key) - return super().get(key, default) - - def same(self, other: dict) -> bool: - """본 키마다 있음 · 값이 `other` 와 같음.""" - return all( - dict.__contains__(self, k) == dict.__contains__(other, k) - and dict.get(self, k) == dict.get(other, k) - for k in self.seen - ) - - class _Prices: - """B09 표 단가 — 노무 · 중기(노 · 재 · 경) · 자재 적용 단가(키 → 값).""" + """엔진 단가(841-1) — M02 엔진이 사본 단가 묶음으로 낸 줄 금액 ÷ 수량 · 텍스트 수식 글이 읽음 + (노무 · 자재 = 수 · 중기 = 노 · 재 · 경). B09 단가 책 · 바탕 표는 안 읽음.""" - def __init__(self, sheets: dict[str, Any]) -> None: - self.labor = _Seen({r["key"]: num(r.get("price")) for r in sheets.get("labor") or []}) - self.machine = _Seen({r["key"]: r for r in sheets.get("machine") or []}) - self.material = _Seen( - {r["key"]: num(r.get("applied")) for r in sheets.get("compare") or []} - ) - - def same(self, other: _Prices) -> bool: - """첫 풀이가 본 단가가 `other` 에서도 같음 — 같으면 풀이도 같음.""" - return all( - getattr(self, k).same(getattr(other, k)) for k in ("labor", "machine", "material") - ) + def __init__(self) -> None: + # ponytail: 키 하나 = 단가 하나(사본마다 기준 시점이 달라도 마지막 본 값) — 글 짓기 전용 + self.labor: dict[str, float | None] = {} + self.material: dict[str, float | None] = {} + self.machine: dict[str, dict[str, float | None]] = {} def _unit(row: dict) -> tuple[str, Decimal]: @@ -323,6 +290,10 @@ def spec_text(whole: Any, given: dict[str, Any]) -> tuple[str, str]: return ", ".join(f"{k}={shown(k, v)}" for k, v in given.items()), raw +def _float(value: Any) -> Any: + return float(value) if isinstance(value, Decimal) else value + + def _money(price: float | None, qty: float | None) -> float | None: """줄 금액 = ROUNDDOWN(단가 × 수량, 1)(엑셀 자리).""" return None if price is None or qty is None else rounddown(price * qty, 1) @@ -340,8 +311,9 @@ def block_totals(lines: list[dict[str, Any]]) -> dict[str, float | None]: class Solver: """한 번 셈 — 산근 블록을 첫 등장 차례로 모음(하위 산근 포함).""" - def __init__(self, sheets: dict[str, Any]) -> None: - self.prices = _Prices(sheets) + def __init__(self) -> None: + self.prices = _Prices() + self.eng: list[dict[str, Any]] = [] # 지금 푸는 로직의 엔진 줄(호표 차례 · 841-1) self.blocks: dict[str, dict[str, Any]] = {} def add( @@ -367,10 +339,13 @@ class Solver: unit, times = _unit(row) rate = Decimal(str(src["rate"])) / 100 if src.get("rate") not in (None, 100) else None lines: list[dict[str, Any]] = [] + # 841-1 단가 = M02 엔진 줄 금액 ÷ 수량(켜쌓기율 없이 · 날 값 · B09 단가 책 안 읽음) + eng = labor_doc.run_line(whole, code, src["given"], None, src.get("picks"), raw=True) try: env = _env(whole, row, src["given"]) with mf.mp.choosing(src.get("picks")): for n, item in enumerate(row.get("호표", [])): + self.eng = eng.get("줄") or [] # 하위 산근 풀이가 바꿔 놓아도 되돌림 lines.append(self._line(whole, item, n, env, times, rate, depth)) ss.places(row, lines) # 줄 자리(792-3) self._extras(whole, row, env, lines) @@ -434,6 +409,14 @@ class Solver: at += n return text[:start], text[-1] + def _each(self, n: int, part: str, q: float) -> float | None: + """엔진 줄 n 비목 단가 = 엔진 금액 ÷ 엔진 수량 · 안 쓰는 줄(수량 0) = 0 · 못 셈 = None.""" + x = self.eng[n] if n < len(self.eng) else {} + qty, money = num(_float(x.get("수량"))), (x.get("비목") or {}).get(PART_COST[part]) + if not qty: + return 0.0 if q == 0 else None + return None if money is None else float(money) / qty + def _blank(self, item: dict, why: str) -> dict[str, Any]: part = COST_PART.get(item.get("비목"), "material") return {"kind": "blank", "name": item.get("이름", ""), "spec": "", "qty": None, @@ -451,11 +434,12 @@ class Solver: zero = dict.fromkeys(PARTS, 0.0) kind = item.get("종류") if kind == "로직": - return self._logic_line(whole, item, env, q, base, zero, depth) + return self._logic_line(whole, item, n, env, q, base, zero, depth) if kind == "인력": key = mf.fill(item["요소"], env) if isinstance(item["요소"], str) else "" - price = self.prices.labor.get(key) - why = None if price is not None else f"노무비목록표에 없음 「{key}」" + price = self._each(n, "labor", q) + self.prices.labor[key] = price + why = None if price is not None else f"노임 단가 없음(M02 엔진) 「{key}」" return {**base, **zero, "kind": "labor", "ref": {"tab": "labor", "key": key}, "price": {"labor": price}, "labor": _money(price, q), "why": why} # fmt: skip ref = ( @@ -468,29 +452,33 @@ class Solver: key = str( goods.get("키") or (source or "").split(".")[0] or (ref if isinstance(ref, str) else "") ) - if key not in self.prices.material: # 자리표시 글(「MO:경유:전국평균」) = 대비표 키로 - try: - key = str((whole.get(key) or {}).get("키") or key) - except (mf.FormulaError, KeyError, TypeError, ValueError): - pass + try: # 자리표시 글(「MO:경유:전국평균」) = 그 줄 키로 + key = str((whole.get(key) or {}).get("키") or key) + except (mf.FormulaError, KeyError, TypeError, ValueError): + pass part = COST_PART.get(item.get("비목"), "material") - price = self.prices.material.get(key) - if q == 0: - price = price if price is not None else 0.0 # 안 쓰는 재료 줄 — 금액 0 - why = None if price is not None else f"자재단가대비표에 없음 「{key}」" + price = self._each(n, part, q) + self.prices.material[key] = price + why = None if price is not None else f"자재 단가 없음(M02 엔진) 「{key}」" spec = goods.get("규격") or "" ref = {"tab": "compare", "key": key} return {**base, **zero, "spec": spec, "kind": "material", "ref": ref, "price": {part: price}, part: _money(price, q), "why": why, "_goods": goods} # fmt: skip - def _logic_line(self, whole, item, env, q, base, zero, depth) -> dict[str, Any]: + def _logic_line(self, whole, item, n, env, q, base, zero, depth) -> dict[str, Any]: call = call_of(whole, item, env) if call and "machine" in call: keys = [call["machine"]] if call["machine"] else [] - m = self.prices.machine.get(keys[0]) if keys else None - price = {p: num((m or {}).get(p)) for p in PARTS} - why = None if m else f"중기목록표에 없음 「{keys[0] if keys else item.get('이름')}」" + price = {p: self._each(n, p, q) for p in PARTS} # 기계 시간당 = M02 엔진 값(841-1) + m = None not in price.values() + if keys: + self.prices.machine[keys[0]] = price + why = ( + None + if m + else f"기계 시간당 없음(M02 엔진) 「{keys[0] if keys else item.get('이름')}」" + ) amounts = {p: _money(price[p], q) if m else None for p in PARTS} ref = {"tab": "machine", "key": keys[0] if keys else ""} return {**base, "kind": "machine", "ref": ref, @@ -618,16 +606,15 @@ def hand_resource_keys(user: dict[str, Any] | None) -> list[str]: def collect( entries: list[tuple[str, Path]], - sheets: dict[str, Any], hands: list[tuple[str, str, dict[str, Any]]] | None = None, memo: dict[str, Any] | None = None, ) -> dict[str, Any]: """`entries` = [(구조물 id, 사본 폴더)] → 블록 · 목록 · 「연결 없음」 · `hands` = 손 줄 로직(166-2 · 사본 뒤 번호 · 마스터 로직) · memo = 같은 사본 · 손 줄의 앞 풀이(두 번 셈) — 앞 풀이가 본 단가가 같으면 그 풀이 사본.""" - solver = Solver(sheets) - if memo and memo["prices"].same(solver.prices): + if memo and "sg" in memo: # 841-1 풀이 = 사본 · 엔진만(바탕 표 안 읽음) → 앞 풀이 그대로 return copy.deepcopy(memo["sg"]) + solver = Solver() for owner, folder in entries: whole = item_prices.load_prices(read_json(folder / PRICE_FILE) or {}) for src in rows_of(folder, whole): @@ -652,13 +639,12 @@ def collect( "sg_hand": hand_keys, } if memo is not None: - memo.update(prices=solver.prices, sg=copy.deepcopy(out)) + memo.update(sg=copy.deepcopy(out)) return out def build( project_root: str | Path, - sheets: dict[str, Any], user: dict[str, Any] | None = None, memo: dict[str, Any] | None = None, ) -> dict[str, Any]: @@ -676,4 +662,4 @@ def build( entries += [ (f"서고 {(m.get('원본') or {}).get('name') or f.name}", f) for f, m in copies(project_root) ] - return collect(entries, sheets, hand_sources(user), memo) + return collect(entries, hand_sources(user), memo)