- 넣을 값 카드(M02_Labor_Values)는 담은 로직 입력 전부를 보임(자리 「선택」 감싸기 없앰) · 이름 「넣을 값」 - 고르기 모달 면적산출 알약 = 복사본 일위대가 알약(가져온 때 그대로) - 입력값.json · 고른자재.json · /calc 는 그대로 · storage 층 inputs/ 파일은 안 건드림(읽기만 끊음) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
150 lines
6.5 KiB
Python
150 lines
6.5 KiB
Python
"""집계 항목 원본 — 층(시스템 · 회사 · 개인)에서 항목 내용을 모음 (PLAN 21-1 · 25-1b · 25-2b).
|
|
|
|
모으는 것 = `{파일 이름: 내용}`:
|
|
`집계.json` SS 줄 + 열 정의 · `도면.json` SD · `면적.json` SA ·
|
|
`일위대가.json` = 층 `labor/<열id>.json` 문서(39-1c · 없으면 옛 UA 조합 줄)
|
|
+ 담은 로직 줄(M01 시스템) + 알약(가져온 때 그대로) ·
|
|
`단가.json` 계산에 닿는 마스터 줄 전부(담은 로직 + 일위대가 자재 MT)
|
|
`원본판` = 모은 내용의 해시 — 지금 층 · 마스터에서 다시 모은 판과 대어 「마스터가 바뀜」 을 앎.
|
|
프로젝트가 고치는 값(`입력값.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 = "구조물집계표"
|
|
#: 열 id → 스냅샷 파일(구조물 도면 · 면적산출)
|
|
PART_FILES = (("structure", "도면.json"), ("area", "면적.json"))
|
|
LABOR_KIND = "labor" # 열 id → 일위대가 문서(39-1c) — 스냅샷은 `LABOR_FILE` 에 감싸서
|
|
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 · 회사 UC · 개인 UP) + 담은 로직 줄 — labor 문서가 없는 열
|
|
(옮기기 전)."""
|
|
from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos
|
|
|
|
ua = combos.find(ua_key)
|
|
return None if ua is None else logic_rows(ua)
|
|
|
|
|
|
def labor_doc_rows(doc: dict[str, Any]) -> dict[str, Any]:
|
|
"""일위대가 문서(39-1a) + 담은 로직 줄 + 알약(`M02_Labor_Doc.pills` · 가져온 때 그대로 —
|
|
복사본 계산이 마스터를 안 읽게)."""
|
|
from M02_MasterTemplete import M02_Labor_Doc as labor_doc
|
|
|
|
return {**logic_rows(doc), "알약": labor_doc.pills(doc)}
|
|
|
|
|
|
def logic_rows(row: dict[str, Any]) -> dict[str, Any]:
|
|
"""`{줄: row, 로직: 담은 로직 줄(M01 시스템), 빠진로직: 없는 로직 키}`."""
|
|
from M01_MasterData import M01_MasterData_Store as m01
|
|
|
|
wanted = [str(entry.get("로직")) for entry in row.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)
|
|
got = row_of(raw[file], key)
|
|
if got is not None:
|
|
found[key] = got
|
|
return {
|
|
"줄": row,
|
|
"로직": [found[key] for key in wanted if key in found],
|
|
"빠진로직": [key for key in wanted if key not in found],
|
|
}
|
|
|
|
|
|
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
|
|
doc = read_json(root / LABOR_KIND / f"{column_id}.json") if column_id else None
|
|
if isinstance(doc, dict):
|
|
labor = labor_doc_rows(doc)
|
|
else:
|
|
labor = labor_rows(str(row["일위대가"])) if row.get("일위대가") else None
|
|
if labor is not None:
|
|
files[LABOR_FILE] = labor
|
|
logic_keys = [str(one.get("키")) for one in (labor or {}).get("로직", [])]
|
|
materials = [str(m.get("코드")) for m in ((labor or {}).get("줄") or {}).get("자재") or []]
|
|
wanted = logic_keys + [code for code in materials if code]
|
|
files[PRICE_FILE] = prices.to_json(prices.collect(logic_keys, materials)) if wanted else {}
|
|
return files
|