diff options
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-dialog.tsx | 544 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-tab.tsx | 225 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-uploaded-list-tab.tsx | 211 | ||||
| -rw-r--r-- | components/form-data/temp-download-btn.tsx | 40 | ||||
| -rw-r--r-- | components/form-data/var-list-download-btn.tsx | 105 |
5 files changed, 596 insertions, 529 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 32273415..51fe5aca 100644 --- a/components/form-data/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -1,59 +1,14 @@ "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, BookDown } 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 { - 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 { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tooltip, @@ -61,13 +16,10 @@ import { TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip"; -import { - getReportTempList, - uploadReportTemp, - getReportTempFileData, - deleteReportTempFile, -} from "@/lib/forms/services"; -import { VendorDataReportTemps } from "@/db/schema/vendorData"; +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 { @@ -80,9 +32,6 @@ interface FormDataReportTempUploadDialogProps { uploaderType: string; } -// 최대 파일 크기 설정 (3000MB) -const MAX_FILE_SIZE = 3000000; - export const FormDataReportTempUploadDialog: FC< FormDataReportTempUploadDialogProps > = ({ @@ -94,98 +43,8 @@ export const FormDataReportTempUploadDialog: FC< formCode, uploaderType, }) => { - const { toast } = useToast(); const [tabValue, setTabValue] = useState<"upload" | "uploaded">("upload"); - 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", - }); - } - }; - return ( <Dialog open={open} onOpenChange={setOpen}> <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}> @@ -213,14 +72,7 @@ export const FormDataReportTempUploadDialog: FC< <TooltipProvider> <Tooltip> <TooltipTrigger asChild> - <Button - variant="ghost" - className="relative p-2" - aria-label="Template Sample Download" - onClick={downloadTempFile} - > - <Download /> - </Button> + <TempDownloadBtn /> </TooltipTrigger> <TooltipContent> <Label>Template Sample Download</Label> @@ -228,14 +80,10 @@ export const FormDataReportTempUploadDialog: FC< </Tooltip> <Tooltip> <TooltipTrigger asChild> - <Button - variant="ghost" - className="relative p-2" - aria-label="Variable List Download" - onClick={downloadReportVarList} - > - <BookDown /> - </Button> + <VarListDownloadBtn + columnsJSON={columnsJSON} + formCode={formCode} + /> </TooltipTrigger> <TooltipContent> <Label>Variable List Download</Label> @@ -245,382 +93,20 @@ export const FormDataReportTempUploadDialog: FC< </div> </div> <TabsContent value="upload"> - <TempUploadTab + <FormDataReportTempUploadTab packageId={packageId} formId={formId} uploaderType={uploaderType} /> </TabsContent> <TabsContent value="uploaded"> - <TempUploadedTab packageId={packageId} formId={formId} /> + <FormDataReportTempUploadedListTab + packageId={packageId} + formId={formId} + /> </TabsContent> </Tabs> </DialogContent> </Dialog> ); }; - -interface TempUploadTabProps { - packageId: number; - formId: number; - uploaderType: string; -} - -const TempUploadTab: FC<TempUploadTabProps> = ({ - packageId, - formId, - uploaderType, -}) => { - const { toast } = useToast(); - 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: "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); - } - }; - - return ( - <> - <div> - <Label>Vendor Document 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> - </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> - )} - - {isUploading && <UploadProgressBox uploadProgress={uploadProgress} />} - <DialogFooter> - <Button disabled={selectedFiles.length === 0} onClick={submitData}> - 업로드 - </Button> - </DialogFooter> - </> - ); -}; - -interface UploadFileItemProps { - selectedFiles: File[]; - removeFile: (index: number) => void; - isUploading: boolean; -} - -const UploadFileItem: FC<UploadFileItemProps> = ({ - selectedFiles, - removeFile, - isUploading, -}) => { - 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">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> - ); -}; - -type UpdateReportTempList = ( - packageId: number, - formId: number, - setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>> -) => Promise<void>; - -const updateReportTempList: UpdateReportTempList = async ( - packageId, - formId, - setPrevReportTemp -) => { - const tempList = await getReportTempList(packageId, formId); - setPrevReportTemp(tempList); -}; - -interface UploadedTempFiles { - prevReportTemp: VendorDataReportTemps[]; - updateReportTempList: () => void; - isLoading: boolean; -} - -const UploadedTempFiles: FC<UploadedTempFiles> = ({ - prevReportTemp, - updateReportTempList, - isLoading, -}) => { - 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", - }); - } - }; - - if (isLoading) { - return ( - <div className="min-h-[157px]"> - <Label>로딩 중...</Label> - </div> - ); - } - - return ( - <ScrollArea className="min-h-[157px] max-h-[337px] overflow-auto"> - <FileList className="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> - ); -}; - -interface TempUploadedTabProps { - packageId: number; - formId: number; -} - -const TempUploadedTab: FC<TempUploadedTabProps> = ({ packageId, formId }) => { - const [prevReportTemp, setPrevReportTemp] = useState<VendorDataReportTemps[]>( - [] - ); - const [isLoading, setIsLoading] = useState(true); - - useEffect(() => { - const getTempFiles = async () => { - await updateReportTempList(packageId, formId, setPrevReportTemp); - setIsLoading(false); - }; - - getTempFiles(); - }, [packageId, formId]); - return ( - <div> - <Label>Uploaded Template File List</Label> - <UploadedTempFiles - prevReportTemp={prevReportTemp} - updateReportTempList={() => - updateReportTempList(packageId, formId, setPrevReportTemp) - } - isLoading={isLoading} - /> - </div> - ); -}; diff --git a/components/form-data/form-data-report-temp-upload-tab.tsx b/components/form-data/form-data-report-temp-upload-tab.tsx new file mode 100644 index 00000000..c09ade28 --- /dev/null +++ b/components/form-data/form-data-report-temp-upload-tab.tsx @@ -0,0 +1,225 @@ +"use client"; + +import React, { FC, useState } 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 { 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/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 [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: "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); + } + }; + + return ( + <> + <div> + <Label>Vendor Document 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> + </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> + )} + + {isUploading && <UploadProgressBox uploadProgress={uploadProgress} />} + <DialogFooter> + <Button disabled={selectedFiles.length === 0} onClick={submitData}> + 업로드 + </Button> + </DialogFooter> + </> + ); +}; + +interface UploadFileItemProps { + selectedFiles: File[]; + removeFile: (index: number) => void; + isUploading: boolean; +} + +const UploadFileItem: FC<UploadFileItemProps> = ({ + selectedFiles, + removeFile, + isUploading, +}) => { + 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">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> + ); +}; diff --git a/components/form-data/form-data-report-temp-uploaded-list-tab.tsx b/components/form-data/form-data-report-temp-uploaded-list-tab.tsx new file mode 100644 index 00000000..7379a312 --- /dev/null +++ b/components/form-data/form-data-report-temp-uploaded-list-tab.tsx @@ -0,0 +1,211 @@ +"use client"; + +import React, { + FC, + Dispatch, + SetStateAction, + useState, + useEffect, +} from "react"; +import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage } from "sonner"; +import { Download, Trash2 } from "lucide-react"; +import { saveAs } from "file-saver"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { Label } from "@/components/ui/label"; +import { + FileList, + FileListAction, + 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, deleteReportTempFile } from "@/lib/forms/services"; +import { VendorDataReportTemps } from "@/db/schema/vendorData"; + +interface FormDataReportTempUploadedListTabProps { + packageId: number; + formId: number; +} + +export const FormDataReportTempUploadedListTab: FC< + FormDataReportTempUploadedListTabProps +> = ({ packageId, formId }) => { + const [prevReportTemp, setPrevReportTemp] = useState<VendorDataReportTemps[]>( + [] + ); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const getTempFiles = async () => { + await updateReportTempList(packageId, formId, setPrevReportTemp); + setIsLoading(false); + }; + + getTempFiles(); + }, [packageId, formId]); + + return ( + <div> + <Label>Uploaded Template File List</Label> + <UploadedTempFiles + prevReportTemp={prevReportTemp} + updateReportTempList={() => + updateReportTempList(packageId, formId, setPrevReportTemp) + } + isLoading={isLoading} + /> + </div> + ); +}; + +type UpdateReportTempList = ( + packageId: number, + formId: number, + setPrevReportTemp: Dispatch<SetStateAction<VendorDataReportTemps[]>> +) => Promise<void>; + +const updateReportTempList: UpdateReportTempList = async ( + packageId, + formId, + setPrevReportTemp +) => { + const tempList = await getReportTempList(packageId, formId); + setPrevReportTemp(tempList); +}; + +interface UploadedTempFiles { + prevReportTemp: VendorDataReportTemps[]; + updateReportTempList: () => void; + isLoading: boolean; +} + +const UploadedTempFiles: FC<UploadedTempFiles> = ({ + prevReportTemp, + updateReportTempList, + isLoading, +}) => { + 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", + }); + } + }; + + if (isLoading) { + return ( + <div className="min-h-[157px]"> + <Label>로딩 중...</Label> + </div> + ); + } + + return ( + <ScrollArea className="min-h-[157px] max-h-[337px] overflow-auto"> + <FileList className="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> + ); +}; diff --git a/components/form-data/temp-download-btn.tsx b/components/form-data/temp-download-btn.tsx new file mode 100644 index 00000000..01fff569 --- /dev/null +++ b/components/form-data/temp-download-btn.tsx @@ -0,0 +1,40 @@ +"use client"; + +import React from "react"; +import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage } from "sonner"; +import { Download } from "lucide-react"; +import { saveAs } from "file-saver"; +import { Button } from "@/components/ui/button"; +import { getReportTempFileData } from "@/lib/forms/services"; + +export const TempDownloadBtn = () => { + const { toast } = useToast(); + + 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", + }); + } + }; + return ( + <Button + variant="ghost" + className="relative p-2" + aria-label="Template Sample Download" + onClick={downloadTempFile} + > + <Download /> + </Button> + ); +}; diff --git a/components/form-data/var-list-download-btn.tsx b/components/form-data/var-list-download-btn.tsx new file mode 100644 index 00000000..964844ce --- /dev/null +++ b/components/form-data/var-list-download-btn.tsx @@ -0,0 +1,105 @@ +"use client"; + +import React, { FC } from "react"; +import { useToast } from "@/hooks/use-toast"; +import { toast as toastMessage } from "sonner"; +import { BookDown } from "lucide-react"; +import ExcelJS from "exceljs"; +import { saveAs } from "file-saver"; +import { Button } from "@/components/ui/button"; +import { DataTableColumnJSON } from "./form-data-table-columns"; + +interface VarListDownloadBtnProps { + columnsJSON: DataTableColumnJSON[]; + formCode: string; +} + +export const VarListDownloadBtn: FC<VarListDownloadBtnProps> = ({ + columnsJSON, + formCode, +}) => { + const { toast } = useToast(); + + 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", + }); + } + }; + + return ( + <Button + variant="ghost" + className="relative p-2" + aria-label="Variable List Download" + onClick={downloadReportVarList} + > + <BookDown /> + </Button> + ); +}; |
