Files
Aislo/B01_Dashboard/B01_Dashboard_UI_TempModal.ts
T
eomsangdonandClaude Fable 5 f60ecc255f feat(B01): 임시 보관함 UI를 그룹+파일 리스트 컨테이너로 개편
프로젝트/사용자 관리 컨테이너와 같은 형태로 통일한다. 등록 폼을 화면에 상시
노출하지 않고, 우측 상단 [+] 모달로 받아 그룹(임시 프로젝트명) 아래 파일 표를
그린다.

프론트엔드
- B01_Dashboard_UI_TempModal.ts 신설: 등록/추가 모달(임시 프로젝트명 + 파일 선택,
  고른 파일을 모달 안 표로 표시, 하단 [취소][확인] — 기존 대시보드 모달과 동일 규격)
- B01_Dashboard_UI_TempUpload.ts: 섹션 전체(제목·[+]·목록)를 반환하도록 변경.
  그룹 카드 + 파일 표(종류/파일명/크기/상태/작업), 그룹별 [파일 추가],
  파일별 [삭제], 진행률은 해당 파일 행 안에서 표시
- 청크마다 목록을 다시 조회하지 않고 막대 DOM만 갱신하도록 정리
- 대시보드 배치: 프로젝트 컨테이너 바로 아래(역할 3분기 모두)

백엔드
- DELETE /api/temp-uploads/{batch_id}/files/{file_type}: DB 행과 임시 저장소
  실제 파일을 함께 삭제. 필수 파일이 빠지면 상태를 uploading으로 되돌리고
  만료일은 최초 완료 시점 기준을 유지

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-08 13:23:58 +09:00

147 lines
4.9 KiB
TypeScript

/* =============================================================================
* B01_Dashboard_UI_TempModal.ts
* 임시 보관함 — 자료 등록/파일 추가 모달.
*
* 모달 껍데기는 대시보드의 다른 모달(B01_Dashboard_UI_Modals)과 같은 클래스를 써서
* 생김새를 맞춘다. 파일을 고르면 모달 안에 리스트로 보여 주고, [확인]을 눌러야
* 그룹 생성·업로드가 시작된다(2026-08-08 사용자 지시).
*
* 크기·확장자 표기 헬퍼도 여기에 둔다 — 보관함 본체(TempUpload)가 이 파일을 가져다
* 쓰므로 import 방향이 한쪽으로만 흐른다.
* ========================================================================== */
import { createButton, createInputField, showToast } from "@ui/ui_template_elements";
import { table, text } from "@ui/ui_template_general_blocks";
import { L } from "./B01_Dashboard_UI_Common";
/** 파일 확장자 = 보관함 슬롯 종류(csv·las·prj·tfw·tif). */
export function tempFileType(fileName: string): string {
const index = fileName.lastIndexOf(".");
return index >= 0 ? fileName.slice(index + 1).toLowerCase() : "";
}
export function formatTempBytes(bytes: number): string {
const gb = bytes / 1024 / 1024 / 1024;
if (gb >= 1) return `${gb.toFixed(2)} GB`;
const mb = bytes / 1024 / 1024;
if (mb >= 1) return `${mb.toFixed(1)} MB`;
return `${Math.max(1, Math.round(bytes / 1024))} KB`;
}
export interface TempModalOptions {
title: string;
/** 신규 등록이면 true — 그룹 이름을 함께 받는다. 기존 그룹 파일 추가면 false. */
askName: boolean;
/** [확인] 눌렀을 때. 모달은 먼저 닫히고, 업로드는 목록에서 진행 상황을 보여 준다. */
onConfirm: (payload: { name: string; files: File[] }) => void;
}
export function openTempFileModal(options: TempModalOptions): void {
const modal = document.createElement("div");
modal.className = "b01-dashboard__modal";
const panel = document.createElement("div");
panel.className = "b01-dashboard__modal-panel";
const heading = document.createElement("h3");
heading.className = "b01-dashboard__modal-title";
heading.textContent = options.title;
const nameField = createInputField({
label: L("B01_Temp_Field_Name"),
placeholder: L("B01_Temp_Field_Name_Placeholder"),
required: true,
});
const picker = document.createElement("input");
picker.type = "file";
picker.multiple = true;
picker.accept = ".csv,.las,.laz,.tif,.tfw,.prj";
picker.className = "b01-temp__hidden-input";
const pickRow = document.createElement("div");
pickRow.className = "b01-temp__modal-pick";
const caption = document.createElement("span");
caption.textContent = L("B01_Temp_Field_Files");
pickRow.append(
caption,
createButton({
label: L("B01_Temp_Btn_Pick"),
variant: "ghost",
onClick: () => picker.click(),
}),
);
const listHost = document.createElement("div");
listHost.className = "b01-temp__modal-list";
const chosen: File[] = [];
function renderList(): void {
listHost.replaceChildren(
table(
[
L("B01_Temp_Table_Type"),
L("B01_Temp_Table_Name"),
L("B01_Temp_Table_Size"),
L("B01_Temp_Table_Action"),
],
chosen.map((file) => [
text(tempFileType(file.name).toUpperCase()),
text(file.name),
text(formatTempBytes(file.size)),
createButton({
label: L("Common_Btn_Delete"),
variant: "ghost",
onClick: () => {
const index = chosen.indexOf(file);
if (index >= 0) chosen.splice(index, 1);
renderList();
},
}),
]),
),
);
}
picker.addEventListener("change", () => {
for (const file of Array.from(picker.files ?? [])) {
// 같은 파일을 두 번 고른 경우는 무시한다.
const duplicated = chosen.some((item) => item.name === file.name && item.size === file.size);
if (!duplicated) chosen.push(file);
}
picker.value = "";
renderList();
});
const actions = document.createElement("div");
actions.className = "b01-dashboard__actions";
actions.append(
createButton({
label: L("Common_Btn_Cancel"),
variant: "ghost",
onClick: () => modal.remove(),
}),
createButton({
label: L("Common_Btn_Confirm"),
onClick: () => {
const name = nameField.input.value.trim();
if (options.askName && !name) {
showToast(L("B01_Temp_Error_Name"), "error");
return;
}
if (chosen.length === 0) {
showToast(L("B01_Temp_Error_Files"), "error");
return;
}
modal.remove();
options.onConfirm({ name, files: [...chosen] });
},
}),
);
renderList();
panel.append(heading);
if (options.askName) panel.append(nameField.root);
panel.append(pickRow, listHost, picker, actions);
modal.append(panel);
document.body.append(modal);
}