diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-13 07:08:01 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-06-13 07:08:01 +0000 |
| commit | c72d0897f7b37843109c86f61d97eba05ba3ca0d (patch) | |
| tree | 887dd877f3f8beafa92b4d9a7b16c84b4a5795d8 /lib/b-rfq/attachment/add-revision-dialog.tsx | |
| parent | ff902243a658067fae858a615c0629aa2e0a4837 (diff) | |
(대표님) 20250613 16시 08분 b-rfq, document 등
Diffstat (limited to 'lib/b-rfq/attachment/add-revision-dialog.tsx')
| -rw-r--r-- | lib/b-rfq/attachment/add-revision-dialog.tsx | 336 |
1 files changed, 336 insertions, 0 deletions
diff --git a/lib/b-rfq/attachment/add-revision-dialog.tsx b/lib/b-rfq/attachment/add-revision-dialog.tsx new file mode 100644 index 00000000..1abefb02 --- /dev/null +++ b/lib/b-rfq/attachment/add-revision-dialog.tsx @@ -0,0 +1,336 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Upload } from "lucide-react" +import { toast } from "sonner" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +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 { Button } from "@/components/ui/button" +import { Textarea } from "@/components/ui/textarea" +import { addRevisionToAttachment } from "../service" + +// 리비전 추가 폼 스키마 +const addRevisionSchema = z.object({ + revisionComment: z.string().optional(), + file: z.instanceof(File, { + message: "파일을 선택해주세요.", + }), +}) + +type AddRevisionFormData = z.infer<typeof addRevisionSchema> + +interface AddRevisionDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + attachmentId: number + currentRevision: string + originalFileName: string + onSuccess?: () => void +} + +export function AddRevisionDialog({ + open, + onOpenChange, + attachmentId, + currentRevision, + originalFileName, + onSuccess +}: AddRevisionDialogProps) { + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState<number>(0) + + const form = useForm<AddRevisionFormData>({ + resolver: zodResolver(addRevisionSchema), + defaultValues: { + revisionComment: "", + file: undefined, + }, + }) + + const selectedFile = form.watch("file") + + // 다이얼로그 닫기 핸들러 + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen && !isSubmitting) { + form.reset() + } + onOpenChange(newOpen) + } + + // 파일 선택 처리 + const handleFileChange = (files: File[]) => { + if (files.length === 0) return + + const file = files[0] + + // 파일 크기 검증 + const maxFileSize = 10 * 1024 * 1024 // 10MB + if (file.size > maxFileSize) { + toast.error(`파일이 너무 큽니다. (최대 10MB)`) + return + } + + form.setValue("file", file) + form.clearErrors("file") + } + + // 파일 제거 + const removeFile = () => { + form.resetField("file") + } + + // 파일 업로드 API 호출 + const uploadFile = async (file: File): Promise<{ + fileName: string + originalFileName: string + filePath: string + fileSize: number + fileType: string + }> => { + const formData = new FormData() + formData.append("attachmentId", attachmentId.toString()) + formData.append("file", file) + formData.append("isRevision", "true") + + const response = await fetch("/api/upload/rfq-attachment-revision", { + method: "POST", + body: formData, + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || "파일 업로드 실패") + } + + return response.json() + } + + // 폼 제출 + const onSubmit = async (data: AddRevisionFormData) => { + setIsSubmitting(true) + setUploadProgress(0) + + try { + // 1단계: 파일 업로드 + setUploadProgress(30) + const uploadedFile = await uploadFile(data.file) + + // 2단계: DB 리비전 레코드 생성 + setUploadProgress(70) + const result = await addRevisionToAttachment(attachmentId, { + fileName: uploadedFile.fileName, + originalFileName: uploadedFile.originalFileName, + filePath: uploadedFile.filePath, + fileSize: uploadedFile.fileSize, + fileType: uploadedFile.fileType, + revisionComment: data.revisionComment, + }) + + setUploadProgress(100) + + if (result.success) { + toast.success(result.message) + form.reset() + handleOpenChange(false) + onSuccess?.() + } else { + toast.error(result.message) + } + + } catch (error) { + console.error("Upload error:", error) + toast.error(error instanceof Error ? error.message : "리비전 추가 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + setUploadProgress(0) + } + } + + // 다음 리비전 번호 계산 + const getNextRevision = (current: string) => { + const match = current.match(/Rev\.(\d+)/) + if (match) { + const num = parseInt(match[1]) + 1 + return `Rev.${num}` + } + return "Rev.1" + } + + const nextRevision = getNextRevision(currentRevision) + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle className="flex items-center gap-2"> + <Upload className="h-5 w-5" /> + 새 리비전 추가 + </DialogTitle> + <DialogDescription> + "{originalFileName}"의 새 버전을 업로드합니다. + 현재 {currentRevision} → {nextRevision} + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + {/* 리비전 코멘트 */} + <FormField + control={form.control} + name="revisionComment" + render={({ field }) => ( + <FormItem> + <FormLabel>리비전 코멘트 (선택)</FormLabel> + <FormControl> + <Textarea + placeholder={`${nextRevision} 업데이트 내용을 입력하세요`} + className="resize-none" + rows={3} + {...field} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {/* 파일 선택 - Dropzone (단일 파일) */} + <FormField + control={form.control} + name="file" + render={({ field }) => ( + <FormItem> + <FormLabel>새 파일 선택</FormLabel> + <FormControl> + <div className="space-y-3"> + <Dropzone + onDrop={(acceptedFiles) => { + handleFileChange(acceptedFiles) + }} + accept={{ + 'application/pdf': ['.pdf'], + 'application/msword': ['.doc'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.ms-excel': ['.xls'], + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], + 'application/vnd.ms-powerpoint': ['.ppt'], + 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx'], + 'application/zip': ['.zip'], + 'application/x-rar-compressed': ['.rar'] + }} + maxSize={10 * 1024 * 1024} // 10MB + multiple={false} + disabled={isSubmitting} + > + <DropzoneZone> + <DropzoneUploadIcon /> + <DropzoneTitle>클릭하여 파일 선택 또는 드래그 앤 드롭</DropzoneTitle> + <DropzoneDescription> + PDF, DOC, XLS, PPT 등 (최대 10MB, 파일 1개) + </DropzoneDescription> + <DropzoneInput /> + </DropzoneZone> + </Dropzone> + + {/* 선택된 파일 표시 */} + {selectedFile && ( + <div className="space-y-2"> + <FileListHeader> + 선택된 파일 ({nextRevision}) + </FileListHeader> + <FileList> + <FileListItem> + <FileListIcon /> + <FileListInfo> + <FileListName>{selectedFile.name}</FileListName> + <FileListDescription> + <FileListSize>{selectedFile.size}</FileListSize> + </FileListDescription> + </FileListInfo> + <FileListAction + onClick={removeFile} + disabled={isSubmitting} + /> + </FileListItem> + </FileList> + </div> + )} + + {/* 업로드 진행률 */} + {isSubmitting && uploadProgress > 0 && ( + <div className="space-y-2"> + <div className="flex justify-between text-sm"> + <span>업로드 진행률</span> + <span>{uploadProgress}%</span> + </div> + <div className="w-full bg-gray-200 rounded-full h-2"> + <div + className="bg-blue-600 h-2 rounded-full transition-all duration-300" + style={{ width: `${uploadProgress}%` }} + /> + </div> + </div> + )} + </div> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => handleOpenChange(false)} + disabled={isSubmitting} + > + 취소 + </Button> + <Button type="submit" disabled={isSubmitting || !selectedFile}> + {isSubmitting ? "업로드 중..." : `${nextRevision} 추가`} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
