분석이 30초 걸리는데 B05는 일반 사용자 화면이다. 관리자 확인용 B04에서
한 번 돌려 저장하고, B05는 그 결과를 읽어 관 보충과 세부유역만 처리한다
(2026-07-31 사용자 지시).
노선 원천 변경
- B05 확정 경로 -> B03 업로드 계획 노선 파일(CSV). 분석이 노선 설계보다
먼저 끝나 있어야 하기 때문. 샘플 planned_route_sample_epsg5187.csv 로 검증.
- common_util_route_geometry.py 신설 — RouteVertex/StructureCandidate/누가거리
보간/세류 교차점/계획 노선 CSV 리더. B04와 B05가 같은 표현을 쓰도록 공용화.
열 이름은 대소문자·한글 표기를 함께 받는다(B03이 여러 형식 수용 예정).
B04 (관리자 확인용, 신규)
- Engine_Watershed_{Grid,Stream,Descent,Flow,Expand,Export} — B05에서 git mv
- Engine_Watershed_Analyze.py — 1~8단계 오케스트레이션
- Router_Watershed.py — GET /drainage/primary-region
- UI_Watershed.ts — 2D 지도 GIS 레이어 그룹에 "배수유역" 토글 추가.
격자/화살표/세류망/1차영역/2차유역/기본관을 겹쳐 그린다.
- 저장 위치 B05_wf2_Route/drainage -> B04_wf1_Surface/drainage
- 03_road_routing 단계 추가: B05가 세부유역을 나눌 최소 배열(셀->도로셀 귀속,
유하장, 강도, 도로셀 제원, 셀 표고) + 계획도로선/기본배관/2차유역 기하
B05 (일반 사용자용, 축소)
- Engine_Drainage_Basin.py — B04 산출물 로더 + 관 보충(9) + 측구 라우팅/세부유역(10,11)
- Engine_Drainage.py 는 관경 산정만 남기고 322 -> 27줄
- Router_Drainage.py 509 -> 142줄. POST /drainage/basins 만 남김
- 화살표·격자·강도 띠 렌더 제거. 계획도로선/기본배관/2차유역만 받는다
삭제
- _legacy_watershed/ 4파일 (능선 행진 방식 원본 보관본)
- Engine_Watershed_Basin.py (B04 Analyze + B05 Drainage_Basin 으로 분할)
- GET /drainage/candidates 와 propose_structure_stations (구방식 후보 제안)
E2E 검증 (실데이터)
B04 분석 28.2s -> 저장(geojson 11KB + npz 2.6MB)
B05 로드 + 세부 설계 0.11s <-- 30초가 0.1초로
면적 457,404m2 로 B04 2차 유역과 정확히 일치
관 편집 재산정 0.12s, 관 3개 -> 세부유역 3개, 면적 보존
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
107 lines
3.0 KiB
Python
107 lines
3.0 KiB
Python
"""B03 파일 입력 요청·응답 검증 모델."""
|
|
|
|
from pathlib import PurePath
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
|
|
from config.config_system import UPLOAD_ALLOWED_EXT, UPLOAD_CHUNK_SIZE_BYTES, UPLOAD_MAX_MB
|
|
|
|
|
|
class FileUploadDescriptor(BaseModel):
|
|
"""업로드 전에 검증할 단일 파일의 이름과 크기."""
|
|
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
original_filename: str = Field(min_length=1, max_length=255)
|
|
size_bytes: int = Field(gt=0)
|
|
|
|
@model_validator(mode="after")
|
|
def validate_file(self) -> "FileUploadDescriptor":
|
|
filename = self.original_filename
|
|
if PurePath(filename).name != filename or "/" in filename or "\\" in filename:
|
|
raise ValueError("파일명에는 경로가 포함될 수 없습니다.")
|
|
|
|
extension = PurePath(filename).suffix.lower()
|
|
if extension not in UPLOAD_ALLOWED_EXT:
|
|
allowed = ", ".join(UPLOAD_ALLOWED_EXT)
|
|
raise ValueError(f"허용되지 않은 파일 확장자입니다. 허용 형식: {allowed}")
|
|
|
|
maximum_bytes = UPLOAD_MAX_MB * 1024 * 1024
|
|
if self.size_bytes > maximum_bytes:
|
|
raise ValueError(f"파일 크기는 {UPLOAD_MAX_MB}MB를 초과할 수 없습니다.")
|
|
return self
|
|
|
|
|
|
class UploadedFileResult(BaseModel):
|
|
"""저장 완료된 단일 입력 파일 정보."""
|
|
|
|
input_file_id: int
|
|
original_filename: str
|
|
file_type: str
|
|
relative_path: str
|
|
size_bytes: int
|
|
metadata: dict[str, Any]
|
|
|
|
|
|
class FileUploadResponse(BaseModel):
|
|
"""B03 다중 파일 업로드 성공 응답."""
|
|
|
|
status: str = "success"
|
|
project_id: str
|
|
files: list[UploadedFileResult]
|
|
|
|
|
|
class ChunkSessionCreateRequest(FileUploadDescriptor):
|
|
"""청크 업로드 세션 생성 요청."""
|
|
|
|
chunk_size_bytes: int = Field(default=UPLOAD_CHUNK_SIZE_BYTES, gt=0)
|
|
|
|
|
|
class ChunkSessionCreateResponse(BaseModel):
|
|
"""청크 업로드 세션 생성 응답."""
|
|
|
|
status: str = "success"
|
|
project_id: str
|
|
upload_session_id: str
|
|
original_filename: str
|
|
file_size_bytes: int
|
|
chunk_size_bytes: int
|
|
total_chunks: int
|
|
completed_chunks: int = 0
|
|
|
|
|
|
class ChunkUploadResponse(BaseModel):
|
|
"""단일 청크 저장 응답."""
|
|
|
|
status: str = "success"
|
|
upload_session_id: str
|
|
chunk_index: int
|
|
completed_chunks: int
|
|
total_chunks: int
|
|
chunk_hash: str
|
|
|
|
|
|
class UploadFinalizeRequest(BaseModel):
|
|
"""청크 업로드 완료 및 병합 요청."""
|
|
|
|
model_config = ConfigDict(extra="forbid", str_strip_whitespace=True)
|
|
|
|
session_id: str = Field(min_length=1, max_length=36)
|
|
total_chunks: int = Field(gt=0)
|
|
complete_upload: bool = True
|
|
|
|
|
|
class UploadStatusResponse(BaseModel):
|
|
"""청크 업로드 상태 조회 응답."""
|
|
|
|
status: str = "success"
|
|
upload_session_id: str
|
|
upload_status: str
|
|
original_filename: str
|
|
file_size_bytes: int
|
|
chunk_size_bytes: int
|
|
total_chunks: int
|
|
completed_chunks: int
|
|
completed_chunk_indexes: list[int]
|