- 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>
185 lines
7.6 KiB
Python
185 lines
7.6 KiB
Python
"""일위대가 규격 열(PLAN 704-4) — 문서 `규격` 칸 검사 · 열 끼우기 · 사본 굳히기 · 쓰는 사본 찾기.
|
|
|
|
규격 = `{이름: "H", 열: ["1.5", "2.0"], 기본: "2.0", 수량: {줄 id: {열: 수}}}` —
|
|
줄 id = 로직 키 · 참조 `ref_id`(`M02_Labor_Doc.line_id`) · 자재 코드.
|
|
규격 수량이 있는 줄 = 고른 열 값이 `{손}` 자리 · 없는 줄 · 빈 열 = 문서 `수량` 그대로(공통).
|
|
규격 없음 = 옛 문서(열 하나 · 셈 그대로 · 옮김 없음).
|
|
짝(CLAUDE.md 5장 ①): `at` ↔ `M02_MasterTemplete_Labor_Calc.ts` `atSpec` —
|
|
거울 시험 `resources/tester/test_m02_labor_calc_mirror.py`.
|
|
사본(`M02_Item_Source.build_source`) = 설계가 고른 열(summary_item `spec`)을 `{손}` 으로 굳히고
|
|
`규격` 은 그 열 하나만 남김(`freeze`).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal, InvalidOperation
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from M02_MasterTemplete import M02_Labor_Ref as refs
|
|
|
|
NAME_MAX = 20 # 규격 이름 · 열 이름 글자 수 상한 — 화면 칸과 같음
|
|
COLS_MAX = 20
|
|
|
|
|
|
class SpecError(ValueError):
|
|
"""규격 칸 꼴이 틀림 — 부르는 쪽이 400 으로."""
|
|
|
|
|
|
def _num(raw: Any) -> int | float | None:
|
|
if raw is None or isinstance(raw, bool):
|
|
return None
|
|
try:
|
|
got = Decimal(str(raw).replace(",", "").strip())
|
|
except InvalidOperation:
|
|
return None
|
|
if not got.is_finite():
|
|
return None
|
|
return int(got) if got == got.to_integral_value() else float(got)
|
|
|
|
|
|
def _text(raw: Any, what: str) -> str:
|
|
text = str(raw).strip() if isinstance(raw, (str, int, float)) and raw is not True else ""
|
|
if not text or len(text) > NAME_MAX:
|
|
raise SpecError(f"규격 {what}은 1~{NAME_MAX}자 글")
|
|
return text
|
|
|
|
|
|
def line_key(one: dict) -> Any:
|
|
"""줄 id — 참조 줄 = `ref_id` · 로직 줄 = 로직 키 · 자재 줄 = 코드."""
|
|
if isinstance(one.get("참조"), dict):
|
|
return refs.ref_id(one["참조"])
|
|
return one.get("로직") or one.get("코드")
|
|
|
|
|
|
def clean(raw: Any, ids: set[Any]) -> dict[str, Any] | None:
|
|
"""받은 `규격` → 저장 꼴 — 열이 없으면 None(규격 없음) · 이름 · 열 이름이 비거나 겹치면 ·
|
|
수량이 수 아니면 SpecError · 없는 줄 · 없는 열 수량은 버림(지운 줄)."""
|
|
if raw is None:
|
|
return None
|
|
if not isinstance(raw, dict):
|
|
raise SpecError("규격은 묶음")
|
|
cols: list[str] = []
|
|
for one in raw.get("열") or []:
|
|
col = _text(one, "열 이름")
|
|
if col in cols:
|
|
raise SpecError(f"같은 규격 열 두 번 「{col}」")
|
|
cols.append(col)
|
|
if not cols:
|
|
return None
|
|
if len(cols) > COLS_MAX:
|
|
raise SpecError(f"규격 열은 {COLS_MAX}개까지")
|
|
qty: dict[str, dict[str, int | float]] = {}
|
|
for lid, row in (raw.get("수량") or {}).items():
|
|
if lid not in ids or not isinstance(row, dict):
|
|
continue
|
|
kept = {}
|
|
for col, value in row.items():
|
|
if col not in cols or value in (None, ""):
|
|
continue
|
|
got = _num(value)
|
|
if got is None:
|
|
raise SpecError(f"규격 수량 「{lid} · {col}」 은 수")
|
|
kept[col] = got
|
|
if kept:
|
|
qty[str(lid)] = kept
|
|
return {
|
|
"이름": _text(raw.get("이름"), "이름"),
|
|
"열": cols,
|
|
"기본": raw.get("기본") if raw.get("기본") in cols else cols[0],
|
|
"수량": qty,
|
|
}
|
|
|
|
|
|
def column(doc: Any, col: Any = None) -> str | None:
|
|
"""쓸 열 — 고른 열이 규격에 있으면 그것 · 없으면 기본 · 규격 없음 = None."""
|
|
spec = doc.get("규격") if isinstance(doc, dict) else None
|
|
if not isinstance(spec, dict) or not spec.get("열"):
|
|
return None
|
|
return col if col in spec["열"] else spec.get("기본") or spec["열"][0]
|
|
|
|
|
|
def at(doc: dict, col: Any = None) -> dict:
|
|
"""열 끼우기 — 그 열 규격 수량이 있는 줄 · 자재의 `수량` 을 `{손: 값}` 으로
|
|
(규격 없음 = 그대로)."""
|
|
use = column(doc, col)
|
|
if use is None:
|
|
return doc
|
|
qty = doc["규격"].get("수량") or {}
|
|
|
|
def put(one: Any) -> Any:
|
|
if not isinstance(one, dict):
|
|
return one
|
|
value = (qty.get(line_key(one)) or {}).get(use)
|
|
return {**one, "수량": {"손": value}} if value is not None else one
|
|
|
|
return {
|
|
**doc,
|
|
"담은로직": [put(one) for one in doc.get("담은로직") or []],
|
|
"자재": [put(one) for one in doc.get("자재") or []],
|
|
}
|
|
|
|
|
|
def given_of(result: dict) -> dict[str, Any]:
|
|
"""셈 답에서 칸 값 되살림 `{알약: 수량}` — 열마다 다시 셀 때 면적 칸 값
|
|
(문서 · 층을 다시 안 읽음)."""
|
|
rows = [*(result.get("줄") or []), *(result.get("자재") or [])]
|
|
return {r["알약"]: r["수량"] for r in rows if r.get("수량출처") == "칸" and r.get("알약")}
|
|
|
|
|
|
def freeze(doc: dict, col: Any = None) -> dict:
|
|
"""사본 굳히기 — 고른 열(없거나 지운 열 = 기본)을 `{손}` 으로 끼우고 규격은 그 열 하나만."""
|
|
use = column(doc, col)
|
|
if use is None:
|
|
return doc
|
|
got = {k: v for k, v in at(doc, use).items() if k != "계산값"} # 기본 열 계산값은 버림
|
|
return {**got, "규격": {"이름": doc["규격"].get("이름"), "열": [use], "기본": use, "수량": {}}}
|
|
|
|
|
|
def users(layer_root: Path, keys: set[str], cols: set[str]) -> list[str]:
|
|
"""그 층 항목(`keys`) · 그 규격 열(`cols`)을 쓰는 프로젝트 사본 —
|
|
`프로젝트 폴더/사본 폴더` 목록.
|
|
|
|
ponytail: 저장소 프로젝트 사본 manifest 를 통째로 훑음 — 열 지우기 · 이름 바꾸기 [저장] 때만 ·
|
|
느리면 사본 색인을 둠."""
|
|
from M02_MasterTemplete import M02_Template_Layers as layers
|
|
from M02_MasterTemplete.M02_Item_Source import layer_root as root_of
|
|
from M02_MasterTemplete.M02_Item_Source import read_json
|
|
|
|
found = []
|
|
here = Path(layer_root).resolve()
|
|
for path in layers.storage_root().glob("*/*/*/templates/structures/*/manifest.json"):
|
|
origin = (read_json(path) or {}).get("원본")
|
|
if not isinstance(origin, dict) or origin.get("key") not in keys:
|
|
continue
|
|
if origin.get("spec") not in cols:
|
|
continue
|
|
try:
|
|
same = root_of(origin).resolve() == here
|
|
except (KeyError, ValueError, TypeError):
|
|
same = False
|
|
if same:
|
|
found.append(f"{path.parents[3].name}/{path.parent.name}")
|
|
return found
|
|
|
|
|
|
def guard(path: Path, new: Any, column: str) -> None:
|
|
"""[저장] 때 빠진 규격 열(지움 · 이름 바꿈)을 쓰는 프로젝트 사본이 있으면 409 —
|
|
지금 층(`store.root`) 그 열 항목(SS 키 · 옛 키) 기준."""
|
|
from M02_MasterTemplete import M02_MasterTemplete_Store as store
|
|
from M02_MasterTemplete.M02_Item_Source import read_json
|
|
|
|
old = (read_json(path) or {}).get("규격") if path.is_file() else None
|
|
gone = set((old or {}).get("열") or []) - set(((new or {}).get("규격") or {}).get("열") or [])
|
|
if not gone:
|
|
return
|
|
rows = [r for r in store.read_combos().get("줄", []) if str(r.get("열")) == column]
|
|
keys = {str(k) for r in rows for k in [r.get("키"), *(r.get("옛키") or [])] if k}
|
|
used = users(store.root(), keys, gone)
|
|
if used:
|
|
raise store.StoreError(
|
|
409,
|
|
f"규격 열 「{', '.join(sorted(gone))}」 을 쓰는 프로젝트 사본 {len(used)}개 — 지울 수 없음 "
|
|
f"({', '.join(used[:3])}{' …' if len(used) > 3 else ''})",
|
|
)
|