summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-04-02 09:56:20 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-04-02 09:56:20 +0000
commit230ce796836c25df26c130dbcd616ef97d12b2ec (patch)
tree033d982409c89b5f90db0b3772827139c7a8bf80 /components/form-data
parentdfdfae3018f8499240f48d28ce634f4a5c56e006 (diff)
parent9b6269c856516f3987ee249c6131a44728e276e2 (diff)
Merge branch 'dev' of https://github.com/DTS-Development/SHI_EVCP into dev
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/form-data-report-batch-dialog.tsx33
-rw-r--r--components/form-data/form-data-report-dialog.tsx10
-rw-r--r--components/form-data/form-data-report-temp-upload-dialog.tsx587
-rw-r--r--components/form-data/form-data-report-temp-upload-tab.tsx228
-rw-r--r--components/form-data/form-data-report-temp-uploaded-list-tab.tsx211
-rw-r--r--components/form-data/form-data-table-columns.tsx2
-rw-r--r--components/form-data/form-data-table.tsx6
-rw-r--r--components/form-data/temp-download-btn.tsx45
-rw-r--r--components/form-data/var-list-download-btn.tsx110
9 files changed, 689 insertions, 543 deletions
diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx
index f4efde64..6a76784c 100644
--- a/components/form-data/form-data-report-batch-dialog.tsx
+++ b/components/form-data/form-data-report-batch-dialog.tsx
@@ -8,10 +8,10 @@ import React, {
useEffect,
} from "react";
import { useToast } from "@/hooks/use-toast";
-import { toast as toastMessage} from "sonner";
+import { toast as toastMessage } from "sonner";
import prettyBytes from "pretty-bytes";
import { X, Loader2 } from "lucide-react";
-import { saveAs } from 'file-saver';
+import { saveAs } from "file-saver";
import { Badge } from "@/components/ui/badge";
import {
Dialog,
@@ -129,7 +129,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
setIsUploading(true);
try {
- const origin = await getOrigin()
+ const origin = await getOrigin();
const targetFiles = selectedFiles[0];
@@ -162,13 +162,13 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
if (reqeustCreateReport.ok) {
const blob = await reqeustCreateReport.blob();
- saveAs(blob, `${formCode}.pdf`);
+ saveAs(blob, `${formCode}.pdf`);
- toastMessage.success("Report 다운로드 완료!")
+ toastMessage.success("Report 다운로드 완료!");
} else {
const err = await reqeustCreateReport.json();
console.error("에러:", err);
- throw new Error(err.message)
+ throw new Error(err.message);
}
} catch (err) {
console.error(err);
@@ -179,8 +179,8 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
});
} finally {
setIsUploading(false);
- setSelectedFiles([])
- setOpen(false)
+ setSelectedFiles([]);
+ setOpen(false);
}
};
@@ -188,13 +188,14 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
<DialogHeader>
- <DialogTitle>Batch Report Download</DialogTitle>
+ <DialogTitle>Vendor Document Create</DialogTitle>
<DialogDescription>
- Report Template을 선택하신 후 갑지를 업로드하여 주시기 바랍니다.
+ Vendor Document Template을 선택하신 후 갑지를 업로드하여 주시기
+ 바랍니다.
</DialogDescription>
</DialogHeader>
<div className="h-[60px]">
- <Label>Report Template Select</Label>
+ <Label>Vendor Document Template Select</Label>
<Select value={selectTemp} onValueChange={setSelectTemp}>
<SelectTrigger className="w-[100%]">
<SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
@@ -213,7 +214,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
</Select>
</div>
<div>
- <Label>Report Cover Page Upload(.docx)</Label>
+ <Label>Vendor Document Cover Page Upload(.docx)</Label>
<Dropzone
maxSize={MAX_FILE_SIZE}
multiple={false}
@@ -265,10 +266,14 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({
<DialogFooter>
<Button
- disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading}
+ disabled={
+ selectedFiles.length === 0 ||
+ selectTemp.length === 0 ||
+ isUploading
+ }
onClick={submitData}
>
- {isUploading && <Loader2 />}다운로드
+ {isUploading && <Loader2 />}Vendor Document Create
</Button>
</DialogFooter>
</DialogContent>
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
index e28b4345..52262bf5 100644
--- a/components/form-data/form-data-report-dialog.tsx
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -103,20 +103,20 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
<Dialog open={reportData.length > 0} onOpenChange={onClose}>
<DialogContent className="w-[70vw]" style={{ maxWidth: "none" }}>
<DialogHeader>
- <DialogTitle>Report</DialogTitle>
+ <DialogTitle>Create Vendor Document</DialogTitle>
<DialogDescription>
- 사용하시고자 하는 Report Template를 선택하여 주시기 바랍니다.
+ 사용하시고자 하는 Vendor Document Template를 선택하여 주시기 바랍니다.
</DialogDescription>
</DialogHeader>
<div className="h-[60px]">
- <Label>Report Template Select</Label>
+ <Label>Vendor Document Template Select</Label>
<Select
value={selectTemp}
onValueChange={setSelectTemp}
disabled={instance === null}
>
<SelectTrigger className="w-[100%]">
- <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
+ <SelectValue placeholder="사용하시고자하는 Vendor Document Template을 선택하여 주시기 바랍니다." />
</SelectTrigger>
<SelectContent>
{tempList.map((c) => {
@@ -145,7 +145,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
<DialogFooter>
<Button onClick={downloadFileData} disabled={selectTemp.length === 0}>
- 다운로드
+ Create Vendor Document
</Button>
</DialogFooter>
</DialogContent>
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>
- );
-};
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..5e6179a8
--- /dev/null
+++ b/components/form-data/form-data-report-temp-upload-tab.tsx
@@ -0,0 +1,228 @@
+"use client";
+
+import React, { FC, useState } from "react";
+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/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));
+ }
+ toastMessage.success("Template File 업로드 완료!");
+ } catch (err) {
+ console.error(err);
+ toast({
+ title: "Error",
+ description: "파일 업로드 중 오류가 발생했습니다.",
+ variant: "destructive",
+ });
+ } finally {
+ setIsUploading(false);
+ setUploadProgress(0);
+ setSelectedFiles([])
+ }
+ };
+
+ return (
+ <div className='flex flex-col gap-4'>
+ <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>
+ </div>
+ );
+};
+
+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/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index b23b2e70..a136b5d3 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -133,7 +133,7 @@ export function getColumns<TData extends object>({
setReportData([original]);
}}
>
- Create Report
+ Create Vendor Document
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
diff --git a/components/form-data/form-data-table.tsx b/components/form-data/form-data-table.tsx
index b696f45b..4caee44f 100644
--- a/components/form-data/form-data-table.tsx
+++ b/components/form-data/form-data-table.tsx
@@ -531,10 +531,10 @@ export default function DynamicTable({
<Popover>
<PopoverTrigger asChild>
<Button variant="default" size="sm">
- Report
+ Vendor Document
</Button>
</PopoverTrigger>
- <PopoverContent className="flex flex-row gap-2">
+ <PopoverContent className="flex flex-row gap-2 w-auto">
<Button
variant="outline"
size="sm"
@@ -547,7 +547,7 @@ export default function DynamicTable({
size="sm"
onClick={() => setBatchDownDialog(true)}
>
- Report Download
+ Vendor Document Create
</Button>
</PopoverContent>
</Popover>
diff --git a/components/form-data/temp-download-btn.tsx b/components/form-data/temp-download-btn.tsx
new file mode 100644
index 00000000..a5f963e4
--- /dev/null
+++ b/components/form-data/temp-download-btn.tsx
@@ -0,0 +1,45 @@
+"use client";
+
+import React from "react";
+import Image from "next/image";
+import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage } from "sonner";
+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}
+ >
+ <Image
+ src="/icons/temp_sample_icon.svg"
+ alt="Template Sample Download Icon"
+ width={20}
+ height={20}
+ />
+ </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..19bb26f9
--- /dev/null
+++ b/components/form-data/var-list-download-btn.tsx
@@ -0,0 +1,110 @@
+"use client";
+
+import React, { FC } from "react";
+import Image from "next/image";
+import { useToast } from "@/hooks/use-toast";
+import { toast as toastMessage } from "sonner";
+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}
+ >
+ <Image
+ src="/icons/var_list_icon.svg"
+ alt="Template Sample Download Icon"
+ width={20}
+ height={20}
+ />
+ </Button>
+ );
+};