"use client" import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { toast } from "sonner" import { useRouter } from "next/navigation" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Badge } from "@/components/ui/badge" import { Upload, X, Loader2, CheckCircle2, AlertCircle, AlertTriangle, FileText, } from "lucide-react" import { SimplifiedDocumentsView } from "@/db/schema" import { bulkUploadB4Documents } from "../enhanced-document-service" // 파일명 파싱 유틸리티 function parseFileName(fileName: string): { docNumber: string | null; revision: string | null } { // 공백으로 단어 분리 (첫 번째는 파일명으로 무시) const words = fileName.trim().split(/\s+/).filter(word => word.length > 0) if (words.length < 2) { return { docNumber: null, revision: null } } // 마지막 단어에서 확장자와 리비전을 분리 const lastWord = words[words.length - 1] const lastDotIndex = lastWord.lastIndexOf('.') let revision: string | null = null if (lastDotIndex !== -1) { // 마지막 '.' 기준으로 확장자 앞부분만 사용 const beforeExt = lastWord.substring(0, lastDotIndex) // revision 패턴 찾기 (R01, r01, REV01, rev01 등) const revisionMatch = beforeExt.match(/[Rr](?:EV)?(\d+)/) revision = revisionMatch ? revisionMatch[0].toUpperCase() : null } // 문서번호: 첫 번째(파일명)와 마지막(REV.ext)을 제외한 모든 단어들을 '-'로 연결 const docWords = words.slice(1, -1) // 첫 번째와 마지막 제외 const docNumber = docWords.length > 0 ? docWords.join('-').toUpperCase() : null return { docNumber, revision } } // Form schema const formSchema = z.object({ projectId: z.string().min(1, "Please select a project"), files: z.array(z.instanceof(File)).min(1, "Please select files"), }) interface BulkB4UploadDialogProps { open: boolean onOpenChange: (open: boolean) => void allDocuments: SimplifiedDocumentsView[] } interface ParsedFile { file: File docNumber: string | null revision: string | null status: 'pending' | 'uploading' | 'success' | 'error' | 'ignored' message?: string } export function BulkB4UploadDialog({ open, onOpenChange, allDocuments }: BulkB4UploadDialogProps) { const [isUploading, setIsUploading] = React.useState(false) const [parsedFiles, setParsedFiles] = React.useState([]) const [isDragging, setIsDragging] = React.useState(false) const [currentProjectId, setCurrentProjectId] = React.useState("") const router = useRouter() // 프로젝트 ID 추출 const projectOptions = React.useMemo(() => { const projectIds = [...new Set(allDocuments.map(doc => doc.projectId).filter(Boolean))] return projectIds.map(id => ({ id: String(id), code: allDocuments.find(doc => doc.projectId === id)?.projectCode || `Project ${id}` })) }, [allDocuments]) const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { projectId: "", files: [], }, }) // 파일 선택 시 파싱 const handleFilesChange = (files: File[]) => { const currentProject = form.watch("projectId") // 프로젝트가 변경되었거나 처음 선택하는 경우 if (!currentProject || currentProject !== currentProjectId) { setCurrentProjectId(currentProject) const parsed = files.map(file => { const { docNumber, revision } = parseFileName(file.name) return { file, docNumber, revision, status: docNumber ? 'pending' as const : 'ignored' as const, message: !docNumber ? 'docNumber를 찾을 수 없음' : undefined } }) setParsedFiles(parsed) form.setValue("files", files) } else { // 같은 프로젝트에서 파일 추가하는 경우 const parsed = files.map(file => { const { docNumber, revision } = parseFileName(file.name) return { file, docNumber, revision, status: docNumber ? 'pending' as const : 'ignored' as const, message: !docNumber ? 'docNumber를 찾을 수 없음' : undefined } }) // 기존 파일들과 새 파일들을 합침 (중복 파일명은 새 것으로 대체) const existingFileNames = new Set(parsedFiles.map(pf => pf.file.name)) const newFiles = parsed.filter(pf => !existingFileNames.has(pf.file.name)) const combinedParsed = [...parsedFiles, ...newFiles] const combinedFiles = [...parsedFiles.map(pf => pf.file), ...newFiles.map(pf => pf.file)] setParsedFiles(combinedParsed) form.setValue("files", combinedFiles) } } // 파일 제거 const removeFile = (index: number) => { const newParsedFiles = parsedFiles.filter((_, i) => i !== index) setParsedFiles(newParsedFiles) form.setValue("files", newParsedFiles.map(pf => pf.file)) } // Drag & Drop 핸들러 const handleDragEnter = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(true) } const handleDragLeave = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() // 자식 요소로 이동할 때도 leave가 발생하므로 실제로 영역을 벗어날 때만 처리 if (e.currentTarget === e.target) { setIsDragging(false) } } const handleDragOver = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() // 드롭 가능하도록 설정 e.dataTransfer.dropEffect = 'copy' } const handleDrop = (e: React.DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(false) const droppedFiles = Array.from(e.dataTransfer.files) if (droppedFiles.length > 0) { handleFilesChange(droppedFiles) } } // 업로드 처리 async function onSubmit(values: z.infer) { setIsUploading(true) try { // 유효한 파일만 필터링 const validFiles = parsedFiles.filter(pf => pf.docNumber && pf.status === 'pending') if (validFiles.length === 0) { toast.error("업로드 가능한 파일이 없습니다") return } // 파일별로 상태 업데이트 setParsedFiles(prev => prev.map(pf => pf.docNumber && pf.status === 'pending' ? { ...pf, status: 'uploading' as const } : pf )) // FormData 생성 const formData = new FormData() formData.append("projectId", values.projectId) validFiles.forEach((pf, index) => { formData.append(`file_${index}`, pf.file) formData.append(`docNumber_${index}`, pf.docNumber!) formData.append(`revision_${index}`, pf.revision || "00") }) formData.append("fileCount", String(validFiles.length)) // 서버 액션 호출 const result = await bulkUploadB4Documents(formData) if (result.success) { // 성공한 파일들 표시 setParsedFiles(prev => prev.map(pf => { const uploadResult = result.results?.find(r => r.docNumber === pf.docNumber && r.revision === (pf.revision || "00") ) if (uploadResult?.success) { return { ...pf, status: 'success' as const, message: uploadResult.message } } else if (uploadResult) { return { ...pf, status: 'error' as const, message: uploadResult.error } } return pf })) toast.success(`${result.successCount}/${validFiles.length} 파일 업로드 완료`) // 모두 성공하면 닫기 if (result.successCount === validFiles.length) { setTimeout(() => { onOpenChange(false) router.refresh() }, 1500) } } else { toast.error(result.error || "업로드 실패") setParsedFiles(prev => prev.map(pf => pf.status === 'uploading' ? { ...pf, status: 'error' as const, message: result.error } : pf )) } } catch { toast.error("업로드 중 오류가 발생했습니다") setParsedFiles(prev => prev.map(pf => pf.status === 'uploading' ? { ...pf, status: 'error' as const, message: '업로드 실패' } : pf )) } finally { setIsUploading(false) } } // 다이얼로그 닫을 때 초기화 React.useEffect(() => { if (!open) { form.reset() setParsedFiles([]) setIsDragging(false) setCurrentProjectId("") } }, [open, form]) const validFileCount = parsedFiles.filter(pf => pf.docNumber).length const ignoredFileCount = parsedFiles.filter(pf => !pf.docNumber).length return ( B4 Document Bulk Upload Document numbers and revisions will be automatically extracted from file names. Format: [filename] [DOC1] [DOC2] ... [DOCN] [REV].[ext] Examples: "testfile TANK ANA R01.pdf" → Document Number: TANK-ANA, Revision: R01 "drawing ABC DEF GHI JKL R02.pdf" → Document Number: ABC-DEF-GHI-JKL, Revision: R02
( Select Project * )} />
Select Files
handleFilesChange(Array.from(e.target.files || []))} className="hidden" id="file-upload" disabled={isUploading} />
{parsedFiles.length > 0 && (
Selected Files
Valid: {validFileCount} {ignoredFileCount > 0 && ( Ignored: {ignoredFileCount} )}
{parsedFiles.map((pf, index) => (

{pf.file.name}

{pf.docNumber ? ( <> Doc: {pf.docNumber} {pf.revision && ( Rev: {pf.revision} )} ) : ( {pf.message} )}
{pf.status === 'uploading' && ( )} {pf.status === 'success' && ( )} {pf.status === 'error' && ( )} {pf.status === 'ignored' && ( )} {!isUploading && ( )}
))}
)}
) }