"""면적산출 범위 → 도면 표(PLAN 41-1) — 변환은 Node 번들 한 벌(`common_util_spreadsheet_node.ts` `areaTable` · 화면 표시 글과 같은 코드) · 여기는 부르기 · 도면 표 맞춤만(파이썬 이식 없음). 도면 문서(`structure/<열>.json`) 맨 위 `연결표` = `[{연결, 시트, 범위, 글자크기, 값}]` — 도면 표 `shapeData.style.연결` 이 `연결` 과 같은 표를 면적 [저장] 때 칸 글로 다시 적음(자리 · 크기 그대로 · 칸 수가 바뀌면 열 너비 · 행 높이도 새로) · `값` = 마지막 칸 글(`{시트id!A1: 글}` · 다름 알림용). 도면에 그 표가 없으면 기록은 그대로 둠. 층 = `store.at(층 폴더)` · 권한은 부르는 쪽. 여러 장(82-3) — 연결표 줄 `장` = 표가 놓인 장id(1장 = 빈 값) · 맞춤은 모든 장(안 열린 장도 서버가). """ from __future__ import annotations import json import logging from typing import Any from common_util import common_util_spreadsheet as sheet_node from common_util.common_util_json import atomic_write_json from M02_MasterTemplete import M02_MasterTemplete_Store as store from M02_MasterTemplete.M02_Template_Layers import drawing_sheets logger = logging.getLogger(__name__) LINKS = "연결표" DROPPED = "지운연결" CANCELS = "보내기취소" # 면적 [저장] 문서의 임시 칸 — 저장 안 함(112-2) # spreadsheet_area_table.ts `NAME_GONE` 과 같은 글자 — 이름이 지워졌을 때 까닭 NAME_GONE = "이름없음" def to_table( column: str, sheet: str, rng: str, font: float | None = None, name: str | None = None ) -> dict[str, Any]: """그 열 면적산출(저장본) 시트 · 범위(또는 `name` 이 있으면 그 이름이 지금 가리키는 범위) → `{표, 시트, 범위, 값}` — 저장 안 함 · 못 바꾸면 400(이름이 지워졌으면 「이름없음」).""" book = store.read("area", column)["문서"] try: got = sheet_node.area_tables( [{"book": book, "시트": sheet, "범위": rng, "이름": name, "글자크기": font}] ) except sheet_node.RecalcError as e: raise store.StoreError(503, str(e)) from e if not got[0].get("ok"): raise store.StoreError(400, got[0].get("까닭")) return {k: got[0][k] for k in ("표", "시트", "범위", "값")} def fresh_names(path: Any, doc: dict[str, Any]) -> list[dict[str, Any]]: """도면 [저장] 문서의 새 연결 줄(저장돼 있던 파일에 없던 `연결`) 중 `이름`이 있는 것 — 면적 문서에 둘 이름(112-2).""" old: set[Any] = set() if path.is_file(): old = { x.get("연결") for x in json.loads(path.read_bytes()).get(LINKS) or [] if isinstance(x, dict) } rows = [x for x in doc.get(LINKS) or [] if isinstance(x, dict)] return [x for x in rows if x.get("이름") and x.get("연결") not in old] def define_names(column: str, rows: list[dict[str, Any]]) -> None: """도면 [저장] 요청 안에서(112-2) 새 연결 줄의 보낸 범위 이름을 면적 시트에 더함/갱신(104-1) — `applyLinked`(행열 넣기 · 지우기)가 이 이름을 따라가게 함. 면적 문서가 없거나 못 풀면 건너뜀 (이름 없는 연결은 적힌 범위로 감).""" path = store._path("area", column) if not rows or not path.is_file(): return book = json.loads(path.read_bytes()) try: for row in rows: ask = { "book": book, "시트": row.get("시트"), "범위": row.get("범위"), "이름": row["이름"], } got = sheet_node.define_names([ask])[0] if got.get("ok"): book = got["book"] else: logger.warning( "보낸 범위 이름 건너뜀 %s %s — %s", column, row["이름"], got.get("까닭") ) except sheet_node.RecalcError as e: logger.warning("보낸 범위 이름 건너뜀 %s — %s", column, e) return with store._LOCK: atomic_write_json(path, book) # 디스크 글 자체가 책(`store.read` 의 "문서") def apply_cancels(column: str, doc: Any) -> tuple[Any, list[str]]: """면적 [저장] 문서의 `보내기취소[]`(112-2 · 이름표 메뉴 [보내기 취소])를 떼어 냄 — 그 연결 줄의 `이름`을 문서의 이름정의에서 뺌 → (문서, 연결 목록). 줄을 빼는 일은 `refresh_drawing(unlink=…)`.""" if not isinstance(doc, dict) or CANCELS not in doc: return doc, [] ids = [x for x in doc[CANCELS] or [] if isinstance(x, str)] doc = {k: v for k, v in doc.items() if k != CANCELS} path = store._path("structure", column) rows = json.loads(path.read_bytes()).get(LINKS) or [] if ids and path.is_file() else [] names = {x.get("이름") for x in rows if isinstance(x, dict) and x.get("연결") in ids} names.discard(None) if names: doc["시트"] = [ {**s, "이름정의": [n for n in s["이름정의"] if n.get("이름") not in names]} if isinstance(s, dict) and s.get("이름정의") else s for s in doc.get("시트") or [] ] return doc, ids def _style_link(entity: Any) -> Any: if not isinstance(entity, dict) or entity.get("type") != "Table": return None return ((entity.get("shapeData") or {}).get("style") or {}).get("연결") def refresh_drawing(column: str, unlink: Any = ()) -> list[str]: """지금 층 그 열 도면(모든 장)의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록. `unlink` = 면적 [저장] 때 [보내기 취소]한 연결 — 그 줄을 연결표에서 뺌 (도면 표는 남음 · 알림 없음 · 112-2). 이름(104-1)이 있으면 그 이름이 지금 가리키는 범위로 `범위` 도 갱신 — 이름이 지워졌으면(범위가 통째로 지워짐) 그 연결을 끊고(도면 표는 남음) `지운연결` 목록에 적어 다음에 열 때 알림.""" try: drawing, area = store._path("structure", column), store._path("area", column) except store.StoreError: return [] if not drawing.is_file() or not area.is_file(): return [] doc = json.loads(drawing.read_bytes()) every = [x for x in doc.get(LINKS) or [] if isinstance(x, dict) and x.get("연결")] links = [x for x in every if x["연결"] not in unlink] if not links: if len(links) != len(every): # 취소로 줄이 다 빠짐 doc[LINKS] = links with store._LOCK: atomic_write_json(drawing, doc) return [] book = json.loads(area.read_bytes()) asks = [ { "book": book, "시트": x.get("시트"), "범위": x.get("범위"), "이름": x.get("이름"), "글자크기": x.get("글자크기"), } for x in links ] try: results = sheet_node.area_tables(asks) except sheet_node.RecalcError as e: logger.warning("도면 표 맞춤 건너뜀 %s — %s", column, e) return [] entities = [e for _, sheet in drawing_sheets(doc) for e in sheet.get("entities") or []] done: list[str] = [] kept: list[dict[str, Any]] = [] dropped: list[str] = [] for link, got in zip(links, results): if not got.get("ok"): if link.get("이름") and got.get("까닭") == NAME_GONE: dropped.append(link["연결"]) # 이름이 통째로 지워짐 — 연결만 끊음(도면 표는 남김) else: kept.append(link) # 그 밖 실패(시트 지움 등)는 다음 저장 때 다시 시도 continue kept.append(link) link["범위"] = got["범위"] # 이름이 지금 가리키는 범위로(104-1) link["값"] = got["값"] new = got["표"]["shapeData"] for entity in entities: if _style_link(entity) != link["연결"]: continue shape = entity["shapeData"] if (len(shape.get("rowHeights") or []), len(shape.get("columnWidths") or [])) != ( len(new["rowHeights"]), len(new["columnWidths"]), ): shape["rowHeights"], shape["columnWidths"] = new["rowHeights"], new["columnWidths"] shape["cells"] = new["cells"] done.append(link["연결"]) if dropped: doc[LINKS] = kept doc[DROPPED] = sorted(set(doc.get(DROPPED) or []) | set(dropped)) elif len(kept) != len(every): doc[LINKS] = kept with store._LOCK: atomic_write_json(drawing, doc) return done