Files
Aislo/B03_FileInput/B03_FileInput_UI_Page.ts
T
2026-07-05 21:27:23 +09:00

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();
}