Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LLCk2y5xE8YGVgjbTntWsY
132 lines
5.7 KiB
Python
132 lines
5.7 KiB
Python
"""B09 설계내역서 연결 — 서고 구조물조합 · 일위대가 사전조합 → 프로젝트 사본(PLAN 204-2).
|
|
|
|
자리 = 프로젝트 `B09_Estimation/v1/library/{폴더}/` · 폴더 = `{종류}@{층}@{주인}@{키}`
|
|
(주인 `/` → `-` · 마스터 `-`) — 같은 것을 다시 고르면 새로 안 들여오고 있는 사본(같은 호표):
|
|
combo 구조물조합 = B08 구조물 사본과 한 꼴(`M02_Item_Snapshot.build_snapshot` — 집계 ·
|
|
도면 · 면적 · 일위대가 · 단가 · 입력값 · 고른자재 첫 값)
|
|
precombo 사전조합 = `일위대가.json` {줄: {담은로직: [{참조, 수량: {손: 1}}]}, 참조사본} ·
|
|
`단가.json`(`M02_Item_Source.price_bundle` — 사본 단가 묶음 한 벌)
|
|
사본 = 원본이 바뀌거나 지워져도 그대로(굳힘) ·
|
|
대가 블록 = `Labor_Source.daega_inputs`(`libs` {폴더: 블록 id}) ·
|
|
단가 수요 = `Linked.linked_resource_keys` · 줄 연결 = {tab: dg, key: 블록 id, lib: 폴더} ·
|
|
[저장] 때 줄이 안 쓰는 사본은 지움(`prune`) · 층 읽기 권한 = 부르는 쪽(`M02_Layer_Access.grant`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from B09_Estimation.B09_Estimation_Store import folder as store_folder
|
|
from B09_Estimation.B09_Estimation_Store import ref_rows
|
|
from common_util.common_util_json import atomic_write_json
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
from M02_MasterTemplete.M02_Item_Snapshot import MANIFEST_NAME, build_snapshot, now_text, origin_of
|
|
from M02_MasterTemplete.M02_Item_Source import (
|
|
LABOR_FILE,
|
|
PRICE_FILE,
|
|
item_name,
|
|
labor_doc_rows,
|
|
price_bundle,
|
|
read_json,
|
|
)
|
|
|
|
LIBRARY = "library"
|
|
KINDS = ("combo", "precombo")
|
|
#: 폴더 이름 — 종류 · 층 · 주인 · 키(영문 · 숫자 · `-` `_`)
|
|
NAME = re.compile(r"^(combo|precombo)@(system|company|personal)@[0-9-]+@[A-Za-z0-9_-]{1,40}$")
|
|
|
|
|
|
def library_dir(project_root: str | Path) -> Path:
|
|
return store_folder(project_root) / LIBRARY
|
|
|
|
|
|
def folder_name(kind: str, layer: str, owner: str | None, key: str) -> str:
|
|
"""고른 서고 것 → 사본 폴더 이름 — 규칙 밖이면 ValueError."""
|
|
name = f"{kind}@{layer}@{(owner or '-').replace('/', '-')}@{key}"
|
|
if not NAME.match(name):
|
|
raise ValueError(f"서고 항목 모양이 틀림 「{name}」")
|
|
return name
|
|
|
|
|
|
def _precombo_files(layer: str, owner: str | None, key: str) -> dict[str, Any]:
|
|
ref = {"층": layer, "주인": owner, "키": key}
|
|
doc = refs.read_ref(ref)
|
|
if doc is None or doc.get("종류") != refs.TITLE["precombo"]:
|
|
raise LookupError(f"사전조합 {key} 이 {layer} 층에 없음")
|
|
line = {"참조": ref, "수량": {"손": 1}}
|
|
labor = labor_doc_rows({"이름": doc.get("이름"), "단위": doc.get("단위"), "담은로직": [line]})
|
|
return {
|
|
LABOR_FILE: labor,
|
|
PRICE_FILE: price_bundle(labor, refs.folder_of(layer, owner)),
|
|
"_원본": {"참조": ref, "name": doc.get("이름") or key, "단위": doc.get("단위") or ""},
|
|
}
|
|
|
|
|
|
def import_copy(
|
|
project_root: str | Path,
|
|
kind: str,
|
|
layer: str,
|
|
owner: str | None,
|
|
key: str,
|
|
who: dict[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""서고 것 하나를 프로젝트 사본으로 — `{폴더, 만듦}` · 이미 있으면 그대로(만듦 거짓) ·
|
|
원본이 없으면 LookupError · 모양이 틀리면 ValueError."""
|
|
if kind not in KINDS:
|
|
raise ValueError(f"서고 종류는 {', '.join(KINDS)} 중 하나")
|
|
name = folder_name(kind, layer, owner, key)
|
|
target = library_dir(project_root) / name
|
|
if isinstance(read_json(target / MANIFEST_NAME), dict):
|
|
return {"폴더": name, "만듦": False}
|
|
if kind == "combo":
|
|
item = {"layer": layer, "owner": owner, "key": key}
|
|
item["name"] = item_name(item)
|
|
files, origin = build_snapshot(item), origin_of(item)
|
|
else:
|
|
files = _precombo_files(layer, owner, key)
|
|
origin = files.pop("_원본")
|
|
if target.is_dir(): # 끊긴 쓰기(manifest 없음) 자리
|
|
shutil.rmtree(target)
|
|
target.mkdir(parents=True)
|
|
for file, content in files.items():
|
|
atomic_write_json(target / file, content)
|
|
stamp = now_text()
|
|
manifest = {"종류": kind, "원본": origin, "가져온날": stamp, "가져온이": who}
|
|
atomic_write_json(target / MANIFEST_NAME, manifest) # 마지막 — 있으면 다 쓴 폴더
|
|
return {"폴더": name, "만듦": True}
|
|
|
|
|
|
def copies(project_root: str | Path) -> list[tuple[Path, dict[str, Any]]]:
|
|
"""다 쓴 사본 `(폴더, manifest)` — 폴더 이름 차례."""
|
|
base = library_dir(project_root)
|
|
out = []
|
|
for child in sorted(base.iterdir()) if base.is_dir() else []:
|
|
manifest = read_json(child / MANIFEST_NAME)
|
|
if child.is_dir() and NAME.match(child.name) and isinstance(manifest, dict):
|
|
out.append((child, manifest))
|
|
return out
|
|
|
|
|
|
def used_folders(user: dict[str, Any]) -> set[str]:
|
|
"""설계내역서 덮개 줄 · 거리이정표 운반 연결(251-5)이 잇는 서고 사본 폴더."""
|
|
return {
|
|
str(ref["lib"])
|
|
for row in ref_rows(user)
|
|
if isinstance(ref := row.get("ref"), dict) and ref.get("tab") == "dg" and ref.get("lib")
|
|
}
|
|
|
|
|
|
def prune(project_root: str | Path, user: dict[str, Any]) -> list[str]:
|
|
"""[저장] 뒤 — 줄이 안 잇는 사본 폴더를 지움(다시 고르면 새로 들여옴) · 지운 이름."""
|
|
keep = used_folders(user)
|
|
base = library_dir(project_root)
|
|
gone = []
|
|
for child in sorted(base.iterdir()) if base.is_dir() else []:
|
|
if child.is_dir() and NAME.match(child.name) and child.name not in keep:
|
|
shutil.rmtree(child)
|
|
gone.append(child.name)
|
|
return gone
|