Files
Aislo/M02_MasterTemplete/M02_Item_Personal.py
T
eomsangdonandClaude Opus 5.5 fbb214cd94 feat(M02): 집계 항목 복사본을 구조물마다 — templates/structures/{구조물 id} · 옛 항목 폴더 옮겨 받기 · 재적용(덮임 알림) · 개인에 저장 · 프로젝트에서 새 항목 · 지정 바꿈 · 삭제 때 고친 것은 _지난 (PLAN 26-2b)
- 같은 항목을 고른 구조물 둘도 도면 · 면적 · 재료 · 자재조합 · 일위대가 · 입력연결 · 입력값 · 고른자재 · 단가를 따로 가짐 · 폴더 = structure_id 또는 pipe@측점
- 옛 templates/items/{층}_{주인}_{키} 는 읽을 때 그 항목을 쓰는 구조물마다 복사(고친 값 유지) · 안 쓰이고 고친 것은 structures/_지난
- 길: 미리 가져오기 {structure, layer, owner, key} · POST …/{폴더}/reapply(refresh 옛 이름) · …/save-as {to, 경로, 이름} · …/items/new {structure, 경로, 이름, 단위} · 회사 없음 409
- manifest 원본에 경로(26-1 · 경로 칸 우선 · 옛 종류 · 분류는 건너뛰며 받음) · 원본이 바뀌거나 지워져도 복사본 · 계산 그대로(목록 마스터바뀜 · 원본없음)
- 채움(M02_Table_Fill)은 구조물 복사본의 열 정의를 읽음

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BU9gbNE3aWFUvE61pS2WGt
2026-09-30 07:45:20 +09:00

121 lines
5.5 KiB
Python

"""프로젝트에서 개인 층으로 — [개인에 저장](save-as) · 프로젝트에서 새 항목(new) (PLAN 26-2b).
개인 층 새 항목 = 구조물집계표 새 열 하나 + 빈 도면 · 면적산출 · 재료산출(번호 · SS 키는 저장소가
줌). [개인에 저장]은 구조물 복사본의 열 정의 · 도면 · 면적 · 재료 · 자재조합 · 입력연결을 담음 —
단가 묶음 · 입력값 · 고른자재는 프로젝트 몫이라 안 가져감. 쓰기는 지금 층 저장소 함수(`store.at` ·
`store.write` · `matset.create` · 항목 연결 `write_file`) — sub4 나무 길
(`POST /layers/{layer}/tree`)이 오면 그리로.
권한(본인 개인 층 · 회사 없음 409)은 부르는 쪽 몫.
"""
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 M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs_store
from M02_MasterTemplete import M02_MasterTemplete_Items as items_store
from M02_MasterTemplete import M02_MasterTemplete_Matset as matset
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_Template_Layers as layers
from M02_MasterTemplete.M02_Item_Source import LINK_FILE, UM_FILE, read_json
#: 복사본 파일 → 층 양식 종류
PART_KINDS = (("도면.json", "structure"), ("면적.json", "area"), ("재료.json", "material"))
LEVELS = 4 # 지금 집계표 제목행 층 수 — 26-2a 경로 모델이 오면 깊이 자유
def heads_of(path: list[str], name: str | None) -> list[str]:
"""경로 + 이름 → 제목행(`머리`) — 빈 칸은 뺌 · 넷을 넘으면 넷째 칸에 이어 붙임.
ponytail: 제목행 4층 한계 — 26-2a(sub4) 경로 모델이 오면 경로를 그대로 적음."""
full = [str(p).strip() for p in [*path, name or ""] if str(p or "").strip()]
if not full:
raise ValueError("새 항목의 경로 · 이름이 비어 있습니다.")
if len(full) > LEVELS:
full = [*full[: LEVELS - 1], " ".join(full[LEVELS - 1 :])]
return [*full, *[""] * (LEVELS - len(full))]
def _new_matset(row: dict[str, Any], fallback: str) -> str:
"""자재 조합 줄을 이 층에 새 키로 — 이름이 겹치면 「이름 (2)」."""
taken = {str(r.get("이름") or "") for r in matset.listing()["줄"]}
name = base = str(row.get("이름") or fallback)
n = 2
while name in taken:
name, n = f"{base} ({n})", n + 1
got = {k: row.get(k) for k in ("담은자재", "소유", "비고")}
return matset.create({**got, "이름": name, "본뜬키": None})["키"]
def create_item(
folder: Path,
path: list[str],
name: str | None,
unit: str | None,
copy: Path | None = None,
origin: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""개인 층 `folder` 에 새 항목 — `copy` = 구조물 복사본 폴더(있으면 그 내용을 담음).
돌려줌 `{key, 열, 이름}`."""
heads = heads_of(path, name)
total = read_json(copy / "집계.json") if copy else None
base_col = dict((total or {}).get("열") or {})
base_row = (total or {}).get("줄") or {}
items_store.ensure(folder)
with store.at(folder):
table = store.read("table", store.STRUCTURE_TABLE)
doc = table["문서"]
used = {c.get("id") for c in doc.get("열", []) if isinstance(c, dict)}
new_id = items_store._fresh_id(str(base_col.get("id") or "item").split("@")[0], used)
um_row = read_json(copy / UM_FILE) if copy else None
um_key = _new_matset(um_row, heads[0]) if isinstance(um_row, dict) else None
column = {
**json.loads(json.dumps(base_col)),
"id": new_id,
"머리": heads,
"꼴": "수",
"단위": unit or base_col.get("단위") or base_row.get("단위"),
"일위대가": base_col.get("일위대가") or base_row.get("일위대가"),
"자재조합": um_key,
}
column.pop("본뜬열", None)
if origin:
column["본뜬"] = origin
doc["열"] = [*doc.get("열", []), column]
store.write("table", store.STRUCTURE_TABLE, table["판"], doc) # 빈 세 파일 · 번호 · SS
for file, kind in PART_KINDS if copy else ():
got = read_json(copy / file)
if not isinstance(got, dict):
continue
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)
links = read_json(copy / LINK_FILE) if copy else None
if isinstance(links, dict) and links.get("줄"):
inputs_store.write_file(new_id, {"판": 1, "줄": links["줄"]})
ss = next(r for r in store.read_combos()["줄"] if r.get("열") == new_id)
return {"key": ss["키"], "열": new_id, "이름": " ".join(h for h in heads if h)}
def personal_item(company: int, user: int, made: dict[str, Any]) -> dict[str, Any]:
"""새로 만든 개인 층 항목의 `summary_item`."""
return {
"layer": "personal",
"owner": f"{company}/{user}",
"key": made["key"],
"name": made["이름"],
}
def personal_folder(company: int, user: int) -> Path:
return layers.personal_dir(company, user)