feat(B03): 재접속 업로드 현황(10) + B05·B06 자동 설계 체인(9)
10. 재접속 현황·완료 표시·재업로드 경고:
- GET /projects/{id}/upload-overview 신설 — 완료 파일 목록(input_files 정본),
중단 청크 세션(진행률), 필수 파일·WF1 분석 완료 여부
- 진입 시 서버 정본으로 슬롯 카드 표시(serverUploaded), localStorage는 보조로 강등
- 파일 재선택 전에도 중단 세션 이어올리기 안내 배너
- 전체 완료 배지 + 완료 슬롯 재업로드 시 교체 확인 모달(승인 시에만 진행)
- 필수 슬롯 검증: 서버 업로드분 있으면 충족 — 단일 파일 교체 업로드 허용
9. 자동 설계 체인 연장 (B03_FileInput_Service_Chain.py):
- WF1 자동 확정 후 같은 백그라운드 태스크에서 ① 계획노선 CSV 기반 B05 기본 경로
계산(solve) ② 경로 확정(stage 2) ③ B06 기본 횡단 설계 확정(stage 3)까지 진행
- 수동 이력 보호: 프로젝트에 경로가 이미 있으면 건너뜀
- 단계별 실패 격리: 실패 단계에서 멈추고 로그·workflow 상태로만 기록
- AUTO_DESIGN_CHAIN_ENABLED config 플래그(기본 True)
typecheck·ruff·B03 unittest(7건) 통과.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""B03 업로드 이후 자동 설계 체인 — WF1 확정 다음을 잇는다.
|
||||
|
||||
WF1(지표면 분석·자동 확정)이 끝나면 사용자가 화면에 없어도 서버가 이어서
|
||||
① B05 기본 경로 계산·확정(계획노선 CSV 기반) ② B06 기본 횡단 설계 확정까지
|
||||
기본값으로 진행해 영구저장소에 남긴다(2026-08-04 사용자 확정). 이후 사용자가
|
||||
대시보드에서 B05/B06에 들어오면 저장본을 바로 로딩해 검토·수정만 하면 된다.
|
||||
|
||||
원칙:
|
||||
- **수동 이력 보호**: 프로젝트에 경로가 하나라도 있으면 체인을 건너뛴다 — 사용자가
|
||||
이미 작업한 것을 자동 계산이 덮어쓰면 안 된다.
|
||||
- **단계별 실패 격리**: 각 단계는 해당 라우터가 자기 workflow stage 전이(실패 기록)를
|
||||
책임진다. 체인은 실패한 단계에서 멈추고 뒤 단계로 오류를 전파하지 않는다 —
|
||||
사용자는 그 페이지에서 수동으로 이어서 진행할 수 있다.
|
||||
- 라우터 함수를 직접 호출한다(HTTP 재진입 없음). solve/confirm 엔드포인트는 인증
|
||||
의존성이 없는 순수 함수 시그니처라 서버 내부 호출이 가능하다.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _planned_route_points_in_project_crs(project_root: Path) -> list[dict[str, float]] | None:
|
||||
"""계획노선 CSV를 읽어 프로젝트 좌표계(m) 점 목록으로 돌려준다. 없으면 None.
|
||||
|
||||
B04 `/planned-route` 조회와 같은 규칙 — CSV가 제 좌표계(crs_epsg)를 적어 두었고
|
||||
프로젝트 좌표계와 다르면 한 번 옮긴다.
|
||||
"""
|
||||
from B04_wf1_Surface.B04_wf1_Surface_Engine_Extent import project_epsg_from_prj
|
||||
from common_util.common_util_route_geometry import (
|
||||
find_planned_route_file,
|
||||
read_planned_route_csv,
|
||||
)
|
||||
|
||||
route_file = find_planned_route_file(project_root / "B03_FileInput" / "input")
|
||||
planned = read_planned_route_csv(route_file) if route_file else None
|
||||
if planned is None or len(planned.vertices) < 2:
|
||||
return None
|
||||
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
|
||||
if source_epsg.upper() != target_epsg.upper():
|
||||
from pyproj import Transformer
|
||||
|
||||
transformer = Transformer.from_crs(source_epsg, target_epsg, always_xy=True)
|
||||
points = [transformer.transform(x, y) for x, y in points]
|
||||
return [{"x": x, "y": y} for x, y in points]
|
||||
|
||||
|
||||
async def run_auto_design_chain(project_id: UUID, surface_model_id: int | None = None) -> None:
|
||||
"""B05 기본 경로 계산·확정 → B06 기본 횡단 설계 확정을 기본값으로 이어 실행한다.
|
||||
|
||||
WF1 자동 확정 직후 같은 백그라운드 태스크에서 호출된다. 어떤 단계가 실패해도
|
||||
예외를 밖으로 던지지 않는다 — 로그와 각 단계의 workflow 상태 기록으로 남긴다.
|
||||
"""
|
||||
from B03_FileInput.B03_FileInput_Repository import get_project_storage_relative_path
|
||||
from B05_wf2_Route.B05_wf2_Route_Repository import get_latest_route
|
||||
from B05_wf2_Route.B05_wf2_Route_Router import confirm_latest_route, solve_route
|
||||
from B05_wf2_Route.B05_wf2_Route_Schema import RoutePoint, RouteSolveRequest
|
||||
from B06_wf3_ProfileCross.B06_wf3_ProfileCross_Router_Confirm import confirm_sections
|
||||
from common_util.common_util_storage import resolve_stored_project_path
|
||||
from common_util.common_util_surface_confirmation import surface_confirmation_defaults
|
||||
from config.config_db import get_db_pool
|
||||
|
||||
pool = get_db_pool()
|
||||
try:
|
||||
# 1) 수동 이력 보호 — 경로가 이미 있으면(사용자 작업 또는 이전 자동 실행) 건너뛴다.
|
||||
async with pool.acquire() as connection:
|
||||
existing = await get_latest_route(connection, project_id)
|
||||
stored_path = await get_project_storage_relative_path(connection, project_id)
|
||||
if existing:
|
||||
logger.info(
|
||||
"자동 설계 체인 건너뜀(기존 경로 있음): project_id=%s route_id=%s",
|
||||
project_id,
|
||||
existing.get("id"),
|
||||
)
|
||||
return
|
||||
|
||||
# 2) 계획노선 CSV → BP/EP/경유점. 없으면 자동 경로를 세울 근거가 없다.
|
||||
project_root = Path(resolve_stored_project_path(stored_path))
|
||||
points = _planned_route_points_in_project_crs(project_root)
|
||||
if not points:
|
||||
logger.warning("자동 설계 체인 중단(계획노선 CSV 없음): project_id=%s", project_id)
|
||||
return
|
||||
|
||||
# 3) B05 경로 계산 — WF1 자동 확정과 같은 config 기본값을 쓴다.
|
||||
defaults = surface_confirmation_defaults()
|
||||
request = RouteSolveRequest(
|
||||
filter_key=str(defaults["source_filter"]),
|
||||
method=str(defaults["method"]),
|
||||
smooth=bool(defaults["smooth"]),
|
||||
surface_model_id=surface_model_id,
|
||||
bp=RoutePoint(**points[0]),
|
||||
ep=RoutePoint(**points[-1]),
|
||||
cp=[
|
||||
RoutePoint(**point, order=index)
|
||||
for index, point in enumerate(points[1:-1], start=1)
|
||||
],
|
||||
)
|
||||
solve_result: Any = await solve_route(project_id, request)
|
||||
if isinstance(solve_result, JSONResponse):
|
||||
logger.error(
|
||||
"자동 설계 체인 중단(B05 경로 계산 실패): project_id=%s status=%s",
|
||||
project_id,
|
||||
solve_result.status_code,
|
||||
)
|
||||
return
|
||||
route_id = int(solve_result.route_id)
|
||||
logger.info(
|
||||
"자동 설계 체인 B05 경로 계산 완료: project_id=%s route_id=%s length=%.1fm",
|
||||
project_id,
|
||||
route_id,
|
||||
float(solve_result.total_length_m or 0.0),
|
||||
)
|
||||
|
||||
# 4) B05 경로 확정 — stage 2 완료 전이 포함.
|
||||
confirm_result = await confirm_latest_route(project_id, None)
|
||||
if isinstance(confirm_result, JSONResponse):
|
||||
logger.error(
|
||||
"자동 설계 체인 중단(B05 경로 확정 실패): project_id=%s status=%s",
|
||||
project_id,
|
||||
confirm_result.status_code,
|
||||
)
|
||||
return
|
||||
|
||||
# 5) B06 횡단 설계 확정 — 미지정 측점을 기본값으로 채워 저장, stage 3 완료 전이 포함.
|
||||
sections_result = await confirm_sections(project_id, route_id, None)
|
||||
if isinstance(sections_result, JSONResponse):
|
||||
logger.error(
|
||||
"자동 설계 체인 중단(B06 횡단 확정 실패): project_id=%s route_id=%s status=%s",
|
||||
project_id,
|
||||
route_id,
|
||||
sections_result.status_code,
|
||||
)
|
||||
return
|
||||
logger.info(
|
||||
"자동 설계 체인 완료(B05·B06 기본값 확정): project_id=%s route_id=%s",
|
||||
project_id,
|
||||
route_id,
|
||||
)
|
||||
except Exception:
|
||||
# 체인은 업로드·WF1 흐름의 부가 작업이다 — 어떤 예외도 밖으로 던지지 않는다.
|
||||
logger.exception("자동 설계 체인 실패: project_id=%s", project_id)
|
||||
Reference in New Issue
Block a user