feat(376-1): B07 구조물 도면 목록 · 단건 서버(연결된 조합 도면 · 조합 키 중복 없앰 · 더한장 장마다 · 도각 도번 · 도면명 · 맨 뒤 덧붙임 · 확정본 kind)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWh5wNNycMkbwMpACz1rte
This commit is contained in:
2026-10-17 14:42:49 +09:00
co-authored by Claude Opus 5.5
parent 7329065c3b
commit 56c8ba96b9
6 changed files with 272 additions and 6 deletions
@@ -90,6 +90,11 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
WATERSHED_LAYOUT,
plan_kind_items,
)
from B07_DesignDetail.B07_DesignDetail_Router_Support_Structure import (
STRUCTURE_PREFIX,
read_structure_drawing,
structure_items,
)
from B07_DesignDetail.B07_DesignDetail_Schema import (
DesignDrawingItem,
)
@@ -242,7 +247,8 @@ def _drawing_list(project_root: Path, longitudinal_path: Path) -> list[DesignDra
confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")),
)
)
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — 사용자 확정 ③, PLAN 3장 ④.
# 구조물 도면(376) — 맨 뒤에 덧붙임(앞 도면번호 안 밀림) · 조합마다 한 장 · 더한장은 장마다.
drawings.extend(structure_items(project_root, manifest_drawings))
_store_drawing_numbers(project_root, drawings)
return drawings
@@ -480,6 +486,8 @@ def _read_drawing(
kind = drawing_id # id와 kind가 같은 단장 도면
elif PLAN_KIND_ID.fullmatch(drawing_id):
kind = "plan"
elif drawing_id.startswith(STRUCTURE_PREFIX):
kind = "structure"
else:
kind = "longitudinal" if _LONG_ID.fullmatch(drawing_id) else "cross"
label = str(manifest_entry.get("label") or drawing_id)
@@ -557,6 +565,9 @@ def _read_drawing(
)
return "plan", label, drawing, False, None
if drawing_id.startswith(STRUCTURE_PREFIX):
return read_structure_drawing(project_root, drawing_id)
if _LONG_ID.fullmatch(drawing_id):
source = _read_json(longitudinal_path)
chunk = next(
@@ -0,0 +1,104 @@
"""B07 구조물 도면(376 · 요구 241-1) — B05 · B06 배치에 연결된 구조물 조합 도면 목록 · 단건.
조합 키 = 항목 사본 폴더 이름(`snapshot_folder_name`) — 같은 조합을 여러 배치가 써도 한 장 ·
더한장은 장마다 · id `structure_{키}[_{장id}]` · 차례 = 도번 · 도면 = 프로젝트 사본 `도면.json` +
얹는 도각(M02 `structure_frame` · 도번 · 도면명). 고치기는 M02 · B08 — 여기는 읽기만.
"""
from __future__ import annotations
import logging
import re
from pathlib import Path
from typing import Any
from B07_DesignDetail.B07_DesignDetail_Schema import DesignDrawingItem
from common_util.common_util_summary_item import snapshot_folder_name
from M02_MasterTemplete import M02_Item_Snapshot as snapshot
from M02_MasterTemplete.M02_Item_Source import read_json
logger = logging.getLogger(__name__)
STRUCTURE_PREFIX = "structure_"
_SHEET_ID = re.compile(r"^[A-Za-z0-9-]{1,40}$")
def _sheet_number(number: str, index: int) -> str:
"""장 도번 — 1장 = 도번 · n 째 장 = 도번-n(M02 `sheetNumber` · B09 인쇄와 같음)."""
return "" if not number else number if index == 0 else f"{number}-{index + 1}"
def structure_sheets(project_root: Path) -> list[dict[str, Any]]:
"""구조물 조합 도면 장 `[{id, label, number, drawing}]` — 도번 차례(도번 없음 = 뒤)."""
docs: dict[str, tuple[str, dict[str, Any]]] = {}
for copy in snapshot.linked_snapshots(project_root).values():
item = copy.get("항목") or {}
if "도면" not in copy["부분"] or not item.get("key"):
continue
key = snapshot_folder_name(item)
if key in docs:
continue
doc = read_json(snapshot.structures_dir(project_root) / copy["폴더"] / "도면.json")
if isinstance(doc, dict) and isinstance(doc.get("도면"), dict):
docs[key] = (str(item.get("name") or key), doc)
sheets: list[dict[str, Any]] = []
order = sorted(docs, key=lambda k: (not docs[k][1].get("도번"), str(docs[k][1].get("도번")), k))
for key in order:
name, doc = docs[key]
number = str(doc.get("도번") or "")
pages = [{"장id": "", "이름": "", "도면": doc["도면"]}, *(doc.get("더한장") or [])]
for index, page in enumerate(pages):
if not isinstance(page, dict) or not isinstance(page.get("도면"), dict):
continue
sheet_id = str(page.get("장id") or "")
suffix = "" if index == 0 else f"_{sheet_id if _SHEET_ID.match(sheet_id) else index}"
sheets.append(
{
"id": f"{STRUCTURE_PREFIX}{key}{suffix}",
"label": f"{name} · {page['이름']}" if page.get("이름") else name,
"number": _sheet_number(number, index),
"drawing": page["도면"],
}
)
return sheets
def structure_items(
project_root: Path, manifest_drawings: dict[str, Any]
) -> list[DesignDrawingItem]:
"""좌측 「구조물 도면」 단추 — 자료가 깨졌으면 빈 목록(나머지 도면은 남김)."""
try:
sheets = structure_sheets(project_root)
except (OSError, ValueError, KeyError) as exc:
logger.warning("B07 구조물 도면 목록 실패 — 빼고 싣는다: %s", exc)
return []
return [
DesignDrawingItem(
id=sheet["id"],
kind="structure",
label=sheet["label"],
confirmed=bool(manifest_drawings.get(sheet["id"], {}).get("confirmed")),
)
for sheet in sheets
]
def read_structure_drawing(
project_root: Path, drawing_id: str
) -> tuple[str, str, dict[str, Any], bool, None]:
"""구조물 도면 한 장 + 도각 — `_read_drawing` 과 같은 (kind, label, drawing, confirmed, 표)."""
# M02 라우터가 B07 라우터를 부름 — 여기서 늦게 불러 순환을 피함(B09 인쇄와 같음)
from M02_MasterTemplete.M02_MasterTemplete_Router_Drawing import structure_frame
sheet = next((s for s in structure_sheets(project_root) if s["id"] == drawing_id), None)
if sheet is None:
raise FileNotFoundError("요청한 구조물 도면을 찾을 수 없습니다.")
drawing = sheet["drawing"]
frame = structure_frame(sheet["number"] or None, sheet["label"])
layers = [*(drawing.get("layers") or []), *([frame["layer"]] if frame["layer"] else [])]
framed = {
**drawing,
"entities": [*frame["entities"], *(drawing.get("entities") or [])],
"layers": layers,
}
return "structure", sheet["label"], framed, False, None
+4 -2
View File
@@ -20,7 +20,8 @@ class DesignDrawingItem(BaseModel):
"landuse",
"plan_lidar",
"cross_standard",
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — kind 도 뺌.
# 구조물 도면(376) — B05 · B06 배치에 연결된 구조물 조합 도면.
"structure",
"blank",
]
label: str
@@ -61,7 +62,8 @@ class DesignDrawingResponse(BaseModel):
"landuse",
"plan_lidar",
"cross_standard",
# 표준도(구조물도)는 2026-09-13 B08 「구조물도」 탭으로 이관 — kind 도 뺌.
# 구조물 도면(376) — B05 · B06 배치에 연결된 구조물 조합 도면.
"structure",
"blank",
]
label: str