diff options
Diffstat (limited to 'components/form-data/form-data-report-temp-upload-dialog.tsx')
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-dialog.tsx | 587 |
1 files changed, 67 insertions, 520 deletions
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx index 69df704e..74cfe7c3 100644 --- a/components/form-data/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -1,66 +1,25 @@ "use client"; -import React, { - FC, - Dispatch, - SetStateAction, - useState, - useEffect, -} from "react"; -import { useToast } from "@/hooks/use-toast"; -import { toast as toastMessage } from "sonner"; -import prettyBytes from "pretty-bytes"; -import { X, Loader2, Download, Delete, Trash2 } from "lucide-react"; -import ExcelJS from "exceljs"; -import { saveAs } from "file-saver"; -import { Badge } from "@/components/ui/badge"; +import React, { FC, Dispatch, SetStateAction, useState } from "react"; 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 { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 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 { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { - getReportTempList, - uploadReportTemp, - getReportTempFileData, - deleteReportTempFile, -} from "@/lib/forms/services"; -import { VendorDataReportTemps } from "@/db/schema/vendorData"; + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { TempDownloadBtn } from "./temp-download-btn"; +import { VarListDownloadBtn } from "./var-list-download-btn"; +import { FormDataReportTempUploadTab } from "./form-data-report-temp-upload-tab"; +import { FormDataReportTempUploadedListTab } from "./form-data-report-temp-uploaded-list-tab"; import { DataTableColumnJSON } from "./form-data-table-columns"; interface FormDataReportTempUploadDialogProps { @@ -73,9 +32,6 @@ interface FormDataReportTempUploadDialogProps { uploaderType: string; } -// 최대 파일 크기 설정 (3000MB) -const MAX_FILE_SIZE = 3000000; - export const FormDataReportTempUploadDialog: FC< FormDataReportTempUploadDialogProps > = ({ @@ -87,483 +43,74 @@ export const FormDataReportTempUploadDialog: FC< formCode, uploaderType, }) => { - const { toast } = useToast(); - const [selectedFiles, setSelectedFiles] = useState<File[]>([]); - const [isUploading, setIsUploading] = useState(false); - const [uploadProgress, setUploadProgress] = useState(0); - const [prevReportTemp, setPrevReportTemp] = useState<VendorDataReportTemps[]>( - [] - ); - - useEffect(() => { - updateReportTempList(packageId, formId, setPrevReportTemp); - }, [packageId, formId]); - - // 드롭존 - 파일 드랍 처리 - const handleDropAccepted = (acceptedFiles: File[]) => { - const newFiles = [...selectedFiles, ...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); - 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)); - } - } catch (err) { - console.error(err); - toast({ - title: "Error", - description: "파일 업로드 중 오류가 발생했습니다.", - variant: "destructive", - }); - } finally { - setIsUploading(false); - setUploadProgress(0); - updateReportTempList(packageId, formId, setPrevReportTemp); - setOpen(false); - } - }; - - const downloadTempFile = async () => { - try { - const { fileName, fileType, base64 } = await getReportTempFileData(); - - saveAs(`data:${fileType};base64,${base64}`, fileName); - - toastMessage.success("Report Sample File 다운로드 완료!"); - } catch (err) { - console.log(err); - toast({ - title: "Error", - description: "Sample File을 찾을 수가 없습니다.", - variant: "destructive", - }); - } - }; - - const downloadReportVarList = async () => { - try { - // Create a new workbook - const workbook = new ExcelJS.Workbook(); - - // 데이터 시트 생성 - const worksheet = workbook.addWorksheet("Data"); - - // 유효성 검사용 숨김 시트 생성 - const validationSheet = workbook.addWorksheet("ValidationData"); - validationSheet.state = "hidden"; // 시트 숨김 처리 - - // 1. 데이터 시트에 헤더 추가 - const headers = ["Table Column Label", "Report Variable"]; - worksheet.addRow(headers); - - // 헤더 스타일 적용 - const headerRow = worksheet.getRow(1); - headerRow.font = { bold: true }; - headerRow.alignment = { horizontal: "center" }; - headerRow.eachCell((cell) => { - cell.fill = { - type: "pattern", - pattern: "solid", - fgColor: { argb: "FFCCCCCC" }, - }; - }); - - // 2. 데이터 행 추가 - columnsJSON.forEach((row) => { - const { displayLabel, label } = row; - - const labelConvert = label.replaceAll(" ", "_"); - - worksheet.addRow([displayLabel, labelConvert]); - }); - - // 3. 컬럼 너비 자동 조정 - headers.forEach((col, idx) => { - const column = worksheet.getColumn(idx + 1); - - // 최적 너비 계산 - let maxLength = col.length; - columnsJSON.forEach((row) => { - const valueKey = idx === 0 ? "displayLabel" : "label"; - - const value = row[valueKey]; - if (value !== undefined && value !== null) { - const valueLength = String(value).length; - if (valueLength > maxLength) { - maxLength = valueLength; - } - } - }); - - // 너비 설정 (최소 10, 최대 50) - column.width = Math.min(Math.max(maxLength + 2, 10), 50); - }); - - const buffer = await workbook.xlsx.writeBuffer(); - saveAs(new Blob([buffer]), `${formCode}_report_varible_list.xlsx`); - toastMessage.success("Report Varible List File 다운로드 완료!"); - } catch (err) { - console.log(err); - toast({ - title: "Error", - description: "Variable List 파일을 찾을 수가 없습니다.", - variant: "destructive", - }); - } - }; + const [tabValue, setTabValue] = useState<"upload" | "uploaded">("upload"); return ( <Dialog open={open} onOpenChange={setOpen}> <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}> <DialogHeader> - <DialogTitle>Report Template Upload</DialogTitle> + <DialogTitle>Vendor Document Template</DialogTitle> <DialogDescription> - 사용하시고자 하는 Report Template(.docx)를 업로드 하여주시기 - 바랍니다. + {/* 사용하시고자 하는 Vendor Document Template(.docx)를 업로드 + 하여주시기 바랍니다. */} </DialogDescription> </DialogHeader> - <div> - <Label>Sample Template Download</Label> - - <FileList className="max-h-[200px] gap-3"> - <FileListItem className="p-3"> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>sample_template_file.docx</FileListName> - </FileListInfo> - <FileListAction onClick={downloadTempFile}> - <Download className="h-4 w-4" /> - <span className="sr-only">Download</span> - </FileListAction> - </FileListHeader> - </FileListItem> - <FileListItem className="p-3"> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>report_variable_list.xlsx</FileListName> - </FileListInfo> - <FileListAction onClick={downloadReportVarList}> - <Download className="h-4 w-4" /> - <span className="sr-only">Download</span> - </FileListAction> - </FileListHeader> - </FileListItem> - </FileList> - </div> - <div> - <Label>Uploaded Template Files</Label> - <UploadedTempFiles - prevReportTemp={prevReportTemp} - updateReportTempList={() => - updateReportTempList(packageId, formId, setPrevReportTemp) - } - /> - </div> - - <div> - <Label>Report Template File Upload(.docx)</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>파일을 여기에 드롭하세요</DropzoneTitle> - <DropzoneDescription> - 또는 클릭하여 파일을 선택하세요. 최대 크기:{" "} - {maxSize ? prettyBytes(maxSize) : "무제한"} - </DropzoneDescription> + <Tabs value={tabValue}> + <div className="flex justify-between items-center"> + <TabsList> + <TabsTrigger value="upload" onClick={() => setTabValue("upload")}> + Upload Template File + </TabsTrigger> + <TabsTrigger + value="uploaded" + onClick={() => setTabValue("uploaded")} + > + Uploaded Template File List + </TabsTrigger> + </TabsList> + <div className="flex flex-row gap-2"> + <TooltipProvider> + <Tooltip> + <TooltipTrigger asChild> + <div> + <TempDownloadBtn /> </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> + </TooltipTrigger> + <TooltipContent> + <Label>Template Sample File Download</Label> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <div> + <VarListDownloadBtn + columnsJSON={columnsJSON} + formCode={formCode} + /> + </div> + </TooltipTrigger> + <TooltipContent> + <Label>Variable List File Download</Label> + </TooltipContent> + </Tooltip> + </TooltipProvider> </div> - <ScrollArea> - <UploadFileItem - selectedFiles={selectedFiles} - removeFile={removeFile} - isUploading={isUploading} - /> - </ScrollArea> </div> - )} - - {isUploading && <UploadProgressBox uploadProgress={uploadProgress} />} - <DialogFooter> - <Button onClick={() => setOpen(false)}>취소</Button> - <Button disabled={selectedFiles.length === 0} onClick={submitData}> - 업로드 - </Button> - </DialogFooter> + <TabsContent value="upload"> + <FormDataReportTempUploadTab + packageId={packageId} + formId={formId} + uploaderType={uploaderType} + /> + </TabsContent> + <TabsContent value="uploaded"> + <FormDataReportTempUploadedListTab + packageId={packageId} + formId={formId} + /> + </TabsContent> + </Tabs> </DialogContent> </Dialog> ); }; - -interface UploadFileItemProps { - selectedFiles: File[]; - removeFile: (index: number) => void; - isUploading: boolean; -} - -const UploadFileItem: FC<UploadFileItemProps> = ({ - selectedFiles, - removeFile, - isUploading, -}) => { - return ( - <FileList className="max-h-[100px] 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> - ); -}; - -const UploadProgressBox: FC<{ uploadProgress: number }> = ({ - uploadProgress, -}) => { - 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">{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> - ); -}; - -const generateFileName = ( - packageId: number, - formId: number, - originalFileName: string, - index: number, - totalFiles: number -) => { - // Get the file extension - const extension = originalFileName.split(".").pop() || ""; - - // Base name without extension - const baseName = `${packageId}_${formId}`; - - // For multiple files, add a suffix - if (totalFiles > 1) { - return `${baseName}_${index + 1}.${extension}`; - } - - // For a single file, no suffix needed - return `${baseName}.${extension}`; -}; - -type UpdateReportTempList = ( - packageId: number, - formId: number, - setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>> -) => void; - -const updateReportTempList: UpdateReportTempList = async ( - packageId, - formId, - setPrevReportTemp -) => { - const tempList = await getReportTempList(packageId, formId); - setPrevReportTemp(tempList); -}; - -interface UploadedTempFiles { - prevReportTemp: VendorDataReportTemps[]; - updateReportTempList: () => void; -} - -const UploadedTempFiles: FC<UploadedTempFiles> = ({ - prevReportTemp, - updateReportTempList, -}) => { - const { toast } = useToast(); - - const downloadTempFile = async (fileName: string, filePath: string) => { - try { - const getTempFile = await fetch(filePath); - - if (getTempFile.ok) { - const blob = await getTempFile.blob(); - - saveAs(blob, fileName); - - toastMessage.success("Report 다운로드 완료!"); - } else { - const err = await getTempFile.json(); - console.error("에러:", err); - throw new Error(err.message); - } - - toastMessage.success("Template File 다운로드 완료!"); - } catch (err) { - console.error(err) - toast({ - title: "Error", - description: "Template File 다운로드 중 오류가 발생했습니다.", - variant: "destructive", - }); - } - }; - - const deleteTempFile = async (id: number) => { - try { - const { result, error } = await deleteReportTempFile(id); - - if (result) { - updateReportTempList(); - toastMessage.success("Template File 삭제 완료!"); - } else { - throw new Error(error); - } - } catch (err) { - toast({ - title: "Error", - description: "Template File 삭제 중 오류가 발생했습니다.", - variant: "destructive", - }); - } - }; - - return ( - <ScrollArea> - <FileList className="max-h-[100px] gap-3"> - {prevReportTemp.map((c) => { - const { fileName, filePath, id } = c; - - return ( - <AlertDialog key={id}> - <FileListItem className="p-3"> - <FileListHeader> - <FileListIcon /> - <FileListInfo> - <FileListName>{fileName}</FileListName> - </FileListInfo> - <FileListAction - onClick={() => { - downloadTempFile(fileName, filePath); - }} - > - <Download className="h-4 w-4" /> - <span className="sr-only">Download</span> - </FileListAction> - <AlertDialogTrigger asChild> - <FileListAction> - <Trash2 className="h-4 w-4" /> - <span className="sr-only">Delete</span> - </FileListAction> - </AlertDialogTrigger> - <AlertDialogContent> - <AlertDialogHeader> - <AlertDialogTitle> - Report Templete File({fileName})을 삭제하시겠습니까? - </AlertDialogTitle> - <AlertDialogDescription /> - </AlertDialogHeader> - <AlertDialogFooter> - <AlertDialogCancel>취소</AlertDialogCancel> - <AlertDialogAction - onClick={() => { - deleteTempFile(id); - }} - > - 삭제 - </AlertDialogAction> - </AlertDialogFooter> - </AlertDialogContent> - </FileListHeader> - </FileListItem> - </AlertDialog> - ); - })} - </FileList> - </ScrollArea> - ); -}; |
