Files
Aislo/B03_FileInput/B03_FileInput_UI_TempPicker.ts
T
eomsangdonandClaude Opus 5 a00c7497c1 feat(B03): 보관함 선택 요약·「업로드할 파일을 선택하세요」 경고 제거
사용자 지시 2건(2026-09-03).

- 임시 보관함 선택 요약(「선택된 보관 자료 없음」·묶음 이름·파일 수)을 없앰. 불러온 자료는
  카드가 곧바로 보여 주고, 고르기 전 상태는 [파일 업로드]가 잠긴 것으로 이미 드러남.
  쓰이지 않게 된 `.b03-file__temp-summary` 규칙과 로케일 2건도 함께 제거.
- 입력 컨테이너의 「업로드할 파일을 선택하세요」 경고를 띄우지 않음 — 아직 아무것도 고르지
  않은 상태를 붉은 글씨로 알릴 이유가 없음. 확장자·개수·필수 슬롯 같은 나머지 사유와
  [파일 업로드]를 눌렀을 때의 검사 결과는 그대로 표시.

검증: 실측 — 보관함 자리에 버튼 하나만 남음(요약 span 없음), 오류 줄 빈 문자열,
한 줄 배치 선택 518px · 보관함 210px · 업로드 102px. typecheck·prettier 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 20:47:02 +09:00

152 lines
5.6 KiB
TypeScript

/* =============================================================================
* B03_FileInput_UI_TempPicker.ts
* 임시 보관함 불러오기 — 대시보드에 미리 올려 둔 자료를 이 프로젝트로 가져온다.
*
* 파일 업로드 컨테이너 안에 버튼을 두고, 누르면 **완료된 보관 묶음만** 목록으로
* 보여 준다. 선택 후 [확인]을 누르면 지정 상태가 되고, 실제 이동은 화면의
* [업로드] 버튼을 눌렀을 때 일어난다(2026-08-08 사용자 지시).
* ========================================================================== */
import { createButton, showToast } from "@ui/ui_template_elements";
import { currentLanguageIndex, ui_locales } from "@ui/ui_template_locale";
import { fetchTempBatches, type TempBatchItem } from "../B01_Dashboard/B01_Dashboard_Api_Temp";
import "./B03_FileInput_UI_Style_Temp.css";
function L(key: keyof typeof ui_locales): string {
return ui_locales[key][currentLanguageIndex];
}
function formatBytes(bytes: number): string {
const gb = bytes / 1024 / 1024 / 1024;
if (gb >= 1) return `${gb.toFixed(2)} GB`;
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
}
export interface TempPickerHandle {
/** 업로드 컨테이너에 붙일 요소(버튼 + 선택 안내). */
root: HTMLElement;
/** 지금 선택된 보관 묶음(없으면 null). */
selected(): TempBatchItem | null;
/** 연결 완료 후 선택 표시를 지운다. */
clear(): void;
}
/**
* 불러오기 버튼과 선택 모달을 만든다.
* `onSelected`는 선택이 바뀔 때마다 호출된다 — 호출측이 업로드 버튼 활성화를 조정한다.
*/
export function createTempPicker(
onSelected: (batch: TempBatchItem | null) => void,
): TempPickerHandle {
const root = document.createElement("div");
root.className = "b03-file__temp-picker";
const openButton = createButton({
label: L("B03_Temp_Btn_Open"),
variant: "ghost",
onClick: () => void openModal(),
});
// 선택 요약(「선택된 보관 자료 없음」·묶음 이름)은 두지 않는다 — 불러온 자료는 카드가
// 곧바로 보여 주고, 고르기 전 상태는 [파일 업로드]가 잠긴 것으로 이미 드러난다
// (2026-09-03 사용자 지시).
root.append(openButton);
let selectedBatch: TempBatchItem | null = null;
function applySelection(batch: TempBatchItem | null): void {
selectedBatch = batch;
onSelected(batch);
}
async function openModal(): Promise<void> {
let batches: TempBatchItem[] = [];
try {
const response = await fetchTempBatches();
// 프로젝트에 넣을 수 있는 것은 필수 파일이 다 찬 미연결 묶음뿐이다.
batches = response.batches.filter(
(item) => item.required_complete && item.status !== "linked",
);
} catch (error) {
showToast(error instanceof Error ? error.message : L("B03_Temp_Load_Failed"), "error");
return;
}
const backdrop = document.createElement("div");
backdrop.className = "b03-file__modal-backdrop";
const modal = document.createElement("div");
modal.className = "b03-file__modal b03-file__temp-modal";
modal.setAttribute("role", "dialog");
modal.setAttribute("aria-modal", "true");
const title = document.createElement("h3");
title.textContent = L("B03_Temp_Modal_Title");
const list = document.createElement("div");
list.className = "b03-file__temp-list";
let pending: TempBatchItem | null = selectedBatch;
if (batches.length === 0) {
const empty = document.createElement("p");
empty.className = "b03-file__temp-empty";
empty.textContent = L("B03_Temp_Modal_Empty");
list.append(empty);
}
for (const batch of batches) {
const option = document.createElement("button");
option.type = "button";
option.className = "b03-file__temp-option";
option.classList.toggle("is-selected", pending?.batch_id === batch.batch_id);
const name = document.createElement("strong");
name.textContent = batch.name;
const meta = document.createElement("span");
meta.textContent = `${batch.files.length}${L("B03_Temp_FileCount")} · ${formatBytes(
batch.total_size_bytes,
)}`;
const files = document.createElement("span");
files.className = "b03-file__temp-option-files";
files.textContent = batch.files.map((file) => file.file_type.toUpperCase()).join(", ");
option.append(name, meta, files);
option.addEventListener("click", () => {
pending = batch;
list
.querySelectorAll(".b03-file__temp-option")
.forEach((element) => element.classList.remove("is-selected"));
option.classList.add("is-selected");
});
list.append(option);
}
const actions = document.createElement("div");
actions.className = "b03-file__temp-modal-actions";
const close = (): void => backdrop.remove();
actions.append(
createButton({ label: L("Common_Btn_Cancel"), variant: "ghost", onClick: close }),
createButton({
label: L("Common_Btn_Confirm"),
variant: "filled",
onClick: () => {
if (!pending) {
showToast(L("B03_Temp_Select_Required"), "warning");
return;
}
applySelection(pending);
close();
},
}),
);
modal.append(title, list, actions);
backdrop.append(modal);
backdrop.addEventListener("click", (event) => {
if (event.target === backdrop) close();
});
document.body.append(backdrop);
}
return {
root,
selected: () => selectedBatch,
clear: () => applySelection(null),
};
}