diff options
| author | kiman Kim <94714426+rlaks5757@users.noreply.github.com> | 2025-03-28 14:06:00 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-03-28 14:06:00 +0900 |
| commit | 23409a310e00f066ba4258637886160df06ea6af (patch) | |
| tree | f02ba5ca9e3c4923c9a6316fc3b9c5515c52759c /components | |
| parent | 281a4060cff0396253192f4e852be6770ad97cbd (diff) | |
| parent | 54a8a851442e675afb13669509cd44ab33386987 (diff) | |
Merge pull request #5 from DTS-Development/features
Vendor Form Data Report 기능 개발
Diffstat (limited to 'components')
| -rw-r--r-- | components/documents/StageList.tsx | 2 | ||||
| -rw-r--r-- | components/documents/view-document-dialog.tsx | 245 | ||||
| -rw-r--r-- | components/form-data/form-data-report-batch-dialog.tsx | 76 | ||||
| -rw-r--r-- | components/form-data/form-data-report-dialog.tsx | 139 | ||||
| -rw-r--r-- | components/form-data/form-data-report-temp-upload-dialog.tsx | 352 | ||||
| -rw-r--r-- | components/form-data/form-data-table.tsx | 48 |
6 files changed, 431 insertions, 431 deletions
diff --git a/components/documents/StageList.tsx b/components/documents/StageList.tsx index 81f8a5ca..6df448df 100644 --- a/components/documents/StageList.tsx +++ b/components/documents/StageList.tsx @@ -55,7 +55,7 @@ interface Version { approvedDate: string | null DocumentSubmitDate: Date attachments: Attachment[] - selected?: boolean + selected?: boolean; } export default function StageList({ document }: StageListProps) { diff --git a/components/documents/view-document-dialog.tsx b/components/documents/view-document-dialog.tsx index 7603fdc0..bd802b77 100644 --- a/components/documents/view-document-dialog.tsx +++ b/components/documents/view-document-dialog.tsx @@ -1,20 +1,13 @@ -<<<<<<< HEAD -"use client"; +"use client" -import * as React from "react"; +import * as React from "react" import { WebViewerInstance } from "@pdftron/webviewer"; import { - Dialog, - DialogTrigger, - DialogContent, - DialogHeader, - DialogTitle, - DialogDescription, - DialogFooter, -} from "@/components/ui/dialog"; -import { Building2, FileIcon, Loader2 } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import fs from "fs"; + Dialog, DialogTrigger, DialogContent, DialogHeader, + DialogTitle, DialogDescription, DialogFooter +} from "@/components/ui/dialog" +import { Building2, FileIcon, Loader2 } from "lucide-react" +import { Button } from "@/components/ui/button" // 인터페이스 interface Attachment { @@ -37,7 +30,7 @@ interface Version { approvedDate: string | null; DocumentSubmitDate: Date; attachments: Attachment[]; - selected: boolean; + selected?: boolean; } type ViewDocumentDialogProps = { @@ -74,95 +67,22 @@ const DocumentViewer: React.FC<{ ); const [viwerLoading, setViewerLoading] = React.useState<boolean>(true); const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true); -======= -"use client" - -import * as React from "react" -import { WebViewerInstance } from "@pdftron/webviewer"; -import { - Dialog, DialogTrigger, DialogContent, DialogHeader, - DialogTitle, DialogDescription, DialogFooter -} from "@/components/ui/dialog" -import { Building2, FileIcon, Loader2 } from "lucide-react" -import { Button } from "@/components/ui/button" -import fs from "fs" - -interface Version { - id: number - stage: string - revision: string - uploaderType: string - uploaderName: string | null - comment: string | null - status: string | null - planDate: string | null - actualDate: string | null - approvedDate: string | null - DocumentSubmitDate: Date - attachments: Attachment[] - selected: boolean -} - -type ViewDocumentDialogProps = { - versions: Version[] -} - -export function ViewDocumentDialog({versions}: ViewDocumentDialogProps){ - const [open, setOpen] = React.useState(false) - - - return ( - <> - <Button - size="sm" - className="border-blue-200" - variant="outline" - onClick={() => setOpen(prev => !prev)} - > - 문서 보기 - </Button> - {open && <DocumentViewer - open={open} - setOpen={setOpen} - versions={versions} - /> - } - </> - ); -} - -function DocumentViewer({open, setOpen, versions}){ - const [instance, setInstance] = React.useState<null | WebViewerInstance>(null) - const [viwerLoading, setViewerLoading] = React.useState<boolean>(true) - const [fileSetLoading, setFileSetLoading] = React.useState<boolean>(true) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d const viewer = React.useRef<HTMLDivElement>(null); const initialized = React.useRef(false); const isCancelled = React.useRef(false); // 초기화 중단용 flag const cleanupHtmlStyle = () => { const htmlElement = document.documentElement; -<<<<<<< HEAD // 기존 style 속성 가져오기 const originalStyle = htmlElement.getAttribute("style") || ""; -======= - - // 기존 style 속성 가져오기 - const originalStyle = htmlElement.getAttribute("style") || ""; - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // "color-scheme: light" 또는 "color-scheme: dark" 찾기 const colorSchemeStyle = originalStyle .split(";") .map((s) => s.trim()) .find((s) => s.startsWith("color-scheme:")); -<<<<<<< HEAD -======= - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d // 새로운 스타일 적용 (color-scheme만 유지) if (colorSchemeStyle) { htmlElement.setAttribute("style", colorSchemeStyle + ";"); @@ -170,18 +90,13 @@ function DocumentViewer({open, setOpen, versions}){ htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제 } -<<<<<<< HEAD console.log("html style 삭제"); -======= - console.log("html style 삭제") ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; React.useEffect(() => { if (open && !initialized.current) { initialized.current = true; isCancelled.current = false; // 다시 열릴 때는 false로 리셋 -<<<<<<< HEAD requestAnimationFrame(() => { if (viewer.current) { @@ -196,7 +111,8 @@ function DocumentViewer({open, setOpen, versions}){ WebViewer( { path: "/pdftronWeb", - licenseKey: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY, + licenseKey: + "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", fullAPI: true, css: "/globals.css", }, @@ -209,41 +125,11 @@ function DocumentViewer({open, setOpen, versions}){ "multiTabsEmptyPage", ]); setViewerLoading(false); -======= - - requestAnimationFrame(() => { - if (viewer.current) { - import("@pdftron/webviewer").then(({ default: WebViewer }) => { - console.log(isCancelled.current) - if (isCancelled.current) { - console.log("📛 WebViewer 초기화 취소됨 (Dialog 닫힘)"); - - return; - } - - WebViewer( - { - path: "/pdftronWeb", - licenseKey: "demo:1739264618684:616161d7030000000091db1c97c6f386d41d3506ab5b507381ef2ee2bd", - fullAPI: true, - css:"/globals.css" - }, - viewer.current as HTMLDivElement - ).then(async (instance: WebViewerInstance) => { - - - setInstance(instance); - instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); - instance.UI.disableElements(["addTabButton", "multiTabsEmptyPage"]); - setViewerLoading(false); - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }); }); } }); } -<<<<<<< HEAD return () => { // cleanup 시에는 중단 flag 세움 @@ -251,20 +137,10 @@ function DocumentViewer({open, setOpen, versions}){ instance.UI.dispose(); } setTimeout(() => cleanupHtmlStyle(), 500); -======= - - return async () => { - // cleanup 시에는 중단 flag 세움 - if(instance){ - await instance.UI.dispose() - } - await setTimeout(() => cleanupHtmlStyle(), 500) ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d }; }, [open]); React.useEffect(() => { -<<<<<<< HEAD const loadDocument = async () => { if (instance && versions.length > 0) { const { UI } = instance; @@ -320,8 +196,7 @@ function DocumentViewer({open, setOpen, versions}){ return ( <Dialog open={open} - onOpenChange={async (val) => { - console.log({ val, fileSetLoading }); + onOpenChange={async (val) => { if (!val && fileSetLoading) { return; } @@ -330,85 +205,18 @@ function DocumentViewer({open, setOpen, versions}){ try { await instance.UI.dispose(); setInstance(null); // 상태도 초기화 -======= - const loadDocument = async () => { - - if(instance && versions.length > 0){ - const { UI } = instance; - - const optionsArray = [] - - versions.forEach(c => { - const {attachments} = c - attachments.forEach(c2 => { - const {fileName, filePath, fileType} = c2 - - const options = { - filename: fileName, - ...(fileType.includes("xlsx") && { - officeOptions: { - formatOptions: { - applyPageBreaksToSheet: true, - }, - }, - }), - }; - - optionsArray.push({ - filePath, - options - }) - }) - }) - - const tabIds = []; - - for (const option of optionsArray) { - const { filePath, options } = option; - const response = await fetch(filePath); - const blob = await response.blob(); - - const tab = await UI.TabManager.addTab(blob, options); - tabIds.push(tab); // 탭 ID 저장 - } - - if (tabIds.length > 0) { - await UI.TabManager.setActiveTab(tabIds[0]); - } - - setFileSetLoading(false) - } - } - loadDocument(); - }, [instance, versions]) - - - return ( - <Dialog open={open} onOpenChange={async (val) => { - console.log({val, fileSetLoading}) - if(!val && fileSetLoading){ - return; - } - - if (instance) { - try { - await instance.UI.dispose(); - setInstance(null); // 상태도 초기화 - ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d } catch (e) { console.warn("dispose error", e); } } -<<<<<<< HEAD // cleanupHtmlStyle() setViewerLoading(false); setOpen((prev) => !prev); - await setTimeout(() => cleanupHtmlStyle(), 1000); + setTimeout(() => cleanupHtmlStyle(), 1000); }} > - <DialogContent className="w-[70vw] h-[90vh]" style={{ maxWidth: "none" }}> + <DialogContent className="w-[90vw] h-[90vh]" style={{ maxWidth: "none" }}> <DialogHeader className="h-[38px]"> <DialogTitle>문서 미리보기</DialogTitle> <DialogDescription>첨부파일 미리보기</DialogDescription> @@ -430,30 +238,3 @@ function DocumentViewer({open, setOpen, versions}){ </Dialog> ); }; -======= - - // cleanupHtmlStyle() - setViewerLoading(false); - setOpen(prev => !prev) - await setTimeout(() => cleanupHtmlStyle(), 1000) - }}> - <DialogContent className="w-[90vw] h-[90vh]" style={{maxWidth: "none"}}> - <DialogHeader className="h-[38px]"> - <DialogTitle> - 문서 미리보기 - </DialogTitle> - <DialogDescription> - 첨부파일 미리보기 - </DialogDescription> - </DialogHeader> - <div ref={viewer} style={{height: "calc(90vh - 20px - 38px - 1rem - 48px)"}}> - {viwerLoading && <div className="flex flex-col items-center justify-center py-12"> - <Loader2 className="h-8 w-8 text-blue-500 animate-spin mb-4" /> - <p className="text-sm text-muted-foreground">문서 뷰어 로딩 중...</p> - </div>} - </div> - </DialogContent> - </Dialog> - ); -} ->>>>>>> cac978d5c77e9b30165e4fbe6930eeac9862204d diff --git a/components/form-data/form-data-report-batch-dialog.tsx b/components/form-data/form-data-report-batch-dialog.tsx index e3fd7ea2..f4efde64 100644 --- a/components/form-data/form-data-report-batch-dialog.tsx +++ b/components/form-data/form-data-report-batch-dialog.tsx @@ -6,11 +6,12 @@ import React, { SetStateAction, useState, useEffect, - useRef, } 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 { saveAs } from 'file-saver'; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -25,7 +26,6 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, - SelectGroup, SelectItem, SelectTrigger, SelectValue, @@ -49,7 +49,7 @@ import { FileListName, } from "@/components/ui/file-list"; import { Button } from "@/components/ui/button"; -import { getReportTempList } from "@/lib/forms/services"; +import { getReportTempList, getOrigin } from "@/lib/forms/services"; import { DataTableColumnJSON } from "./form-data-table-columns"; const MAX_FILE_SIZE = 3000000; @@ -129,30 +129,58 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ setIsUploading(true); try { - const totalFiles = selectedFiles.length; - let successCount = 0; + const origin = await getOrigin() - for (let i = 0; i < totalFiles; i++) { - const file = selectedFiles[i]; + const targetFiles = selectedFiles[0]; - const formData = new FormData(); - formData.append("file", file); - formData.append("customFileName", file.name); + const reportDatas = reportData.map((c) => { + const reportValue = stringifyAllValues(c); - // await uploadReportTemp(packageId, formId, formData); + const reportValueMapping: { [key: string]: any } = {}; - successCount++; + 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(); + + saveAs(blob, `${formCode}.pdf`); + + toastMessage.success("Report 다운로드 완료!") + } else { + const err = await reqeustCreateReport.json(); + console.error("에러:", err); + throw new Error(err.message) } } catch (err) { console.error(err); toast({ title: "Error", - description: "파일 업로드 중 오류가 발생했습니다.", + description: "Report 생성 중 오류가 발생했습니다.", variant: "destructive", }); } finally { setIsUploading(false); - setOpen(false); + setSelectedFiles([]) + setOpen(false) } }; @@ -185,7 +213,7 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ </Select> </div> <div> - <Label>Report Cover Page Upload</Label> + <Label>Report Cover Page Upload(.docx)</Label> <Dropzone maxSize={MAX_FILE_SIZE} multiple={false} @@ -237,10 +265,10 @@ export const FormDataReportBatchDialog: FC<FormDataReportBatchDialogProps> = ({ <DialogFooter> <Button - disabled={selectedFiles.length === 0 || selectTemp.length === 0} + disabled={selectedFiles.length === 0 || selectTemp.length === 0 || isUploading} onClick={submitData} > - 다운로드 + {isUploading && <Loader2 />}다운로드 </Button> </DialogFooter> </DialogContent> @@ -305,3 +333,17 @@ const updateReportTempList: UpdateReportTempList = async ( }) ); }; + +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) : ""; + } +}; diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx index deb0873b..e28b4345 100644 --- a/components/form-data/form-data-report-dialog.tsx +++ b/components/form-data/form-data-report-dialog.tsx @@ -8,11 +8,10 @@ import React, { useEffect, useRef, } from "react"; -import { WebViewerInstance, Core } from "@pdftron/webviewer"; -import { useToast } from "@/hooks/use-toast"; -import prettyBytes from "pretty-bytes"; -import { X, Loader2 } from "lucide-react"; -import { Badge } from "@/components/ui/badge"; +import { WebViewerInstance } from "@pdftron/webviewer"; +import { Loader2 } from "lucide-react"; +import { saveAs } from "file-saver"; +import { toast } from "sonner"; import { Dialog, DialogContent, @@ -25,11 +24,11 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, - SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; + import { Button } from "@/components/ui/button"; import { getReportTempList } from "@/lib/forms/services"; import { DataTableColumnJSON } from "./form-data-table-columns"; @@ -58,7 +57,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ setReportData, packageId, formId, + formCode, }) => { + const [tempList, setTempList] = useState<tempFile[]>([]); const [selectTemp, setSelectTemp] = useState<string>(""); const [instance, setInstance] = useState<null | WebViewerInstance>(null); @@ -92,46 +93,9 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ // }, }); - const blob = new Blob([fileData], { - type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - }); + saveAs(new Blob([fileData]), fileName); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = fileName; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - - // const allTabs = UI.TabManager.getAllTabs() as { - // id: number; - // src: Core.Document; - // }[]; - - // for (const tab of allTabs) { - // // await UI.TabManager.setActiveTab(tab.id); - // await activateTabAndWaitForLoad(instance, tab.id); - // const tabDoc = tab.src; - // const fileName = tabDoc.getFilename(); - - // const fileData = await tabDoc.getFileData({ - // includeAnnotations: true, - // }); - - // console.log({ fileData }); - - // const blob = new Blob([fileData], { - // type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", - // }); - - // // 다운로드 - // // const link = document.createElement("a"); - // // link.href = URL.createObjectURL(blob); - // // link.download = fileName; - // // document.body.appendChild(link); - // // link.click(); - // // document.body.removeChild(link); - // } + toast.success("Report 다운로드 완료!"); } }; @@ -175,6 +139,7 @@ export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({ instance={instance} setInstance={setInstance} setFileLoading={setFileLoading} + formCode={formCode} /> </div> @@ -195,6 +160,7 @@ interface ReportWebViewerProps { instance: null | WebViewerInstance; setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>; setFileLoading: Dispatch<SetStateAction<boolean>>; + formCode: string; } const ReportWebViewer: FC<ReportWebViewerProps> = ({ @@ -204,6 +170,7 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({ instance, setInstance, setFileLoading, + formCode, }) => { const [viwerLoading, setViewerLoading] = useState<boolean>(true); const viewer = useRef<HTMLDivElement>(null); @@ -234,12 +201,6 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({ viewer.current as HTMLDivElement ).then(async (instance: WebViewerInstance) => { setInstance(instance); - // //Tab 메뉴 사용 필요시 활성화 - // instance.UI.enableFeatures([instance.UI.Feature.MultiTab]); - // instance.UI.disableElements([ - // "addTabButton", - // "multiTabsEmptyPage", - // ]); setViewerLoading(false); }); }); @@ -262,9 +223,10 @@ const ReportWebViewer: FC<ReportWebViewerProps> = ({ instance, reportDatas, reportTempPath, - setFileLoading + setFileLoading, + formCode ); - }, [reportTempPath, reportDatas, instance, columnsJSON]); + }, [reportTempPath, reportDatas, instance, columnsJSON, formCode]); return ( <div ref={viewer} className="h-[100%]"> @@ -319,7 +281,8 @@ type ImportReportData = ( instance: null | WebViewerInstance, reportDatas: ReportData[], reportTempPath: string, - setFileLoading: Dispatch<SetStateAction<boolean>> + setFileLoading: Dispatch<SetStateAction<boolean>>, + formCode: string ) => void; const importReportData: ImportReportData = async ( @@ -327,7 +290,8 @@ const importReportData: ImportReportData = async ( instance, reportDatas, reportTempPath, - setFileLoading + setFileLoading, + formCode ) => { setFileLoading(true); try { @@ -352,12 +316,13 @@ const importReportData: ImportReportData = async ( }); const doc = await createDocument(reportFileBlob, { + filename: `${formCode}_report.docx`, extension: "docx", }); await doc.applyTemplateValues(reportValueMapping); - documentViewer.loadDocument(doc, { + documentViewer.loadDocument(doc, { extension: "docx", enableOfficeEditing: true, officeOptions: { @@ -373,68 +338,6 @@ const importReportData: ImportReportData = async ( } }; -const importReportDataTab: ImportReportData = async ( - columnJSON, - instance, - reportDatas, - reportTempPath, - setFileLoading -) => { - setFileLoading(true); - try { - if (instance && reportDatas.length > 0 && reportTempPath.length > 0) { - const { UI, Core } = instance; - const { createDocument } = Core; - - const getFileData = await fetch(reportTempPath); - const reportFileBlob = await getFileData.blob(); - - const prevTab = UI.TabManager.getAllTabs(); - - (prevTab as object[] as { id: number }[]).forEach((c) => { - const { id } = c; - UI.TabManager.deleteTab(id); - }); - - const fileOptions = reportDatas.map((c) => { - const { tagNumber } = c; - - const options = { - filename: `${tagNumber}_report.docx`, - }; - - return { options, reportData: c }; - }); - - const tabIds = []; - - for (const fileOption of fileOptions) { - let doc = await createDocument(reportFileBlob, { - ...fileOption.options, - extension: "docx", - }); - - await doc.applyTemplateValues( - stringifyAllValues(fileOption.reportData) - ); - - const tab = await UI.TabManager.addTab(doc, { - ...fileOption.options, - }); - - tabIds.push(tab); // 탭 ID 저장 - } - - if (tabIds.length > 0) { - await UI.TabManager.setActiveTab(tabIds[0]); - } - } - } catch (err) { - } finally { - setFileLoading(false); - } -}; - type UpdateReportTempList = ( packageId: number, formId: number, 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 b646c3e6..69df704e 100644 --- a/components/form-data/form-data-report-temp-upload-dialog.tsx +++ b/components/form-data/form-data-report-temp-upload-dialog.tsx @@ -8,8 +8,11 @@ import React, { useEffect, } 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 { X, Loader2, Download, Delete, Trash2 } from "lucide-react"; +import ExcelJS from "exceljs"; +import { saveAs } from "file-saver"; import { Badge } from "@/components/ui/badge"; import { Dialog, @@ -40,10 +43,28 @@ import { FileListItem, FileListName, } from "@/components/ui/file-list"; -import { getReportTempList, uploadReportTemp } from "@/lib/forms/services"; +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"; +import { DataTableColumnJSON } from "./form-data-table-columns"; interface FormDataReportTempUploadDialogProps { + columnsJSON: DataTableColumnJSON[]; open: boolean; setOpen: Dispatch<SetStateAction<boolean>>; packageId: number; @@ -57,7 +78,15 @@ const MAX_FILE_SIZE = 3000000; export const FormDataReportTempUploadDialog: FC< FormDataReportTempUploadDialogProps -> = ({ open, setOpen, packageId, formId, uploaderType }) => { +> = ({ + columnsJSON, + open, + setOpen, + packageId, + formId, + formCode, + uploaderType, +}) => { const { toast } = useToast(); const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [isUploading, setIsUploading] = useState(false); @@ -131,55 +160,177 @@ export const FormDataReportTempUploadDialog: FC< } }; + 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" }}> <DialogHeader> <DialogTitle>Report Template Upload</DialogTitle> <DialogDescription> - 사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다. + 사용하시고자 하는 Report Template(.docx)를 업로드 하여주시기 + 바랍니다. </DialogDescription> </DialogHeader> - {/* {prevReportTemp.length > 0 && ( - <> - <Label>Prev Report Template</Label> - <ScrollArea className="max-h-[100px]"> - {prevReportTemp.map((c, i) => { - return <div key={i}>{i}</div>; - })} - </ScrollArea> - </> - )} */} - - <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> + <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> + </div> </div> - </div> - </DropzoneZone> - <Label className="text-xs text-muted-foreground"> - 여러 파일을 선택할 수 있습니다. - </Label> - </> - )} - </Dropzone> + </DropzoneZone> + <Label className="text-xs text-muted-foreground"> + 여러 파일을 선택할 수 있습니다. + </Label> + </> + )} + </Dropzone> + </div> {selectedFiles.length > 0 && ( <div className="grid gap-2"> @@ -223,7 +374,7 @@ const UploadFileItem: FC<UploadFileItemProps> = ({ isUploading, }) => { return ( - <FileList className="max-h-[200px] gap-3"> + <FileList className="max-h-[100px] gap-3"> {selectedFiles.map((file, index) => ( <FileListItem key={index} className="p-3"> <FileListHeader> @@ -303,3 +454,116 @@ const updateReportTempList: UpdateReportTempList = async ( 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-table.tsx b/components/form-data/form-data-table.tsx index 9feaf3b2..b696f45b 100644 --- a/components/form-data/form-data-table.tsx +++ b/components/form-data/form-data-table.tsx @@ -11,19 +11,22 @@ import { DataTableColumnJSON, ColumnType, } from "./form-data-table-columns"; - import type { DataTableAdvancedFilterField } from "@/types/table"; import { Button } from "../ui/button"; import { Download, Loader, Save, Upload } from "lucide-react"; import { toast } from "sonner"; import { syncMissingTags, updateFormDataInDB } from "@/lib/forms/services"; import { UpdateTagSheet } from "./update-form-sheet"; - import ExcelJS from "exceljs"; import { saveAs } from "file-saver"; import { FormDataReportTempUploadDialog } from "./form-data-report-temp-upload-dialog"; import { FormDataReportDialog } from "./form-data-report-dialog"; import { FormDataReportBatchDialog } from "./form-data-report-batch-dialog"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; interface GenericData { [key: string]: any; @@ -44,7 +47,6 @@ export default function DynamicTable({ formCode, formId, }: DynamicTableProps) { - console.log({ columnsJSON }); const params = useParams(); const lng = (params?.lng as string) || "ko"; const { t } = useTranslation(lng, "translation"); @@ -526,20 +528,29 @@ export default function DynamicTable({ {/* 버튼 그룹 */} <div className="flex items-center gap-2"> {/* 태그 불러오기 버튼 */} - <Button - variant="default" - size="sm" - onClick={() => setBatchDownDialog(true)} - > - Report Batch - </Button> - <Button - variant="default" - size="sm" - onClick={() => setTempUpDialog(true)} - > - Temp Upload - </Button> + <Popover> + <PopoverTrigger asChild> + <Button variant="default" size="sm"> + Report + </Button> + </PopoverTrigger> + <PopoverContent className="flex flex-row gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => setTempUpDialog(true)} + > + Template Upload + </Button> + <Button + variant="outline" + size="sm" + onClick={() => setBatchDownDialog(true)} + > + Report Download + </Button> + </PopoverContent> + </Popover> <Button variant="default" size="sm" @@ -611,6 +622,7 @@ export default function DynamicTable({ /> {tempUpDialog && ( <FormDataReportTempUploadDialog + columnsJSON={columnsJSON} open={tempUpDialog} setOpen={setTempUpDialog} packageId={contractItemId} @@ -645,5 +657,3 @@ export default function DynamicTable({ </> ); } - - |
