"""M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2). 항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 도번 SD · 면적번 SA. 층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름. 권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만. """ from __future__ import annotations import json from datetime import datetime from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Store as store # 21-1 목록 칸 — SS 줄에서 그대로(키는 `key`) ITEM_FIELDS = ( "열", "경로", "이름", "종류", "분류1", "분류2", "분류3", "단위", "일위대가", "도번", "면적번", ) def ensure(folder: Path) -> bool: """회사 · 개인 층 첫 진입 — 빈 집계표(열 0 · 층 이름 · 줄 머리는 시스템 것) · 조합 장부(`다음` 1) · 종류 폴더. 이미 집계표가 있으면 아무것도 안 함(시스템 층은 늘 있음).""" table = Path(folder) / "table" / f"{store.STRUCTURE_TABLE}.json" if table.is_file(): return False system = json.loads((store.FOLDER / "table" / f"{store.STRUCTURE_TABLE}.json").read_bytes()) doc = { **system, "열": [], "줄": [{**r, "값": {}} for r in system.get("줄", []) if isinstance(r, dict)], "합계줄": [], "보기": {}, } with store.at(folder): for kind in store.KINDS: (Path(folder) / kind).mkdir(parents=True, exist_ok=True) with store._LOCK: if table.is_file(): # 그 사이 다른 요청이 만듦 return False atomic_write_json(table, doc) store.sync_combos() return True def items(folder: Path) -> list[dict[str, Any]]: """그 층 항목 — SS 줄마다 `{key, 열, 경로, 이름, 종류, 분류1~3, 단위, 일위대가, 도번, 면적번, names}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 · 분류1~3 은 경로 앞 넷) · `names` = 화면 이름(키 대신 · 없으면 null) — 일위대가 = 조합 이름 · 도번 · 면적번 = 항목(열) 이름.""" with store.at(folder): ua = combos.names() # 마스터 · 이 층이 쓸 수 있는 회사 · 개인 조합(27-2c) rows = store.read_combos().get("줄", []) out = [] for r in rows: head = r.get("이름") names = { "일위대가": ua.get(str(r.get("일위대가"))) if r.get("일위대가") else None, **{k: (head or r.get("열")) if r.get(k) else None for k in ("도번", "면적번")}, } out.append( { "key": r.get("키"), **{k: r.get(k) for k in ITEM_FIELDS}, "names": names, **_list_extras(Path(folder), str(r.get("열"))), } ) return out def _list_extras(folder: Path, col: str) -> dict[str, Any]: """목록 칸(39-5) — `수정일` = 도면 · 면적산출 · 일위대가 파일 시각 · `labor` = 일위대가 문서의 번호 · 담은 로직 수 · 합계(`계산값.반영합.계` · 없으면 null) — 파일이 없으면 칸 없음.""" stamps: dict[str, str] = {} for kind in ("structure", "area", "labor"): path = folder / kind / f"{col}.json" if path.is_file(): stamps[kind] = datetime.fromtimestamp(path.stat().st_mtime).isoformat( timespec="seconds" ) labor = None path = folder / "labor" / f"{col}.json" if path.is_file(): try: doc = json.loads(path.read_bytes()) except ValueError: doc = {} total = ((doc.get("계산값") or {}).get("반영합") or {}).get("계") try: total = None if total is None or isinstance(total, bool) else float(total) except (TypeError, ValueError): total = None # 수 아닌 값(옛 꼴)은 「—」 labor = { "번호": doc.get("일위대가번"), "로직수": len(doc.get("담은로직") or []), "합계": total, } return {"수정일": stamps, "labor": labor} def _fresh_id(want: str, used: set[str]) -> str: out, n = want, 2 while out in used: out, n = f"{want}_{n}", n + 1 return out def copy_item( source: Path, target: Path, key: str, origin: dict[str, Any], path: list[str] | None = None, ) -> dict[str, Any]: """다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 바인딩 그대로 · 도면 · 일위대가(새 SU · 조합 키는 안 받음 · 39-1b) · 면적산출 내용(알약 연결 포함) 복사 · 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키) · `path` = 새 자리(폴더 + 이름 · 없으면 원래 경로 · 붙여넣기 PLAN 30-2). 돌려줌 `{key, 열, 이름}`.""" with store.at(source): row = store.find_combo(key) # 옛 키(회사 · 개인 층 SS)로도 if row is None: raise store.StoreError(404, f"없는 항목 「{key}」") col_id = row["열"] col = next(c for c in store._structure_columns() if c["id"] == col_id) docs = {} for kind, _, _, _ in store.PARTS: try: docs[kind] = store.read(kind, col_id)["문서"] except store.StoreError: pass # 없는 파일은 새 층에서 빈 문서로 섬 ensure(target) with store.at(target): table = store.read("table", store.STRUCTURE_TABLE) doc = table["문서"] new_id = _fresh_id(col_id, {c.get("id") for c in doc.get("열", []) if isinstance(c, dict)}) made = { **json.loads(json.dumps(col)), "id": new_id, "일위대가": None, # 정본 = 일위대가 문서(아래 복사) "본뜬": origin, } made.pop("자재조합", None) # 옛 자재 조합 칸은 안 옮김 if path: made.update({"경로": list(path), "머리": list(path)}) # 머리 줄 수는 저장 맞춤이 채움 doc["열"] = [*doc.get("열", []), made] store.write("table", store.STRUCTURE_TABLE, table["판"], doc) # 빈 세 파일 · 번호 · SS for kind, got in docs.items(): number = store._KEY[kind] have = store.read(kind, new_id)["문서"] body = {k: v for k, v in got.items() if k != "계산값"} body = {**body, "열": new_id, number: have.get(number)} body = store._server(kind, body) with store._LOCK: atomic_write_json(store._path(kind, new_id), body) store._refresh_labor(new_id) # 면적이 일위대가 뒤에 서므로 칸 값으로 다시 ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id) return {"key": ss["키"], "열": new_id, "이름": ss["이름"]}