260705_2
This commit is contained in:
@@ -1,29 +1,188 @@
|
||||
/* =============================================================================
|
||||
* B03_FileInput_UI_Page.ts
|
||||
* 로그인 후 03: 파일 입력 (지형·포인트클라우드·도면 업로드)
|
||||
*
|
||||
* ⚠️ 본문(업로드 드롭존/파일 목록)은 준비 중 — 헤더/안내만 구현.
|
||||
* 실제 업로드 UI는 0_old 참고하여 추후 구체화.
|
||||
*
|
||||
* 제약 준수 (frontend.md): 문구는 locale 참조(§3), 공통 컴포넌트 사용(§2).
|
||||
* ========================================================================== */
|
||||
/* B03 파일 입력 페이지 */
|
||||
|
||||
import { ui_locales, currentLanguageIndex } from "@ui/ui_template_locale";
|
||||
import { renderPendingContent } from "../A00_Common/b_page_scaffold";
|
||||
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";
|
||||
|
||||
/** locale 헬퍼 */
|
||||
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 {
|
||||
renderPendingContent(root, {
|
||||
pageClass: "b03-file",
|
||||
title: L("B03_File_Title"),
|
||||
subtitle: L("B03_File_Subtitle"),
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user