"""M02 층 집계표 항목 — 회사 · 개인 층 빈 뼈대 · 항목 목록 · 항목 복사(PLAN 21-1 · 21-2). 항목 = 구조물집계표조합(SS) 한 줄 — 그 열의 일위대가(UA) · 자재조합(UM) · 도번 SD · 면적번 SA · 재료번 SM. 층 폴더(시스템 · 회사 · 개인)는 모양이 같아 저장소 함수를 `store.at(층 폴더)` 안에서 그대로 부름. 권한은 부르는 쪽(`M02_MasterTemplete_Router_Items.py`) 몫 — 여기는 폴더와 파일만. """ from __future__ import annotations import json from pathlib import Path from typing import Any from common_util.common_util_json import atomic_write_json from M01_MasterData import M01_MasterData_Store as m01 from M01_MasterData import M01_MasterData_Store_Combo as combo_store from M02_MasterTemplete import M02_MasterTemplete_Matset as matset 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() mat = Path(folder) / matset.FILE if not mat.is_file(): atomic_write_json(mat, matset._load()[0]) return True def _combo_names() -> dict[str, str]: """일위대가 조합 키 → 이름(M01 시스템 마스터 · 층 무관) — 못 읽으면 빈 표.""" try: rows = m01.read(combo_store.FILE)[0].get("줄") or [] except m01.StoreError: return {} return {str(r.get("키")): str(r.get("이름") or "") for r in rows} def items(folder: Path) -> list[dict[str, Any]]: """그 층 항목 — SS 줄마다 `{key, 열, 종류, 분류1~3, 단위, 일위대가, 자재조합, 도번, 면적번, 재료번, names}` · `names` = 화면 이름(키 대신 · 없으면 null) — 일위대가 = 조합 이름 · 자재조합 = 그 층 자재 조합 이름 · 도번 · 면적번 · 재료번 = 항목(열) 이름.""" ua = _combo_names() with store.at(folder): rows = store.read_combos().get("줄", []) um = {str(r.get("키")): str(r.get("이름") or "") for r in matset.listing()["줄"]} out = [] for r in rows: head = " ".join(str(r[k]) for k in ("종류", "분류1", "분류2", "분류3") if r.get(k)) names = { "일위대가": ua.get(str(r.get("일위대가"))) if r.get("일위대가") else None, "자재조합": um.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}) return out 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]) -> dict[str, Any]: """다른 층 항목(SS `key`) → `target` 층 새 열 하나 — 머리 · 단위 · 일위대가 · 바인딩 그대로 · 도면 · 면적산출 · 재료산출 내용(알약 연결 포함) · 자재 조합 줄(새 UM) 복사 · 새 번호 · 새 SS 키 · 열 `본뜬` = `origin`(원래 층 · 주인 · 키). 돌려줌 `{key, 열, 이름}`.""" with store.at(source): row = next((r for r in store.read_combos().get("줄", []) if r.get("키") == key), None) 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 # 없는 파일은 새 층에서 빈 문서로 섬 um = None if col.get("자재조합"): try: um = matset.one(col["자재조합"]) except store.StoreError: um = None # 가리키던 줄이 없으면 비움 ensure(target) with store.at(target): new_um = None if um: names = matset.listing()["줄"] taken = {str(r.get("이름") or "") for r in names} name, n = str(um.get("이름") or key), 2 base = name while name in taken: name, n = f"{base} ({n})", n + 1 got = {k: um.get(k) for k in ("담은자재", "소유", "비고")} new_um = matset.create({**got, "이름": name, "본뜬키": None})["키"] 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, "자재조합": new_um, "본뜬": origin} 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)} if kind in store.SHEETS: body = store._server_values(body) with store._LOCK: atomic_write_json(store._path(kind, new_id), body) ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id) return {"key": ss["키"], "열": new_id, "이름": " ".join(h for h in store._heads(made) if h)}