summaryrefslogtreecommitdiff
path: root/lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx')
-rw-r--r--lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx492
1 files changed, 492 insertions, 0 deletions
diff --git a/lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx b/lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx
new file mode 100644
index 00000000..81a1d486
--- /dev/null
+++ b/lib/vendor-document-list/plant/upload/components/multi-upload-dialog.tsx
@@ -0,0 +1,492 @@
+// 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<FileWithMetadata[]>([])
+ const [isValidating, setIsValidating] = useState(false)
+ const [isUploading, setIsUploading] = useState(false)
+
+ // 디버깅용 로그
+ console.log("Current files:", files)
+
+ // 파일 추가 핸들러 - onChange 이벤트용
+ const handleFilesChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
+ 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
+ }))s
+
+ 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<HTMLDivElement>) => {
+ 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<HTMLDivElement>) => {
+ 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 <FileWarning className="h-4 w-4 text-destructive" />
+ }
+
+ switch (fileData.status) {
+ case 'validating':
+ return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+ case 'uploading':
+ return <Loader2 className="h-4 w-4 animate-spin text-primary" />
+ case 'success':
+ return <CheckCircle2 className="h-4 w-4 text-success" />
+ case 'error':
+ return <AlertCircle className="h-4 w-4 text-destructive" />
+ default:
+ if (fileData.matched) {
+ return <CheckCircle2 className="h-4 w-4 text-success" />
+ } else {
+ return <AlertCircle className="h-4 w-4 text-warning" />
+ }
+ }
+ }
+
+ // 파일별 상태 설명
+ 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 (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" className="gap-2">
+ <CloudUpload className="h-4 w-4" />
+ Multi-Upload
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-5xl max-h-[90vh] flex flex-col">
+ <DialogHeader>
+ <DialogTitle>Bulk Document Upload</DialogTitle>
+ <DialogDescription>
+ Upload multiple files at once. Files should be named as: DocNumber_StageName_Revision.ext
+ </DialogDescription>
+ </DialogHeader>
+
+ {/* Custom Dropzone with input */}
+ <div
+ className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center hover:border-gray-400 transition-colors cursor-pointer"
+ onDrop={handleDrop}
+ onDragOver={handleDragOver}
+ onClick={() => document.getElementById('file-upload')?.click()}
+ >
+ <input
+ id="file-upload"
+ type="file"
+ multiple
+ className="hidden"
+ onChange={handleFilesChange}
+ accept="*/*"
+ />
+ <Upload className="mx-auto h-12 w-12 text-gray-400 mb-4" />
+ <p className="text-lg font-medium">Drop files here or click to browse</p>
+ <p className="text-sm text-gray-500 mt-1">
+ Maximum 10GB total • Format: DocNumber_StageName_Revision.ext
+ </p>
+ </div>
+
+ {/* Stats */}
+ {files.length > 0 && (
+ <div className="flex gap-2 flex-wrap">
+ <Badge variant="outline">Total: {stats.total}</Badge>
+ <Badge variant={stats.valid === stats.total ? "success" : "secondary"}>
+ Valid Format: {stats.valid}
+ </Badge>
+ <Badge variant={stats.matched > 0 ? "success" : "secondary"}>
+ Matched: {stats.matched}
+ </Badge>
+ <Badge variant={stats.ready > 0 ? "default" : "outline"}>
+ Ready: {stats.ready}
+ </Badge>
+ <Badge variant="outline">
+ Size: {formatFileSize(stats.totalSize)}
+ </Badge>
+ </div>
+ )}
+
+ {/* File List */}
+ {files.length > 0 && (
+ <div className="flex-1 rounded-md border overflow-y-auto" style={{ minHeight: 200, maxHeight: 400 }}>
+ <FileList className="p-4">
+ <FileListHeader>
+ <div className="text-sm font-medium">Files ({files.length})</div>
+ </FileListHeader>
+
+ {files.map((fileData, index) => (
+ <FileListItem key={index}>
+ <FileListIcon>
+ {getStatusIcon(fileData)}
+ </FileListIcon>
+
+ <FileListInfo>
+ <FileListName>{fileData.file.name}</FileListName>
+ <FileListDescription>
+ {getStatusDescription(fileData)}
+ </FileListDescription>
+ </FileListInfo>
+
+ <FileListSize>
+ {fileData.file.size}
+ </FileListSize>
+
+ <FileListAction>
+ <Button
+ variant="ghost"
+ size="icon"
+ className="h-8 w-8"
+ onClick={(e) => {
+ e.stopPropagation()
+ removeFile(index)
+ }}
+ disabled={isUploading || fileData.status === 'uploading'}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </FileListAction>
+ </FileListItem>
+ ))}
+ </FileList>
+ </div>
+ )}
+ {/* Error Alert */}
+ {files.filter(f => !f.parsed.isValid).length > 0 && (
+ <Alert variant="destructive">
+ <AlertCircle className="h-4 w-4" />
+ <AlertDescription>
+ {files.filter(f => !f.parsed.isValid).length} file(s) have invalid naming format.
+ Expected: ProjectCode_DocNumber_StageName_Rev0.ext
+ </AlertDescription>
+ </Alert>
+ )}
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={() => {
+ setOpen(false)
+ setFiles([])
+ }}
+ disabled={isUploading}
+ >
+ Cancel
+ </Button>
+ <Button
+ onClick={handleUpload}
+ disabled={stats.ready === 0 || isUploading || isValidating}
+ className="gap-2"
+ >
+ {isUploading ? (
+ <>
+ <Loader2 className="h-4 w-4 animate-spin" />
+ Uploading {stats.ready} files...
+ </>
+ ) : (
+ <>
+ <Upload className="h-4 w-4" />
+ Upload {stats.ready} file{stats.ready !== 1 ? 's' : ''}
+ </>
+ )}
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file