"""B09 [단가 갱신] = 프로젝트 사본 자원 단가 줄 갱신(PLAN 841-1 · 요구 854 · 866) — 서버 단독. B09 금액 = 프로젝트 사본(구조물 사본 · 서고 사본)을 M02 엔진으로 셈한 값(B09 단가 책으로 다시 안 셈). 그래서 사본 값을 지금 M01 로 올리는 일 = 사본 `단가.json` 자원 줄(LB · MT · EQ · MO · FX)만 지금 M01 줄로 (로직 줄 · 일위대가 · 입력값 · 고른 자재 · 덮개 그대로 · M01 에서 지워진 키 = 마지막 값 그대로) — M02 [단가 갱신](`M02_Sangeun_Copy.refresh`)과 같은 뜻 · 사용자가 누를 때만(프로젝트 안은 자동 아님). 미리 보기 = `OVER` 덮개(파일 안 씀 · 사본 읽는 곳이 `bundle` 로 읽음)로 같은 셈. """ from __future__ import annotations from contextvars import ContextVar from pathlib import Path from typing import Any from B09_Estimation import B09_Estimation_Bill_Library as library from common_util.common_util_json import atomic_write_json from M01_MasterData import M01_MasterData_Store as m01 from M02_MasterTemplete import M02_Item_Prices as item_prices from M02_MasterTemplete import M02_Sangeun_Copy as skc from M02_MasterTemplete.M02_Item_Snapshot import linked_snapshots, structures_dir from M02_MasterTemplete.M02_Item_Source import PRICE_FILE, read_json #: 미리 보기 덮개 {사본 폴더 글: 새 `단가.json`} — 없으면 파일 OVER: ContextVar[dict[str, dict] | None] = ContextVar("b09_copy_prices", default=None) def bundle(folder: Path) -> dict[str, Any]: """사본 `단가.json` — 미리 보기 덮개가 있으면 그것.""" over = OVER.get() if over and str(folder) in over: return over[str(folder)] return read_json(folder / PRICE_FILE) or {} def folders(root: str | Path) -> list[Path]: """연결된 구조물 사본 + 서고 사본 폴더.""" base = structures_dir(root) out = [base / one["폴더"] for one in linked_snapshots(root).values()] out += [folder for folder, _manifest in library.copies(root)] return list(dict.fromkeys(out)) def _shown(row: dict[str, Any]) -> Any: """미리 보기 값 — 노임 · 유가 = 값 · 기계 = 가격(천원) · 자재 = 출처 값 묶음.""" value = row.get("값") if isinstance(value, dict): return value.get("가격") if value is not None: return value return {k: row.get(k) for k in ("조달청", "물가자료", "유통물가", "물가정보", "거래가격")} def refreshed(root: str | Path) -> tuple[dict[str, dict], list[dict[str, Any]]]: """({바뀐 사본 폴더 글: 새 `단가.json`}, 바뀐 자원 [{키, 이름, 전, 후}]) — 파일 안 씀.""" whole = skc.whole_() news: dict[str, dict] = {} changed: dict[str, dict[str, Any]] = {} for folder in folders(root): data = read_json(folder / PRICE_FILE) if not isinstance(data, dict): continue out, hit = {}, False for name, part in data.items(): slot = m01.items_key(part) if isinstance(part, dict) else "" rows = part.get(slot) if slot else None if not isinstance(rows, list): out[name] = part continue fresh = [] for row in rows: key = str((row or {}).get("키") or "") live = whole.index.get(key[:2], {}).get(key) if key[:2] in skc.RES else None new = item_prices.to_json(live) if live is not None else row if new != row: hit = True changed.setdefault( key, {"키": key, "이름": new.get("이름"), "전": _shown(row), "후": _shown(new)}, ) fresh.append(new) out[name] = {**part, slot: fresh} if hit: news[str(folder)] = out return news, list(changed.values()) def write(news: dict[str, dict]) -> int: """새 `단가.json` 을 사본 폴더에 씀 — 쓴 사본 수.""" for folder, data in news.items(): atomic_write_json(Path(folder) / PRICE_FILE, data) return len(news)