// lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx "use client" import * as React from "react" import { useState, useCallback } from "react" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" import { Alert, AlertDescription } from "@/components/ui/alert" import { ScrollArea } from "@/components/ui/scroll-area" import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list" import { Upload, X, CheckCircle2, AlertCircle, Loader2, CloudUpload, FileWarning } from "lucide-react" import { toast } from "sonner" import { validateFiles } from "../../document-stages-service" import { parseFileName, ParsedFileName } from "../util/filie-parser" interface FileWithMetadata { file: File parsed: ParsedFileName matched?: { documentId: number stageId: number documentTitle: string currentRevision?: string // number에서 string으로 변경 } status: 'pending' | 'validating' | 'uploading' | 'success' | 'error' error?: string progress?: number } interface MultiUploadDialogProps { projectId: number // projectCode: string onUploadComplete?: () => void } export function MultiUploadDialog({ projectId, // projectCode, onUploadComplete }: MultiUploadDialogProps) { const [open, setOpen] = useState(false) const [files, setFiles] = useState([]) const [isValidating, setIsValidating] = useState(false) const [isUploading, setIsUploading] = useState(false) // 디버깅용 로그 console.log("Current files:", files) // 파일 추가 핸들러 - onChange 이벤트용 const handleFilesChange = useCallback((e: React.ChangeEvent) => { const fileList = e.target.files console.log("Files selected via input:", fileList) if (fileList && fileList.length > 0) { handleFilesAdded(Array.from(fileList)) } }, []) // 파일 추가 핸들러 - 공통 const handleFilesAdded = useCallback(async (newFiles: File[]) => { console.log("handleFilesAdded called with:", newFiles) if (!newFiles || newFiles.length === 0) { console.log("No files provided") return } const processedFiles: FileWithMetadata[] = newFiles.map(file => { const parsed = parseFileName(file.name) console.log(`Parsed ${file.name}:`, parsed) return { file, parsed, status: 'pending' as const } }) setFiles(prev => { const updated = [...prev, ...processedFiles] console.log("Updated files state:", updated) return updated }) // 유효한 파일들만 검증 const validFiles = processedFiles.filter(f => f.parsed.isValid) console.log("Valid files for validation:", validFiles) if (validFiles.length > 0) { await validateFilesWithServer(validFiles) } }, []) // 서버 검증 const validateFilesWithServer = async (filesToValidate: FileWithMetadata[]) => { console.log("Starting validation for:", filesToValidate) setIsValidating(true) setFiles(prev => prev.map(file => filesToValidate.some(f => f.file === file.file) ? { ...file, status: 'validating' as const } : file )) try { const validationData = filesToValidate.map(f => ({ projectId, // projectCode 대신 projectId 사용 docNumber: f.parsed.docNumber, stageName: f.parsed.stageName, revision: f.parsed.revision })); console.log("Sending validation data:", validationData) const results = await validateFiles(validationData) console.log("Validation results:", results) // 매칭 결과 업데이트 - projectCode 체크 제거 setFiles(prev => prev.map(file => { const result = results.find(r => r.docNumber === file.parsed.docNumber && r.stageName === file.parsed.stageName ) if (result && result.matched) { console.log(`File ${file.file.name} matched:`, result.matched) return { ...file, matched: result.matched, status: 'pending' as const } } return { ...file, status: 'pending' as const } })) } catch (error) { console.error("Validation error:", error) toast.error("Failed to validate files") setFiles(prev => prev.map(file => filesToValidate.some(f => f.file === file.file) ? { ...file, status: 'error' as const, error: 'Validation failed' } : file )) } finally { setIsValidating(false) } } // Drag and Drop 핸들러 const handleDrop = useCallback((e: React.DragEvent) => { e.preventDefault() e.stopPropagation() const droppedFiles = Array.from(e.dataTransfer.files) console.log("Files dropped:", droppedFiles) if (droppedFiles.length > 0) { handleFilesAdded(droppedFiles) } }, [handleFilesAdded]) const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault() e.stopPropagation() }, []) // 파일 제거 const removeFile = (index: number) => { console.log("Removing file at index:", index) setFiles(prev => prev.filter((_, i) => i !== index)) } // 업로드 실행 const handleUpload = async () => { const uploadableFiles = files.filter(f => f.parsed.isValid && f.matched) console.log("Files to upload:", uploadableFiles) if (uploadableFiles.length === 0) { toast.error("No valid files to upload") return } setIsUploading(true) // 업로드 중 상태로 변경 setFiles(prev => prev.map(file => uploadableFiles.includes(file) ? { ...file, status: 'uploading' as const } : file )) try { const formData = new FormData() uploadableFiles.forEach((fileData, index) => { formData.append(`files`, fileData.file) formData.append(`metadata[${index}]`, JSON.stringify({ documentId: fileData.matched!.documentId, stageId: fileData.matched!.stageId, revision: fileData.parsed.revision, originalName: fileData.file.name })) }) console.log("Sending upload request") const response = await fetch('/api/stage-submissions/bulk-upload', { method: 'POST', body: formData }) if (!response.ok) { const error = await response.text() console.error("Upload failed:", error) throw new Error('Upload failed') } const result = await response.json() console.log("Upload result:", result) // 성공 상태 업데이트 setFiles(prev => prev.map(file => uploadableFiles.includes(file) ? { ...file, status: 'success' as const } : file )) toast.success(`Successfully uploaded ${result.uploaded} files`) setTimeout(() => { setOpen(false) setFiles([]) onUploadComplete?.() }, 2000) } catch (error) { console.error("Upload error:", error) toast.error("Upload failed") setFiles(prev => prev.map(file => uploadableFiles.includes(file) ? { ...file, status: 'error' as const, error: 'Upload failed' } : file )) } finally { setIsUploading(false) } } // 통계 계산 const stats = { total: files.length, valid: files.filter(f => f.parsed.isValid).length, matched: files.filter(f => f.matched).length, ready: files.filter(f => f.parsed.isValid && f.matched).length, totalSize: files.reduce((acc, f) => acc + f.file.size, 0) } const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes' const k = 1024 const sizes = ['Bytes', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] } // 파일별 상태 아이콘 const getStatusIcon = (fileData: FileWithMetadata) => { if (!fileData.parsed.isValid) { return } switch (fileData.status) { case 'validating': return case 'uploading': return case 'success': return case 'error': return default: if (fileData.matched) { return } else { return } } } // 파일별 상태 설명 const getStatusDescription = (fileData: FileWithMetadata) => { if (!fileData.parsed.isValid) { return fileData.parsed.error || "Invalid format" } switch (fileData.status) { case 'validating': return "Checking..." case 'uploading': return "Uploading..." case 'success': return "Uploaded" case 'error': return fileData.error || "Failed" default: if (fileData.matched) { // projectCode 제거 return `${fileData.parsed.docNumber}_${fileData.parsed.stageName}` } else { return "Document not found in system" } } } return ( Bulk Document Upload Upload multiple files at once. Files should be named as: DocNumber_StageName_Revision.ext {/* Custom Dropzone with input */}
document.getElementById('file-upload')?.click()} >

Drop files here or click to browse

Maximum 10GB total • Format: DocNumber_StageName_Revision.ext

{/* Stats */} {files.length > 0 && (
Total: {stats.total} Valid Format: {stats.valid} 0 ? "success" : "secondary"}> Matched: {stats.matched} 0 ? "default" : "outline"}> Ready: {stats.ready} Size: {formatFileSize(stats.totalSize)}
)} {/* File List */} {files.length > 0 && (
Files ({files.length})
{files.map((fileData, index) => ( {getStatusIcon(fileData)} {fileData.file.name} {getStatusDescription(fileData)} {fileData.file.size} ))}
)} {/* Error Alert */} {files.filter(f => !f.parsed.isValid).length > 0 && ( {files.filter(f => !f.parsed.isValid).length} file(s) have invalid naming format. Expected: ProjectCode_DocNumber_StageName_Rev0.ext )}
) }