"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 { useSession } from "next-auth/react" import { mutate } from "swr" // ✅ SWR mutate import 추가 import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" import { FileList, FileListAction, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list" import { Badge } from "@/components/ui/badge" import { ScrollArea } from "@/components/ui/scroll-area" import { Upload, X, Loader2 } from "lucide-react" import prettyBytes from "pretty-bytes" import { EnhancedDocumentsView } from "@/db/schema/vendorDocu" // 리비전 업로드 스키마 const revisionUploadSchema = z.object({ stage: z.string().min(1, "스테이지는 필수입니다"), revision: z.string().min(1, "리비전은 필수입니다"), uploaderName: z.string().optional(), comment: z.string().optional(), attachments: z.array(z.instanceof(File)).min(1, "최소 1개 파일이 필요합니다"), }) type RevisionUploadSchema = z.infer interface RevisionUploadDialogProps { open: boolean onOpenChange: (open: boolean) => void document: EnhancedDocumentsView | null projectType: "ship" | "plant" presetStage?: string presetRevision?: string mode?: 'new' | 'append' onUploadComplete?: () => void // ✅ 업로드 완료 콜백 추가 } function getTargetSystem(projectType: "ship" | "plant") { return projectType === "ship" ? "DOLCE" : "SWP" } export function RevisionUploadDialog({ open, onOpenChange, document, projectType, presetStage, presetRevision, mode = 'new', onUploadComplete, // ✅ 추가된 prop }: RevisionUploadDialogProps) { const targetSystem = React.useMemo( () => getTargetSystem(projectType), [projectType] ) const [selectedFiles, setSelectedFiles] = React.useState([]) const [isUploading, setIsUploading] = React.useState(false) const [uploadProgress, setUploadProgress] = React.useState(0) const router = useRouter() // ✅ next-auth session 가져오기 const { data: session } = useSession() // 사용 가능한 스테이지 옵션 const stageOptions = React.useMemo(() => { if (document?.allStages) { return document.allStages.map(stage => stage.stageName) } return ["Issued for Review", "AFC", "Final Issue"] }, [document]) const form = useForm({ resolver: zodResolver(revisionUploadSchema), defaultValues: { stage: presetStage || document?.currentStageName || "", revision: presetRevision || "", uploaderName: session?.user?.name || "", comment: "", attachments: [], }, }) // ✅ session이 로드되면 uploaderName 업데이트 React.useEffect(() => { if (session?.user?.name) { form.setValue('uploaderName', session.user.name) } }, [session?.user?.name, form]) // ✅ presetStage와 presetRevision이 변경될 때 폼 값 업데이트 React.useEffect(() => { if (presetStage) { form.setValue('stage', presetStage) } if (presetRevision) { form.setValue('revision', presetRevision) } }, [presetStage, presetRevision, form]) // 파일 드롭 처리 const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles] setSelectedFiles(newFiles) form.setValue('attachments', newFiles, { shouldValidate: true }) } const removeFile = (index: number) => { const updatedFiles = [...selectedFiles] updatedFiles.splice(index, 1) setSelectedFiles(updatedFiles) form.setValue('attachments', updatedFiles, { shouldValidate: true }) } // ✅ 캐시 갱신 함수 const refreshCaches = async () => { try { // 1. 서버 컴포넌트 캐시 갱신 (Enhanced Documents 등) router.refresh() // 2. SWR 캐시 갱신 (Sync Status) if (document?.contractId) { await mutate(`/api/sync/status/${document.contractId}/${targetSystem}`) console.log('✅ Sync status cache refreshed') } // 3. 다른 관련 SWR 캐시들도 갱신 (필요시) await mutate(key => typeof key === 'string' && key.includes('sync') && key.includes(String(document?.contractId)) ) // 4. 상위 컴포넌트 콜백 호출 onUploadComplete?.() console.log('✅ All caches refreshed after upload') } catch (error) { console.error('❌ Cache refresh failed:', error) } } // 업로드 처리 async function onSubmit(data: RevisionUploadSchema) { if (!document) return setIsUploading(true) setUploadProgress(0) try { const formData = new FormData() formData.append("documentId", String(document.documentId)) formData.append("stage", data.stage) formData.append("revision", data.revision) formData.append("mode", mode) formData.append("targetSystem", targetSystem) if (data.uploaderName) { formData.append("uploaderName", data.uploaderName) } if (data.comment) { formData.append("comment", data.comment) } // 파일들 추가 data.attachments.forEach((file) => { formData.append("attachments", file) }) // 진행률 업데이트 시뮬레이션 const updateProgress = (progress: number) => { setUploadProgress(Math.min(progress, 95)) } // 파일 크기에 따른 진행률 시뮬레이션 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) updateProgress(progress) }, 300) // ✅ 실제 API 호출 const response = await fetch('/api/revision-upload', { 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(async () => { await refreshCaches() handleDialogClose() }, 1000) } catch (error) { console.error('❌ 업로드 오류:', error) toast.error(error instanceof Error ? error.message : "업로드 중 오류가 발생했습니다") } finally { setIsUploading(false) setTimeout(() => setUploadProgress(0), 2000) } } const handleDialogClose = () => { form.reset({ stage: presetStage || document?.currentStageName || "", revision: presetRevision || "", uploaderName: session?.user?.name || "", comment: "", attachments: [], }) setSelectedFiles([]) setIsUploading(false) setUploadProgress(0) onOpenChange(false) } return ( {mode === 'new' ? '새 리비전 업로드' : '파일 추가'} {document ? `${document.docNumber} - ${document.title}` : mode === 'new' ? "문서에 새 리비전을 업로드합니다." : "기존 리비전에 파일을 추가합니다."}
{projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} {/* ✅ 타겟 시스템 표시 추가 */} → {targetSystem} {session?.user?.name && ( 업로더: {session.user.name} )} {mode === 'append' && presetRevision && ( 리비전 {presetRevision}에 파일 추가 )} {mode === 'new' && presetRevision && ( 다음 리비전: {presetRevision} )}
( 스테이지 )} /> ( 리비전 {mode === 'new' && presetRevision && (

자동으로 계산된 다음 리비전입니다.

)} {mode === 'append' && (

기존 리비전에 파일을 추가합니다.

)}
)} />
( 업로더명

로그인된 사용자 정보가 자동으로 입력됩니다.

)} /> ( 코멘트 (선택)