/* B03 파일 입력 페이지 */ import { CURRENT_PROJECT_ID_KEY, UPLOAD_ALLOWED_EXT, UPLOAD_MAX_FILES, UPLOAD_MAX_MB, } from "@config/config_frontend"; import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale"; import { createButton, createCard, hideLoadingOverlay, showLoadingOverlay, showToast, } from "@ui/ui_template_elements"; import { uploadProjectFiles, type UploadedFileResult } from "./B03_FileInput_Api_Fetch"; import "./B03_FileInput_UI_Style.css"; function L(key: keyof typeof ui_locales): string { return ui_locales[key][currentLanguageIndex]; } function validateFiles(files: readonly File[]): string | null { if (files.length === 0) return L("B03_File_Error_Required"); if (files.length > UPLOAD_MAX_FILES) return L("B03_File_Error_Count"); const maximumBytes = UPLOAD_MAX_MB * 1024 * 1024; const normalizedNames = new Set(); let lasCount = 0; for (const file of files) { const extensionIndex = file.name.lastIndexOf("."); const extension = extensionIndex >= 0 ? file.name.slice(extensionIndex).toLowerCase() : ""; if (!UPLOAD_ALLOWED_EXT.includes(extension as (typeof UPLOAD_ALLOWED_EXT)[number])) { return `${L("B03_File_Error_Extension")} ${file.name}`; } if (file.size === 0 || file.size > maximumBytes) { return `${L("B03_File_Error_Size")} ${file.name}`; } const normalizedName = file.name.toLocaleLowerCase(); if (normalizedNames.has(normalizedName)) return `${L("B03_File_Error_Extension")} ${file.name}`; normalizedNames.add(normalizedName); if (extension === ".las" || extension === ".laz") lasCount += 1; } return lasCount === 1 ? null : L("B03_File_Error_Las"); } export function renderB03FileInput(root: HTMLElement): void { let selectedFiles: File[] = []; const page = document.createElement("div"); page.className = "b03-file"; const header = document.createElement("div"); header.className = "b03-file__header"; const title = document.createElement("h1"); title.className = "b03-file__title"; title.textContent = L("B03_File_Title"); const subtitle = document.createElement("p"); subtitle.className = "b03-file__subtitle"; subtitle.textContent = L("B03_File_Subtitle"); header.append(title, subtitle); const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.multiple = true; fileInput.accept = UPLOAD_ALLOWED_EXT.join(","); fileInput.className = "b03-file__native-input"; const dropzone = document.createElement("div"); dropzone.className = "b03-file__dropzone"; dropzone.tabIndex = 0; const dropzoneLabel = document.createElement("strong"); dropzoneLabel.textContent = L("B03_File_Select_Label"); const dropzoneHint = document.createElement("span"); dropzoneHint.textContent = L("B03_File_Select_Hint"); dropzone.append(dropzoneLabel, dropzoneHint, fileInput); const errorSlot = document.createElement("p"); errorSlot.className = "b03-file__error"; errorSlot.setAttribute("role", "alert"); const selectedTitle = document.createElement("h2"); selectedTitle.className = "b03-file__section-title"; selectedTitle.textContent = L("B03_File_Selected_Title"); const selectedList = document.createElement("ul"); selectedList.className = "b03-file__list"; const resultList = document.createElement("ul"); resultList.className = "b03-file__results"; function renderSelectedFiles(): void { selectedList.replaceChildren(); if (selectedFiles.length === 0) { const empty = document.createElement("li"); empty.className = "b03-file__empty"; empty.textContent = L("B03_File_Selected_Empty"); selectedList.append(empty); return; } for (const file of selectedFiles) { const item = document.createElement("li"); const name = document.createElement("span"); name.textContent = file.name; const size = document.createElement("span"); size.textContent = `${(file.size / (1024 * 1024)).toFixed(2)} MB`; item.append(name, size); selectedList.append(item); } } function setSelectedFiles(files: readonly File[]): void { selectedFiles = Array.from(files); errorSlot.textContent = validateFiles(selectedFiles) ?? ""; renderSelectedFiles(); } function renderUploadResults(results: readonly UploadedFileResult[]): void { resultList.replaceChildren(); for (const result of results) { const item = document.createElement("li"); const filename = document.createElement("strong"); filename.textContent = result.original_filename; const path = document.createElement("span"); path.textContent = `${L("B03_File_Result_Path")}: ${result.relative_path}`; item.append(filename, path); resultList.append(item); } } function onB03_File_Select_Change(): void { setSelectedFiles(fileInput.files ? Array.from(fileInput.files) : []); } function onB03_File_Drop(event: DragEvent): void { event.preventDefault(); dropzone.classList.remove("is-dragging"); setSelectedFiles(event.dataTransfer?.files ? Array.from(event.dataTransfer.files) : []); } async function onB03_File_Upload_Click(): Promise { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY); const validationError = validateFiles(selectedFiles); if (!projectId) { errorSlot.textContent = L("B03_File_Error_Project"); return; } if (validationError) { errorSlot.textContent = validationError; return; } errorSlot.textContent = ""; showLoadingOverlay(); try { const response = await uploadProjectFiles(projectId, selectedFiles); renderUploadResults(response.files); showToast(L("B03_File_Upload_Success"), "success"); } catch (error) { const detail = error instanceof Error ? error.message : L("B03_File_Upload_Failed"); errorSlot.textContent = `${L("B03_File_Upload_Failed")} ${detail}`; showToast(L("B03_File_Upload_Failed"), "error"); } finally { hideLoadingOverlay(); } } fileInput.addEventListener("change", onB03_File_Select_Change); dropzone.addEventListener("click", () => fileInput.click()); dropzone.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") fileInput.click(); }); dropzone.addEventListener("dragover", (event) => { event.preventDefault(); dropzone.classList.add("is-dragging"); }); dropzone.addEventListener("dragleave", () => dropzone.classList.remove("is-dragging")); dropzone.addEventListener("drop", onB03_File_Drop); const uploadButton = createButton({ label: L("B03_File_Upload_Button"), variant: "filled", onClick: () => void onB03_File_Upload_Click(), }); const body = document.createElement("div"); body.className = "b03-file__body"; body.append(dropzone, errorSlot, selectedTitle, selectedList, uploadButton, resultList); page.append(header, createCard({ body: [body], raised: true })); root.replaceChildren(page); renderSelectedFiles(); }