- M02 종류별 탭 [종류 더하기] = 본보기(구조물 정본 원본) 복사 — 키 자동 · 이름 「본보기 2」 · 옵션 · 표시 · 그룹 · 배치 · 규칙 · 종류 덧칸 · 반영 그대로 · 종류.{새}.본보기 · 관 지점 종류 · 복사본은 본보기 밖
- 구조물 표 = 본보기 처음값 이어받음(resolveCatalog · resolve_catalog 짝 · 줄 칸 base) · baseType/base_type · specFor(명세) · 덧단 · 길이 전후 풀기 · 옹벽 추가 단 콘크리트
- 검사(화면 = 서버 짝) — 본보기 = 원본 · 같은 군 · 자리꼴 · 정본 · 만든 뒤 고정 · 복사본은 본보기가 숨긴 칸 이어받음 · 서버 덧칸 본보기 받음
- 특수 동작 = 동작 이름 — 바로 안 놓기 · 포장 나누기 = 포장_물넘이_겹침 · 월류 높이 기다림 = 월류단면
- 정본 목록 = 표 — 파이썬 관 지점 종류 = catalog owner · B05 시설 서브폼 = 표 형상 · 안 쓰는 손 목록 걷음
- drawing_views — 스키마에서 빼고 읽을 때 버림 · M02 [저장] 때 빠짐(코드는 마스터 안 고침)
- 이관 = 지난 판 이름 대신 측점 계획 자리(planted)로 가름 · all_type_names 걷음
- 허용 거리 = common_util_structure_station.py 한 곳(SAME_STATION_M · EDGE_TOLERANCE_M · PIPE_MATCH_M) · TS 표는 station.ts 머리
- 시험: test_323_7_new_type(템플릿 · 일곱 갈래 · 그림 · 검사 짝 · 서버 읽기) · 322-0 유령 시험 = 자리 · 옛 도움 파일 동작 · 표 길 · 기대 글 고침
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ji4nuyLVHcxrXWHTdb5nRD
187 lines
8.0 KiB
Python
187 lines
8.0 KiB
Python
"""기존 비정규 측점 → 구조물 정본 마이그레이션.
|
|
|
|
기존 화면은 구조물을 4종(배관·기성막이·대피로·기타)으로 받아 왔고, 서버에는 `chainage_m` +
|
|
표시 문자열만 남겼다. 신규 레지스트리 타입으로 옮기면서 종류별 의미를 살린다.
|
|
|
|
- 배관: 옮기지 않는다 — `pipe_points.json`이 정본이다.
|
|
- 기성막이 → 기슭막이(`revetment`). 기존 이름이 오기였다 (2026-08-16 사용자 확정).
|
|
기슭막이는 2026-08-28 이관으로 관 지점 시설이 됐다 — 구조물 목록이 아니라
|
|
`pipe_points.json`으로 간다 (2026-09-01 사용자 확정, 건너뛰기는 데이터 유실).
|
|
- 대피로 → 대피소(`refuge`)로 통합. 폭 값은 대피소 너비 옵션으로 옮긴다 (동 확정).
|
|
- 그 밖: 이름을 살려 `기타`.
|
|
|
|
구간형으로 바뀌는 타입(대피소)은 기존 데이터에 종점이 없다. 기점만 알고 있으므로 최소
|
|
구간을 임시로 주고, 사용자가 화면에서 종점을 조정한다 — 값을 지어내는 것보다 낫다.
|
|
관 시설로 가는 기슭막이는 레지스트리 기본값(사용자 확정분)을 승계한다.
|
|
"""
|
|
|
|
import re
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass, field
|
|
from typing import Any, Iterable
|
|
|
|
from B05_Profile.B05_Profile_Structures_Schema import (
|
|
StructureInstance,
|
|
structure_type_map,
|
|
)
|
|
from common_util.common_util_drainage_pipes import PIPE_SOURCE_USER, PipePoint
|
|
from common_util.common_util_structure_station import SAME_STATION_M
|
|
|
|
PIPE_STRUCTURE_NAME = "배관"
|
|
# 구조물 정본이 아니라 관 지점 정본이 관리한다는 표식(레지스트리 `managed_by`).
|
|
PIPE_POINTS_OWNER = "pipe_points"
|
|
|
|
|
|
@dataclass
|
|
class MigrationPlan:
|
|
"""이관 결과 — 정본이 갈리므로 목록도 갈린다(2026-09-01 사용자 확정)."""
|
|
|
|
structures: list[StructureInstance] = field(default_factory=list)
|
|
pipe_facilities: list[PipePoint] = field(default_factory=list)
|
|
|
|
|
|
# 기존 타입 이름 → 신규 type_id.
|
|
LEGACY_TYPE_MAP = {
|
|
"기성막이": "revetment",
|
|
"대피로": "refuge",
|
|
"기타": "etc",
|
|
}
|
|
# 종점을 모르는 구간형 항목에 주는 임시 길이(m). 화면에서 조정하라는 표시값이다.
|
|
DEFAULT_INTERVAL_LENGTH_M = 15.0
|
|
|
|
# 확정 저장분은 표시 라벨 문자열뿐이다(예: "파형강관 D800", "대피로 2.0m").
|
|
# 라벨 생성 규칙은 B05_Profile_UI_IrregularStations.ts의 structureLabel()이 정본.
|
|
_PIPE_LABEL = re.compile(r"^(배관|.*관)\s+D\d+", re.UNICODE)
|
|
_ESCAPE_LABEL = re.compile(r"^대피로(?:\s+([\d.]+)\s*m)?", re.UNICODE)
|
|
|
|
|
|
def _is_managed_elsewhere(entry: dict[str, Any], planted: Sequence[float]) -> bool:
|
|
"""정본(관 지점 · 구조물)이 이미 관리하는 측점인가 — 재이관하면 정본 이중화 → 「기타」 유령.
|
|
|
|
자리로 가름(323-7 · 지금 관 · 구조물이 심는 측점 계획 자리 `planted`) — 옛 이름 되짚기
|
|
(지난 판 이름 전부 · 322-0)는 걷음 · 이름을 바꿔도 자리는 그대로라 옛 이름 측점도 알아봄.
|
|
지금 판 종류 이름 · 관 라벨 꼴(관종 D관경)은 자리를 못 받은 부름(옛 확정분만)을 위해 그대로.
|
|
"""
|
|
label = str(entry.get("structure", "")).strip()
|
|
at = float(entry.get("chainage_m", 0.0))
|
|
return (
|
|
entry.get("origin") == "pipe"
|
|
or entry.get("structureType") == PIPE_STRUCTURE_NAME
|
|
or label == PIPE_STRUCTURE_NAME
|
|
or label in {item.name for item in structure_type_map().values()}
|
|
or bool(_PIPE_LABEL.match(label))
|
|
or any(abs(at - p) < SAME_STATION_M for p in planted)
|
|
)
|
|
|
|
|
|
def _legacy_type_of(entry: dict[str, Any]) -> str | None:
|
|
"""기존 종류 이름을 알아낸다 — structureType이 없으면 라벨 문자열에서 판별한다.
|
|
|
|
확정 저장 형식(`{chainage_m, structure}`)에는 structureType이 없다(크로스체크
|
|
지적 1, 2026-08-16). 라벨은 structureLabel()이 만든 고정 형식이라 되짚을 수 있다.
|
|
"""
|
|
explicit = entry.get("structureType")
|
|
if explicit in LEGACY_TYPE_MAP:
|
|
return str(explicit)
|
|
label = str(entry.get("structure", "")).strip()
|
|
if label == "기성막이":
|
|
return "기성막이"
|
|
if _ESCAPE_LABEL.match(label):
|
|
return "대피로"
|
|
return None
|
|
|
|
|
|
def _escape_width_of(entry: dict[str, Any]) -> float | None:
|
|
"""대피로 폭 — 명시 필드가 우선, 없으면 라벨("대피로 2.0m")에서 파싱한다."""
|
|
width = entry.get("escapeWidthM")
|
|
if width is not None:
|
|
return float(width)
|
|
match = _ESCAPE_LABEL.match(str(entry.get("structure", "")).strip())
|
|
if match and match.group(1):
|
|
return float(match.group(1))
|
|
return None
|
|
|
|
|
|
def migrate_irregular_stations(
|
|
entries: Iterable[dict[str, Any]], planted: Sequence[float] = ()
|
|
) -> MigrationPlan:
|
|
"""기존 비정규 측점 목록을 정본별로 나눠 옮긴다(같은 입력이면 같은 결과).
|
|
|
|
타입이 다른 정본 소관(`managed_by`)이면 구조물 목록이 아니라 그 정본으로 간다 —
|
|
기슭막이가 관 지점 시설로 옮겨간 뒤(2026-08-28) 구조물로 저장하면 저장소가 거절한다
|
|
(2026-09-01 사용자 확정: 건너뛰지 않고 관 지점으로 옮긴다).
|
|
"""
|
|
types = structure_type_map()
|
|
structures: list[StructureInstance] = []
|
|
facilities: list[PipePoint] = []
|
|
seen: set[tuple[str, float]] = set()
|
|
|
|
for entry in entries:
|
|
if _is_managed_elsewhere(entry, planted):
|
|
continue
|
|
|
|
chainage = float(entry.get("chainage_m", 0.0))
|
|
legacy_type = _legacy_type_of(entry)
|
|
type_id = LEGACY_TYPE_MAP.get(legacy_type or "", "etc")
|
|
definition = types[type_id]
|
|
|
|
key = (type_id, round(chainage, 3))
|
|
if key in seen:
|
|
continue
|
|
seen.add(key)
|
|
|
|
if definition.managed_by == PIPE_POINTS_OWNER:
|
|
facilities.append(_pipe_facility_for(definition, chainage))
|
|
continue
|
|
|
|
data: dict[str, Any] = {
|
|
"type_id": type_id,
|
|
"placement": definition.placement,
|
|
"options": _options_for(type_id, entry),
|
|
"placement_source": "manual",
|
|
}
|
|
if definition.placement == "interval":
|
|
data["start_m"] = chainage
|
|
data["end_m"] = chainage + DEFAULT_INTERVAL_LENGTH_M
|
|
data["memo"] = "구 비정규 측점 이관 — 종점 확인 필요"
|
|
else:
|
|
data["chainage_m"] = chainage
|
|
structures.append(StructureInstance.model_validate(data))
|
|
|
|
return MigrationPlan(structures=structures, pipe_facilities=facilities)
|
|
|
|
|
|
def _pipe_facility_for(definition, chainage: float) -> PipePoint:
|
|
"""관 지점 정본이 관리하는 타입 하나를 관 시설로 만든다.
|
|
|
|
레거시 측점에는 제원이 없다(누가거리 + 표시 문자열뿐). 값을 지어내지 않고
|
|
**레지스트리 기본값**을 그대로 승계한다 — 기슭막이의 형태·높이·길이·전/후는
|
|
사용자 확정 기본값이다(2026-08-17·08-19). 옵션 키는 옛 전용 키 한 벌로 두면
|
|
B05 폼(`legacyRevetOptions`)과 B06 세트(`_revet_set`)가 양쪽 벽으로 펼쳐 읽는다.
|
|
"""
|
|
options = {
|
|
option.key: option.default
|
|
for option in definition.options
|
|
if option.phase == "b05" and option.default is not None
|
|
}
|
|
before = float(options.get("before_m") or 0.0)
|
|
after = float(options.get("after_m") or 0.0)
|
|
return PipePoint(
|
|
chainage_m=chainage,
|
|
source=PIPE_SOURCE_USER,
|
|
facility=definition.type_id,
|
|
start_m=chainage - before,
|
|
end_m=chainage + after,
|
|
options=options,
|
|
)
|
|
|
|
|
|
def _options_for(type_id: str, entry: dict[str, Any]) -> dict[str, Any]:
|
|
if type_id == "refuge":
|
|
width = _escape_width_of(entry)
|
|
return {"width_m": width} if width is not None else {}
|
|
if type_id == "etc":
|
|
name = entry.get("customName") or entry.get("structure") or "기타 구조물"
|
|
return {"name": str(name).strip()}
|
|
return {}
|