summaryrefslogtreecommitdiff
path: root/components/form-data
diff options
context:
space:
mode:
Diffstat (limited to 'components/form-data')
-rw-r--r--components/form-data/form-data-report-dialog.tsx457
-rw-r--r--components/form-data/form-data-report-temp-upload-dialog.tsx305
-rw-r--r--components/form-data/form-data-table-columns.tsx77
3 files changed, 807 insertions, 32 deletions
diff --git a/components/form-data/form-data-report-dialog.tsx b/components/form-data/form-data-report-dialog.tsx
new file mode 100644
index 00000000..5ddc5e0c
--- /dev/null
+++ b/components/form-data/form-data-report-dialog.tsx
@@ -0,0 +1,457 @@
+"use client";
+
+import React, {
+ FC,
+ Dispatch,
+ SetStateAction,
+ useState,
+ 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 {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+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";
+
+type ReportData = {
+ [key: string]: any;
+};
+
+interface tempFile {
+ fileName: string;
+ filePath: string;
+}
+
+interface FormDataReportDialogProps {
+ columnsJSON: DataTableColumnJSON[];
+ reportData: ReportData[];
+ setReportData: Dispatch<SetStateAction<ReportData[]>>;
+ packageId: number;
+ formId: number;
+ formCode: string;
+}
+
+export const FormDataReportDialog: FC<FormDataReportDialogProps> = ({
+ columnsJSON,
+ reportData,
+ setReportData,
+ packageId,
+ formId,
+}) => {
+ const [tempList, setTempList] = useState<tempFile[]>([]);
+ const [selectTemp, setSelectTemp] = useState<string>("");
+ const [instance, setInstance] = useState<null | WebViewerInstance>(null);
+ const [fileLoading, setFileLoading] = useState<boolean>(true);
+
+ useEffect(() => {
+ updateReportTempList(packageId, formId, setTempList);
+ }, [packageId, formId]);
+
+ const onClose = async (value: boolean) => {
+ if (fileLoading) {
+ return;
+ }
+ if (!value) {
+ setTimeout(() => cleanupHtmlStyle(), 1000);
+ setReportData([]);
+ }
+ };
+
+ const downloadFileData = async () => {
+ if (instance) {
+ const { UI, Core } = instance;
+ const { documentViewer } = Core;
+
+ const doc = documentViewer.getDocument();
+ const fileName = doc.getFilename();
+ const fileData = await doc.getFileData({
+ includeAnnotations: true, // 사용자가 추가한 폼 필드 및 입력 포함
+ // officeOptions: {
+ // outputFormat: "docx",
+ // },
+ });
+
+ 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);
+
+ // 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);
+ // }
+ }
+ };
+
+ return (
+ <Dialog open={reportData.length > 0} onOpenChange={onClose}>
+ <DialogContent className="w-[70vw]" style={{ maxWidth: "none" }}>
+ <DialogHeader>
+ <DialogTitle>Report</DialogTitle>
+ <DialogDescription>
+ 사용하시고자 하는 Report Template를 선택하여 주시기 바랍니다.
+ </DialogDescription>
+ </DialogHeader>
+ <div className="h-[60px]">
+ <Label>Report Template Select</Label>
+ <Select
+ value={selectTemp}
+ onValueChange={setSelectTemp}
+ disabled={instance === null}
+ >
+ <SelectTrigger className="w-[100%]">
+ <SelectValue placeholder="사용하시고자하는 Report Template를 선택하여 주시기 바랍니다." />
+ </SelectTrigger>
+ <SelectContent>
+ {tempList.map((c) => {
+ const { fileName, filePath } = c;
+
+ return (
+ <SelectItem key={filePath} value={filePath}>
+ {fileName}
+ </SelectItem>
+ );
+ })}
+ </SelectContent>
+ </Select>
+ </div>
+ <div className="h-[calc(70vh-60px)]">
+ <ReportWebViewer
+ columnsJSON={columnsJSON}
+ reportTempPath={selectTemp}
+ reportDatas={reportData}
+ instance={instance}
+ setInstance={setInstance}
+ setFileLoading={setFileLoading}
+ />
+ </div>
+
+ <DialogFooter>
+ <Button onClick={downloadFileData} disabled={selectTemp.length === 0}>
+ 다운로드
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+};
+
+interface ReportWebViewerProps {
+ columnsJSON: DataTableColumnJSON[];
+ reportTempPath: string;
+ reportDatas: ReportData[];
+ instance: null | WebViewerInstance;
+ setInstance: Dispatch<SetStateAction<WebViewerInstance | null>>;
+ setFileLoading: Dispatch<SetStateAction<boolean>>;
+}
+
+const ReportWebViewer: FC<ReportWebViewerProps> = ({
+ columnsJSON,
+ reportTempPath,
+ reportDatas,
+ instance,
+ setInstance,
+ setFileLoading,
+}) => {
+ const [viwerLoading, setViewerLoading] = useState<boolean>(true);
+ const viewer = useRef<HTMLDivElement>(null);
+ const initialized = React.useRef(false);
+ const isCancelled = React.useRef(false); // 초기화 중단용 flag
+
+ useEffect(() => {
+ if (!initialized.current) {
+ initialized.current = true;
+ isCancelled.current = false; // 다시 열릴 때는 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: process.env.NEXT_PUBLIC_PDFTRON_WEBVIEW_KEY,
+ fullAPI: true,
+ },
+ 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);
+ });
+ });
+ }
+ });
+ }
+
+ return () => {
+ // cleanup 시에는 중단 flag 세움
+ if (instance) {
+ instance.UI.dispose();
+ }
+ setTimeout(() => cleanupHtmlStyle(), 500);
+ };
+ }, []);
+
+ useEffect(() => {
+ importReportData(
+ columnsJSON,
+ instance,
+ reportDatas,
+ reportTempPath,
+ setFileLoading
+ );
+ }, [reportTempPath, reportDatas, instance, columnsJSON]);
+
+ return (
+ <div ref={viewer} className="h-[100%]">
+ {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>
+ );
+};
+
+const cleanupHtmlStyle = () => {
+ const htmlElement = document.documentElement;
+
+ // 기존 style 속성 가져오기
+ const originalStyle = htmlElement.getAttribute("style") || "";
+
+ // "color-scheme: light" 또는 "color-scheme: dark" 찾기
+ const colorSchemeStyle = originalStyle
+ .split(";")
+ .map((s) => s.trim())
+ .find((s) => s.startsWith("color-scheme:"));
+
+ // 새로운 스타일 적용 (color-scheme만 유지)
+ if (colorSchemeStyle) {
+ htmlElement.setAttribute("style", colorSchemeStyle + ";");
+ } else {
+ htmlElement.removeAttribute("style"); // color-scheme도 없으면 style 속성 자체 삭제
+ }
+
+ console.log("html style 삭제");
+};
+
+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) : "";
+ }
+};
+
+type ImportReportData = (
+ columnsJSON: DataTableColumnJSON[],
+ instance: null | WebViewerInstance,
+ reportDatas: ReportData[],
+ reportTempPath: string,
+ setFileLoading: Dispatch<SetStateAction<boolean>>
+) => void;
+
+const importReportData: ImportReportData = async (
+ columnsJSON,
+ instance,
+ reportDatas,
+ reportTempPath,
+ setFileLoading
+) => {
+ setFileLoading(true);
+ try {
+ if (instance && reportDatas.length > 0 && reportTempPath.length > 0) {
+ const { UI, Core } = instance;
+ const { documentViewer, createDocument } = Core;
+
+ const getFileData = await fetch(reportTempPath);
+ const reportFileBlob = await getFileData.blob();
+
+ const reportData = reportDatas[0];
+ const reportValue = stringifyAllValues(reportData);
+
+ const reportValueMapping: { [key: string]: any } = {};
+
+ columnsJSON.forEach((c) => {
+ const { key, label } = c;
+
+ const objKey = label.split(" ").join("_");
+
+ reportValueMapping[objKey] = reportValue[key];
+ });
+
+ const doc = await createDocument(reportFileBlob, {
+ extension: "docx",
+ });
+
+ await doc.applyTemplateValues(reportValueMapping);
+
+ documentViewer.loadDocument(doc, {
+ extension: "docx",
+ enableOfficeEditing: true,
+ officeOptions: {
+ formatOptions: {
+ applyPageBreaksToSheet: true,
+ },
+ },
+ });
+ }
+ } catch (err) {
+ } finally {
+ setFileLoading(false);
+ }
+};
+
+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,
+ setPrevReportTemp: Dispatch<SetStateAction<tempFile[]>>
+) => void;
+
+const updateReportTempList: UpdateReportTempList = async (
+ packageId,
+ formId,
+ setTempList
+) => {
+ const tempList = await getReportTempList(packageId, formId);
+
+ setTempList(
+ tempList.map((c) => {
+ const { fileName, filePath } = c;
+ return { fileName, filePath };
+ })
+ );
+};
diff --git a/components/form-data/form-data-report-temp-upload-dialog.tsx b/components/form-data/form-data-report-temp-upload-dialog.tsx
new file mode 100644
index 00000000..b646c3e6
--- /dev/null
+++ b/components/form-data/form-data-report-temp-upload-dialog.tsx
@@ -0,0 +1,305 @@
+"use client";
+
+import React, {
+ FC,
+ Dispatch,
+ SetStateAction,
+ useState,
+ useEffect,
+} from "react";
+import { useToast } from "@/hooks/use-toast";
+import prettyBytes from "pretty-bytes";
+import { X, Loader2 } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogDescription,
+ DialogFooter,
+} from "@/components/ui/dialog";
+import { ScrollArea } from "@/components/ui/scroll-area";
+import { Label } from "@/components/ui/label";
+import { 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 { getReportTempList, uploadReportTemp } from "@/lib/forms/services";
+import { VendorDataReportTemps } from "@/db/schema/vendorData";
+
+interface FormDataReportTempUploadDialogProps {
+ open: boolean;
+ setOpen: Dispatch<SetStateAction<boolean>>;
+ packageId: number;
+ formCode: string;
+ formId: number;
+ uploaderType: string;
+}
+
+// 최대 파일 크기 설정 (3000MB)
+const MAX_FILE_SIZE = 3000000;
+
+export const FormDataReportTempUploadDialog: FC<
+ FormDataReportTempUploadDialogProps
+> = ({ open, setOpen, packageId, formId, 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);
+ }
+ };
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogContent className="w-[600px]" style={{ maxWidth: "none" }}>
+ <DialogHeader>
+ <DialogTitle>Report Template Upload</DialogTitle>
+ <DialogDescription>
+ 사용하시고자 하는 Report Template를 업로드 하여주시기 바랍니다.
+ </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>
+ </div>
+ </DropzoneZone>
+ <Label className="text-xs text-muted-foreground">
+ 여러 파일을 선택할 수 있습니다.
+ </Label>
+ </>
+ )}
+ </Dropzone>
+
+ {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 onClick={() => setOpen(false)}>취소</Button>
+ <Button disabled={selectedFiles.length === 0} onClick={submitData}>
+ 업로드
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+};
+
+interface UploadFileItemProps {
+ selectedFiles: File[];
+ removeFile: (index: number) => void;
+ isUploading: boolean;
+}
+
+const UploadFileItem: FC<UploadFileItemProps> = ({
+ selectedFiles,
+ removeFile,
+ isUploading,
+}) => {
+ return (
+ <FileList className="max-h-[200px] gap-3">
+ {selectedFiles.map((file, index) => (
+ <FileListItem key={index} className="p-3">
+ <FileListHeader>
+ <FileListIcon />
+ <FileListInfo>
+ <FileListName>{file.name}</FileListName>
+ <FileListDescription>
+ {prettyBytes(file.size)}
+ </FileListDescription>
+ </FileListInfo>
+ <FileListAction
+ onClick={() => removeFile(index)}
+ disabled={isUploading}
+ >
+ <X className="h-4 w-4" />
+ <span className="sr-only">Remove</span>
+ </FileListAction>
+ </FileListHeader>
+ </FileListItem>
+ ))}
+ </FileList>
+ );
+};
+
+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);
+};
diff --git a/components/form-data/form-data-table-columns.tsx b/components/form-data/form-data-table-columns.tsx
index d44616f8..38f5cad8 100644
--- a/components/form-data/form-data-table-columns.tsx
+++ b/components/form-data/form-data-table-columns.tsx
@@ -27,24 +27,27 @@ export type ColumnType = "STRING" | "NUMBER" | "LIST"
export interface DataTableColumnJSON {
- key: string
+ key: string;
/** 실제 Excel 등에서 구분용으로 쓰이는 label (고정) */
label: string
/** UI 표시용 label (예: 단위를 함께 표시) */
displayLabel?: string
- type: ColumnType
- options?: string[]
- uom?: string
+ type: ColumnType;
+ options?: string[];
+ uom?: string;
}
-/**
- * getColumns 함수에 필요한 props
+/**
+ * getColumns 함수에 필요한 props
* - TData: 테이블에 표시할 행(Row)의 타입
*/
interface GetColumnsProps<TData> {
- columnsJSON: DataTableColumnJSON[]
- setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<TData> | null>>
+ columnsJSON: DataTableColumnJSON[];
+ setRowAction: React.Dispatch<
+ React.SetStateAction<DataTableRowAction<TData> | null>
+ >;
+ setReportData: React.Dispatch<React.SetStateAction<{ [key: string]: any }[]>>;
}
/**
@@ -55,8 +58,8 @@ interface GetColumnsProps<TData> {
export function getColumns<TData extends object>({
columnsJSON,
setRowAction,
+ setReportData,
}: GetColumnsProps<TData>): ColumnDef<TData>[] {
-
// (1) 기본 컬럼들
const baseColumns: ColumnDef<TData>[] = columnsJSON.map((col) => ({
accessorKey: col.key,
@@ -71,7 +74,7 @@ export function getColumns<TData extends object>({
excelHeader: col.label,
minWidth: 80,
paddingFactor: 1.2,
- maxWidth: col.key ==="tagNumber"?120:150,
+ maxWidth: col.key === "tagNumber" ? 120 : 150,
},
// (2) 실제 셀(cell) 렌더링: type에 따라 분기 가능
cell: ({ row }) => {
@@ -81,7 +84,9 @@ export function getColumns<TData extends object>({
switch (col.type) {
case "NUMBER":
// 예: number인 경우 콤마 등 표시
- return <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+ return (
+ <div>{cellValue ? Number(cellValue).toLocaleString() : ""}</div>
+ );
// case "date":
// // 예: 날짜 포맷팅
@@ -108,30 +113,38 @@ export function getColumns<TData extends object>({
header: "",
cell: ({ row }) => (
<DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- aria-label="Open menu"
- variant="ghost"
- className="flex size-8 p-0 data-[state=open]:bg-muted"
- >
- <Ellipsis className="size-4" aria-hidden="true" />
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" className="w-40">
- <DropdownMenuItem
- onSelect={() => setRowAction({ row, type: "update" })}
- >
- Edit
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="Open menu"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-40">
+ <DropdownMenuItem
+ onSelect={() => setRowAction({ row, type: "update" })}
+ >
+ Edit
+ </DropdownMenuItem>
+ <DropdownMenuItem
+ onSelect={() => {
+ const { original } = row;
+ setReportData([original]);
+ }}
+ >
+ Create Report
+ </DropdownMenuItem>
+ </DropdownMenuContent>
+ </DropdownMenu>
),
- size:40,
- meta:{
- maxWidth:40
+ size: 40,
+ meta: {
+ // maxWidth: 40,
},
enablePinning: true,
- }
+ };
// (4) 최종 반환
return [...baseColumns, actionColumn]