diff options
Diffstat (limited to 'components/form-data-plant/form-data-report-temp-upload-tab.tsx')
| -rw-r--r-- | components/form-data-plant/form-data-report-temp-upload-tab.tsx | 243 |
1 files changed, 243 insertions, 0 deletions
diff --git a/components/form-data-plant/form-data-report-temp-upload-tab.tsx b/components/form-data-plant/form-data-report-temp-upload-tab.tsx new file mode 100644 index 00000000..81186ba4 --- /dev/null +++ b/components/form-data-plant/form-data-report-temp-upload-tab.tsx @@ -0,0 +1,243 @@ +"use client"; + +import React, { FC, useState } from "react"; +import { useParams } from "next/navigation"; +import { useTranslation } from "@/i18n/client"; +import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage } from "sonner"; +import prettyBytes from "pretty-bytes"; +import { X, Loader2 } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { DialogFooter } from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { + Dropzone, + DropzoneDescription, + DropzoneInput, + DropzoneTitle, + DropzoneUploadIcon, + DropzoneZone, +} from "@/components/ui/dropzone"; +import { + FileList, + FileListAction, + FileListDescription, + FileListHeader, + FileListIcon, + FileListInfo, + FileListItem, + FileListName, +} from "@/components/ui/file-list"; +import { uploadReportTemp } from "@/lib/forms-plant/services"; + +// 최대 파일 크기 설정 (3000MB) +const MAX_FILE_SIZE = 3000000; + +interface FormDataReportTempUploadTabProps { + packageId: number; + formId: number; + uploaderType: string; +} + +export const FormDataReportTempUploadTab: FC< + FormDataReportTempUploadTabProps +> = ({ packageId, formId, uploaderType }) => { + const { toast } = useToast(); + const params = useParams(); + const lng = (params?.lng as string) || "ko"; + const { t } = useTranslation(lng, "engineering"); + + const [selectedFiles, setSelectedFiles] = useState<File[]>([]); + const [isUploading, setIsUploading] = useState(false); + const [uploadProgress, setUploadProgress] = useState(0); + + // 드롭존 - 파일 드랍 처리 + const handleDropAccepted = (acceptedFiles: File[]) => { + const newFiles = [...selectedFiles, ...acceptedFiles]; + setSelectedFiles(newFiles); + }; + + // 드롭존 - 파일 거부(에러) 처리 + const handleDropRejected = (fileRejections: any[]) => { + fileRejections.forEach((rejection) => { + toast({ + variant: "destructive", + title: t("templateUploadTab.fileError"), + description: `${rejection.file.name}: ${ + rejection.errors[0]?.message || t("templateUploadTab.uploadFailed") + }`, + }); + }); + }; + + // 파일 제거 핸들러 + const removeFile = (index: number) => { + const updatedFiles = [...selectedFiles]; + updatedFiles.splice(index, 1); + setSelectedFiles(updatedFiles); + }; + + const submitData = async () => { + setIsUploading(true); + setUploadProgress(0); + try { + const totalFiles = selectedFiles.length; + let successCount = 0; + + for (let i = 0; i < totalFiles; i++) { + const file = selectedFiles[i]; + + const formData = new FormData(); + formData.append("file", file); + formData.append("customFileName", file.name); + formData.append("uploaderType", uploaderType); + + await uploadReportTemp(packageId, formId, formData); + + successCount++; + setUploadProgress(Math.round((successCount / totalFiles) * 100)); + } + toastMessage.success(t("templateUploadTab.uploadComplete")); + } catch (err) { + console.error(err); + toast({ + title: t("templateUploadTab.error"), + description: t("templateUploadTab.uploadError"), + variant: "destructive", + }); + } finally { + setIsUploading(false); + setUploadProgress(0); + setSelectedFiles([]) + } + }; + + return ( + <div className='flex flex-col gap-4'> + <div> + <Label>{t("templateUploadTab.uploadLabel")}</Label> + <Dropzone + maxSize={MAX_FILE_SIZE} + multiple={true} + accept={{ accept: [".docx"] }} + onDropAccepted={handleDropAccepted} + onDropRejected={handleDropRejected} + disabled={isUploading} + > + {({ maxSize }) => ( + <> + <DropzoneZone className="flex justify-center"> + <DropzoneInput /> + <div className="flex items-center gap-6"> + <DropzoneUploadIcon /> + <div className="grid gap-0.5"> + <DropzoneTitle>{t("templateUploadTab.dropFileHere")}</DropzoneTitle> + <DropzoneDescription> + {t("templateUploadTab.orClickToSelect", { + maxSize: maxSize ? prettyBytes(maxSize) : t("templateUploadTab.unlimited") + })} + </DropzoneDescription> + </div> + </div> + </DropzoneZone> + <Label className="text-xs text-muted-foreground"> + {t("templateUploadTab.multipleFilesAllowed")} + </Label> + </> + )} + </Dropzone> + </div> + + {selectedFiles.length > 0 && ( + <div className="grid gap-2"> + <div className="flex items-center justify-between"> + <h6 className="text-sm font-semibold"> + {t("templateUploadTab.selectedFiles", { count: selectedFiles.length })} + </h6> + <Badge variant="secondary"> + {t("templateUploadTab.fileCount", { count: selectedFiles.length })} + </Badge> + </div> + <ScrollArea> + <UploadFileItem + selectedFiles={selectedFiles} + removeFile={removeFile} + isUploading={isUploading} + t={t} + /> + </ScrollArea> + </div> + )} + + {isUploading && <UploadProgressBox uploadProgress={uploadProgress} t={t} />} + <DialogFooter> + <Button disabled={selectedFiles.length === 0} onClick={submitData}> + {t("templateUploadTab.upload")} + </Button> + </DialogFooter> + </div> + ); +}; + +interface UploadFileItemProps { + selectedFiles: File[]; + removeFile: (index: number) => void; + isUploading: boolean; + t: (key: string, options?: any) => string; +} + +const UploadFileItem: FC<UploadFileItemProps> = ({ + selectedFiles, + removeFile, + isUploading, + t, +}) => { + return ( + <FileList className="max-h-[150px] gap-3"> + {selectedFiles.map((file, index) => ( + <FileListItem key={index} className="p-3"> + <FileListHeader> + <FileListIcon /> + <FileListInfo> + <FileListName>{file.name}</FileListName> + <FileListDescription> + {prettyBytes(file.size)} + </FileListDescription> + </FileListInfo> + <FileListAction + onClick={() => removeFile(index)} + disabled={isUploading} + > + <X className="h-4 w-4" /> + <span className="sr-only">{t("templateUploadTab.remove")}</span> + </FileListAction> + </FileListHeader> + </FileListItem> + ))} + </FileList> + ); +}; + +const UploadProgressBox: FC<{ + uploadProgress: number; + t: (key: string, options?: any) => string; +}> = ({ uploadProgress, t }) => { + return ( + <div className="flex flex-col gap-1 mt-2"> + <div className="flex items-center gap-2"> + <Loader2 className="h-4 w-4 animate-spin" /> + <span className="text-sm"> + {t("templateUploadTab.uploadingProgress", { progress: uploadProgress })} + </span> + </div> + <div className="h-2 w-full bg-muted rounded-full overflow-hidden"> + <div + className="h-full bg-primary rounded-full transition-all" + style={{ width: `${uploadProgress}%` }} + /> + </div> + </div> + ); +};
\ No newline at end of file |
