"""B09 「내 자료집에 저장」(PLAN 703-2) — 펼친 일위대가(대가 블록) → 개인 층 사전조합(SC). 담는 것 = 담은로직(넣을 값 · 고른 자재 얹음) · 자재 · 줄 수량(= 수량산출 수기 `손`) — 맨 위 줄 수량 덮개 = 손(원래 손 × 덮개 ÷ 블록 줄 수량 · 대가 줄 ÷ 배를 되돌림) 안 담는 것 = 품 덮개(부른 블록 안 줄 · 품셈 블록 줄 — 로직 통째 배수로 바꾸면 다른 줄까지 바뀜) · 단가 덮개(사전조합 꼴에 단가 칸 없음) → 수만 알림(품은 PLAN 705 고정 줄이 생기면 담김) 원천 = 블록 cover — 조합 `S:`/`L:` = 사본 폴더(일위대가 · 넣을 값 · 고른 자재 · 셈 수량) · 사전조합 `C:`/`L:` = 굳힌 참조사본 문서 · 품셈 `P:` = 로직 한 줄 · 복사본 `K:`(703-2) = 원본까지 따라감(이름 · 덮개 = 복사본 것) 같은 이름 = 409 `{같은이름: 키, 이름}` → 부르는 쪽이 덮어쓰기(`overwrite` = 그 키) · 새 이름 · 쓰기 = 지금 층(`store.at` · 층 권한 · 폴더는 부르는 쪽). """ from __future__ import annotations from pathlib import Path from typing import Any from B09_Estimation import B09_Estimation_Bill_Library as library from B09_Estimation.B09_Estimation_Calc_Labor import COPY, with_copies from B09_Estimation.B09_Estimation_Labor_Source import AREA_FILE, daega_inputs from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Labor_Ref as refs from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete.M02_Item_Snapshot import ( MANIFEST_NAME, PICKS_FILE, VALUES_FILE, structures_dir, ) from M02_MasterTemplete.M02_Item_Source import LABOR_FILE, PRICE_FILE, read_json KIND = "precombo" PRICE_KEYS = ("labor", "material", "expense") def _combo(folder: Path) -> tuple[list[dict], list[dict]]: """구조물조합 사본 → (담은로직 · 자재) — 넣을 값 · 고른 자재 얹고 수량 = 셈 수량(손).""" labor = read_json(folder / LABOR_FILE) or {} values = read_json(folder / VALUES_FILE) or {} picks = read_json(folder / PICKS_FILE) or {} got = item_prices.calc( labor, read_json(folder / PRICE_FILE) or {}, values, picks, read_json(folder / AREA_FILE) ) doc = item_prices.with_values(labor.get("줄") or {}, values, picks) lines = [ {**one, "수량": {"손": done.get("수량")}} for one, done in zip(doc.get("담은로직") or [], got.get("줄") or []) ] mats = [ {**one, "수량": {"손": done.get("수량")}} for one, done in zip(doc.get("자재") or [], got.get("자재") or []) ] return lines, mats def _frozen(root: Path, rid: str) -> dict[str, Any] | None: """굳힌 사전조합 문서 — 구조물 사본 · 서고 사본의 `참조사본` 에서 찾음.""" folders = [p for p in sorted(structures_dir(root).glob("*")) if p.is_dir()] folders += [p for p, _m in library.copies(root)] for folder in folders: doc = ((read_json(folder / LABOR_FILE) or {}).get("참조사본") or {}).get(rid) if isinstance(doc, dict): return doc return None def _precombo(doc: dict[str, Any]) -> tuple[list[dict], list[dict]]: lines = [ {**one, "수량": {"손": (one.get("수량") or {}).get("손", 1)}} for one in doc.get("담은로직") or [] if isinstance(one, dict) ] return lines, [m for m in doc.get("자재") or [] if isinstance(m, dict)] def source(root: Path, block: dict[str, Any], base: str) -> tuple[list[dict], list[dict]]: """블록 원천(`base` = 복사본이면 원본 cover) → (담은로직 · 자재) · 못 찾으면 LookupError.""" head, _, rest = base.partition(":") if block["kind"] == "품셈": origin = block.get("origin") or {} one = {"로직": origin.get("로직"), "입력값": origin.get("입력값") or {}} one.update({k: origin[k] for k in ("켜쌓기율", "고름") if origin.get(k) is not None}) return [{**one, "수량": {"손": 1}}], [] if head == "S": return _combo(structures_dir(root) / rest) if head == "L": folder = library.library_dir(root) / rest manifest = read_json(folder / MANIFEST_NAME) or {} if manifest.get("종류") != "precombo": return _combo(folder) ref = (manifest.get("원본") or {}).get("참조") doc = ((read_json(folder / LABOR_FILE) or {}).get("참조사본") or {}).get(refs.ref_id(ref)) if isinstance(doc, dict): return _precombo(doc) if head == "C" and (doc := _frozen(root, rest)) is not None: return _precombo(doc) raise LookupError(f"일위대가 원천을 못 찾음 「{base}」") def build(root: Path, cover_key: str, cover: dict[str, Any], name: str | None = None) -> dict: """블록 cover → `{문서, 담음, 안담음}` — 문서 = 사전조합 꼴(키 · 층 쓰기 전).""" raw = daega_inputs(root).get("blocks") or [] blocks = {b["cover"]: b for b in with_copies(raw, cover)} block = blocks.get(cover_key) if block is None: raise LookupError(f"없는 일위대가 「{cover_key}」") base = cover_key while base.startswith("K:"): # 복사본 → 원본 cover base = str(((cover.get(base) or {}).get(COPY) or {}).get("from") or "") lines, mats = source(root, blocks.get(base, block), base) mine = cover.get(cover_key) or {} kept = {"수량": 0, "넣을값": sum(bool(ln.get("입력값")) for ln in lines)} kept["고른자재"] = sum(bool(ln.get("고름")) for ln in lines) skip = {"품": 0, "단가": 0} top = block["lines"] if block["kind"] != "품셈" else [] for line, one in zip(top, [*lines, *mats]): cell = mine.get(line.get("cid")) or {} skip["단가"] += sum(k in cell for k in PRICE_KEYS) if "qty" not in cell: continue hand, each = (one.get("수량") or {}).get("손"), line.get("qty") if not isinstance(hand, (int, float)) or not each: skip["품"] += 1 continue one["수량"] = {"손": round(hand * cell["qty"] / each, 9)} kept["수량"] += 1 # 맨 위 밖 덮개 = 품셈 블록 줄 · 부른 블록 안 줄(품) · 단가 by_id = {b["id"]: b for b in blocks.values()} todo = [] if top else [block["id"]] todo += [ln.get("key") for ln in top if ln.get("kind") == "daega"] seen: set[str] = set() while todo: sub = by_id.get(todo.pop()) if sub is None or sub["id"] in seen: continue seen.add(sub["id"]) for cell in (cover.get(sub["cover"]) or {}).values(): skip["품"] += "qty" in cell skip["단가"] += sum(k in cell for k in PRICE_KEYS) todo += [ln.get("key") for ln in sub["lines"] if ln.get("kind") == "daega"] doc = { "이름": (name or block.get("name") or "").strip(), "단위": block.get("unit") or "", "담은로직": lines, "자재": mats, } return {"문서": doc, "담음": kept, "안담음": skip} def write(doc: dict[str, Any], overwrite: str | None = None) -> dict[str, Any]: """지금 층(`store.at` 안)에 씀 — 같은 이름이면 409 · `overwrite` = 같은 이름 그 키를 덮음.""" same = [ path.stem for path in sorted((store.root() / KIND).glob("SC-*.json")) if (read_json(path) or {}).get("이름") == doc["이름"] ] if overwrite: if overwrite not in same: raise store.StoreError(409, f"덮어쓸 「{doc['이름']}」 이 없음 — 다시 누름") old = store.read(KIND, overwrite) return store.write(KIND, overwrite, old["판"], {**doc, "키": overwrite}) if same: raise store.StoreError(409, {"같은이름": same[0], "이름": doc["이름"]}) return refs.create(KIND, doc)