feat(M02): 구조물 도면 여러 장 저장 꼴 — 더한장 · 연결표 장 · 모든 장 표 맞춤 · 장 도번 도각(82-2)
- 더한장 = [{장id, 이름, 도면:{format, entities, layers}}] 뼈대 검사 · drawing_sheets
- 면적 [저장] 때 안 열린 장 표도 서버가 갱신
- /m02/structure-frame?number= 장 도번을 도각 {{도면번호}} 에
- StructureDoc 더한장 · 연결표 장 타입
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
@@ -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"):
|
||||
|
||||
@@ -150,9 +150,13 @@ export interface StructureDoc {
|
||||
열: string;
|
||||
도번: string;
|
||||
도면: { entities: unknown[] } & Record<string, unknown>;
|
||||
/** 여러 장(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<string, unknown>;
|
||||
}
|
||||
|
||||
/** 시스템 층 구조물 도면 목록 */
|
||||
export const listStructures = async (): Promise<TemplateInfo[]> =>
|
||||
(await listTemplates("system", null)).filter((t) => t.종류 === "structure");
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user