fix(B03): LAS 없이 설계 업로드가 지형 PRJ·TFW 필수 검사에 막히던 것 해소

- 「LAS 없이 설계」면 지형 한 벌(포인트클라우드·지형 PRJ·TFW)을 필수에서 면제
- 업로드 요청 검사도 완료 검사와 같은 기준으로 노선 PRJ 와 지형 PRJ 를 갈라 셈
  (종전에는 확장자만 봐서 노선 PRJ 를 지형 PRJ 로 오인)
- 업로드 현황의 「필수 충족」은 stage 0 완료를 우선 근거로 판정 — B04 이동 버튼 복구

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-06 17:45:43 +09:00
co-authored by Claude Opus 5
parent d0d3f2a311
commit 2d0895d975
2 changed files with 42 additions and 7 deletions
+11 -5
View File
@@ -94,6 +94,9 @@ from B03_FileInput.B03_FileInput_Router_Helpers import (
from B03_FileInput.B03_FileInput_Router_Helpers import (
_write_stage_metadata as _write_stage_metadata,
)
from B03_FileInput.B03_FileInput_Router_Helpers import (
upload_file_types as upload_file_types,
)
from B03_FileInput.B03_FileInput_Schema import (
FileUploadDescriptor,
FileUploadResponse,
@@ -156,12 +159,13 @@ async def upload_project_files(
"message": "LAS 없이 설계를 켠 상태에서는 LAS/LAZ 파일을 올릴 수 없습니다.",
},
)
if not las_free and las_count != 1:
# 지형 파일은 도엽별로 여러 장이 올 수 있다 — 합쳐서 전처리한다(2026-09-06 사용자 확정).
if not las_free and las_count < 1:
return JSONResponse(
status_code=400,
content={
"status": "error",
"message": "LAS 또는 LAZ 파일을 정확히 1개 포함해야 합니다.",
"message": "LAS 또는 LAZ 파일을 1개 이상 포함해야 합니다.",
},
)
# 계획노선은 shapefile 또는 CSV 한 벌이다 (2026-08-31) — 문구도 그렇게 맞춘다
@@ -175,7 +179,7 @@ async def upload_project_files(
"message": "계획노선 파일(shapefile 의 .shp 또는 .csv)을 정확히 1개 포함해야 합니다.",
},
)
request_file_types = {Path(filename).suffix.lower().lstrip(".") for filename in filenames}
request_file_types = upload_file_types(filenames)
missing_required = _missing_required_file_types(request_file_types, las_free)
if missing_required:
return JSONResponse(
@@ -350,8 +354,10 @@ async def get_project_upload_overview(
)
for row in sessions
],
required_complete=_REQUIRED_FILE_TYPES <= file_types
and (point_cloud_id is not None or stage0_complete),
# stage 0 을 마쳤으면 서버 필수검사를 이미 통과한 것이다 — LAS 없이 설계는
# 지형 한 벌(prj·tfw)이 아예 없으므로 여기서 다시 세면 B04 이동이 막힌다.
required_complete=stage0_complete
or (_REQUIRED_FILE_TYPES <= file_types and point_cloud_id is not None),
analysis_complete=analysis_complete,
)
except Exception:
+31 -2
View File
@@ -62,14 +62,34 @@ def _is_point_cloud_result(result: UploadedFileResult) -> bool:
return result.file_type.lower() in _POINT_CLOUD_FILE_TYPES
def upload_file_types(filenames: list[str]) -> set[str]:
"""업로드 요청의 파일명을 **완료 검사와 같은 기준**으로 유형화한다.
확장자만 세면 노선 세트의 `.prj`가 지형 PRJ로 오인돼, 요청 검사는 통과하고 저장 뒤
완료 검사에서 누락으로 갈린다(2026-09-06 실측 — LAS 없이 설계가 이 어긋남으로 막혔다).
노선 도형(`.shp`)과 basename이 같은 PRJ는 화면의 카드 배정과 같은 규칙으로 `route_prj`.
"""
route_stem = next(
(Path(name).stem for name in filenames if Path(name).suffix.lower() == ".shp"), None
)
types: set[str] = set()
for name in filenames:
suffix = Path(name).suffix.lower().lstrip(".")
if suffix == "prj" and route_stem is not None and Path(name).stem == route_stem:
suffix = "route_prj"
types.add(suffix)
return types
def _missing_required_file_types(file_types: set[str], las_free: bool = False) -> list[str]:
missing = sorted(_REQUIRED_FILE_TYPES - file_types)
# LAS 없는 설계(도엽등고선 기반, 2026-08-30)는 지형 한 벌(포인트클라우드·지형 PRJ·
# TFW)을 통째로 받지 않는다 — 화면도 그 카드들을 필수에서 뺀다.
missing = [] if las_free else sorted(_REQUIRED_FILE_TYPES - file_types)
if not file_types.intersection(_ROUTE_FILE_TYPES):
missing.append("csv/shp")
# shapefile로 왔으면 형제 파일이 다 있어야 노선을 읽는다.
if "shp" in file_types:
missing.extend(sorted(_SHAPEFILE_REQUIRED_TYPES - file_types))
# LAS 없는 설계(도엽등고선 기반, 2026-08-30)는 LAS 필수를 면제한다.
if not las_free and not file_types.intersection(_POINT_CLOUD_FILE_TYPES):
missing.append("las/laz")
return missing
@@ -143,6 +163,15 @@ async def _complete_file_input_if_ready(
# 아직 다시 만들어지지 않은 뒷단계 화면이 옛 결과를 새 자료 것인 양 보여준다.
stored_path = await get_project_storage_relative_path(connection, project_id)
project_root = Path(resolve_stored_project_path(stored_path))
# 지형 파일 여러 장은 합쳐서 전처리한다 — 다른 사업지 파일이 섞이면 합친 범위가
# 통째로 어긋나므로 여기서 막는다(2026-09-06 사용자 지시). 머리글만 읽어 즉시 끝난다.
from B04_PreProcess.B04_PreProcess_Engine_Structurize import merge_gap_error
from B04_PreProcess.B04_PreProcess_Repository import list_project_point_cloud_paths
terrain_paths = await list_project_point_cloud_paths(connection, project_id, project_root)
gap_message = merge_gap_error(terrain_paths)
if gap_message:
raise ValueError(gap_message)
clear_designing(project_root)
discard_initial_snapshot(project_root)
await purge_project_outputs(connection, str(project_id), project_root)