"use client" import * as React from "react" import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose, } from "@/components/ui/sheet" import { Button } from "@/components/ui/button" import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, } from "@/components/ui/file-list" import { Badge } from "@/components/ui/badge" import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel } from "@/components/ui/form" import { toast } from "sonner" import { Download, Loader, Trash2, X } from "lucide-react" import prettyBytes from "pretty-bytes" import { useSession } from "next-auth/react" import { useForm } from "react-hook-form" import { formatDate } from "@/lib/utils" import { getTechSalesVendorQuotationEmlAttachments, processTechSalesVendorQuotationEmlAttachments, } from "@/lib/techsales-rfq/service" const MAX_FILE_SIZE = 6e8 // 600MB export interface VendorEmlAttachment { id: number quotationId: number revisionId: number fileName: string originalFileName: string fileSize: number fileType: string | null filePath: string description: string | null uploadedBy: number | null vendorId: number | null isVendorUpload: boolean createdAt: Date updatedAt: Date } interface QuotationInfo { id: number quotationCode: string | null vendorName?: string rfqCode?: string } interface TechSalesVendorEmlAttachmentsSheetProps extends React.ComponentPropsWithRef { quotation: QuotationInfo | null attachments: VendorEmlAttachment[] onAttachmentsChange?: (attachments: VendorEmlAttachment[]) => void isLoading?: boolean } export function TechSalesVendorEmlAttachmentsSheet({ quotation, attachments, onAttachmentsChange, isLoading = false, ...props }: TechSalesVendorEmlAttachmentsSheetProps) { const session = useSession() const [isPending, setIsPending] = React.useState(false) const [existing, setExisting] = React.useState(attachments) const [newUploads, setNewUploads] = React.useState([]) const [deleteIds, setDeleteIds] = React.useState([]) const form = useForm({ defaultValues: { dummy: true, }, }) // sync when parent changes React.useEffect(() => { setExisting(attachments) setNewUploads([]) setDeleteIds([]) }, [attachments]) const handleDownloadClick = React.useCallback(async (attachment: VendorEmlAttachment) => { try { const { downloadFile } = await import("@/lib/file-download") await downloadFile(attachment.filePath, attachment.originalFileName || attachment.fileName, { showToast: true, onError: (error) => { console.error("다운로드 오류:", error) toast.error(error) }, }) } catch (error) { console.error("다운로드 오류:", error) toast.error("파일 다운로드 중 오류가 발생했습니다.") } }, []) const handleDropAccepted = React.useCallback((accepted: File[]) => { setNewUploads((prev) => [...prev, ...accepted]) }, []) const handleDropRejected = React.useCallback(() => { toast.error("파일 크기가 너무 크거나 지원하지 않는 형식입니다.") }, []) const handleRemoveExisting = React.useCallback((id: number) => { setDeleteIds((prev) => (prev.includes(id) ? prev : [...prev, id])) setExisting((prev) => prev.filter((att) => att.id !== id)) }, []) const handleRemoveNewUpload = React.useCallback((index: number) => { setNewUploads((prev) => prev.filter((_, i) => i !== index)) }, []) const handleSubmit = async () => { if (!quotation) { toast.error("견적 정보를 찾을 수 없습니다.") return } const userId = Number(session.data?.user.id || 0) if (!userId) { toast.error("로그인 정보를 확인해주세요.") return } setIsPending(true) try { const result = await processTechSalesVendorQuotationEmlAttachments({ quotationId: quotation.id, newFiles: newUploads.map((file) => ({ file })), deleteAttachmentIds: deleteIds, uploadedBy: userId, }) if (result.error) { toast.error(result.error) return } const refreshed = result.data || (await getTechSalesVendorQuotationEmlAttachments(quotation.id)).data || [] setExisting(refreshed) setNewUploads([]) setDeleteIds([]) onAttachmentsChange?.(refreshed) toast.success("Eml 첨부파일이 저장되었습니다.") props.onOpenChange?.(false) } catch (error) { console.error("eml 첨부파일 저장 오류:", error) toast.error("eml 첨부파일 저장 중 오류가 발생했습니다.") } finally { setIsPending(false) } } const totalNewSize = newUploads.reduce((acc, f) => acc + f.size, 0) return ( eml 첨부파일
{quotation?.vendorName &&
벤더: {quotation.vendorName}
} {quotation?.rfqCode &&
RFQ: {quotation.rfqCode}
}
e.preventDefault()} className="flex flex-1 flex-col gap-6"> {/* 기존 첨부 */}
기존 첨부파일 ({existing.length}개)
{isLoading ? (
로딩 중...
) : existing.length === 0 ? (
첨부파일이 없습니다.
) : ( existing.map((att) => (

{att.originalFileName || att.fileName}

rev {att.revisionId}

{prettyBytes(att.fileSize)} • {formatDate(att.createdAt, "KR")}

{att.description && (

{att.description}

)}
)) )}
{/* 새 업로드 */} {({ maxSize }) => ( ( 새 eml 파일 업로드
파일을 드래그하거나 클릭하세요 최대 크기: {maxSize ? prettyBytes(maxSize) : "600MB"}
복수 파일 업로드 가능
)} /> )}
{newUploads.length > 0 && (
새 파일 ({newUploads.length}개)
총 용량 {prettyBytes(totalNewSize)}
{newUploads.map((file, idx) => ( {file.name} {prettyBytes(file.size)} handleRemoveNewUpload(idx)}> 제거 ))}
)}
) }