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-attachment-dialog.tsx | |
| parent | ff902243a658067fae858a615c0629aa2e0a4837 (diff) | |
(대표님) 20250613 16시 08분 b-rfq, document 등
Diffstat (limited to 'lib/b-rfq/attachment/add-attachment-dialog.tsx')
| -rw-r--r-- | lib/b-rfq/attachment/add-attachment-dialog.tsx | 355 |
1 files changed, 355 insertions, 0 deletions
diff --git a/lib/b-rfq/attachment/add-attachment-dialog.tsx b/lib/b-rfq/attachment/add-attachment-dialog.tsx new file mode 100644 index 00000000..665e0f88 --- /dev/null +++ b/lib/b-rfq/attachment/add-attachment-dialog.tsx @@ -0,0 +1,355 @@ +"use client" + +import * as React from "react" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { z } from "zod" +import { Plus ,X} from "lucide-react" +import { toast } from "sonner" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} 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 { + 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 { addRfqAttachmentRecord } from "../service" + +// 첨부파일 추가 폼 스키마 (단일 파일) +const addAttachmentSchema = z.object({ + attachmentType: z.enum(["구매", "설계"], { + required_error: "문서 타입을 선택해주세요.", + }), + description: z.string().optional(), + file: z.instanceof(File, { + message: "파일을 선택해주세요.", + }), +}) + +type AddAttachmentFormData = z.infer<typeof addAttachmentSchema> + +interface AddAttachmentDialogProps { + rfqId: number +} + +export function AddAttachmentDialog({ rfqId }: AddAttachmentDialogProps) { + const [open, setOpen] = React.useState(false) + const [isSubmitting, setIsSubmitting] = React.useState(false) + const [uploadProgress, setUploadProgress] = React.useState<number>(0) + + const form = useForm<AddAttachmentFormData>({ + resolver: zodResolver(addAttachmentSchema), + defaultValues: { + attachmentType: undefined, + description: "", + file: undefined, + }, + }) + + const selectedFile = form.watch("file") + + // 다이얼로그 닫기 핸들러 + const handleOpenChange = (newOpen: boolean) => { + if (!newOpen && !isSubmitting) { + form.reset() + } + setOpen(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("rfqId", rfqId.toString()) + formData.append("file", file) + + const response = await fetch("/api/upload/rfq-attachment", { + method: "POST", + body: formData, + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || "파일 업로드 실패") + } + + return response.json() + } + + // 폼 제출 + const onSubmit = async (data: AddAttachmentFormData) => { + setIsSubmitting(true) + setUploadProgress(0) + + try { + // 1단계: 파일 업로드 + setUploadProgress(30) + const uploadedFile = await uploadFile(data.file) + + // 2단계: DB 레코드 생성 (시리얼 번호 자동 생성) + setUploadProgress(70) + const attachmentRecord = { + rfqId, + attachmentType: data.attachmentType, + description: data.description, + fileName: uploadedFile.fileName, + originalFileName: uploadedFile.originalFileName, + filePath: uploadedFile.filePath, + fileSize: uploadedFile.fileSize, + fileType: uploadedFile.fileType, + } + + const result = await addRfqAttachmentRecord(attachmentRecord) + + setUploadProgress(100) + + if (result.success) { + toast.success(result.message) + form.reset() + handleOpenChange(false) + } else { + toast.error(result.message) + } + + } catch (error) { + console.error("Upload error:", error) + toast.error(error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다.") + } finally { + setIsSubmitting(false) + setUploadProgress(0) + } + } + + return ( + <Dialog open={open} onOpenChange={handleOpenChange}> + <DialogTrigger asChild> + <Button variant="outline" size="sm" className="gap-2"> + <Plus className="size-4" aria-hidden="true" /> + <span className="hidden sm:inline">새 첨부</span> + </Button> + </DialogTrigger> + + <DialogContent className="sm:max-w-[500px]"> + <DialogHeader> + <DialogTitle>새 첨부파일 추가</DialogTitle> + <DialogDescription> + RFQ에 첨부할 문서를 업로드합니다. 시리얼 번호는 자동으로 부여됩니다. + </DialogDescription> + </DialogHeader> + + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + {/* 문서 타입 선택 */} + <FormField + control={form.control} + name="attachmentType" + render={({ field }) => ( + <FormItem> + <FormLabel>문서 타입</FormLabel> + <Select onValueChange={field.onChange} value={field.value}> + <FormControl> + <SelectTrigger> + <SelectValue placeholder="문서 타입을 선택하세요" /> + </SelectTrigger> + </FormControl> + <SelectContent> + <SelectItem value="구매">구매</SelectItem> + <SelectItem value="설계">설계</SelectItem> + </SelectContent> + </Select> + <FormMessage /> + </FormItem> + )} + /> + + {/* 설명 */} + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>설명 (선택)</FormLabel> + <FormControl> + <Textarea + placeholder="첨부파일에 대한 설명을 입력하세요" + 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> + 선택된 파일 + </FileListHeader> + <FileList> + <FileListItem className="flex items-center justify-between gap-3"> + <FileListIcon /> + <FileListInfo> + <FileListName>{selectedFile.name}</FileListName> + <FileListDescription> + <FileListSize>{selectedFile.size}</FileListSize> + </FileListDescription> + </FileListInfo> + <FileListAction + onClick={removeFile} + disabled={isSubmitting} + > + <X className="h-4 w-4" /> + </FileListAction> + </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 ? "업로드 중..." : "업로드"} + </Button> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ) +}
\ No newline at end of file |
