"""조합 갈라 내기 — 구조물 열 하나가 가리키는 조합을 고친 내용으로 새로 만들어 그 열만 새 키를 가리키게 함(PLAN 15장 · 검토 `14_조합편집검토.md` 1장). · 일위대가 조합(UA · UC · UP) — 새 줄은 지금 층 조합 저장소가 만듦(`Combo_Store` · PLAN 27-2c: 마스터 층 = UA · 회사 층 = UC · 개인 층 = UP · 고르기는 세 층 중 이 층이 쓸 수 있는 것) · 자재 조합(UM · 마스터 템플릿 `자재조합.json`) — M02 자재 조합 저장소가 만듦(표 「다음」 번호) 조합은 여러 열 · 모든 프로젝트가 같이 씀 — 제자리 고치지 않음. 담은 것(차례 · 메모까지)이 이미 있는 조합과 같으면 새로 안 만들고 그 키(원래 키 먼저) · 새 줄의 본뜬키 = 원래 키 · 소유 현장. """ from __future__ import annotations from typing import Any, Callable from M02_MasterTemplete import M02_MasterTemplete_Combo_Store as combos from M02_MasterTemplete import M02_MasterTemplete_Matset as matset from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete import M02_MasterTemplete_Tree as tree _OWNER = "현장" def _free_name(name: str, rows: list[dict]) -> str: taken = {str(r.get("이름") or "") for r in rows} n, out = 2, name while out in taken: out, n = f"{name} ({n})", n + 1 return out def _branch( column: str, version: str, field: str, rows: list[dict], slot: str, ident: tuple[str, ...], items: list[dict], source: str, make: Callable[[str, list[dict]], dict], name: str, ) -> dict[str, Any]: """집계표 판 검사 → 같은 내용 찾기 · 없으면 `make(이름, 줄들)` 로 새 줄 → 집계표 그 열 `field`. `slot` = 담은 칸(담은로직 · 담은자재) · `ident` = 담은 줄에서 견주는 칸(로직 · 메모 / 코드).""" table = _table(column, version) if not isinstance(items, list) or not items: raise store.StoreError(400, "담은 것이 비었음") def body(got) -> tuple: # 차례대로 · 빈 메모 = null return tuple( tuple(str(x.get(k) or "") for k in ident) for x in got or [] if isinstance(x, dict) ) same = [r for r in rows if body(r.get(slot)) == body(items)] hit = next((r for r in same if source and r.get("키") == source), same[0] if same else None) if hit: key, made_name, made = str(hit["키"]), str(hit.get("이름") or ""), False else: made_name = _free_name(name, rows) key, made = make(made_name, rows), True # ponytail: 조합 쓰기와 집계표 쓰기는 한 묶음 아님 — 사이에 집계표가 바뀌면 409 뒤 주인 없는 # 조합 하나가 남음(다시 하면 같은 내용이라 그 키를 씀) 판 = _point(table, column, field, key) return {"키": key, "이름": made_name, "새로만듦": made, "집계표판": 판} def _table(column: str, version: str) -> dict: """집계표 판 검사 · 구조물 열 있는지 — 읽은 표(`{판, 문서}`).""" table = store.read("table", store.STRUCTURE_TABLE) if version and table["판"] != version: raise store.StoreError(409, {"stale": [store.STRUCTURE_TABLE], "판": table["판"]}) if not any(c["id"] == column for c in store._structure_columns()): raise store.StoreError(404, f"{store.STRUCTURE_TABLE}에 없는 구조물 열 「{column}」") return table def _point(table: dict, column: str, field: str, key: str) -> str: """집계표 그 열 `field` = `key` 로 저장 — 새 판.""" doc = table["문서"] for c in doc.get("열", []): if isinstance(c, dict) and c.get("id") == column: c[field] = key return store.write("table", store.STRUCTURE_TABLE, table["판"], doc)["판"] # SS 도 여기서 맞춤 def use_combo(column: str, field: str, key: str, version: str = "") -> dict[str, Any]: """있는 조합을 이 열에 씀 — `field` 일위대가(UA) · 자재조합(UM) · 조합은 그대로(고르기만).""" if field == "일위대가": name = _combo(key).get("이름") elif field == "자재조합": name = material_combo(key).get("이름") else: raise store.StoreError(400, f"모르는 조합 종류 「{field}」") 판 = _point(_table(column, version), column, field, key) return {"키": key, "이름": str(name or ""), "새로만듦": False, "집계표판": 판} def _combo(key: str) -> dict[str, Any]: """이 층이 쓸 수 있는 조합(마스터 · 같은 회사 · 내 것) — 없으면 404 · 남의 층 것은 403.""" row = combos.find(key) if row is None: raise store.StoreError(404, f"없는 조합 「{key}」") if not key.startswith(combos.heads()): raise store.StoreError( 403, f"이 층에서 못 쓰는 조합 「{key}」 — 마스터 · 같은 회사 · 내 것만" ) return row def branch_combo( column: str, source: str, items: list[dict], name: str, version: str = "" ) -> dict[str, Any]: """일위대가 조합 갈라 내기 — 구분 · 상세구분 · 단위 · 출처 = 본뜬 것 · 새 줄은 지금 층에.""" was = _combo(source) items = [ {"로직": str(x.get("로직") or ""), "메모": x.get("메모")} for x in items or [] if isinstance(x, dict) ] def make(made_name: str, _rows) -> str: row = { "이름": made_name, **{k: was.get(k) for k in ("구분", "상세구분", "단위")}, "담은로직": items, "본뜬키": source, "출처": was.get("출처"), } return str(combos.create({**row, "소유": _OWNER})["키"]) name = name.strip() or str(was.get("이름") or source) rows = combos.rows() return _branch( column, version, "일위대가", rows, "담은로직", ("로직", "메모"), items, source, make, name ) def material_combo(key: str) -> dict[str, Any]: return matset.one(key) def branch_material_combo( column: str, source: str, items: list[dict], name: str, version: str = "" ) -> dict[str, Any]: """자재 조합 갈라 내기 — `source` 가 빈 글이면 이 열의 첫 자재 조합(본뜬키 null).""" was = material_combo(source) if source else {} items = [ {k: x.get(k) for k in ("코드", "이름", "규격", "단위")} for x in items or [] if isinstance(x, dict) ] def make(made_name: str, _rows) -> str: row = {"이름": made_name, "담은자재": items, "본뜬키": source or None, "소유": _OWNER} return matset.create(row)["키"] heads = next((c for c in store._structure_columns() if c["id"] == column), {}) default = tree.name_of(tree.path_of(heads)) if heads else column # 경로 이음(26-1) name = name.strip() or str(was.get("이름") or default or column) rows = matset.listing()["줄"] return _branch( column, version, "자재조합", rows, "담은자재", ("코드",), items, source, make, name )