Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Items.py
eomsangdonandClaude Opus 5.5 760e43c8df feat(M02): 항목 키 체계 · 품번 새 모양(PLAN 26-4a)
- 항목 키 = 마스터 SS + 6자리 · 회사 SC{회사}- + 6자리 · 개인 SP{사용자}- + 6자리(장부 key_head · 층 폴더로 가름) · 한 번 주면 안 바뀜 · 지운 키 다시 안 씀 · 층 사이 복사 · 개인에 저장 · 새 항목은 그 층 새 키 + 본뜬 곳
- 회사 · 개인 층의 옛 SS 키는 장부를 맞출 때(처음 읽을 때 한 번) 새 모양으로 옮김(숫자 여섯 그대로 · 겹치면 다음 번호) · 옛 키는 줄 옛키 에 남김 · 옛 키로도 찾음(store.find_combo · 나무 고침 · 입력 목록 · 항목 복사 · M02_Item_Source.row_of)
- summary_item 키 검사 — SC · SP 는 그 층 · 머리 번호 = owner 일 때만 · 옛 SS 는 어느 층이든 받음
- 품번(도번 SD · 면적번 SA · 재료번 SM) 새 일련 = 붙임표 + 6자리(SD-000001) — 옛 8자리(제목행 자리) · 12장 옛 6자리(SD010101)와 모양이 달라 안 겹침 · 옛 번호는 그대로 유효 · 새 모양끼리 셈 · 지운 번호 안 씀
- 시험 test_m02_item_keys 7(층마다 키 모양 · 안 바뀜 · 다시 안 씀 · 복사 본뜬 곳 · 옛 SS 옮김 · 옛 키로 찾음 · 겹친 숫자 · 참조 키 모양 · 120 형제 깊이 6 번호 · 층마다 번호 따로 · 키대장 안 겹침) · 번호 · 키 시험 기대값을 새 모양으로

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KmELpdXFDRZHhfbGuRhSmL
2026-09-30 09:04:39 +09:00

162 lines
7.0 KiB
Python

"""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_Inputs as inputs_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}` · 경로 = 폴더 이름 배열 · 이름 = 경로 이음(26-1 · 종류 ·
분류1~3 은 경로 앞 넷) · `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 = r.get("이름")
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 = 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 # 없는 파일은 새 층에서 빈 문서로 섬
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)
inputs_store.copy_between(source, target, col_id, new_id) # 항목 연결도 같이(판 1)
return {"key": ss["키"], "열": new_id, "이름": ss["이름"]}