summaryrefslogtreecommitdiff
path: root/lib/rfq-last/attachment/add-attachment-dialog.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-09-04 08:31:31 +0000
commitb67e36df49f067cbd5ba899f9fbcc755f38d4b4f (patch)
tree5a71c5960f90d988cd509e3ef26bff497a277661 /lib/rfq-last/attachment/add-attachment-dialog.tsx
parentb7f54b06c1ef9e619f5358fb0a5caad9703c8905 (diff)
(대표님, 최겸, 임수민) 작업사항 커밋
Diffstat (limited to 'lib/rfq-last/attachment/add-attachment-dialog.tsx')
-rw-r--r--lib/rfq-last/attachment/add-attachment-dialog.tsx365
1 files changed, 365 insertions, 0 deletions
diff --git a/lib/rfq-last/attachment/add-attachment-dialog.tsx b/lib/rfq-last/attachment/add-attachment-dialog.tsx
new file mode 100644
index 00000000..14baf7c7
--- /dev/null
+++ b/lib/rfq-last/attachment/add-attachment-dialog.tsx
@@ -0,0 +1,365 @@
+"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, FileIcon } 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 { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { Progress } from "@/components/ui/progress"
+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 { useRouter } from "next/navigation";
+
+const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
+const MAX_FILES = 10; // 최대 10개 파일
+
+const addAttachmentSchema = z.object({
+ description: z.string().optional(),
+ files: z
+ .array(z.instanceof(File))
+ .min(1, "최소 1개 이상의 파일을 선택해주세요.")
+ .max(MAX_FILES, `최대 ${MAX_FILES}개까지 업로드 가능합니다.`)
+ .refine(
+ (files) => files.every((file) => file.size <= MAX_FILE_SIZE),
+ `각 파일 크기는 100MB를 초과할 수 없습니다.`
+ ),
+})
+
+type AddAttachmentFormData = z.infer<typeof addAttachmentSchema>
+
+interface AddAttachmentDialogProps {
+ rfqId: number;
+ attachmentType: "구매" | "설계";
+ onSuccess?: () => void;
+}
+
+export function AddAttachmentDialog({
+ rfqId,
+ attachmentType,
+ onSuccess
+}: AddAttachmentDialogProps) {
+ const [open, setOpen] = React.useState(false)
+ const [isSubmitting, setIsSubmitting] = React.useState(false)
+ const [uploadProgress, setUploadProgress] = React.useState(0)
+ const [selectedFiles, setSelectedFiles] = React.useState<File[]>([])
+ const router = useRouter();
+
+ const form = useForm<AddAttachmentFormData>({
+ resolver: zodResolver(addAttachmentSchema),
+ defaultValues: {
+ description: "",
+ files: [],
+ },
+ })
+
+ // 파일 크기 포맷팅 함수
+ const formatFileSize = (bytes: number) => {
+ if (bytes === 0) return "0 Bytes";
+ const k = 1024;
+ const sizes = ["Bytes", "KB", "MB", "GB"];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
+ };
+
+ // 파일 확장자 가져오기
+ const getFileExtension = (fileName: string) => {
+ return fileName.split('.').pop()?.toUpperCase() || 'FILE';
+ };
+
+ // 파일 추가 처리
+ const handleFilesAdd = (newFiles: FileList | null) => {
+ if (!newFiles) return;
+
+ const filesArray = Array.from(newFiles);
+ const totalFiles = selectedFiles.length + filesArray.length;
+
+ if (totalFiles > MAX_FILES) {
+ toast.error(`최대 ${MAX_FILES}개까지만 업로드할 수 있습니다.`);
+ return;
+ }
+
+ // 파일 크기 체크
+ const oversizedFiles = filesArray.filter(file => file.size > MAX_FILE_SIZE);
+ if (oversizedFiles.length > 0) {
+ toast.error(`다음 파일들이 100MB를 초과합니다: ${oversizedFiles.map(f => f.name).join(", ")}`);
+ return;
+ }
+
+ const updatedFiles = [...selectedFiles, ...filesArray];
+ setSelectedFiles(updatedFiles);
+ form.setValue("files", updatedFiles, { shouldValidate: true });
+ };
+
+
+ const onSubmit = async (data: AddAttachmentFormData) => {
+ setIsSubmitting(true);
+ setUploadProgress(0);
+
+ try {
+ const formData = new FormData();
+ formData.append("rfqId", rfqId.toString());
+ formData.append("attachmentType", attachmentType);
+ formData.append("description", data.description || "");
+
+ // 모든 파일 추가
+ data.files.forEach((file) => {
+ formData.append("files", file);
+ });
+
+ // 진행률 시뮬레이션
+ const progressInterval = setInterval(() => {
+ setUploadProgress((prev) => {
+ if (prev >= 90) {
+ clearInterval(progressInterval);
+ return 90;
+ }
+ return prev + 10;
+ });
+ }, 200);
+
+ const response = await fetch("/api/rfq-attachments/upload", {
+ method: "POST",
+ body: formData,
+ });
+
+ clearInterval(progressInterval);
+ setUploadProgress(100);
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.message || "파일 업로드 실패");
+ }
+
+ const result = await response.json();
+
+ if (result.success) {
+ toast.success(`${result.uploadedCount}개 파일이 성공적으로 업로드되었습니다`);
+ form.reset();
+ setSelectedFiles([]);
+ setOpen(false);
+
+ router.refresh()
+ onSuccess?.();
+ } else {
+ toast.error(result.message);
+ }
+ } catch (error) {
+ toast.error(error instanceof Error ? error.message : "파일 업로드 중 오류가 발생했습니다");
+ } finally {
+ setIsSubmitting(false);
+ setUploadProgress(0);
+ }
+ };
+
+ // 다이얼로그 닫을 때 상태 초기화
+ const handleOpenChange = (newOpen: boolean) => {
+ if (!newOpen) {
+ form.reset();
+ setSelectedFiles([]);
+ setUploadProgress(0);
+ }
+ setOpen(newOpen);
+ };
+
+ const handleDropAccepted = (acceptedFiles: File[]) => {
+ const newFiles = [...selectedFiles, ...acceptedFiles]
+ setSelectedFiles(newFiles)
+ form.setValue('files', newFiles, { shouldValidate: true })
+ }
+
+ const removeFile = (index: number) => {
+ const updatedFiles = [...selectedFiles]
+ updatedFiles.splice(index, 1)
+ setSelectedFiles(updatedFiles)
+ form.setValue('files', updatedFiles, { shouldValidate: true })
+ }
+
+ return (
+ <Dialog open={open} onOpenChange={handleOpenChange}>
+ <DialogTrigger asChild>
+ <Button size="sm">
+ <Plus className="h-4 w-4 mr-2" />
+ 파일 업로드
+ </Button>
+ </DialogTrigger>
+
+ <DialogContent className="sm:max-w-[600px] max-h-[90vh] flex flex-col">
+ <DialogHeader className="flex-shrink-0">
+ <DialogTitle>새 첨부파일 추가</DialogTitle>
+ <DialogDescription>
+ {attachmentType} 문서를 업로드합니다. (파일당 최대 100MB, 최대 {MAX_FILES}개)
+ </DialogDescription>
+ </DialogHeader>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col flex-1 min-h-0">
+ <div className="flex-1 overflow-y-auto px-1">
+ <div className="space-y-4 py-4">
+ <FormField
+ control={form.control}
+ name="files"
+ render={() => (
+ <FormItem>
+ <FormLabel>파일 첨부</FormLabel>
+ <Dropzone
+ maxSize={MAX_FILE_SIZE} // 3GB
+ multiple={true}
+ onDropAccepted={handleDropAccepted}
+ disabled={isSubmitting}
+ >
+ <DropzoneZone className="flex justify-center">
+ <FormControl>
+ <DropzoneInput />
+ </FormControl>
+ <div className="flex items-center gap-6">
+ <DropzoneUploadIcon />
+ <div className="grid gap-0.5">
+ <DropzoneTitle>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneDescription>
+ 또는 클릭하여 파일을 선택하세요
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ </Dropzone>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 선택된 파일 목록 */}
+ {selectedFiles.length > 0 && (
+ <div className="space-y-2">
+ <FileList>
+ <FileListHeader>
+ <div className="flex justify-between items-center">
+ <span className="text-sm font-medium">
+ 선택된 파일 ({selectedFiles.length}/{MAX_FILES})
+ </span>
+ <span className="text-sm text-muted-foreground">
+ 총 {formatFileSize(selectedFiles.reduce((acc, file) => acc + file.size, 0))}
+ </span>
+ </div>
+ </FileListHeader>
+ {selectedFiles.map((file, index) => (
+ <FileListItem key={index}>
+ <FileListIcon>
+ <FileIcon className="h-4 w-4" />
+ </FileListIcon>
+ <FileListInfo>
+ <FileListName>{file.name}</FileListName>
+ <FileListDescription>
+ {getFileExtension(file.name)} • {formatFileSize(file.size)}
+ </FileListDescription>
+ </FileListInfo>
+ <FileListAction>
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ onClick={() => removeFile(index)}
+ disabled={isSubmitting}
+ >
+ <X className="h-4 w-4" />
+ </Button>
+ </FileListAction>
+ </FileListItem>
+ ))}
+ </FileList>
+ </div>
+ )}
+
+ <FormField
+ control={form.control}
+ name="description"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>설명 (선택)</FormLabel>
+ <FormControl>
+ <Textarea
+ placeholder="첨부파일에 대한 설명을 입력하세요"
+ className="resize-none"
+ rows={3}
+ disabled={isSubmitting}
+ {...field}
+ />
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {isSubmitting && (
+ <div className="space-y-2">
+ <div className="flex justify-between text-sm">
+ <span>업로드 진행중...</span>
+ <span>{uploadProgress}%</span>
+ </div>
+ <Progress value={uploadProgress} />
+ </div>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ type="button"
+ variant="outline"
+ onClick={() => handleOpenChange(false)}
+ disabled={isSubmitting}
+ >
+ 취소
+ </Button>
+ <Button
+ type="submit"
+ disabled={isSubmitting || selectedFiles.length === 0}
+ >
+ {isSubmitting
+ ? `업로드 중... (${uploadProgress}%)`
+ : `${selectedFiles.length}개 파일 업로드`
+ }
+ </Button>
+ </DialogFooter>
+ </form>
+ </Form>
+ </DialogContent>
+ </Dialog>
+ )
+} \ No newline at end of file