Files
Aislo/M02_MasterTemplete/M02_MasterTemplete_Book.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

148 lines
7.1 KiB
Python

"""M02 구조물집계표조합(SS 장부) — 구조물 열마다 줄 하나 · 경로 옮겨 받기(PLAN 26-2a · 26-4a).
제어용 장부 · 관리자는 집계표 화면(또는 나무)으로 고침 · 서버가 집계표 [저장] 때 맞춤 ·
키(층마다 머리가 다름 · 폴더 · 종류 뜻 없는 일련번호 6자리) —
마스터 `SS000001` · 회사 `SC{회사번호}-000001` · 개인 `SP{사용자번호}-000001`
한 번 준 키는 그 열에 남음 · 지운 열의 키는 다시 안 씀(`다음` 이 기억) · 층 사이 복사 ·
개인에 저장 · 프로젝트에서 새 항목은 그 층의 새 키(본뜬 곳은 열 `본뜬`). 회사 · 개인 층에 남은
옛 `SS` 키는 맞출 때 새 모양으로 옮기고(숫자 여섯 그대로 · 겹치면 다음 번호) 옛 키는 줄 `옛키` 에
남김 — 프로젝트 복사본이 들고 있는 옛 키로도 찾음(`find`).
줄 = `{키, 열, 경로, 이름, 종류, 분류1~3(경로 앞 넷 — 옛 코드가 읽음), 단위, 일위대가, 자재조합,
도번, 면적번, 재료번, 바인딩, 펼침, 옛키?}`. `M02_MasterTemplete_Store.py` 가 커서 떼어 냄 —
저장소는 `store.sync_combos` · `store.read_combos` · `store.find_combo` 이름으로 이 파일을 부름.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
from common_util.common_util_json import atomic_write_json
from M02_MasterTemplete import M02_MasterTemplete_Store as store
from M02_MasterTemplete import M02_MasterTemplete_Tree as tree
from M02_MasterTemplete import M02_Template_Layers as layers
HEAD = "SS" # 마스터(과 회사 · 개인 · 프로젝트 층 밖 모든 층)
_OLD = re.compile(r"^SS\d{6}$")
NOTE = (
"구조물집계표 구조물 열(꼴=수)마다 한 줄 · 서버가 집계표 [저장] 때 맞춤(손으로 고치지 않음) · "
"키(마스터 SS · 회사 SC{회사}- · 개인 SP{사용자}- + 6자리)는 한 번 주면 안 바뀌고 "
"지운 열의 키는 다시 안 씀 · 옛키 = 옮기기 전 키 · 경로 = 폴더 이름 배열"
)
def key_head(root: Path) -> str:
"""층 폴더 → 키 머리 — 회사 `SC{회사}-` · 개인 `SP{사용자}-` ·
그 밖(마스터 · 프로젝트 사본) `SS`."""
try:
parts = Path(root).resolve().relative_to(layers.storage_root()).parts
except ValueError:
return HEAD
if len(parts) == 2 and parts[1] == layers.TEMPLATES_DIRNAME:
return f"SC{parts[0]}-"
if len(parts) == 3 and parts[2] == layers.TEMPLATES_DIRNAME:
return f"SP{parts[1]}-"
return HEAD
def _number(head: str, key: str) -> int | None:
"""그 머리의 키면 여섯 자리 수 · 아니면 None."""
found = re.fullmatch(re.escape(head) + r"(\d{6})", str(key))
return int(found.group(1)) if found else None
def find(rows: list[dict[str, Any]], key: str) -> dict[str, Any] | None:
"""키로 줄 — 지금 키 또는 옮기기 전 옛 키(프로젝트 복사본 · 구조물 기록이 들고 있는 것)."""
key = str(key)
for row in rows:
if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])):
return row
return None
def _row(
key: str, col: dict[str, Any], numbers: dict[str, str], olds: list[str] | None = None
) -> dict[str, Any]:
path = tree.path_of(col)
extra = {"옛키": olds} if olds else {}
return {
"키": key,
"열": col["id"],
"경로": path,
"이름": tree.name_of(path),
**tree.old_fields(path),
"단위": col.get("단위"),
"일위대가": col.get("일위대가"),
"자재조합": col.get("자재조합"),
**{field: numbers.get(field, "") for _, field, _, _ in store.PARTS},
"바인딩": col.get("바인딩"),
"펼침": col.get("펼침"),
**extra,
}
def sync() -> dict[str, Any]:
"""장부를 집계표에 맞춤 — 새 열 = 그 층 새 키 · 지운 열 = 줄 지움 · 고친 열 = 칸 갱신 ·
옛 모양 키(회사 · 개인 층의 SS)는 새 모양으로(옛 키는 `옛키`) · 줄 차례 = 집계표 열 차례.
바뀐 것이 없으면 파일을 안 씀."""
path = store.root() / store.COMBO_FILE
head = key_head(store.root())
with store._LOCK:
old = json.loads(path.read_bytes()) if path.is_file() else {}
before = {row["열"]: row for row in old.get("줄", []) if isinstance(row, dict)}
taken = {n for row in before.values() if (n := _number(head, row.get("키"))) is not None}
after = max(old.get("다음", 1), max(taken, default=0) + 1)
numbers = store.structure_numbers()
rows = []
for col in store._structure_columns():
was = before.get(col["id"]) or {}
key, olds = was.get("키"), list(was.get("옛키") or [])
if key is not None and _number(head, key) is None: # 옛 모양 — 숫자 여섯 그대로 옮김
olds.append(key)
digits = int(key[2:]) if _OLD.match(str(key)) else None
key = f"{head}{digits:06d}" if digits and digits not in taken else None
if key:
taken.add(digits)
if key is None:
key = f"{head}{after:06d}"
taken.add(after)
after += 1
rows.append(_row(key, col, numbers.get(col["id"], {}), olds))
doc = {"그룹": "구조물집계표조합", "판": 1, "비고": NOTE, "다음": after}
if old.get("지운번호"):
doc["지운번호"] = old["지운번호"] # 지운 열의 번호 — 다시 안 줌
doc["줄"] = rows
if doc != old:
atomic_write_json(path, doc)
return doc
def read() -> dict[str, Any]:
"""장부 — 파일이 아직 없으면 지금 열 전부로 처음 한 번 만듦 · 회사 · 개인 층의 옛 SS 키는 새
모양으로 옮김 · `경로` 가 없던 옛 줄은 읽을 때 종류 · 분류1~3 에서 채움(빈 분류 건너뜀)."""
path = store.root() / store.COMBO_FILE
doc = json.loads(path.read_bytes()) if path.is_file() else sync()
head = key_head(store.root())
if head != HEAD and any(_number(head, r.get("키")) is None for r in doc.get("줄") or []):
doc = sync() # 회사 · 개인 층의 옛 SS 키 — 처음 읽을 때 한 번 새 모양으로(옛키 남김)
for row in doc.get("줄") or []:
if "경로" not in row:
row["경로"] = tree.row_path(row)
row["이름"] = tree.name_of(row["경로"])
return doc
def migrate_paths() -> bool:
"""옛 집계표 · 장부 → 경로 모양(열 `경로` · 머리 줄 수 · 장부 `경로` · `이름`) — 한 번 · 두 번
돌려도 같음. 바뀐 것이 없으면 파일을 안 씀 · 바꿨으면 참. 지금 층(`store.at`) 기준."""
got = store.read("table", store.STRUCTURE_TABLE)
if tree.normalize_table(got["문서"]) != got["문서"]:
store.write("table", store.STRUCTURE_TABLE, got["판"], got["문서"])
return True
path = store.root() / store.COMBO_FILE
before = path.read_bytes() if path.is_file() else b""
sync()
return (path.read_bytes() if path.is_file() else b"") != before