/* ============================================================================= * B09_Estimation_UI_Tabs_Base_Quote.ts * 자재단가대비표 「견적서」 칸 작은 모달(PLAN 166-4) — 자재 줄 하나의 견적서 여러 파일 · * 목록 · 올리기(여러 개) · 내려받기 · 지우기. 파일은 올리는 즉시 서버에 둠([저장]과 따로). * * 길 = `B09_Estimation_Router_Quotes.py` · 바뀐 목록은 onChange(이름들)로 표 칸 「n 파일」에 반영 * ========================================================================== */ import { API_BASE_URL, CURRENT_PROJECT_ID_KEY } from "@config/config_frontend"; import { openModal } from "@ui/ui_template_modal"; import { createButton, el, showToast } from "@ui/ui_template_elements"; interface QuoteFile { name: string; size: number; at: string; } const url = (key: string, name?: string): string => { const projectId = localStorage.getItem(CURRENT_PROJECT_ID_KEY) ?? ""; const q = new URLSearchParams({ key, ...(name ? { name } : {}) }); return `${API_BASE_URL}/projects/${projectId}/estimation/quotes${name ? "/file" : ""}?${q}`; }; async function call(input: string, init: RequestInit = {}): Promise { const response = await fetch(input, { credentials: "include", ...init }); if (!response.ok) { const body = (await response.json().catch(() => null)) as { detail?: unknown } | null; throw new Error(typeof body?.detail === "string" ? body.detail : `HTTP ${response.status}`); } return response; } async function download(key: string, name: string): Promise { const blob = await (await call(url(key, name))).blob(); const a = el("a", { attrs: { href: URL.createObjectURL(blob), download: name } }); a.click(); URL.revokeObjectURL(a.href); } /** 견적서 모달 — label = 「품명 규격」 */ export function openQuotes(key: string, label: string, onChange: (names: string[]) => void): void { openModal({ title: `견적서 — ${label}`, closeLabel: "닫기", mount: (body) => { const list = el("ul", { className: "b09-quotes" }); const show = (files: QuoteFile[]): void => { onChange(files.map((f) => f.name)); list.replaceChildren( ...(files.length ? files.map((f) => el("li", { children: [ createButton({ label: `${f.name} (${Math.ceil(f.size / 1024).toLocaleString()} KB)`, variant: "ghost", size: "sm", onClick: () => void download(key, f.name).catch(fail), }), createButton({ label: "지우기", variant: "danger", size: "sm", onClick: () => void call(url(key, f.name).replace("/file?", "?"), { method: "DELETE" }) .then((r) => r.json()) .then(show, fail), }), ], }), ) : [el("li", { text: "올린 견적서 없음" })]), ); }; const fail = (error: unknown): void => showToast(`하지 못했습니다 — ${String(error)}`, "error"); const input = el("input", { attrs: { type: "file", multiple: "" } }); input.addEventListener("change", () => { const form = new FormData(); for (const file of Array.from(input.files ?? [])) form.append("files", file); input.value = ""; void call(url(key), { method: "POST", body: form }) .then((r) => r.json()) .then(show, fail); }); const pick = createButton({ label: "견적서 올리기(여러 개)", variant: "filled", size: "sm", onClick: () => input.click(), }); input.hidden = true; body.append(pick, input, list); void call(url(key)) .then((r) => r.json()) .then(show, fail); }, }); }