diff --git a/B03_FileInput/B03_FileInput_Router.py b/B03_FileInput/B03_FileInput_Router.py index fb748686..5349a403 100644 --- a/B03_FileInput/B03_FileInput_Router.py +++ b/B03_FileInput/B03_FileInput_Router.py @@ -317,8 +317,17 @@ async def upload_project_files( content={"status": "error", "message": "동일한 파일명을 중복 업로드할 수 없습니다."}, ) las_count = sum(Path(filename).suffix.lower() in {".las", ".laz"} for filename in filenames) - # LAS 없는 설계(las_free)는 LAS 0개를 허용한다 — 올렸다면 정상 경로로 취급. - if las_count != 1 and not (las_free and las_count == 0): + # LAS 없는 설계(las_free)는 LAS를 **0개만** 받는다. 섞여 들어오면 되돌린다 — + # 올려 두면 전처리가 어느 쪽 경로인지 갈리지 않는다(2026-08-30 사용자 지시). + if las_free and las_count: + return JSONResponse( + status_code=400, + content={ + "status": "error", + "message": "LAS 없이 설계를 켠 상태에서는 LAS/LAZ 파일을 올릴 수 없습니다.", + }, + ) + if not las_free and las_count != 1: return JSONResponse( status_code=400, content={ @@ -446,6 +455,16 @@ async def create_project_upload_session( session: dict[str, Any] = Depends(verify_session), ) -> ChunkSessionCreateResponse | JSONResponse: """대용량 파일 청크 업로드 세션을 생성한다.""" + # LAS 없는 설계를 켠 상태면 포인트클라우드는 받지 않는다 — 큰 LAS는 이 경로로 + # 들어오므로 여기서 막지 않으면 `/files` 검사를 통째로 비켜 간다. + if payload.las_free and Path(payload.original_filename).suffix.lower() in {".las", ".laz"}: + return JSONResponse( + status_code=400, + content={ + "status": "error", + "message": "LAS 없이 설계를 켠 상태에서는 LAS/LAZ 파일을 올릴 수 없습니다.", + }, + ) chunk_size_bytes = min(payload.chunk_size_bytes, UPLOAD_CHUNK_SIZE_BYTES) total_chunks = _total_chunks(payload.size_bytes, chunk_size_bytes) session_id = str(uuid4()) diff --git a/B03_FileInput/B03_FileInput_UI_Page.ts b/B03_FileInput/B03_FileInput_UI_Page.ts index 3dd678ca..a9372e94 100644 --- a/B03_FileInput/B03_FileInput_UI_Page.ts +++ b/B03_FileInput/B03_FileInput_UI_Page.ts @@ -332,8 +332,24 @@ export async function renderB03FileInput(root: HTMLElement): Promise { renderSlot(state.slot); } - function onFileSelected(files: readonly File[], targetSlot?: FileSlot): void { - if (files.length === 0) return; + function onFileSelected( + selection: readonly File[], + targetSlot?: FileSlot, + ): void { + if (selection.length === 0) return; + // LAS 없이 설계를 켜면 포인트클라우드는 아예 받지 않는다 (2026-08-30 사용자 지시) — + // 카드를 회색으로 덮어도 파일 선택 영역·드롭으로 들어올 수 있어 여기서 걸러 낸다. + const pointCloudExtensions = slots.get("las_laz")?.extensions ?? []; + const files = lasFreeDesign + ? selection.filter( + (file) => !pointCloudExtensions.includes(getExtension(file.name)), + ) + : selection; + const blocked = files.length !== selection.length; + if (blocked && files.length === 0) { + pageError.textContent = L("B03_File_Error_LasFreeBlocked"); + return; + } // 개수는 "고른 파일 수"가 아니라 **최종적으로 차는 슬롯 수**로 센다. // 같은 슬롯을 다시 고르는 것은 교체라 개수가 늘지 않는다 — 더하기로 세면 5개를 고른 // 뒤 파일 선택 영역으로 하나만 바꾸려 해도 초과로 막힌다(2026-08-08). @@ -351,7 +367,7 @@ export async function renderB03FileInput(root: HTMLElement): Promise { pageError.textContent = L("B03_File_Error_Count"); return; } - pageError.textContent = ""; + pageError.textContent = blocked ? L("B03_File_Error_LasFreeBlocked") : ""; void (async () => { for (const file of files) await assignFileToSlot(file, targetSlot); await detectPausedUploads(); @@ -773,9 +789,18 @@ export async function renderB03FileInput(root: HTMLElement): Promise { lasFreeRow.append(lasFreeCheck, lasFreeText); function applyLasFreeState(): void { lasFreeCheck.checked = lasFreeDesign; - cardMap - .get("las_laz") - ?.classList.toggle("b03-file__card--disabled", lasFreeDesign); + const card = cardMap.get("las_laz"); + card?.classList.toggle("b03-file__card--disabled", lasFreeDesign); + // 카드를 회색으로 덮는 것만으로는 선택이 막히지 않는다 — 버튼·input을 실제로 잠근다. + card + ?.querySelectorAll( + ".b03-file__card-select, .b03-file__slot-input", + ) + .forEach((element) => { + element.disabled = lasFreeDesign; + }); + // 켜기 전에 이미 골라 둔 LAS는 내린다 — 켠 채로 남아 올라가는 사고를 막는다. + if (lasFreeDesign && slots.get("las_laz")?.file) removeFile("las_laz"); } lasFreeCheck.addEventListener("change", () => { lasFreeDesign = lasFreeCheck.checked; diff --git a/ui_template/ui_template_locale_b1.ts b/ui_template/ui_template_locale_b1.ts index 6a3bbcb0..44277d3f 100644 --- a/ui_template/ui_template_locale_b1.ts +++ b/ui_template/ui_template_locale_b1.ts @@ -299,6 +299,10 @@ export const ui_locales_b1 = { "LAS 또는 LAZ 파일을 정확히 1개 선택하세요.", "Select exactly one LAS or LAZ file.", ], + B03_File_Error_LasFreeBlocked: [ + "LAS 없이 설계를 켠 상태에서는 LAS/LAZ 파일을 넣을 수 없습니다.", + "LAS/LAZ files cannot be added while designing without LAS.", + ], B03_File_Error_Extension: [ "허용되지 않은 파일 형식입니다.", "Unsupported file type.",