Files
Aislo/M02_MasterTemplete/M02_Labor_Spec.py
T
eomsangdonandClaude Opus 5.5 425a4a9dfb feat(규격 열 고르기): summary_item spec · 사본이 고른 열 굳힘 · 같은 항목 = 열까지 · B07 한 장 = 항목 + 열 · 쓰는 열 지움 409 (PLAN 704-4)
- 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>
2026-10-31 14:16:06 +09:00

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 ''})",
)