diff --git a/M02_MasterTemplete/M02_Area_Table.py b/M02_MasterTemplete/M02_Area_Table.py index 3dcea0a1c..9e2ab79ff 100644 --- a/M02_MasterTemplete/M02_Area_Table.py +++ b/M02_MasterTemplete/M02_Area_Table.py @@ -5,6 +5,7 @@ `shapeData.style.연결` 이 `연결` 과 같은 표를 면적 [저장] 때 칸 글로 다시 적음(자리 · 크기 그대로 · 칸 수가 바뀌면 열 너비 · 행 높이도 새로) · `값` = 마지막 칸 글(`{시트id!A1: 글}` · 다름 알림용). 도면에 그 표가 없으면 기록은 그대로 둠. 층 = `store.at(층 폴더)` · 권한은 부르는 쪽. +여러 장(82-3) — 연결표 줄 `장` = 표가 놓인 장id(1장 = 빈 값) · 맞춤은 모든 장(안 열린 장도 서버가). """ from __future__ import annotations @@ -16,6 +17,7 @@ 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 = "연결표" @@ -40,7 +42,7 @@ def _style_link(entity: Any) -> Any: def refresh_drawing(column: str) -> list[str]: - """지금 층 그 열 도면의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록.""" + """지금 층 그 열 도면(모든 장)의 연결표마다 칸 글 다시 · 마지막 값 갱신 → 칸을 간 표의 `연결` 목록.""" try: drawing, area = store._path("structure", column), store._path("area", column) except store.StoreError: @@ -61,7 +63,7 @@ def refresh_drawing(column: str) -> list[str]: except sheet_node.RecalcError as e: logger.warning("도면 표 맞춤 건너뜀 %s — %s", column, e) return [] - entities = (doc.get("도면") or {}).get("entities") or [] + entities = [e for _, sheet in drawing_sheets(doc) for e in sheet.get("entities") or []] done: list[str] = [] for link, got in zip(links, results): if not got.get("ok"): diff --git a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts index a76619d33..b52eb7eac 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts +++ b/M02_MasterTemplete/M02_MasterTemplete_Api_Fetch.ts @@ -150,9 +150,13 @@ export interface StructureDoc { 열: string; 도번: string; 도면: { entities: unknown[] } & Record; + /** 여러 장(82-2) — `도면` = 1장 · 더한 장 차례대로 · 장 도번 = 도번 + `-n`(둘째 = -2) */ + 더한장?: StructureSheet[]; /** 면적산출 → 도면 표 연결 기록(41 · `AreaToDrawing` · `M02_Area_Table`) — 표 style.연결 = 연결 · 값 = 마지막 칸 글 */ 연결표?: { 연결: string; + /** 표가 놓인 장id(82-3) — 1장 = 빈 값(옛 기록은 칸 없음 = 1장) */ + 장?: string; 시트: string; 범위: string; 글자크기: number; @@ -160,6 +164,13 @@ export interface StructureDoc { }[]; } +/** 구조물 도면 더한 장 하나(82-2) */ +export interface StructureSheet { + 장id: string; + 이름: string; + 도면: { entities: unknown[] } & Record; +} + /** 시스템 층 구조물 도면 목록 */ export const listStructures = async (): Promise => (await listTemplates("system", null)).filter((t) => t.종류 === "structure"); diff --git a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py index 871f27967..959d054f1 100644 --- a/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py +++ b/M02_MasterTemplete/M02_MasterTemplete_Router_Drawing.py @@ -104,15 +104,19 @@ def _on_frame_layer(entity: dict) -> dict: } -def structure_frame() -> dict: - """구조물 도면에 얹는 도각 — B07 시스템 양식 1:1 · 외곽 좌하단 (0,0) · 보여주기 층.""" +def structure_frame(number: str | None = None) -> dict: + """구조물 도면에 얹는 도각 — B07 시스템 양식 1:1 · 외곽 좌하단 (0,0) · 보여주기 층 · + `number` = 장 도번(82-2 · 도각 `{{도면번호}}`).""" + fields = {"도면번호": number} if number else None placed = frame_entities("m02-structure", usable_bbox(), fit=False) box = entities_bbox(placed) if not box: return {"entities": [], "layer": None} ox, oy = box[0], box[1] x0, y0, x1, y1 = usable_bbox() - entities = frame_entities("m02-structure", (x0 - ox, y0 - oy, x1 - ox, y1 - oy), fit=False) + entities = frame_entities( + "m02-structure", (x0 - ox, y0 - oy, x1 - ox, y1 - oy), fit=False, fields=fields + ) return { "entities": [_on_frame_layer(entity) for entity in entities], "layer": { @@ -125,9 +129,9 @@ def structure_frame() -> dict: @router.get("/structure-frame") -async def get_structure_frame() -> JSONResponse: - """구조물 도면 상세 · 카드 미리보기가 얹는 도각(56장).""" - return JSONResponse({"status": "success", **structure_frame()}) +async def get_structure_frame(number: str | None = None) -> JSONResponse: + """구조물 도면 상세 · 카드 미리보기가 얹는 도각(56장) · `number` = 장 도번(82-2).""" + return JSONResponse({"status": "success", **structure_frame(number)}) @router.get("/drawing-fields") diff --git a/M02_MasterTemplete/M02_Template_Layers.py b/M02_MasterTemplete/M02_Template_Layers.py index 3b90b61d6..b1ab41fbf 100644 --- a/M02_MasterTemplete/M02_Template_Layers.py +++ b/M02_MasterTemplete/M02_Template_Layers.py @@ -190,6 +190,44 @@ def check_skeleton(kind: str, document: Any) -> None: node = node.get(part) if isinstance(node, dict) else None if not isinstance(node, dict) or not isinstance(node.get(last), list): raise ValueError(f"양식 문서에 `{key}` 목록이 없습니다 — 빈 문서는 저장하지 않습니다.") + if kind == "structure": + _check_more_sheets(document.get(MORE_SHEETS)) + + +#: 구조물 도면 여러 장(82-2) — `도면` = 1장 · `더한장` = [{장id, 이름, 도면:{format, entities, layers}}] +MORE_SHEETS = "더한장" + + +def _check_more_sheets(more: Any) -> None: + """`더한장` 은 없거나 목록 — 장마다 장id(겹침 없음) · 이름 · `도면.entities` 목록.""" + if more is None: + return + if not isinstance(more, list): + raise ValueError("`더한장` 은 목록이어야 합니다.") + seen: set[str] = set() + for sheet in more: + sheet_id = sheet.get("장id") if isinstance(sheet, dict) else None + if not isinstance(sheet_id, str) or not sheet_id or sheet_id in seen: + raise ValueError("`더한장` 장마다 겹치지 않는 `장id` 가 있어야 합니다.") + seen.add(sheet_id) + drawing = sheet.get("도면") + if not isinstance(sheet.get("이름"), str) or not isinstance(drawing, dict): + raise ValueError(f"`더한장` 장 「{sheet_id}」 에 `이름` · `도면` 이 없습니다.") + if not isinstance(drawing.get("entities"), list): + raise ValueError(f"`더한장` 장 「{sheet_id}」 에 `도면.entities` 목록이 없습니다.") + + +def drawing_sheets(document: Any) -> list[tuple[str, dict[str, Any]]]: + """구조물 도면 문서의 장 `[(장id, 도면)]` — 1장 장id = "" · 모양이 틀린 장은 건너뜀.""" + if not isinstance(document, dict): + return [] + first = document.get("도면") + out = [("", first)] if isinstance(first, dict) else [] + for sheet in document.get(MORE_SHEETS) or []: + drawing = sheet.get("도면") if isinstance(sheet, dict) else None + if isinstance(drawing, dict): + out.append((str(sheet.get("장id") or ""), drawing)) + return out def write_template( diff --git a/resources/tester/test_m02_drawing_sheets.py b/resources/tester/test_m02_drawing_sheets.py new file mode 100644 index 000000000..0bae1f668 --- /dev/null +++ b/resources/tester/test_m02_drawing_sheets.py @@ -0,0 +1,160 @@ +"""M02 구조물 도면 여러 장(PLAN 82-2 · 82-3) — 저장 꼴 `더한장` · 뼈대 검사 · 모든 장 표 맞춤 · +복사 · 스냅샷 · 개인 저장이 더한장을 같이 나름 · 장 도번 도각 `{{도면번호}}`. + +시스템 층 = 견본 표(임시 폴더 · `test_m02_area_table.world`) · storage 는 임시. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest + +from M02_MasterTemplete import M02_Item_Personal as personal +from M02_MasterTemplete import M02_Item_Source as source +from M02_MasterTemplete import M02_MasterTemplete_Store as store +from M02_MasterTemplete import M02_Template_Layers as layers +from M02_MasterTemplete.M02_MasterTemplete_Router_Drawing import structure_frame +from resources.tester.test_m02_area_table import world # noqa: F401 — 같은 견본 층 + +_ = world + + +def _table(link: str) -> dict[str, Any]: + return { + "type": "Table", + "shapeData": { + "origin": {"x": 0, "y": 0}, + "columnWidths": [9, 9], + "rowHeights": [3, 3], + "cells": [[{"text": "낡음"}, {"text": "낡음"}], [None, {"text": "낡음"}]], + "style": {"fontSize": 2.5, "연결": link}, + }, + } + + +def _sheet(sheet_id: str, name: str, entities: list) -> dict[str, Any]: + return { + "장id": sheet_id, + "이름": name, + "도면": {"format": 6, "entities": entities, "layers": [{"id": "기본", "name": "기본"}]}, + } + + +def _write_sheets(folder: Path) -> dict[str, Any]: + """rv 도면 = 1장(T1 표) + 더한장 둘(s2 = T2 표 · s3 = 빈 장) · 연결표 줄에 `장`.""" + with store.at(folder): + got = store.read("structure", "rv") + doc = got["문서"] + doc["도면"]["entities"] = [_table("T1")] + doc[layers.MORE_SHEETS] = [_sheet("s2", "단면", [_table("T2")]), _sheet("s3", "상세", [])] + doc["연결표"] = [ + {"연결": "T1", "장": "", "시트": "s1", "범위": "A1:B2", "글자크기": 2.5, "값": {}}, + {"연결": "T2", "장": "s2", "시트": "s1", "범위": "A1:B2", "글자크기": 2.5, "값": {}}, + ] + store.write("structure", "rv", got["판"], doc) + return store.read("structure", "rv")["문서"] + + +@pytest.mark.parametrize( + "more", + [ + "장", + [{"이름": "가", "도면": {"entities": []}}], + [_sheet("s2", "가", []), _sheet("s2", "나", [])], + [{"장id": "s2", "이름": "가", "도면": {}}], + [{"장id": "s2", "도면": {"entities": []}}], + ], +) +def test_더한장_모양이_틀리면_거절(more): + doc = {"도면": {"entities": []}, layers.MORE_SHEETS: more} + with pytest.raises(ValueError): + layers.check_skeleton("structure", doc) + + +def test_더한장_없거나_맞으면_통과_장_목록(): + layers.check_skeleton("structure", {"도면": {"entities": []}}) # 옛 문서 그대로 + doc = {"도면": {"entities": [1]}, layers.MORE_SHEETS: [_sheet("s2", "가", [2])]} + layers.check_skeleton("structure", doc) + assert layers.drawing_sheets(doc) == [("", doc["도면"]), ("s2", doc["더한장"][0]["도면"])] + assert layers.drawing_sheets({"도면": {"entities": []}, "더한장": ["틀림"]}) == [ + ("", {"entities": []}) + ] + + +def test_저장_다시_읽기_둘째_장_그대로(world): # noqa: F811 + doc = _write_sheets(world["system"]) + assert [s["장id"] for s in doc["더한장"]] == ["s2", "s3"] + assert doc["더한장"][0]["도면"]["entities"][0]["shapeData"]["style"]["연결"] == "T2" + with store.at(world["system"]), pytest.raises(store.StoreError): + got = store.read("structure", "rv") + store.write("structure", "rv", got["판"], {**got["문서"], "더한장": [{"장id": ""}]}) + + +def test_면적_저장_뒤_안_열린_둘째_장_표도_갱신(world): # noqa: F811 + system = world["system"] + _write_sheets(system) + with store.at(system): + got = store.read("area", "rv") + got["문서"]["시트"][0]["칸"]["B1"] = {"값": 10, "서식": 1} + store.write("area", "rv", got["판"], got["문서"]) + doc = store.read("structure", "rv")["문서"] + first = doc["도면"]["entities"][0]["shapeData"]["cells"] + second = doc["더한장"][0]["도면"]["entities"][0]["shapeData"]["cells"] + assert first[0][1]["text"] == "10.00" and second[1][1]["text"] == "20.00" + assert [row["장"] for row in doc["연결표"]] == ["", "s2"] # 장 칸 그대로 + assert doc["연결표"][1]["값"]["s1!B2"] == "20.00" + + +def test_다른_열로_복사_더한장_같이(world): # noqa: F811 + _write_sheets(world["system"]) + with store.at(world["system"]): + if not store._path("structure", "c1").is_file(): + store.create_structure("c1") + store.copy_from("structure", "c1", "rv") + doc = store.read("structure", "c1")["문서"] + rv = store.read("structure", "rv")["문서"] + assert [s["장id"] for s in doc["더한장"]] == ["s2", "s3"] + assert doc["도번"] != rv["도번"] # 번호는 받는 열 그대로 + + +def _rv_key(system: Path) -> str: + with store.at(system): + return next(r["키"] for r in store.read_combos()["줄"] if r.get("열") == "rv") + + +def test_스냅샷_원본이_더한장_같이_나름(world): # noqa: F811 + _write_sheets(world["system"]) + files = source.build_source({"layer": "system", "key": _rv_key(world["system"])}) + assert len(files["도면.json"]["더한장"]) == 2 + + +def test_개인에_저장_더한장_같이(world, tmp_path): # noqa: F811 + _write_sheets(world["system"]) + files = source.build_source({"layer": "system", "key": _rv_key(world["system"])}) + copy = tmp_path / "copy" + copy.mkdir() + for name in ("집계.json", "도면.json"): + (copy / name).write_text(json.dumps(files[name], ensure_ascii=False), encoding="utf-8") + folder = layers.personal_dir(7, 99) + made = personal.create_item(folder, ["가"], "나", "m", copy=copy) + with store.at(folder): + doc = store.read("structure", made["열"])["문서"] + assert [s["이름"] for s in doc["더한장"]] == ["단면", "상세"] + + +def test_장_도번이_도각_도면번호에(): + texts = [ + (e.get("shapeData") or {}).get("label") + for e in structure_frame("SD-000010-2")["entities"] + if e.get("type") == "Text" + ] + assert "SD-000010-2" in texts + blank = [ + (e.get("shapeData") or {}).get("label") + for e in structure_frame()["entities"] + if e.get("type") == "Text" + ] + assert "SD-000010-2" not in blank and not any("{{" in str(t) for t in blank)