fix(B03): LAS 없이 설계를 켜면 LAS/LAZ를 아예 받지 않는다
사용자 보고(2026-08-30): 체크한 상태에서 실수로 넣은 LAS가 그대로 업로드됐다. 카드에 --disabled 클래스만 걸려 있어 회색으로 보일 뿐 선택 버튼과 input이 살아 있었고, 서버도 las_free일 때 LAS 1개를 정상 경로로 받아 줬다. - 화면: 선택 버튼·file input을 실제로 잠그고, 켤 때 이미 고른 LAS는 내린다. 파일 선택 영역·드롭으로 들어오는 것은 onFileSelected에서 걸러 낸다. - 서버(신뢰 경계): /files는 las_free면 LAS 0개만 받는다. 큰 LAS가 타는 청크 업로드 세션 생성도 같은 검사를 건다 — 여기서 막지 않으면 /files 검사를 통째로 비켜 간다.
This commit is contained in:
@@ -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())
|
||||
|
||||
@@ -332,8 +332,24 @@ export async function renderB03FileInput(root: HTMLElement): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<HTMLButtonElement | HTMLInputElement>(
|
||||
".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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user