- summary_item spec(파이썬 · TS 짝) — 없으면 기본 열 · 모르는 키 거절 그대로 - 사본 = 고른 열 수량을 손으로 굳힘 · manifest 규격 · same_item 에 spec(열 바꿈 = 새 사본 · 덮개 열쇠 S:폴더 그대로) - B07 구조물 도면 키 = 항목 + 열 해시 — 열이 다르면 따로 장 - M02 일위대가 [저장] — 쓰는 사본 있는 열 지움 · 이름 바꿈 409 - 항목 목록 labor.규격(열 · 열마다 합계) — 고르기 창 열 단추 - 시험 test_704_4_spec 7(열 바꿈 뒤 덮개 남음 · 짝 잃음 0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
116 lines
5.2 KiB
Python
116 lines
5.2 KiB
Python
"""구조물 집계표 항목 참조 `summary_item` — 모양 검사 · 스냅샷 폴더 이름 (PLAN 21-1).
|
|
|
|
summary_item = {layer: "system"|"company"|"personal",
|
|
owner: null|"{회사}"|"{회사}/{사용자}", key: "SS000004", name: "…",
|
|
spec?: "2.0"}
|
|
|
|
`spec`(PLAN 704-4) = 그 항목 일위대가 규격 열 이름 — 없으면 기본 열(옛 기록 · 규격 없는 항목).
|
|
|
|
항목 키(PLAN 26-4a) — 마스터 `SS000004` · 회사 `SC{회사}-000001` · 개인 `SP{사용자}-000001`
|
|
(머리의 번호 = owner 의 회사 · 사용자). 옛 `SS` 키는 어느 층이든 그대로 받음 — 회사 · 개인 층이 새
|
|
모양으로 옮기기 전에 적힌 기록(층 장부의 `옛키` 로 찾음).
|
|
|
|
구조물 기록(`structures.json` `StructureInstance`) · 관 지점 기록(`pipe_points.json` 점) 두 곳이
|
|
같은 칸을 씀 — 검사는 여기 한 벌. 없으면 null(옛 파일 호환).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
SUMMARY_LAYERS = ("system", "company", "personal")
|
|
_KEY = re.compile(r"^SS\d{6}$") # 마스터 · 옛 키
|
|
_LAYER_KEY = {
|
|
"company": re.compile(r"^SC(\d+)-\d{6}$"),
|
|
"personal": re.compile(r"^SP(\d+)-\d{6}$"),
|
|
}
|
|
_OWNER = {
|
|
"system": None,
|
|
"company": re.compile(r"^\d+$"),
|
|
"personal": re.compile(r"^\d+/\d+$"),
|
|
}
|
|
_NAME_MAX = 200
|
|
_SPEC_MAX = 20 # 규격 열 이름 상한(`M02_Labor_Spec.NAME_MAX`)
|
|
|
|
|
|
def parse_summary_item(value: Any) -> dict[str, Any] | None:
|
|
"""모양이 맞으면 `{layer, owner, key, name, spec?}` 새 dict · 빈 값은 None ·
|
|
틀리면 ValueError."""
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, dict):
|
|
raise ValueError("집계 항목(summary_item)은 객체여야 합니다.")
|
|
extra = set(value) - {"layer", "owner", "key", "name", "spec"}
|
|
if extra:
|
|
raise ValueError(f"집계 항목에 모르는 칸이 있습니다: {', '.join(sorted(extra))}")
|
|
layer = value.get("layer")
|
|
if layer not in SUMMARY_LAYERS:
|
|
raise ValueError("집계 항목 층은 system · company · personal 중 하나입니다.")
|
|
owner = value.get("owner")
|
|
rule = _OWNER[layer]
|
|
if rule is None:
|
|
if owner is not None:
|
|
raise ValueError("시스템 층 집계 항목의 owner 는 null 이어야 합니다.")
|
|
elif not isinstance(owner, str) or not rule.match(owner):
|
|
shape = "{회사}" if layer == "company" else "{회사}/{사용자}"
|
|
raise ValueError(f"{layer} 층 집계 항목의 owner 는 「{shape}」 숫자 모양입니다.")
|
|
key = value.get("key")
|
|
if not isinstance(key, str) or not _key_ok(layer, owner, key):
|
|
raise ValueError(
|
|
"집계 항목 key 는 마스터 SS000004 · 회사 SC{회사}-000001 · "
|
|
"개인 SP{사용자}-000001 모양입니다(옛 SS 키는 어느 층이든 받음)."
|
|
)
|
|
name = value.get("name")
|
|
if not isinstance(name, str) or not name.strip() or len(name) > _NAME_MAX:
|
|
raise ValueError(f"집계 항목 name 은 1~{_NAME_MAX}자 글입니다.")
|
|
spec = value.get("spec")
|
|
if spec is not None and (not isinstance(spec, str) or not 0 < len(spec.strip()) <= _SPEC_MAX):
|
|
raise ValueError(f"집계 항목 spec(규격 열)은 1~{_SPEC_MAX}자 글입니다.")
|
|
out = {"layer": layer, "owner": owner, "key": key, "name": name}
|
|
return {**out, "spec": spec.strip()} if spec is not None else out
|
|
|
|
|
|
#: 옵션 그룹 id(PLAN 46 · `M02_SO_Groups.GROUP_ID` 와 같은 규칙)
|
|
_GROUP_ID = re.compile(r"^g[0-9a-z]{1,16}$")
|
|
#: 그룹 복사본 이름 잇개 — `{구조물 id}__{그룹 id}`(구조물 id 는 hex · 관은 `pipe@측점`)
|
|
GROUP_JOIN = "__"
|
|
|
|
|
|
def parse_group_items(value: Any) -> dict[str, dict[str, Any]] | None:
|
|
"""그룹별 집계 항목 `{그룹 id: summary_item}`(PLAN 46-3) — 빈 값 · 빈 묶음은 None ·
|
|
null 항목은 뺌 · 그룹 id · 항목 모양이 틀리면 ValueError."""
|
|
if value is None:
|
|
return None
|
|
if not isinstance(value, dict):
|
|
raise ValueError("그룹 집계 항목(group_items)은 묶음이어야 합니다.")
|
|
out: dict[str, dict[str, Any]] = {}
|
|
for gid, item in value.items():
|
|
if not isinstance(gid, str) or not _GROUP_ID.match(gid):
|
|
raise ValueError(f"그룹 id 모양이 아닙니다: {gid}")
|
|
parsed = parse_summary_item(item)
|
|
if parsed:
|
|
out[gid] = parsed
|
|
return out or None
|
|
|
|
|
|
def _key_ok(layer: str, owner: Any, key: str) -> bool:
|
|
"""옛 · 마스터 SS 키는 어느 층이든 · SC · SP 는 그 층에서 머리 번호가 owner 와 같을 때만."""
|
|
if _KEY.match(key):
|
|
return True
|
|
rule = _LAYER_KEY.get(layer)
|
|
found = rule.match(key) if rule else None
|
|
if not found or not isinstance(owner, str):
|
|
return False
|
|
return found.group(1) == (owner if layer == "company" else owner.split("/")[-1])
|
|
|
|
|
|
def snapshot_folder_name(item: dict[str, Any]) -> str:
|
|
"""`{layer}_{owner 슬러그}_{key}` — owner 없음 = `none` · `/` = `-`.
|
|
|
|
예: `system_none_SS000004` · `company_3_SC3-000001` · `personal_3-12_SP12-000002`.
|
|
"""
|
|
owner = item.get("owner")
|
|
slug = str(owner).replace("/", "-") if owner else "none"
|
|
return f"{item['layer']}_{slug}_{item['key']}"
|