"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 { ScrollArea } from "@/components/ui/scroll-area" import { Separator } from "@/components/ui/separator" import { Badge } from "@/components/ui/badge" import { Upload, X, Loader2, FileSpreadsheet, Files, 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 nameWithoutExt = fileName.replace(/\.[^.]+$/, "") // revision 패턴 찾기 (R01, r01, REV01, rev01 등) const revisionMatch = nameWithoutExt.match(/[Rr](?:EV)?(\d+)/g) const revision = revisionMatch ? revisionMatch[revisionMatch.length - 1].toUpperCase() : null // revision 제거한 나머지에서 docNumber 찾기 let cleanedName = nameWithoutExt if (revision) { // revision과 그 앞의 구분자를 제거 const revPattern = new RegExp(`[-_\\s]*${revision.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")}.*$`, 'i') cleanedName = cleanedName.replace(revPattern, "") } // docNumber 패턴 찾기 (XX-XX-XX 형태) // 공백이나 언더스코어를 하이픈으로 정규화 const normalizedName = cleanedName.replace(/[\s_]+/g, '-') // 2~3자리 코드가 2~3개 연결된 패턴 찾기 const docNumberPatterns = [ /\b([A-Za-z]{2,3})-([A-Za-z]{2,3})-([A-Za-z0-9]{2,4})\b/, /\b([A-Za-z]{2,3})\s+([A-Za-z]{2,3})\s+([A-Za-z0-9]{2,4})\b/, ] let docNumber = null for (const pattern of docNumberPatterns) { const match = normalizedName.match(pattern) || cleanedName.match(pattern) if (match) { docNumber = `${match[1]}-${match[2]}-${match[3]}`.toUpperCase() break } } 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 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 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) } // 파일 제거 const removeFile = (index: number) => { const newParsedFiles = parsedFiles.filter((_, i) => i !== index) setParsedFiles(newParsedFiles) form.setValue("files", newParsedFiles.map(pf => pf.file)) } // 업로드 처리 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 (error) { 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([]) } }, [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. Example: "agadfg de na oc R01.pdf" → Document Number: DE-NA-OC, Revision: R01
( Select Project * )} />
Select Files
handleFilesChange(Array.from(e.target.files || []))} className="hidden" id="file-upload" />
{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 && ( )}
))}
)}
) }