"""구조물도 양식 **라이브러리** — 3단(개인·회사·프로그램) 목록과 프로젝트로 **가져오기** (PLAN 4장). ⛔ 표를 그릴 때 라이브러리를 읽지 않음 — 가져온 양식은 **프로젝트 작업본에 박히고** (`{프로젝트}/B08_Quantity/library/.json`) 표는 그것만 읽음. 3단은 가져오기 고르개에서만 돎. 여는 사람마다 값이 갈리지 않게 하려는 것(브레인 판정 Ⓑ — 개인 = 로그인한 사람). ⚠ 아직 아무것도 안 가져온 종류는 프로그램 기본을 읽음 — 보는 사람과 무관한 한 벌. ⚠ 저장은 **목록**(항목마다 파일 한 개 · 코드 `AX-ST-<8자리 16진>`, 명세 2장 ②) — 지금은 종류당 하나만 씀(판정 Ⓐ). 고르기 칸은 뒤에 붙임. """ from __future__ import annotations import json import re import secrets from pathlib import Path from typing import Any from B08_Quantity.B08_Quantity_Engine_StructureTemplate import TEMPLATE_DIR from config.config_system import STORAGE_BASE_DIR #: 단 이름 — 고르개에 이 차례로 보임(가까운 것부터). TIERS = ("personal", "company", "program") #: 항목 코드 — 파일 이름이 되므로 모양을 먼저 봄(경로 벗어남 막이). CODE_PATTERN = re.compile(r"^AX-ST-[0-9a-f]{8}$") LIBRARY_SUBDIR = "library" def tier_dirs(company_id: Any, user_id: Any) -> dict[str, Path]: """로그인한 사람의 3단 폴더. 회사가 없으면(시스템 관리자) 개인·회사 단은 없음.""" dirs: dict[str, Path] = {} if company_id is not None: company = Path(STORAGE_BASE_DIR).resolve() / str(company_id) if user_id is not None: dirs["personal"] = company / str(user_id) / LIBRARY_SUBDIR dirs["company"] = company / LIBRARY_SUBDIR dirs["program"] = TEMPLATE_DIR return dirs def _items(folder: Path) -> list[dict[str, Any]]: if not folder.is_dir(): return [] return [json.loads(p.read_text(encoding="utf-8")) for p in sorted(folder.glob("*.json"))] def list_items(dirs: dict[str, Path], type_id: str) -> list[dict[str, Any]]: """가져오기 고르개 — 단마다 그 종류의 항목(단·코드·이름).""" return [ {"tier": tier, "code": item.get("code"), "name": item.get("name"), "type_id": type_id} for tier in TIERS if tier in dirs for item in _items(dirs[tier]) if item.get("type_id") == type_id and item.get("code") ] def find_item(dirs: dict[str, Path], tier: str, code: str) -> dict[str, Any] | None: folder = dirs.get(tier) if folder is None: return None return next((item for item in _items(folder) if item.get("code") == code), None) def project_library_dir(project_root: str | Path) -> Path: return Path(project_root) / "B08_Quantity" / LIBRARY_SUBDIR def project_templates(project_root: str | Path | None) -> dict[str, dict[str, Any]]: """프로젝트에 박힌 양식 — 종류(type_id)별 하나. **표를 그릴 때 읽는 유일한 양식 자리.**""" if not project_root: return {} return { str(item["type_id"]): item for item in _items(project_library_dir(project_root)) if item.get("type_id") } def import_item(project_root: str | Path, item: dict[str, Any], tier: str) -> None: """고른 항목을 프로젝트 작업본에 박음 — 같은 종류의 옛 것은 지움(종류당 하나). ⚠ 새 것을 먼저 쓰고 옛 것을 지움 — 쓰다 실패하면 옛 양식이 남아 조용히 기본으로 안 떨어짐. """ code = str(item.get("code") or "") if not CODE_PATTERN.match(code): raise ValueError(f"항목 코드 모양이 아님: {code!r}") folder = project_library_dir(project_root) folder.mkdir(parents=True, exist_ok=True) body = {**item, "imported_from": tier} target = folder / f"{code}.json" target.write_text(json.dumps(body, ensure_ascii=False, indent=2), encoding="utf-8") for path in folder.glob("*.json"): if path == target: continue if json.loads(path.read_text(encoding="utf-8")).get("type_id") == item.get("type_id"): path.unlink() def _write(folder: Path, item: dict[str, Any]) -> None: folder.mkdir(parents=True, exist_ok=True) text = json.dumps(item, ensure_ascii=False, indent=2) (folder / f"{item['code']}.json").write_text(text, encoding="utf-8") def save_personal(folder: Path, template: dict[str, Any], overrides: dict[str, Any] | None) -> str: """[내 라이브러리에 저장] — 양식 + 프로젝트에서 고친 식을 **개인 단에 한 벌**로 씀. 코드. ⚠ 반대 방향(작업본 → 개인 단)이라 프로젝트는 안 바꿈(브레인 판정 Ⓑ). ⚠ 개인 단에 같은 종류가 있으면 **그 코드로 덮어씀** — 지금은 종류당 하나(판정 Ⓐ). """ from B08_Quantity.B08_Quantity_Engine_StructureTemplate import overridden_rows type_id = template.get("type_id") same = [item for item in _items(folder) if item.get("type_id") == type_id] code = str(same[0]["code"]) if same else f"AX-ST-{secrets.token_hex(4)}" # 고친 식·반올림이 곧 이 항목의 값 — 「사용자」 표시와 되돌릴 자리는 떼어 냄. dropped = {"default_formula", "default_rounding"} rows = [ {**{k: v for k, v in row.items() if k not in dropped}, "source": "library"} for row in overridden_rows(template, overrides) ] item = {k: v for k, v in template.items() if k != "imported_from"} _write(folder, {**item, "code": code, "library_tier": "personal", "rows": rows}) return code def delete_personal(folder: Path, type_id: str) -> int: """[내 것 지우기] — 개인 단의 그 종류만 지움. 프로젝트 작업본은 안 바꿈. 지운 수.""" count = 0 for path in folder.glob("*.json") if folder.is_dir() else []: if json.loads(path.read_text(encoding="utf-8")).get("type_id") == type_id: path.unlink() count += 1 return count def pin_program_templates(project_root: str | Path) -> int: """[확정] 때 — 아직 안 박힌 종류는 **그 시점 프로그램 기본**을 박음. 박은 수. 확정은 정본을 굳히는 자리라, 뒤에 프로그램 기본을 고쳐도 확정한 값이 안 흔들려야 함 (브레인 판정). 확정 전(GET 만 한 프로젝트)은 기본을 그대로 읽음 — 정본이 아님. """ pinned = project_templates(project_root) count = 0 for item in _items(TEMPLATE_DIR): if item.get("type_id") and item.get("code") and item["type_id"] not in pinned: import_item(project_root, item, "program") count += 1 return count