"use client" import React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Button } from "@/components/ui/button" import { Progress } from "@/components/ui/progress" import { Upload, FileText, X, Loader2, CheckCircle } from "lucide-react" import { toast } from "sonner" import { useSession } from "next-auth/react" import { createUploadRevisionSchema, getUsageOptions, getUsageTypeOptions, getRevisionGuide, B3RevisionInput } from "./revision-validation" // 기존 메인 컴포넌트에서 추가할 import // import { NewRevisionDialog } from "./new-revision-dialog" /* ------------------------------------------------------------------------------------------------- * Schema & Types * -----------------------------------------------------------------------------------------------*/ interface NewRevisionDialogProps { open: boolean onOpenChange: (open: boolean) => void documentId: number documentTitle?: string drawingKind: string onSuccess?: (result?: unknown) => void } /* ------------------------------------------------------------------------------------------------- * File Upload Component * -----------------------------------------------------------------------------------------------*/ function FileUploadArea({ files, onFilesChange }: { files: File[] onFilesChange: (files: File[]) => void }) { const fileInputRef = React.useRef(null) // 파일 검증 함수 const validateFiles = (filesToValidate: File[]): { valid: File[], invalid: string[] } => { const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB const FORBIDDEN_EXTENSIONS = ['exe', 'com', 'dll', 'vbs', 'js', 'asp', 'aspx', 'bat', 'cmd'] const valid: File[] = [] const invalid: string[] = [] filesToValidate.forEach(file => { // 파일 크기 검증 if (file.size > MAX_FILE_SIZE) { invalid.push(`${file.name}: 파일 크기가 1GB를 초과합니다 (${formatFileSize(file.size)})`) return } // 파일 확장자 검증 const extension = file.name.split('.').pop()?.toLowerCase() if (extension && FORBIDDEN_EXTENSIONS.includes(extension)) { invalid.push(`${file.name}: 금지된 파일 형식입니다 (.${extension})`) return } valid.push(file) }) return { valid, invalid } } const handleFileSelect = (event: React.ChangeEvent) => { const selectedFiles = Array.from(event.target.files || []) if (selectedFiles.length > 0) { const { valid, invalid } = validateFiles(selectedFiles) if (invalid.length > 0) { invalid.forEach(msg => toast.error(msg)) } if (valid.length > 0) { onFilesChange([...files, ...valid]) } } } const handleDrop = (event: React.DragEvent) => { event.preventDefault() const droppedFiles = Array.from(event.dataTransfer.files) if (droppedFiles.length > 0) { const { valid, invalid } = validateFiles(droppedFiles) if (invalid.length > 0) { invalid.forEach(msg => toast.error(msg)) } if (valid.length > 0) { onFilesChange([...files, ...valid]) } } } const handleDragOver = (event: React.DragEvent) => { event.preventDefault() } const removeFile = (index: number) => { onFilesChange(files.filter((_, i) => i !== index)) } 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] } return (
fileInputRef.current?.click()} >

Drag files here or click to select

Supports PDF, Word, Excel, Image, Text, ZIP files (max 1GB)

Note: File names cannot contain these characters: < > : " ' | ? *

Forbidden file types: .exe, .com, .dll, .vbs, .js, .asp, .aspx, .bat, .cmd

{files.length > 0 && (

Selected Files ({files.length})

{files.map((file, index) => (

{file.name}

{formatFileSize(file.size)}

))}
)}
) } /* ------------------------------------------------------------------------------------------------- * Main Dialog Component * -----------------------------------------------------------------------------------------------*/ export function NewRevisionDialog({ open, onOpenChange, documentId, documentTitle, drawingKind, onSuccess }: NewRevisionDialogProps) { const [isUploading, setIsUploading] = React.useState(false) const [uploadProgress, setUploadProgress] = React.useState(0) const { data: session } = useSession() const [nextSerialNo, setNextSerialNo] = React.useState("1") const [isLoadingSerialNo, setIsLoadingSerialNo] = React.useState(false) // Serial No 조회 const fetchNextSerialNo = React.useCallback(async () => { console.log('🔍 fetchNextSerialNo called with documentId:', documentId) // documentId 유효성 검사 if (!documentId || documentId === undefined || documentId === null) { console.warn('⚠️ Invalid documentId, using default serialNo: 1') setNextSerialNo("1") return } setIsLoadingSerialNo(true) try { const apiUrl = `/api/revisions/max-serial-no?documentId=${documentId}` console.log('🔍 Calling API:', apiUrl) const response = await fetch(apiUrl) console.log('🔍 API Response status:', response.status) if (response.ok) { const data = await response.json() console.log('🔍 API Response data:', data) console.log('🔍 data.nextSerialNo:', data.nextSerialNo) const serialNoString = String(data.nextSerialNo) console.log('🔍 Setting nextSerialNo to:', serialNoString) setNextSerialNo(serialNoString) console.log('🔍 nextSerialNo state updated') } else { const errorData = await response.json().catch(() => ({})) console.error('🔍 API call failed with status:', response.status, errorData) // API 실패 시 기본값 1 사용 console.warn('⚠️ Using default serialNo: 1') setNextSerialNo("1") } } catch (error) { console.error('❌ Failed to fetch serial no:', error) // 에러 시 기본값 1 사용 console.warn('⚠️ Using default serialNo: 1 due to error') setNextSerialNo("1") } finally { setIsLoadingSerialNo(false) } }, [documentId]) // Dialog 열릴 때 Serial No 조회 React.useEffect(() => { console.log('🎯 useEffect triggered - open:', open, 'documentId:', documentId, 'type:', typeof documentId) if (open) { if (documentId && typeof documentId === 'number' && documentId > 0) { console.log('🎯 Calling fetchNextSerialNo') fetchNextSerialNo() } else { console.warn('🎯 Invalid documentId, using default serialNo: 1') setNextSerialNo("1") } } }, [open, documentId, fetchNextSerialNo]) const userName = React.useMemo(() => { return session?.user?.name ? session.user.name : null; }, [session]); // drawingKind에 따른 동적 스키마 및 옵션 생성 const revisionUploadSchema = React.useMemo(() => createUploadRevisionSchema(drawingKind), [drawingKind]) const usageOptions = React.useMemo(() => getUsageOptions(drawingKind), [drawingKind]) const showUsageType = drawingKind === 'B3' type RevisionUploadSchema = z.infer const form = useForm({ resolver: zodResolver(revisionUploadSchema), defaultValues: { usage: "", revision: "", comment: "", usageType: showUsageType ? "" : undefined, attachments: [], }, }) const watchedFiles = form.watch("attachments") const watchedUsage = form.watch("usage") // 용도 선택에 따른 용도 타입 옵션 업데이트 const usageTypeOptions = React.useMemo(() => { if (drawingKind === 'B3' && watchedUsage) { return getUsageTypeOptions(watchedUsage) } return [] }, [drawingKind, watchedUsage]) // 용도 변경 시 용도 타입 초기화 또는 자동 설정 React.useEffect(() => { if (showUsageType && watchedUsage) { if (watchedUsage === "Comments") { form.setValue("usageType", "Comments") } else { form.setValue("usageType", "") } } }, [watchedUsage, showUsageType, form]) // 리비전 가이드 텍스트 const revisionGuide = React.useMemo(() => { return getRevisionGuide(drawingKind) }, [drawingKind]) const handleDialogClose = () => { if (!isUploading) { form.reset() setUploadProgress(0) onOpenChange(false) } } const onSubmit = async (data: RevisionUploadSchema) => { console.log('🚀 onSubmit called with data:', data) console.log('🚀 Current nextSerialNo state:', nextSerialNo) console.log('🚀 documentId:', documentId) setIsUploading(true) setUploadProgress(0) try { const formData = new FormData() formData.append("documentId", String(documentId)) formData.append("serialNo", nextSerialNo) // 추가 console.log('🚀 Appending serialNo to formData:', nextSerialNo) formData.append("usage", data.usage) formData.append("revision", data.revision) formData.append("uploaderName", userName || "evcp") if (data.comment) { formData.append("comment", data.comment) } // B3인 경우에만 usageType 추가 if (showUsageType && 'usageType' in data && data.usageType) { formData.append("usageType", data.usageType) } // 파일들 추가 data.attachments.forEach((file) => { formData.append("attachments", file) }) // 진행률 업데이트 시뮬레이션 const totalSize = data.attachments.reduce((sum, file) => sum + file.size, 0) let uploadedSize = 0 const progressInterval = setInterval(() => { uploadedSize += totalSize * 0.1 const progress = Math.min((uploadedSize / totalSize) * 100, 90) setUploadProgress(progress) }, 300) const response = await fetch('/api/revision-upload-ship', { method: 'POST', body: formData, }) clearInterval(progressInterval) if (!response.ok) { const errorData = await response.json() throw new Error(errorData.error || errorData.details || 'Upload failed.') } const result = await response.json() setUploadProgress(100) toast.success( result.message || `Revision ${data.revision} uploaded successfully. (${result.data?.uploadedFiles?.length || 0} files)` ) console.log('✅ Upload successful:', result) setTimeout(() => { handleDialogClose() onSuccess?.(result) }, 1000) } catch (error) { console.error('❌ Upload error:', error) let userMessage = "An error occurred during upload" if (error instanceof Error) { const message = error.message.toLowerCase() // 파일명 관련 에러 if (message.includes("안전하지 않은 파일명") || message.includes("unsafe filename") || message.includes("filename") && message.includes("invalid")) { userMessage = "File name contains invalid characters. Please avoid using < > : \" ' | ? * in file names. filename can't start with '..'." } // 파일명 길이 에러 else if (message.includes("파일명이 너무 깁니다") || message.includes("filename too long") || message.includes("파일명") && message.includes("길이")) { userMessage = "File name is too long. Please use a shorter name (max 255 characters)." } // 파일 크기 에러 else if (message.includes("파일 크기가 너무 큽니다") || message.includes("file size") || message.includes("1gb limit") || message.includes("exceeds") && message.includes("limit")) { userMessage = "File size is too large. Please use files smaller than 1GB." } // 클라이언트측 네트워크 에러 else if (message.includes("network") || message.includes("fetch") || message.includes("connection") || message.includes("timeout")) { userMessage = "Network error occurred. Please check your connection and try again." } // 서버측 오류는 보안상 일반적인 메시지로 처리 else if (message.includes("500") || message.includes("server") || message.includes("database") || message.includes("internal") || message.includes("security") || message.includes("validation")) { userMessage = "Please try again later. If the problem persists, please contact the administrator." } else { userMessage = "Please try again later. If the problem persists, please contact the administrator." } } toast.error(userMessage) } finally { setIsUploading(false) setTimeout(() => setUploadProgress(0), 2000) } } return ( {/* 고정 헤더 */} Upload New Revision {documentTitle && (
Document: {documentTitle}
Drawing Type: {drawingKind} | Serial No: {isLoadingSerialNo ? ( <> Loading... ) : nextSerialNo}
)}
{/* 스크롤 가능한 중간 영역 */}
{/* 용도 선택 */} ( Usage )} /> {/* 용도 타입 선택 (B3만, Comments가 아닐 때만) */} {showUsageType && watchedUsage && watchedUsage !== "Comments" && ( ( Usage Type )} /> )} {/* 리비전 입력 */} ( Revision {drawingKind === 'B3' ? ( ) : ( <> { const upperValue = e.target.value.toUpperCase() if (upperValue.length <= 3) { field.onChange(upperValue) } }} />
{revisionGuide.helpText}
)}
)} /> {/* 코멘트 */} ( Comment