2026-08-28 사용자 지시 4건.
① 물넘이 파임을 3D 코리도에도 반영
classifyStation 이 노면·노견 조각 표고를 횡단도와 같은 산식(fordDeckElevationAt)
으로 내린다. 노면 밖 비탈은 그대로라 노견 끝에 수직 단차가 선다. BUILD_VERSION 85.
② 표준횡단면 저장분을 포장 재계산에 싣는다
확정 때 종단 정본 options 에 실린 standard_cross_section 을 읽어
enforce_pavement_ranges / attach_default_designs 에 넘긴다. 없으면 config 기본값.
③ 포장 구간 기본값 10 / 5 / 5 (기슭막이와 같은 출발값).
④ 독립 기슭막이
- 측점: is_station_planting_type() 규칙 신설(A군 + D군 구간형). 구간형은
시작·기준·종료에 측점을 심는다 — 길이가 길수록 횡단도가 여러 장 나온다.
구조물 재이관 차단도 같은 규칙을 본다(기타 고스트 방지).
- 서버: B06_Section_Engine_Revetment 신설 — 구간 안 측점 전부에 section.revetment.
- 횡단도: B06_Section_UI_Cross_Revetment 신설 — 벽 상단 = 성토면 끝(설계선-지반
교차점), 아래로 높이 + 근입 0.5m, 전면 1:0.3(교본 7-3). 설치 측은 사용자가
좌/우로 고른다(레지스트리 side 옵션 신설 — 자동 판정 없음).
- 3D: 같은 폴리곤을 기준측점 전/후만큼 스윕. BUILD_VERSION 86 + 구조물 해시에
물넘이·기슭막이 제원 추가.
검증: pytest 241 passed / 7 skipped(신규 4건). 공용 브라우저 실측 —
물넘이 240m 투입 시 3D 노면이 계획고보다 0.38~0.42m 아래(대조군 200m 정상),
기슭막이 225~255 투입 시 측점 23→25·횡단 카드 3장·3D revet 솔리드 3개(31링).
임시 데이터는 원래 정본으로 복구했다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
5.2 KiB
Python
132 lines
5.2 KiB
Python
"""기존 비정규 측점 → 구조물 정본 마이그레이션.
|
|
|
|
기존 화면은 구조물을 4종(배관·기성막이·대피로·기타)으로 받아 왔고, 서버에는 `chainage_m` +
|
|
표시 문자열만 남겼다. 신규 레지스트리 타입으로 옮기면서 종류별 의미를 살린다.
|
|
|
|
- 배관: 옮기지 않는다 — `pipe_points.json`이 정본이다.
|
|
- 기성막이 → 기슭막이(`revetment`). 기존 이름이 오기였다 (2026-08-16 사용자 확정).
|
|
- 대피로 → 대피소(`refuge`)로 통합. 폭 값은 대피소 너비 옵션으로 옮긴다 (동 확정).
|
|
- 그 밖: 이름을 살려 `기타`.
|
|
|
|
구간형으로 바뀌는 타입(기슭막이·대피소)은 기존 데이터에 종점이 없다. 기점만 알고 있으므로
|
|
최소 구간을 임시로 주고, 사용자가 화면에서 종점을 조정한다 — 값을 지어내는 것보다 낫다.
|
|
"""
|
|
|
|
import re
|
|
from typing import Any, Iterable
|
|
|
|
from B05_Profile.B05_Profile_Structures_Schema import (
|
|
StructureInstance,
|
|
station_planting_labels,
|
|
structure_type_map,
|
|
)
|
|
|
|
PIPE_STRUCTURE_NAME = "배관"
|
|
# 기존 타입 이름 → 신규 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 _managed_elsewhere_labels() -> set[str]:
|
|
"""서버가 종단 정본에 직접 심는 라벨 — 구조물로 재이관하면 안 된다(정본 이중화 →
|
|
"기타" 고스트). 규칙은 측점 생성과 **같은 자리**에서 온다(station_planting_labels).
|
|
"""
|
|
return station_planting_labels()
|
|
|
|
|
|
def _is_managed_elsewhere(entry: dict[str, Any]) -> bool:
|
|
"""다른 정본(pipe_points.json·구조물 A군)이 관리하는 측점인가."""
|
|
label = str(entry.get("structure", "")).strip()
|
|
return (
|
|
entry.get("origin") == "pipe"
|
|
or entry.get("structureType") == PIPE_STRUCTURE_NAME
|
|
or label == PIPE_STRUCTURE_NAME
|
|
or label in _managed_elsewhere_labels()
|
|
or bool(_PIPE_LABEL.match(label))
|
|
)
|
|
|
|
|
|
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]]) -> list[StructureInstance]:
|
|
"""기존 비정규 측점 목록을 구조물 인스턴스로 옮긴다(같은 입력이면 같은 결과)."""
|
|
types = structure_type_map()
|
|
migrated: list[StructureInstance] = []
|
|
seen: set[tuple[str, float]] = set()
|
|
|
|
for entry in entries:
|
|
if _is_managed_elsewhere(entry):
|
|
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)
|
|
|
|
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
|
|
migrated.append(StructureInstance.model_validate(data))
|
|
|
|
return migrated
|
|
|
|
|
|
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 {}
|