feat(B07): 계획평면도 종류 · 정보 조각 서버 (PLAN 60-1 · 60-2)

- 정보 조각(등고선 · 계류 · 중심선 · 성토사면 외곽선 · 비탈 눈금 · IP · R · 측점 NO · 시종점 · 대피소/사토장 · 라이다 배경)마다 장별 파일(plan_pieces/{장}/{조각}.json)
- 외곽선 = 노견 끝 + 설계선이 지반에 닿는 자리(B06 횡단 설계 결과) · 측점 자리는 횡단 파일 중심 · 좌축
- 종류 = 조각 고르기 목록(plan_kinds/{id}.json) · 기본 넷(지형 · 노선배치도 · 대피소 · 라이다) · 더하기 · 지우기 API
- 대피소 · 사토장 = 구조물 정본 refuge · spoil_bank + 사용자 지정 자리(plan_marks.json)
- 도면 목록 · 단건 조회를 종류 × 장으로 · 옛 plan_* id 그대로 이어짐

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPJGtfXo6GMbcpiboQC4uK
This commit is contained in:
2026-10-06 17:51:40 +09:00
co-authored by Claude Opus 5.5
parent 2fc95c51c9
commit 138e80c79f
8 changed files with 1168 additions and 86 deletions
@@ -28,22 +28,13 @@ from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Landuse import (
LANDUSE_LABEL,
build_landuse_drawing,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Lidar import (
LIDAR_LABEL,
build_lidar_plan_drawing,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Long import (
build_longitudinal_drawing,
longitudinal_chunks,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_MassHaul import build_mass_haul_drawing
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import (
PLAN_KINDS,
build_plan_drawing,
plan_chunks,
plan_drawing_id,
plan_drawing_label,
)
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Plan import plan_chunks
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_PlanKinds import build_plan_kind_drawing
from B07_DesignDetail.B07_DesignDetail_Engine_Cad_Sheet import (
CROSS_SHEET_ID,
build_cross_sheet,
@@ -90,6 +81,10 @@ from B07_DesignDetail.B07_DesignDetail_Router_Support_Io import (
_station_map,
_write_manifest,
)
from B07_DesignDetail.B07_DesignDetail_Router_Support_Plan import (
PLAN_KIND_ID,
plan_kind_items,
)
from B07_DesignDetail.B07_DesignDetail_Schema import (
DesignDrawingItem,
)
@@ -151,32 +146,16 @@ def _drawing_list(
confirmed=bool(manifest_drawings.get(sheet["id"], {}).get("confirmed")),
)
)
# 계획평면도 3종 — 축척 1/1,200 고정이라 노선이 길면 장이 나뉜다(장수는 노선이 정한다).
plan_sheets = plan_chunks(plan_stations(longitudinal))
for kind, _label, *_rest in PLAN_KINDS:
for chunk in plan_sheets:
drawing_id = plan_drawing_id(kind, chunk, len(plan_sheets))
drawings.append(
DesignDrawingItem(
id=drawing_id,
kind="plan",
label=plan_drawing_label(kind, chunk, len(plan_sheets)),
confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")),
)
)
# 계획평면도(라이다) — 지표면 음영기복 배경. 같은 축척·같은 장 나눔.
for chunk in plan_sheets:
drawing_id = "plan_lidar" if len(plan_sheets) <= 1 else f"plan_lidar_{chunk['number']}"
# 계획평면도 — 종류(지형 · 노선배치도 · 대피소 · 라이다 · 사용자 추가) × 장(PLAN 60장).
for entry in plan_kind_items(project_root, longitudinal):
drawings.append(
DesignDrawingItem(
id=drawing_id,
kind="plan_lidar",
label=LIDAR_LABEL
if len(plan_sheets) <= 1
else f"{LIDAR_LABEL} {chunk['number']}장",
confirmed=bool(manifest_drawings.get(drawing_id, {}).get("confirmed")),
**entry,
kind="plan",
confirmed=bool(manifest_drawings.get(entry["id"], {}).get("confirmed")),
)
)
plan_sheets = plan_chunks(plan_stations(longitudinal))
# 용지도 — 계획평면도와 같은 축척·같은 장 나눔을 쓴다.
for chunk in plan_sheets:
drawing_id = "landuse" if len(plan_sheets) <= 1 else f"landuse_{chunk['number']}"
@@ -427,12 +406,10 @@ def _read_drawing(
if saved.get("format") == DRAWING_FORMAT:
if drawing_id in (COVER_ID, MASS_HAUL_ID, WATERSHED_ID, CROSS_STANDARD_ID):
kind = drawing_id # id와 kind가 같은 단장 도면
elif PLAN_ID.fullmatch(drawing_id):
elif PLAN_KIND_ID.fullmatch(drawing_id):
kind = "plan"
elif LANDUSE_ID.fullmatch(drawing_id):
kind = "landuse"
elif LIDAR_ID.fullmatch(drawing_id):
kind = "plan_lidar"
else:
kind = "longitudinal" if _LONG_ID.fullmatch(drawing_id) else "cross"
label = str(manifest_entry.get("label") or drawing_id)
@@ -468,20 +445,16 @@ def _read_drawing(
None,
)
if LIDAR_ID.fullmatch(drawing_id):
# stored_design = lidar_source()가 만든 노선 + 지표면 음영기복 그림.
if PLAN_KIND_ID.fullmatch(drawing_id):
# stored_design = plan_kind_source()가 계산 · 저장한 조각 중 그 종류가 고른 것.
if not isinstance(stored_design, dict):
raise FileNotFoundError("라이다 계획평면도 자료가 없습니다.")
label = str(stored_design.get("label") or LIDAR_LABEL)
raise FileNotFoundError("계획평면도 자료가 없습니다.")
label = str(stored_design.get("label") or drawing_id)
return (
"plan_lidar",
"plan",
label,
build_lidar_plan_drawing(
drawing_id,
label,
stored_design.get("route_xy") or [],
stored_design.get("shade_image"),
stored_design.get("shade_box"),
build_plan_kind_drawing(
drawing_id, label, stored_design["box"], stored_design.get("pieces") or {}
),
False,
None,
@@ -508,31 +481,6 @@ def _read_drawing(
None,
)
if PLAN_ID.fullmatch(drawing_id):
# stored_design = plan_source()가 모아 준 노선·측점·배경·구조물 좌표(사업지 CRS).
if not isinstance(stored_design, dict):
raise FileNotFoundError("계획평면도 자료가 없습니다.")
longitudinal = _read_json(longitudinal_path)
interval = infer_station_interval(longitudinal.get("stations") or [])
label = str(stored_design.get("label") or drawing_id)
return (
"plan",
label,
build_plan_drawing(
str(stored_design.get("kind") or "plan_terrain"),
drawing_id,
label,
stored_design.get("route_xy") or [],
stored_design.get("stations") or [],
stored_design.get("contours") or [],
stored_design.get("streams") or [],
stored_design.get("structures") or [],
interval,
),
False,
None,
)
if drawing_id == WATERSHED_ID:
# stored_design = watershed_source()가 모아 준 노선·유역·배경 좌표(사업지 CRS).
if not isinstance(stored_design, dict):