Files
Aislo/B09_Estimation/B09_Estimation_Daega_Personal.py
T
eomsangdonandClaude Opus 5.5 1c1dc9c523 feat(B09 설계내역서 펼침): 우클릭 복사해 새 일위대가 · 내 자료집에 저장 · 어디 쓰였나 (PLAN 703-2)
- 복사 = 덮개 K:… 의 @copy {from · name · spec} + 원본 덮개째(B09 사용자 값 · 브레인 정함) — 셈 짝 Calc_Labor py·ts 가 원본 블록을 본뜬 새 호표(번호 끝 · 기본값 = 원본) · 이 줄 단가연결을 새 호표로 바꿀지 물음 · 복사본 지우기(잇는 줄 있으면 막음)
- 원본 없음 = 짝 잃음 알림 · 저장 다듬기 @copy 글만 · 서고 사본 원본 = prune 에서 지킴
- 내 자료집 = POST …/estimation/daega/personal → 개인 층 사전조합(B09_Estimation_Daega_Personal) — 넣을 값 · 고른 자재 · 줄 수량 덮개(손) 담음 · 품 · 단가 덮개 = 안 담은 수 알림 · 같은 이름 409 → 덮어쓰기 · 새 이름
- 어디 쓰였나 = 누른 줄 노임 · 자재 · 중기(그 밖 = 호표)를 쓰는 내역 줄 목록 → 누르면 그 줄로 · 펼침
- 새 파일 B09_Estimation_UI_Tabs_Bill_Fold_Menu.ts · 시험 test_703_b09_daega_copy(거울 · 임시 storage 길)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:35:44 +09:00

166 lines
7.8 KiB
Python

"""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)