189 lines
6.9 KiB
TypeScript
189 lines
6.9 KiB
TypeScript
/* 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<string>();
|
|
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<void> {
|
|
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();
|
|
}
|