summaryrefslogtreecommitdiff
path: root/components/form-data-plant/form-data-report-temp-upload-tab.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-10-23 10:10:21 +0000
commitf7f5069a2209cfa39b65f492f32270a5f554bed0 (patch)
tree933c731ec2cb7d8bc62219a0aeed45a5e97d5f15 /components/form-data-plant/form-data-report-temp-upload-tab.tsx
parentd49ad5dee1e5a504e1321f6db802b647497ee9ff (diff)
(대표님) EDP 해양 관련 개발 사항들
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.tsx243
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