"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 { Badge } from "@/components/ui/badge" import { Upload, FileText, X, Loader2, AlertCircle, 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 = 50 * 1024 * 1024 // 50MB const ACCEPTED_FILE_TYPES = [ 'application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'application/vnd.ms-excel', 'image/jpeg', 'image/png', 'image/gif', 'text/plain', 'application/zip', 'application/x-zip-compressed' ] // drawingKind에 따른 동적 스키마 생성 const createRevisionUploadSchema = (drawingKind: string) => { const baseSchema = { usage: z.string().min(1, "용도를 선택해주세요"), revision: z.string().min(1, "리비전을 입력해주세요").max(50, "리비전은 50자 이내로 입력해주세요"), comment: z.string().optional(), attachments: z .array(z.instanceof(File)) .min(1, "최소 1개의 파일을 업로드해주세요") .max(10, "최대 10개의 파일까지 업로드 가능합니다") .refine( (files) => files.every((file) => file.size <= MAX_FILE_SIZE), "파일 크기는 50MB 이하여야 합니다" ) .refine( (files) => files.every((file) => ACCEPTED_FILE_TYPES.includes(file.type)), "지원하지 않는 파일 형식입니다" ), } // B3인 경우에만 usageType 필드 추가 if (drawingKind === 'B3') { return z.object({ ...baseSchema, usageType: z.string().min(1, "용도 타입을 선택해주세요"), }) } 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: "Reference", label: "Reference" }, ] 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: "Approval Submission Full", label: "Approval Submission Full" }, { value: "Approval Submission Partial", label: "Approval Submission Partial" }, { value: "Approval Completion Full", label: "Approval Completion Full" }, { value: "Approval Completion Partial", label: "Approval Completion Partial" }, ] case 'Working': return [ { value: "Working Full", label: "Working Full" }, { value: "Working Partial", label: "Working Partial" }, ] case 'Reference': return [ { value: "Reference Full", label: "Reference Full" }, { value: "Reference Partial", label: "Reference Partial" }, { value: "Reference Series Full", label: "Reference Series Full" }, { value: "Reference Series Partial", label: "Reference Series Partial" }, ] default: return [] } } // 리비전 형식 가이드 생성 const getRevisionGuide = (drawingKind: string, usage: string, usageType: string) => { if (drawingKind === 'B4') { return "R00, R01, R02... 형식으로 입력하세요" } if (drawingKind === 'B5') { return "A, B, C... 형식으로 입력하세요" } if (drawingKind === 'B3') { if (usage === 'Reference') { return "00, 01, 02... 형식으로 입력하세요" } if (usageType === 'Working Partial') { return "00, 01, 02... 형식으로 입력하세요" } if (usageType?.includes('Full') && (usageType.includes('Working') || usageType.includes('Approval'))) { return "A, B, C... 형식으로 입력하세요" } if (usageType?.includes('Partial') && usageType.includes('Approval')) { return "리비전 형식은 추후 정의 예정입니다" } } return "리비전을 입력하세요" } 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()} >

파일을 드래그하여 놓거나 클릭하여 선택하세요

PDF, Word, Excel, 이미지, 텍스트, ZIP 파일 지원 (최대 50MB)

{files.length > 0 && (

선택된 파일 ({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 watchedUsageType = form.watch("usageType") // 용도 선택에 따른 용도 타입 옵션 업데이트 const usageTypeOptions = React.useMemo(() => { if (drawingKind === 'B3' && watchedUsage) { return getUsageTypeOptions(watchedUsage) } return [] }, [drawingKind, watchedUsage]) // 용도 변경 시 용도 타입 초기화 React.useEffect(() => { if (showUsageType && watchedUsage) { form.setValue("usageType", "") } }, [watchedUsage, showUsageType, form]) // 리비전 가이드 텍스트 const revisionGuide = React.useMemo(() => { return getRevisionGuide(drawingKind, watchedUsage, watchedUsageType || "") }, [drawingKind, watchedUsage, watchedUsageType]) 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 || '업로드에 실패했습니다.') } const result = await response.json() setUploadProgress(100) toast.success( result.message || `리비전 ${data.revision}이 성공적으로 업로드되었습니다. (${result.data?.uploadedFiles?.length || 0}개 파일)` ) console.log('✅ 업로드 성공:', result) setTimeout(() => { handleDialogClose() onSuccess?.(result) // ✅ API 응답 결과를 콜백에 전달 }, 1000) } catch (error) { console.error('❌ 업로드 오류:', error) toast.error(error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다") } finally { setIsUploading(false) setTimeout(() => setUploadProgress(0), 2000) } } return ( {/* 고정 헤더 */} 새 리비전 업로드 {documentTitle && (
문서: {documentTitle}
)}
{/* 스크롤 가능한 중간 영역 */}
{/* 용도 선택 */} ( 용도 )} /> {/* 용도 타입 선택 (B3만) */} {showUsageType && watchedUsage && ( ( 용도 타입 )} /> )} {/* 리비전 */} ( 리비전
{revisionGuide}
)} /> {/* 코멘트 */} ( 코멘트