From 688d9884ca98b50d04ac78fc1f6e28e034a519c0 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Wed, 3 Sep 2025 12:44:32 +0000 Subject: (대표님) rfq-last 작업, vendorDocu 스키마 변경, 벤더 문서 관련 변경 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ship/bulk-b4-upload-dialog.tsx | 428 +++++++++++++++++++++ 1 file changed, 428 insertions(+) create mode 100644 lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx (limited to 'lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx') diff --git a/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx new file mode 100644 index 00000000..65166bd6 --- /dev/null +++ b/lib/vendor-document-list/ship/bulk-b4-upload-dialog.tsx @@ -0,0 +1,428 @@ +"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 && ( + + )} +
+
+
+ ))} +
+
+
+ )} + + + + + + + +
+
+ ) +} \ No newline at end of file -- cgit v1.2.3