"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 // import { NewRevisionDialog } from "./new-revision-dialog" /* ------------------------------------------------------------------------------------------------- * Schema & Types * -----------------------------------------------------------------------------------------------*/ // 파일 검증 스키마 const MAX_FILE_SIZE = 1024 * 1024 * 1024 // 1GB // drawingKind에 따른 동적 스키마 생성 const createRevisionUploadSchema = (drawingKind: string) => { const baseSchema = { usage: z.string().min(1, "Please select a usage"), revision: z.string().min(1, "Please enter a revision").max(50, "Revision must be 50 characters or less"), comment: z.string().optional(), attachments: z .array(z.instanceof(File)) .min(1, "Please upload at least 1 file") .refine( (files) => files.every((file) => file.size <= MAX_FILE_SIZE), "File size must be 50MB or less" ) // .refine( // (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)), // "Unsupported file format" // ), } // B3인 경우에만 usageType 필드 추가 if (drawingKind === 'B3') { return z.object({ ...baseSchema, usageType: z.string().min(1, "Please select a usage type"), }) } else { return z.object({ ...baseSchema, usageType: z.string().optional(), }) } } // drawingKind에 따른 용도 옵션 const getUsageOptions = (drawingKind: string) => { switch (drawingKind) { case 'B3': return [ { value: "Approval", label: "Approval" }, { value: "Working", label: "Working" }, { value: "Comments", label: "Comments" }, ] case 'B4': return [ { value: "Pre", label: "Pre" }, { value: "Working", label: "Working" }, ] case 'B5': return [ { value: "Pre", label: "Pre" }, { value: "Working", label: "Working" }, ] default: return [ { value: "Pre", label: "Pre" }, { value: "Working", label: "Working" }, ] } } // B3 전용 용도 타입 옵션 const getUsageTypeOptions = (usage: string) => { switch (usage) { case 'Approval': return [ { value: "Full", label: "Full" }, { value: "Partial", label: "Partial" }, ] case 'Working': return [ { value: "Full", label: "Full" }, { value: "Partial", label: "Partial" }, ] case 'Comments': return [ { value: "Comments", label: "Comments" }, ] default: return [] } } // 리비전 형식 가이드 생성 const getRevisionGuide = () => { return "Enter in R01, R02, R03... format" } interface NewRevisionDialogProps { open: boolean onOpenChange: (open: boolean) => void documentId: number documentTitle?: string drawingKind: string onSuccess?: (result?: any) => void // ✅ result 파라미터 추가 } /* ------------------------------------------------------------------------------------------------- * File Upload Component * -----------------------------------------------------------------------------------------------*/ function FileUploadArea({ files, onFilesChange }: { files: File[] onFilesChange: (files: File[]) => void }) { const fileInputRef = React.useRef(null) const handleFileSelect = (event: React.ChangeEvent) => { const selectedFiles = Array.from(event.target.files || []) if (selectedFiles.length > 0) { onFilesChange([...files, ...selectedFiles]) } } const handleDrop = (event: React.DragEvent) => { event.preventDefault() const droppedFiles = Array.from(event.dataTransfer.files) if (droppedFiles.length > 0) { onFilesChange([...files, ...droppedFiles]) } } 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: < > : " ' | ? *

{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 userName = React.useMemo(() => { return session?.user?.name ? session.user.name : null; }, [session]); // drawingKind에 따른 동적 스키마 및 옵션 생성 const revisionUploadSchema = React.useMemo(() => createRevisionUploadSchema(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() }, []) const handleDialogClose = () => { if (!isUploading) { form.reset() setUploadProgress(0) onOpenChange(false) } } const onSubmit = async (data: RevisionUploadSchema) => { setIsUploading(true) setUploadProgress(0) try { const formData = new FormData() formData.append("documentId", String(documentId)) 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', { // ✅ 올바른 API 엔드포인트 사용 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) // ✅ API 응답 결과를 콜백에 전달 }, 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}
)}
{/* 스크롤 가능한 중간 영역 */}
{/* 용도 선택 */} ( Usage )} /> {/* 용도 타입 선택 (B3만, Comments가 아닐 때만) */} {showUsageType && watchedUsage && watchedUsage !== "Comments" && ( ( Usage Type )} /> )} {/* 리비전 */} ( Revision
{revisionGuide}
)} /> {/* 코멘트 */} ( Comment