"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개 파일이 필요합니다"), // ✅ B3 문서용 usage 필드 추가 usage: z.string().optional(), }).refine((data) => { // B3 문서이고 특정 stage인 경우 usage 필수 // 이 검증은 컴포넌트 내에서 조건부로 처리 return true; }, { message: "Usage는 필수입니다", path: ["usage"], }); const getUsageOptions = (stageName: string): string[] => { const stageNameLower = stageName.toLowerCase(); if (stageNameLower.includes('approval')) { return ['Approval (Partial)', 'Approval (Full)']; } else if (stageNameLower.includes('working')) { return ['Working (Partial)', 'Working (Full)']; } return []; }; 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, }: 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() 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: [], usage: "", // ✅ usage 기본값 추가 }, }) // ✅ 현재 선택된 stage 값을 watch const currentStage = form.watch('stage') // ✅ B3 문서 여부 확인 const isB3Document = document?.drawingKind === 'B3' // ✅ 현재 stage에 따른 usage 옵션 const usageOptions = React.useMemo(() => { if (!isB3Document || !currentStage) return [] return getUsageOptions(currentStage) }, [isB3Document, currentStage]) // ✅ usage 필드가 필요한지 확인 const isUsageRequired = isB3Document && usageOptions.length > 0 // 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]) // ✅ stage가 변경될 때 usage 값 리셋 React.useEffect(() => { if (isB3Document) { const newUsageOptions = getUsageOptions(currentStage) if (newUsageOptions.length === 0) { form.setValue('usage', '') } else { // 기존 값이 새로운 옵션에 없으면 리셋 const currentUsage = form.getValues('usage') if (currentUsage && !newUsageOptions.includes(currentUsage)) { form.setValue('usage', '') } } } }, [currentStage, isB3Document, 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 { router.refresh() if (document?.contractId) { await mutate(`/api/sync/status/${document.contractId}/${targetSystem}`) console.log('✅ Sync status cache refreshed') } await mutate(key => typeof key === 'string' && key.includes('sync') && key.includes(String(document?.contractId)) ) onUploadComplete?.() console.log('✅ All caches refreshed after upload') } catch (error) { console.error('❌ Cache refresh failed:', error) } } // ✅ 업로드 처리 - usage 필드 검증 및 전송 async function onSubmit(data: RevisionUploadSchema) { if (!document) return // ✅ B3 문서에서 usage가 필요한 경우 검증 if (isUsageRequired && !data.usage) { form.setError('usage', { type: 'required', message: 'Usage 선택은 필수입니다' }) 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) } // ✅ B3 문서인 경우 usage 추가 if (isB3Document && data.usage) { formData.append("usage", data.usage) } // 파일들 추가 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) 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: [], usage: "", // ✅ usage 리셋 추가 }) setSelectedFiles([]) setIsUploading(false) setUploadProgress(0) onOpenChange(false) } return ( {mode === 'new' ? '새 리비전 업로드' : '파일 추가'} {document ? `${document.docNumber} - ${document.title}` : mode === 'new' ? "문서에 새 리비전을 업로드합니다." : "기존 리비전에 파일을 추가합니다."}
{projectType === "ship" ? "조선 프로젝트" : "플랜트 프로젝트"} → {targetSystem} {/* ✅ B3 문서 표시 */} {isB3Document && ( B3 문서 )} {session?.user?.name && ( 업로더: {session.user.name} )} {mode === 'append' && presetRevision && ( 리비전 {presetRevision}에 파일 추가 )} {mode === 'new' && presetRevision && ( 다음 리비전: {presetRevision} )}
( 스테이지 )} /> ( 리비전 {mode === 'new' && presetRevision && (

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

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

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

)}
)} />
{/* ✅ B3 문서용 Usage 필드 - 조건부 표시 */} {isB3Document && usageOptions.length > 0 && ( ( 용도 {isUsageRequired && *}

{currentStage} 스테이지에 필요한 용도를 선택하세요.

)} /> )} ( 업로더명

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

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