summaryrefslogtreecommitdiff
path: root/components/form-data/form-data-report-batch-dialog.tsx
diff options
context:
space:
mode:
authorrlaks5757 <rlaks5757@gmail.com>2025-03-26 16:51:54 +0900
committerrlaks5757 <rlaks5757@gmail.com>2025-03-27 17:32:42 +0900
commit92ddb4f13d48cbf344dc2bf63df4457b3c713608 (patch)
tree38108e1ca08a86c1b36941d39acc47601529a14a /components/form-data/form-data-report-batch-dialog.tsx
parent2ca4c91514feadb5edd0c9411670c7d9964d21e3 (diff)
feat: report batch download 기능 완료
Diffstat (limited to 'components/form-data/form-data-report-batch-dialog.tsx')
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx348
1 files changed, 348 insertions, 0 deletions
diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
new file mode 100644
index 00000000..614f890e
--- /dev/null
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -0,0 +1,348 @@
+"use client";
+
+import React, {
+ FC,
+ Dispatch,
+ SetStateAction,
+ useState,
+ useEffect,
+} from "react";
+import { useToast } from "@/hooks/use-toast";
+import prettyBytes from "pretty-bytes";
+import { X, Loader2 } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Label } from "@/components/ui/label";
+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,
+} from "@/components/ui/file-list";
+import { Button } from "@/components/ui/button";
+import { getReportTempList, getOrigin } from "@/lib/forms/services";
+import { DataTableColumnJSON } from "./form-data-table-columns";
+
+const MAX_FILE_SIZE = 3000000;
+
+type ReportData = {
+ [key: string]: any;
+};
+
+interface tempFile {
+ fileName: string;
+ filePath: string;
+}
+
+interface FormDataReportBatchDialogProps {
+ open: boolean;
+ setOpen: Dispatch<SetStateAction<boolean>>;
+ columnsJSON: DataTableColumnJSON[];
+ reportData: ReportData[];
+ packageId: number;
+ formId: number;
+ formCode: string;
+}
+
+export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
+ open,
+ setOpen,
+ columnsJSON,
+ reportData,
+ packageId,
+ formId,
+ formCode,
+}) => {
+ const { toast } = useToast();
+ const [tempList, setTempList] = useState<tempFile[]>([]);
+ const [selectTemp, setSelectTemp] = useState<string>("");
+ const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
+ const [isUploading, setIsUploading] = useState(false);
+
+ useEffect(() => {
+ updateReportTempList(packageId, formId, setTempList);
+ }, [packageId, formId]);
+
+ const onClose = () => {
+ if (isUploading) {
+ return;
+ }
+ setOpen(false);
+ };
+
+ // 드롭존 - 파일 드랍 처리
+ const handleDropAccepted = (acceptedFiles: File[]) => {
+ const newFiles = [...acceptedFiles];
+ setSelectedFiles(newFiles);
+ };
+
+ // 드롭존 - 파일 거부(에러) 처리
+ const handleDropRejected = (fileRejections: any[]) => {
+ fileRejections.forEach((rejection) => {
+ toast({
+ variant: "destructive",
+ title: "File Error",
+ description: `${rejection.file.name}: ${
+ rejection.errors[0]?.message || "Upload failed"
+ }`,
+ });
+ });
+ };
+
+ // 파일 제거 핸들러
+ const removeFile = (index: number) => {
+ const updatedFiles = [...selectedFiles];
+ updatedFiles.splice(index, 1);
+ setSelectedFiles(updatedFiles);
+ };
+
+ const submitData = async () => {
+ setIsUploading(true);
+
+ try {
+ const origin = await getOrigin()
+
+ const targetFiles = selectedFiles[0];
+
+ const reportDatas = reportData.map((c) => {
+ const reportValue = stringifyAllValues(c);
+
+ const reportValueMapping: { [key: string]: any } = {};
+
+ columnsJSON.forEach((c2) => {
+ const { key, label } = c2;
+
+ const objKey = label.split(" ").join("_");
+
+ reportValueMapping[objKey] = reportValue?.[key] ?? "";
+ });
+
+ return reportValueMapping;
+ });
+ const formData = new FormData();
+ formData.append("file", targetFiles);
+ formData.append("customFileName", `${formCode}.pdf`);
+ formData.append("reportDatas", JSON.stringify(reportDatas));
+ formData.append("reportTempPath", selectTemp);
+
+ const reqeustCreateReport = await fetch(
+ `${origin}/api/pdftron/createVendorDataReports`,
+ { method: "POST", body: formData }
+ );
+
+ if (reqeustCreateReport.ok) {
+ const blob = await reqeustCreateReport.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = `${formCode}.pdf`;
+ a.click();
+ window.URL.revokeObjectURL(url);
+ } else {
+ const err = await reqeustCreateReport.json();
+ console.error("에러:", err);
+ }
+ } catch (err) {
+ console.error(err);
+ toast({
+ title: "Error",
+ description: "Report 생성 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsUploading(false);
+ setSelectedFiles([])
+ setOpen(false)
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={onClose}>
+ <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
+ <DialogHeader>
+ <DialogTitle>Batch Report Download</DialogTitle>
+ <DialogDescription>
+ Report Template을 선택하신 후 갑지를 업로드하여 주시기 바랍니다.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="h-[60px]">
+ <Label>Report Template Select</Label>
+ <Select value={selectTemp} onValueChange={setSelectTemp}>
+ <SelectTrigger className="w-[100%]">
+ <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
+ </SelectTrigger>
+ <SelectContent>
+ {tempList.map((c) => {
+ const { fileName, filePath } = c;
+
+ return (
+ <SelectItem key={filePath} value={filePath}>
+ {fileName}
+ </SelectItem>
+ );
+ })}
+ </SelectContent>
+ </Select>
+ </div>
+ <div>
+ <Label>Report Cover Page Upload</Label>
+ <Dropzone
+ maxSize={MAX_FILE_SIZE}
+ multiple={false}
+ 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>파일을 여기에 드롭하세요</DropzoneTitle>
+ <DropzoneDescription>
+ 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "}
+ {maxSize ? prettyBytes(maxSize) : "무제한"}
+ </DropzoneDescription>
+ </div>
+ </div>
+ </DropzoneZone>
+ <Label className="text-xs text-muted-foreground">
+ 여러 파일을 선택할 수 있습니다.
+ </Label>
+ </>
+ )}
+ </Dropzone>
+ </div>
+
+ {selectedFiles.length > 0 && (
+ <div className="grid gap-2">
+ <div className="flex items-center justify-between">
+ <h6 className="text-sm font-semibold">
+ 선택된 파일 ({selectedFiles.length})
+ </h6>
+ <Badge variant="secondary">{selectedFiles.length}개 파일</Badge>
+ </div>
+ <ScrollArea>
+ <UploadFileItem
+ selectedFiles={selectedFiles}
+ removeFile={removeFile}
+ isUploading={isUploading}
+ />
+ </ScrollArea>
+ </div>
+ )}
+
+ <DialogFooter>
+ <Button
+ disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading}
+ onClick={submitData}
+ >
+ {isUploading && <Loader2 />}다운로드
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+};
+
+interface UploadFileItemProps {
+ selectedFiles: File[];
+ removeFile: (index: number) => void;
+ isUploading: boolean;
+}
+
+const UploadFileItem: FC<UploadFileItemProps> = ({
+ selectedFiles,
+ removeFile,
+ isUploading,
+}) => {
+ return (
+ <FileList className="max-h-[200px] 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">Remove</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ );
+};
+
+type UpdateReportTempList = (
+ packageId: number,
+ formId: number,
+ setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
+) => void;
+
+const updateReportTempList: UpdateReportTempList = async (
+ packageId,
+ formId,
+ setTempList
+) => {
+ const tempList = await getReportTempList(packageId, formId);
+
+ setTempList(
+ tempList.map((c) => {
+ const { fileName, filePath } = c;
+ return { fileName, filePath };
+ })
+ );
+};
+
+const stringifyAllValues = (obj: any): any => {
+ if (Array.isArray(obj)) {
+ return obj.map((item) => stringifyAllValues(item));
+ } else if (typeof obj === "object" && obj !== null) {
+ const result: any = {};
+ for (const key in obj) {
+ result[key] = stringifyAllValues(obj[key]);
+ }
+ return result;
+ } else {
+ return obj !== null && obj !== undefined ? String(obj) : "";
+ }
+};