feat(B03): 계획노선 shapefile 입력과 PRJ 2개 분리를 지원한다
원청 정식 계획노선이 shapefile(UTM-K)로, 지형이 별도 PRJ(동부원점 Bessel)로 들어오는데 입력 경로가 shapefile 확장자를 막고 PRJ를 프로젝트당 1개로 전제했다. - 업로드 허용에 .shp/.shx/.dbf/.cpg 추가, 한 번에 보낼 파일 수 5 -> 10 - B03_FileInput_Engine_Shapefile: ESRI 규격 직접 파싱(GDAL 미사용). 형제 파일이 아직 안 왔어도 .shp 하나로 기하를 읽는다. .cpg 내용이 949뿐인 실물을 CP949로 정규화해 한글 속성을 살린다. - 노선 판독을 read_planned_route로 일원화(CSV/shapefile), PlannedRoute에 crs_input 추가 - 변환 입력은 EPSG 코드가 아니라 crs_input_from_prj가 주는 값(EPSG:n 또는 원문 WKT)이다. 실물 PRJ 2종 모두 to_epsg가 None이다. - shapefile 세트를 input/shp/ 한 폴더에 모은다(GDAL 요건). 노선 PRJ가 그 안에 남으므로 지형 PRJ(input/prj/)와 파일명 정렬 운에 기대지 않고 갈린다. find_project_prj가 지형 PRJ를 프로젝트 좌표계로 고른다. - 필수 세트를 노선 1종(csv 또는 shp) + prj + tfw로 완화, shp면 shx/dbf 동반 필수. - UI: 확장자 단독 슬롯 매칭을 basename 그룹핑으로 바꿔 노선 PRJ와 지형 PRJ가 같은 슬롯을 다투지 않게 하고, 노선 슬롯이 파일 한 벌을 담아 함께 전송한다. 자체검증: tmp/tests/test_route_shapefile_input.py 9개 통과, tsc --noEmit 통과, ruff check/format 통과. 전체 스위트 잔여 실패 11건은 HEAD 사본(git archive)에서 동일하게 재현되는 기존 실패다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -279,11 +279,16 @@ def download_geodata(
|
||||
실패해도 예외를 밖으로 던지지 않는다 — 분석 본체를 막지 않는다.
|
||||
"""
|
||||
try:
|
||||
# 입력 파일과 같은 폴더의 .prj를 우선 사용 (PLAN E-1: 전체 재귀 glob 제거)
|
||||
prj_candidates = sorted(prj_search_dir.glob("*.prj")) or sorted(
|
||||
project_root.glob("B03_FileInput/**/*.prj")
|
||||
# 입력 파일과 같은 폴더의 .prj를 우선 사용 (PLAN E-1: 전체 재귀 glob 제거).
|
||||
# 없으면 지형 PRJ — 노선 shapefile 세트의 PRJ가 섞이지 않도록 고른다(2026-08-31).
|
||||
from common_util.common_util_crs import find_project_prj
|
||||
|
||||
prj_candidates = sorted(prj_search_dir.glob("*.prj"))
|
||||
prj_path = (
|
||||
prj_candidates[0]
|
||||
if prj_candidates
|
||||
else (find_project_prj(project_root) or project_root / "result.prj")
|
||||
)
|
||||
prj_path = prj_candidates[0] if prj_candidates else project_root / "result.prj"
|
||||
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Extent import (
|
||||
download_extent,
|
||||
|
||||
@@ -144,10 +144,16 @@ def planned_route_bounds(project_root: Path, target_epsg: str) -> dict[str, floa
|
||||
|
||||
|
||||
def project_epsg_from_prj(project_root: Path) -> str:
|
||||
"""프로젝트 PRJ에서 좌표계를 읽는다. 없으면 중부원점(EPSG:5186)."""
|
||||
"""프로젝트 PRJ에서 좌표계를 읽는다. 없으면 중부원점(EPSG:5186).
|
||||
|
||||
PRJ가 둘 이상 올라오므로(노선 세트 + 지형) 지형 PRJ를 고른다 — `find_project_prj`.
|
||||
"""
|
||||
from common_util.common_util_crs import find_project_prj
|
||||
|
||||
from .B04_PreProcess_Engine_VWorld import get_epsg_from_prj
|
||||
|
||||
for prj_path in sorted(project_root.glob("B03_FileInput/**/*.prj")):
|
||||
prj_path = find_project_prj(project_root)
|
||||
if prj_path is not None:
|
||||
return get_epsg_from_prj(prj_path.read_text(encoding="utf-8", errors="ignore"))
|
||||
return "EPSG:5186"
|
||||
|
||||
|
||||
@@ -451,14 +451,14 @@ def build_sheet_surface_from_route(
|
||||
"""B03 업로드 계획노선 CSV를 찾아 방식별 도엽 서피스를 만든다. 없으면 빈 목록."""
|
||||
from common_util.common_util_route_geometry import (
|
||||
find_planned_route_file,
|
||||
read_planned_route_csv,
|
||||
read_planned_route,
|
||||
)
|
||||
|
||||
route_file = find_planned_route_file(project_root / "B03_FileInput" / "input")
|
||||
if route_file is None:
|
||||
logger.warning("도엽 서피스: 계획 노선 파일이 없습니다.")
|
||||
return []
|
||||
planned = read_planned_route_csv(route_file)
|
||||
planned = read_planned_route(route_file)
|
||||
if planned is None or len(planned.vertices) < 2:
|
||||
logger.warning("도엽 서피스: 계획 노선 파일을 읽지 못했습니다: %s", route_file.name)
|
||||
return []
|
||||
@@ -478,7 +478,7 @@ def run_sheet_surface_analysis(
|
||||
|
||||
반환 형식은 `run_surface_analysis()`와 같다(save_surface_analysis_to_db 호환).
|
||||
"""
|
||||
from common_util.common_util_route_geometry import read_planned_route_csv
|
||||
from common_util.common_util_route_geometry import read_planned_route
|
||||
|
||||
def _report(percent: int, stage: str, message: str) -> None:
|
||||
if on_progress is not None:
|
||||
@@ -490,7 +490,7 @@ def run_sheet_surface_analysis(
|
||||
processed_dir.mkdir(parents=True, exist_ok=True)
|
||||
models_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
planned = read_planned_route_csv(route_csv_path)
|
||||
planned = read_planned_route(route_csv_path)
|
||||
if planned is None or len(planned.vertices) < 2:
|
||||
raise ValueError(f"계획 노선 파일을 읽지 못했습니다: {route_csv_path.name}")
|
||||
epsg = planned.epsg or 5186
|
||||
|
||||
@@ -250,7 +250,7 @@ async def get_planned_route(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
from B04_PreProcess.B04_PreProcess_Engine_Extent import project_epsg_from_prj
|
||||
from common_util.common_util_route_geometry import (
|
||||
find_planned_route_file,
|
||||
read_planned_route_csv,
|
||||
read_planned_route,
|
||||
)
|
||||
|
||||
pool = get_db_pool()
|
||||
@@ -259,7 +259,7 @@ async def get_planned_route(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
route_file = find_planned_route_file(project_root / "B03_FileInput" / "input")
|
||||
planned = read_planned_route_csv(route_file) if route_file else None
|
||||
planned = read_planned_route(route_file) if route_file else None
|
||||
if planned is None or len(planned.vertices) < 2:
|
||||
return {"status": "success", "points": []}
|
||||
|
||||
@@ -267,7 +267,7 @@ async def get_planned_route(project_id: UUID) -> dict[str, Any] | JSONResponse:
|
||||
# 그것이 프로젝트 좌표계와 다르면 여기서 한 번 옮긴다.
|
||||
target_epsg = project_epsg_from_prj(project_root)
|
||||
points = [(float(vertex.x), float(vertex.y)) for vertex in planned.vertices]
|
||||
source_epsg = f"EPSG:{planned.epsg}" if planned.epsg else target_epsg
|
||||
source_epsg = planned.crs_input or target_epsg
|
||||
if source_epsg != target_epsg:
|
||||
from pyproj import Transformer
|
||||
|
||||
|
||||
Reference in New Issue
Block a user