"use client" import * as React from "react" import { useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { z } from "zod" import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Input } from "@/components/ui/input" import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, FormDescription } from "@/components/ui/form" import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" import { Textarea } from "@/components/ui/textarea" import { createRevisionAction } from "@/lib/vendor-document/service" import { useToast } from "@/hooks/use-toast" import { FilePlus, X, Loader2, Building2, User, Building, Plus } from "lucide-react" import { Badge } from "@/components/ui/badge" import { Dropzone, DropzoneDescription, DropzoneInput, DropzoneTitle, DropzoneUploadIcon, DropzoneZone, } from "@/components/ui/dropzone" import { FileList, FileListAction, FileListDescription, FileListHeader, FileListIcon, FileListInfo, FileListItem, FileListName, FileListSize, } from "@/components/ui/file-list" import prettyBytes from "pretty-bytes" import { ScrollArea } from "../ui/scroll-area" // 최대 파일 크기 설정 (3000MB) const MAX_FILE_SIZE = 3e9 // zod 스키마 export const createDocumentVersionSchema = z.object({ attachments: z.array(z.instanceof(File)).min(1, "At least one file is required"), stage: z.string().min(1, "Stage is required"), revision: z.string().min(1, "Revision is required"), uploaderType: z.enum(["vendor", "client", "shi"]).default("vendor"), uploaderName: z.string().optional(), comment: z.string().optional(), }) export type CreateDocumentVersionSchema = z.infer // First, let's add a function to generate filenames based on your pattern const generateFileName = ( documentNo: string, stage: string, revision: string, originalFileName: string, index: number, totalFiles: number ) => { // Get the file extension const extension = originalFileName.split('.').pop() || ''; // Base name without extension const baseName = `${documentNo}_${stage}_${revision}`; // For multiple files, add a suffix if (totalFiles > 1) { return `${baseName}_${index + 1}.${extension}`; } // For a single file, no suffix needed return `${baseName}.${extension}`; }; // AddDocumentDialog Props interface AddDocumentDialogProps { onSuccess?: () => void stageOptions?: string[] documentId: number documentNo: string // 업로더 타입 uploaderType?: "vendor" | "client" | "shi" // 버튼 라벨 추가 buttonLabel?: string } export function AddDocumentDialog({ onSuccess, stageOptions = [], documentId, documentNo, uploaderType = "vendor", buttonLabel = "Add Document", }: AddDocumentDialogProps) { const [open, setOpen] = React.useState(false) const [selectedFiles, setSelectedFiles] = React.useState([]) const [isUploading, setIsUploading] = React.useState(false) const [uploadProgress, setUploadProgress] = React.useState(0) const { toast } = useToast() const form = useForm({ resolver: zodResolver(createDocumentVersionSchema), defaultValues: { stage: "", revision: "", attachments: [], uploaderType, uploaderName: "", comment: "", }, }) // 업로더 타입이 바뀌면 폼 값도 업데이트 React.useEffect(() => { form.setValue('uploaderType', uploaderType); }, [uploaderType, form]); // 드롭존 - 파일 드랍 처리 const handleDropAccepted = (acceptedFiles: File[]) => { const newFiles = [...selectedFiles, ...acceptedFiles]; setSelectedFiles(newFiles); form.setValue('attachments', newFiles, { shouldValidate: true }); }; // 드롭존 - 파일 거부(에러) 처리 const handleDropRejected = (fileRejections: any[]) => { fileRejections.forEach((rejection) => { toast({ variant: "destructive", title: "File Error", description: `${rejection.file.name}: ${rejection.errors[0]?.message || "Upload failed"}`, }); }); }; // 파일 제거 핸들러 const removeFile = (index: number) => { const updatedFiles = [...selectedFiles] updatedFiles.splice(index, 1) setSelectedFiles(updatedFiles) form.setValue('attachments', updatedFiles, { shouldValidate: true }) } // Submit async function onSubmit(data: CreateDocumentVersionSchema) { setIsUploading(true) setUploadProgress(0) try { // 각 파일별로 별도 요청 const totalFiles = data.attachments.length; let successCount = 0; for (let i = 0; i < totalFiles; i++) { const file = data.attachments[i]; const newFileName = generateFileName( documentNo, data.stage, data.revision, file.name, i, totalFiles ); const fData = new FormData(); fData.append("documentId", String(documentId)); fData.append("stage", data.stage); fData.append("revision", data.revision); fData.append("uploaderType", data.uploaderType); if (data.uploaderName) { fData.append("uploaderName", data.uploaderName); } if (data.comment) { fData.append("comment", data.comment); } fData.append("attachment", file); fData.append("customFileName", newFileName); // 각 파일 업로드를 요청 await createRevisionAction(fData); // 진행 상황 업데이트 successCount++; setUploadProgress(Math.round((successCount / totalFiles) * 100)); } // 성공 메시지 toast({ title: "Success", description: `${successCount} 파일이 업로드되었습니다.`, }); // 폼 초기화 form.reset(); setSelectedFiles([]); setOpen(false); // 콜백 실행 if (onSuccess) { onSuccess(); } } catch (err) { console.error(err); toast({ title: "Error", description: "파일 업로드 중 오류가 발생했습니다.", variant: "destructive", }); } finally { setIsUploading(false); setUploadProgress(0); } } function handleDialogOpenChange(nextOpen: boolean) { if (!nextOpen) { form.reset(); form.setValue('uploaderType', uploaderType); setSelectedFiles([]); setIsUploading(false); setUploadProgress(0); } setOpen(nextOpen); } // 업로더 타입에 따른 UI 설정 const uploaderConfig = { vendor: { icon: , buttonStyle: "bg-blue-600 hover:bg-blue-700", badgeStyle: "bg-blue-100 text-blue-800", title: "업체 문서 등록", nameLabel: "업체 이름", namePlaceholder: "예: 홍길동(ABC 업체)" }, client: { icon: , buttonStyle: "bg-amber-600 hover:bg-amber-700", badgeStyle: "bg-amber-100 text-amber-800", title: "고객사 문서 등록", nameLabel: "담당자 이름", namePlaceholder: "예: 김철수(고객사)" }, shi: { icon: , buttonStyle: "bg-purple-600 hover:bg-purple-700", badgeStyle: "bg-purple-100 text-purple-800", title: "삼성중공업 문서 등록", nameLabel: "담당자 이름", namePlaceholder: "예: 이영희(삼성중공업)" } } const config = uploaderConfig[uploaderType as keyof typeof uploaderConfig]; return ( {config.title} 스테이지/리비전을 입력하고 파일을 업로드하세요.
{/* stage */} ( Stage {stageOptions.length > 0 ? ( ) : ( )} )} /> {/* revision */} ( Revision )} /> {/* attachments - 드롭존 */} ( 파일 첨부 {({ maxSize }) => ( <>
파일을 여기에 드롭하세요 또는 클릭하여 파일을 선택하세요. 최대 크기: {maxSize ? prettyBytes(maxSize) : "무제한"}
여러 파일을 선택할 수 있습니다. )}
)} /> {/* 선택된 파일 목록 */} {selectedFiles.length > 0 && (
선택된 파일 ({selectedFiles.length})
{selectedFiles.length}개 파일
{selectedFiles.map((file, index) => ( {file.name} {prettyBytes(file.size)} removeFile(index)} disabled={isUploading}> Remove ))}
)} {/* 업로드 진행 상태 */} {isUploading && (
{uploadProgress}% 업로드 중...
)} {/* 선택적 필드들 */} {/*
( {config.nameLabel} (선택) )} />
*/} {/* comment (optional) */} {/* ( 코멘트 (선택)