Files
Aislo/M02_MasterTemplete/M02_Item_Source.py
T
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

151 lines
6.4 KiB
Python

"""집계 항목 원본 — 층(시스템 · 회사 · 개인)에서 항목 내용을 모음 (PLAN 21-1 · 25-1b · 25-2b).
모으는 것 = `{파일 이름: 내용}`:
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA · `재료.json` SM ·
`자재조합.json` UM 줄 · `일위대가.json` UA 줄 + 담은 로직 줄(M01 시스템) ·
`입력연결.json` 층의 `inputs/<열id>.json`(없으면 빈 연결) ·
`단가.json` 계산에 닿는 마스터 줄 전부
`원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎.
프로젝트가 고치는 값(`입력값.json` · `고른자재.json`)은 원본이 아니라 여기서 안 모음.
원본 층 자리 = `M02_Template_Layers` 경로 · 일위대가 · 단가는 M01 시스템 것.
"""
from __future__ import annotations
import hashlib
import json
from pathlib import Path
from typing import Any
from M02_MasterTemplete import M02_Template_Layers as layers
TABLE_NAME = "구조물집계표"
UM_FILE = "자재조합.json"
UA_FILE = "일위대가조합.json" # M01 시스템(`resources/master_data/`)
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출 · 재료산출)
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"), ("material", "재료.json"))
LINK_FILE = "입력연결.json"
PRICE_FILE = "단가.json"
LABOR_FILE = "일위대가.json"
def layer_root(item: dict[str, Any]) -> Path:
"""항목 원본 층 뿌리 — 시스템 · 회사 · 개인."""
if item["layer"] == "system":
return layers.system_dir()
if item["layer"] == "company":
return layers.company_dir(item["owner"])
company, user = str(item["owner"]).split("/")
return layers.personal_dir(company, user)
def read_json(path: Path) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except (OSError, ValueError):
return None
def row_of(document: Any, key: str) -> dict[str, Any] | None:
rows = document.get("줄") if isinstance(document, dict) else None
for row in rows or []:
# 옛 키로도 — 회사 · 개인 층 SS 키는 새 모양(SC · SP)으로 옮기며 `옛키` 에 남김(PLAN 26-4a)
if isinstance(row, dict) and (row.get("키") == key or key in (row.get("옛키") or [])):
return row
return None
def row_path(row: dict[str, Any]) -> list[str]:
"""SS 줄의 경로(26-1) — `경로` 칸이 있으면 그것 · 옛 줄은 종류 · 분류1~3(빈 분류는 건너뜀)."""
if isinstance(row.get("경로"), list):
return [str(p) for p in row["경로"] if str(p or "").strip()]
return [str(row[k]) for k in ("종류", "분류1", "분류2", "분류3") if row.get(k)]
def item_path(item: dict[str, Any]) -> list[str]:
"""층 SS 줄의 경로 — 없으면 LookupError."""
row = row_of(read_json(layer_root(item) / layers.COMBO_FILE), str(item.get("key")))
if row is None:
raise LookupError(f"집계 항목 {item.get('key')} 이 {item.get('layer')} 층에 없음")
return row_path(row) or [str(row["키"])]
def item_name(item: dict[str, Any]) -> str:
"""층 SS 줄의 보이는 이름(경로를 이은 글) — 없으면 LookupError."""
return " ".join(item_path(item))
def version_of(content: Any) -> str:
raw = json.dumps(content, ensure_ascii=False, sort_keys=True).encode("utf-8")
return hashlib.sha256(raw).hexdigest()[:16]
def labor_rows(ua_key: str) -> dict[str, Any] | None:
"""UA 줄 + 담은 로직 줄(M01 시스템 · 없는 로직은 빼고 키만 `빠진로직`)."""
from M01_MasterData import M01_MasterData_Store as m01
ua = row_of(read_json(Path(m01.FOLDER) / UA_FILE), ua_key)
if ua is None:
return None
wanted = [str(entry.get("로직")) for entry in ua.get("담은로직") or [] if entry.get("로직")]
found: dict[str, dict[str, Any]] = {}
raw: dict[str, Any] = {} # M01 적재본은 수가 Decimal — 줄은 원본 파일 JSON 그대로 담음
for key in wanted:
try:
file = m01.logic(key)["file"]
except m01.StoreError:
continue
if file not in raw:
raw[file] = read_json(Path(m01.FOLDER) / file)
row = row_of(raw[file], key)
if row is not None:
found[key] = row
return {
"줄": ua,
"로직": [found[key] for key in wanted if key in found],
"빠진로직": [key for key in wanted if key not in found],
}
def input_links(root: Path, column_id: str) -> dict[str, Any]:
"""층의 항목 연결 `inputs/<열id>.json`(sub4 `_Inputs.read_file`) · 없으면 빈 연결."""
from M02_MasterTemplete import M02_MasterTemplete_Inputs as inputs
from M02_MasterTemplete import M02_MasterTemplete_Store as store
if not column_id:
return {"판": None, "줄": []}
with store.at(root):
found = inputs.read_file(column_id)
if isinstance(found, dict) and isinstance(found.get("줄"), list):
return found
return {"판": None, "줄": []}
def build_source(item: dict[str, Any]) -> dict[str, Any]:
"""원본 층 · 마스터에서 항목 내용을 모음 — SS 줄이 없으면 LookupError."""
from M02_MasterTemplete import M02_Item_Prices as prices
root = layer_root(item)
row = row_of(read_json(root / layers.COMBO_FILE), item["key"])
if row is None:
raise LookupError(f"집계 항목 {item['key']} 이 {item['layer']} 층에 없음")
column_id = str(row.get("열") or "")
table = read_json(root / "table" / f"{TABLE_NAME}.json") or {}
column = next((c for c in table.get("열") or [] if c.get("id") == column_id), None)
files: dict[str, Any] = {"집계.json": {"줄": row, "열": column}}
for kind, name in PART_FILES:
part = read_json(root / kind / f"{column_id}.json") if column_id else None
if part is not None:
files[name] = part
if row.get("자재조합"):
um = row_of(read_json(root / UM_FILE), str(row["자재조합"]))
if um is not None:
files[UM_FILE] = um
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
if labor is not None:
files[LABOR_FILE] = labor
files[LINK_FILE] = input_links(root, column_id)
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys)) if logic_keys else {}
return files