Files
Aislo/M02_MasterTemplete/M02_Labor_Spec.py
T
eomsangdonandClaude Opus 5.5 3028801cd8 feat(일위대가 규격 열): 문서 「규격」 칸 검사 · settle 열 끼우기 짝 · 열마다 반영합 (PLAN 704-4)
- M02_Labor_Spec 새 파일 — 규격 칸 정리 · 열 끼우기 · 사본 굳히기 · 쓰는 사본 찾기
- settle(파이썬 · TS) 에 col — 그 열 수량이 있는 줄 = 손 자리 · 규격 없음 = 그대로
- 계산값 규격합 = 열마다 반영합(고르기 창 열 단추)
- 거울 시험 — 규격 열 · 모르는 열 = 기본 · 꼴 틀림 400 · 열 없는 옛 문서

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:10:04 +09:00

164 lines
6.4 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 = at(doc, use)
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