"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; // Card 컴포넌트 제거 - 외부 박스 없이 직접 렌더링 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Separator } from "@/components/ui/separator"; import { Search, ChevronLeft, ChevronRight, RotateCcw, Database, Loader2, Plus, Edit, Trash2, File, Download, Eye, X, ZoomIn, ZoomOut, RotateCw, Folder, FolderOpen, } from "lucide-react"; import { tableTypeApi } from "@/lib/api/screen"; import { commonCodeApi } from "@/lib/api/commonCode"; import { getCurrentUser, UserInfo } from "@/lib/api/client"; import { DataTableComponent, DataTableColumn, DataTableFilter } from "@/types/screen-legacy-backup"; import { cn } from "@/lib/utils"; import { downloadFile, getLinkedFiles, getFilePreviewUrl, getDirectFileUrl } from "@/lib/api/file"; import { toast } from "sonner"; import { FileUpload } from "@/components/screen/widgets/FileUpload"; import { AdvancedSearchFilters } from "./filters/AdvancedSearchFilters"; // 파일 데이터 타입 정의 (AttachedFileInfo와 호환) interface FileInfo { // AttachedFileInfo 기본 속성들 objid: string; savedFileName: string; realFileName: string; fileSize: number; fileExt: string; filePath: string; docType: string; docTypeName: string; targetObjid: string; parentTargetObjid?: string; companyCode: string; writer: string; regdate: string; status: string; // 추가 호환성 속성들 path?: string; // filePath와 동일 name?: string; // realFileName과 동일 id?: string; // objid와 동일 size?: number; // fileSize와 동일 type?: string; // docType과 동일 uploadedAt?: string; // regdate와 동일 } interface FileColumnData { files: FileInfo[]; totalCount: number; totalSize: number; lastModified: string; } interface InteractiveDataTableProps { component: DataTableComponent; className?: string; style?: React.CSSProperties; } export const InteractiveDataTable: React.FC = ({ component, className = "", style = {}, }) => { const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [searchValues, setSearchValues] = useState>({}); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [total, setTotal] = useState(0); const [selectedRows, setSelectedRows] = useState>(new Set()); const [showAddModal, setShowAddModal] = useState(false); const [addFormData, setAddFormData] = useState>({}); const [isAdding, setIsAdding] = useState(false); const [showEditModal, setShowEditModal] = useState(false); const [editFormData, setEditFormData] = useState>({}); const [editingRowData, setEditingRowData] = useState | null>(null); const [isEditing, setIsEditing] = useState(false); // 이미지 미리보기 상태 const [previewImage, setPreviewImage] = useState(null); const [showPreviewModal, setShowPreviewModal] = useState(false); const [zoom, setZoom] = useState(1); const [rotation, setRotation] = useState(0); const [imageLoadError, setImageLoadError] = useState(false); const [alternativeImageUrl, setAlternativeImageUrl] = useState(null); // 파일 관리 상태 const [fileStatusMap, setFileStatusMap] = useState>({}); // 행별 파일 상태 const [showFileManagementModal, setShowFileManagementModal] = useState(false); const [selectedRowForFiles, setSelectedRowForFiles] = useState | null>(null); const [selectedColumnForFiles, setSelectedColumnForFiles] = useState(null); // 선택된 컬럼 정보 const [linkedFiles, setLinkedFiles] = useState([]); // 공통코드 관리 상태 const [codeOptions, setCodeOptions] = useState>>({}); // 공통코드 옵션 가져오기 const loadCodeOptions = useCallback( async (categoryCode: string) => { if (codeOptions[categoryCode]) { return codeOptions[categoryCode]; // 이미 로드된 경우 캐시된 데이터 사용 } try { const response = await commonCodeApi.options.getOptions(categoryCode); if (response.success && response.data) { const options = response.data.map((code) => ({ value: code.value, label: code.label, })); setCodeOptions((prev) => ({ ...prev, [categoryCode]: options, })); return options; } } catch (error) { console.error(`공통코드 옵션 로드 실패: ${categoryCode}`, error); } return []; }, [codeOptions], ); // 파일 상태 확인 함수 const checkFileStatus = useCallback( async (rowData: Record) => { if (!component.tableName) return; // 첫 번째 컬럼을 기본키로 사용 (실제로는 더 정교한 로직 필요) const primaryKeyField = Object.keys(rowData)[0]; // 임시로 첫 번째 컬럼 사용 const recordId = rowData[primaryKeyField]; if (!recordId) return; try { const response = await getLinkedFiles(component.tableName, recordId); const hasFiles = response.files && response.files.length > 0; const fileCount = response.files ? response.files.length : 0; return { hasFiles, fileCount, files: response.files || [] }; } catch (error) { console.error("파일 상태 확인 오류:", error); return { hasFiles: false, fileCount: 0, files: [] }; } }, [component.tableName], ); // 파일 폴더 아이콘 클릭 핸들러 (전체 행 파일 관리) const handleFileIconClick = useCallback( async (rowData: Record) => { const fileStatus = await checkFileStatus(rowData); if (fileStatus) { setSelectedRowForFiles(rowData); setLinkedFiles(fileStatus.files); setShowFileManagementModal(true); } }, [checkFileStatus], ); // 컬럼별 파일 상태 확인 const checkColumnFileStatus = useCallback( async (rowData: Record, column: DataTableColumn) => { if (!component.tableName) return null; const primaryKeyField = Object.keys(rowData)[0]; const recordId = rowData[primaryKeyField]; if (!recordId) return null; try { // 가상 파일 컬럼의 경우: tableName:recordId:columnName 형태로 target_objid 생성 const targetObjid = column.isVirtualFileColumn ? `${component.tableName}:${recordId}:${column.columnName}` : `${component.tableName}:${recordId}`; const response = await getLinkedFiles(component.tableName, recordId); // 가상 파일 컬럼의 경우 해당 컬럼의 파일만 필터링 let files = response.files || []; if (column.isVirtualFileColumn) { // 현재 컬럼명으로 먼저 시도 files = files.filter( (file: any) => file.targetObjid === targetObjid || file.targetObjid?.endsWith(`:${column.columnName}`), // target_objid → targetObjid ); // 파일이 없는 경우 fallback: 모든 파일 컬럼 패턴 시도 if (files.length === 0) { // 해당 테이블:레코드의 모든 파일 컬럼 파일들을 가져옴 files = (response.files || []).filter( (file: any) => file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`), // target_objid → targetObjid ); } } const hasFiles = files.length > 0; const fileCount = files.length; return { hasFiles, fileCount, files, targetObjid }; } catch (error) { console.error("컬럼별 파일 상태 확인 오류:", error); return { hasFiles: false, fileCount: 0, files: [], targetObjid: null }; } }, [component.tableName], ); // 컬럼별 파일 클릭 핸들러 const handleColumnFileClick = useCallback( async (rowData: Record, column: DataTableColumn) => { // 컬럼별 파일 상태 확인 const fileStatus = await checkColumnFileStatus(rowData, column); setSelectedRowForFiles(rowData); setSelectedColumnForFiles(column); // 선택된 컬럼 정보 저장 setLinkedFiles(fileStatus?.files || []); setShowFileManagementModal(true); // TODO: 모달에 컬럼 정보 전달하여 해당 컬럼 전용 파일 업로드 가능하게 하기 }, [checkColumnFileStatus], ); // 이미지 미리보기 핸들러들 const handlePreviewImage = useCallback((fileInfo: FileInfo) => { setPreviewImage(fileInfo); setShowPreviewModal(true); setZoom(1); setRotation(0); setImageLoadError(false); setAlternativeImageUrl(null); }, []); const closePreviewModal = useCallback(() => { setShowPreviewModal(false); setPreviewImage(null); setZoom(1); setRotation(0); setImageLoadError(false); setAlternativeImageUrl(null); }, []); const handleZoom = useCallback((direction: "in" | "out") => { setZoom((prev) => { if (direction === "in") { return Math.min(prev + 0.25, 3); } else { return Math.max(prev - 0.25, 0.25); } }); }, []); const handleRotate = useCallback(() => { setRotation((prev) => (prev + 90) % 360); }, []); const formatFileSize = useCallback((bytes: number): string => { if (bytes === 0) return "0 Bytes"; const k = 1024; const sizes = ["Bytes", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; }, []); // 이미지 로딩 실패 시 대체 URL 시도 const handleImageError = useCallback(() => { if (!imageLoadError && previewImage) { console.error("이미지 로딩 실패:", previewImage); setImageLoadError(true); // 대체 URL 생성 (직접 파일 경로 사용) if (previewImage.path) { const altUrl = getDirectFileUrl(previewImage.path); console.log("대체 URL 시도:", altUrl); setAlternativeImageUrl(altUrl); } else { toast.error("이미지를 불러올 수 없습니다."); } } else { toast.error("이미지를 불러올 수 없습니다."); } }, [imageLoadError, previewImage]); const [showFileModal, setShowFileModal] = useState(false); const [currentFileData, setCurrentFileData] = useState(null); const [currentFileColumn, setCurrentFileColumn] = useState(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [isDeleting, setIsDeleting] = useState(false); // 현재 사용자 정보 const [currentUser, setCurrentUser] = useState(null); // 테이블 컬럼 타입 정보 (웹 타입 포함) const [tableColumns, setTableColumns] = useState([]); // 파일 업로드 관련 상태 const [uploadingFiles, setUploadingFiles] = useState>({}); const [uploadedFiles, setUploadedFiles] = useState>({}); // 검색 가능한 컬럼만 필터링 const visibleColumns = component.columns?.filter((col: DataTableColumn) => col.visible) || []; // 컬럼의 실제 웹 타입 정보 찾기 const getColumnWebType = useCallback( (columnName: string) => { // 먼저 컴포넌트에 설정된 컬럼에서 찾기 (화면 관리에서 설정한 값 우선) const componentColumn = component.columns?.find((col) => col.columnName === columnName); if (componentColumn?.widgetType && componentColumn.widgetType !== "text") { return componentColumn.widgetType; } // 없으면 테이블 타입 관리에서 설정된 값 찾기 const tableColumn = tableColumns.find((col) => col.columnName === columnName); return tableColumn?.webType || "text"; }, [component.columns, tableColumns], ); // 컬럼의 상세 설정 정보 찾기 const getColumnDetailSettings = useCallback( (columnName: string) => { // 먼저 컴포넌트에 설정된 컬럼에서 찾기 (화면 관리에서 설정한 값 우선) const componentColumn = component.columns?.find((col) => col.columnName === columnName); if (componentColumn?.webTypeConfig) { return componentColumn.webTypeConfig; } // 없으면 테이블 타입 관리에서 설정된 값 찾기 const tableColumn = tableColumns.find((col) => col.columnName === columnName); try { return tableColumn?.detailSettings ? JSON.parse(tableColumn.detailSettings) : {}; } catch { console.warn("상세 설정 파싱 실패:", tableColumn?.detailSettings); return {}; } }, [component.columns, tableColumns], ); // 컬럼의 코드 카테고리 가져오기 const getColumnCodeCategory = useCallback( (columnName: string) => { const column = component.columns.find((col) => col.columnName === columnName); // webTypeConfig가 CodeTypeConfig인 경우 codeCategory 반환 const webTypeConfig = column?.webTypeConfig as any; return webTypeConfig?.codeCategory || column?.codeCategory; }, [component.columns], ); // 그리드 컬럼 계산 const totalGridColumns = visibleColumns.reduce((sum, col) => sum + (col.gridColumns || 2), 0); // 페이지 크기 설정 const pageSize = component.pagination?.pageSize || 10; // 데이터 로드 함수 const loadData = useCallback( async (page: number = 1, searchParams: Record = {}) => { if (!component.tableName) return; setLoading(true); try { const result = await tableTypeApi.getTableData(component.tableName, { page, size: pageSize, search: searchParams, }); setData(result.data); setTotal(result.total); setTotalPages(result.totalPages); setCurrentPage(result.page); // 각 행의 파일 상태 확인 (전체 행 + 가상 파일 컬럼별) const fileStatusPromises = result.data.map(async (rowData: Record) => { const primaryKeyField = Object.keys(rowData)[0]; const recordId = rowData[primaryKeyField]; if (!recordId) return { rowKey: recordId, statuses: {} }; try { const fileResponse = await getLinkedFiles(component.tableName, recordId); const allFiles = fileResponse.files || []; // 전체 행에 대한 파일 상태 const rowStatus = { hasFiles: allFiles.length > 0, fileCount: allFiles.length, }; // 가상 파일 컬럼별 파일 상태 const columnStatuses: Record = {}; // 가상 파일 컬럼 찾기 const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn); virtualFileColumns.forEach((column) => { // 해당 컬럼의 파일만 필터링 (targetObjid로 수정) let columnFiles = allFiles.filter((file: any) => file.targetObjid?.endsWith(`:${column.columnName}`)); // fallback: 컬럼명으로 찾지 못한 경우 모든 파일 컬럼 파일 포함 if (columnFiles.length === 0) { columnFiles = allFiles.filter((file: any) => file.targetObjid?.startsWith(`${component.tableName}:${recordId}:file_column_`), ); } const columnKey = `${recordId}_${column.columnName}`; columnStatuses[columnKey] = { hasFiles: columnFiles.length > 0, fileCount: columnFiles.length, }; }); return { rowKey: recordId, statuses: { [recordId]: rowStatus, // 전체 행 상태 ...columnStatuses, // 컬럼별 상태 }, }; } catch { // 에러 시 기본값 const defaultStatuses: Record = { [recordId]: { hasFiles: false, fileCount: 0 }, }; // 가상 파일 컬럼에 대해서도 기본값 설정 const virtualFileColumns = component.columns.filter((col) => col.isVirtualFileColumn); virtualFileColumns.forEach((column) => { const columnKey = `${recordId}_${column.columnName}`; defaultStatuses[columnKey] = { hasFiles: false, fileCount: 0 }; }); return { rowKey: recordId, statuses: defaultStatuses }; } }); // 파일 상태 업데이트 Promise.all(fileStatusPromises).then((results) => { const statusMap: Record = {}; results.forEach((result) => { Object.assign(statusMap, result.statuses); }); setFileStatusMap(statusMap); }); } catch (error) { console.error("❌ 테이블 데이터 조회 실패:", error); setData([]); setTotal(0); setTotalPages(1); } finally { setLoading(false); } }, [component.tableName, pageSize], ); // 현재 사용자 정보 로드 useEffect(() => { const fetchCurrentUser = async () => { try { const response = await getCurrentUser(); if (response.success && response.data) { setCurrentUser(response.data); } } catch (error) { console.error("현재 사용자 정보 로드 실패:", error); } }; fetchCurrentUser(); }, []); // 파일 상태 새로고침 이벤트 리스너 useEffect(() => { const handleRefreshFileStatus = async (event: CustomEvent) => { const { tableName, recordId, columnName, targetObjid, fileCount } = event.detail; console.log("🔄 InteractiveDataTable 파일 상태 새로고침 이벤트 수신:", { tableName, recordId, columnName, targetObjid, fileCount, currentTableName: component.tableName }); // 현재 테이블과 일치하는지 확인 if (tableName === component.tableName) { // 해당 행의 파일 상태 업데이트 const columnKey = `${recordId}_${columnName}`; setFileStatusMap(prev => ({ ...prev, [recordId]: { hasFiles: fileCount > 0, fileCount }, [columnKey]: { hasFiles: fileCount > 0, fileCount } })); console.log("✅ 파일 상태 업데이트 완료:", { recordId, columnKey, hasFiles: fileCount > 0, fileCount }); } }; if (typeof window !== 'undefined') { window.addEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener); return () => { window.removeEventListener('refreshFileStatus', handleRefreshFileStatus as EventListener); }; } }, [component.tableName]); // 테이블 컬럼 정보 로드 (웹 타입 정보 포함) useEffect(() => { const fetchTableColumns = async () => { try { const columns = await tableTypeApi.getColumns(component.tableName); setTableColumns(columns); } catch (error) { console.error("테이블 컬럼 정보 로드 실패:", error); } }; if (component.tableName) { fetchTableColumns(); } }, [component.tableName]); // 실제 사용할 필터 (설정된 필터만 사용, 자동 생성 안함) const searchFilters = useMemo(() => { return component.filters || []; }, [component.filters]); // 초기 데이터 로드 useEffect(() => { loadData(1, searchValues); }, [loadData]); // 검색 실행 const handleSearch = useCallback(() => { loadData(1, searchValues); }, [searchValues, loadData]); // 검색값 변경 const handleSearchValueChange = useCallback((columnName: string, value: any) => { setSearchValues((prev) => ({ ...prev, [columnName]: value, })); }, []); // 페이지 변경 const handlePageChange = useCallback( (page: number) => { loadData(page, searchValues); }, [loadData, searchValues], ); // 행 선택 핸들러 const handleRowSelect = useCallback((rowIndex: number, isSelected: boolean) => { setSelectedRows((prev) => { const newSet = new Set(prev); if (isSelected) { newSet.add(rowIndex); } else { newSet.delete(rowIndex); } return newSet; }); }, []); // 전체 선택/해제 핸들러 const handleSelectAll = useCallback( (isSelected: boolean) => { if (isSelected) { setSelectedRows(new Set(Array.from({ length: data.length }, (_, i) => i))); } else { setSelectedRows(new Set()); } }, [data.length], ); // 모달에 표시할 컬럼 계산 const getDisplayColumns = useCallback(() => { const { hiddenFields, fieldOrder, advancedFieldConfigs } = component.addModalConfig || {}; // 숨겨진 필드와 고급 설정에서 숨겨진 필드 제외 let displayColumns = visibleColumns.filter((col) => { // 기본 숨김 필드 체크 if (hiddenFields?.includes(col.columnName)) return false; // 고급 설정에서 숨김 체크 const config = advancedFieldConfigs?.[col.columnName]; if (config?.inputType === "hidden") return false; return true; }); // 필드 순서 적용 if (fieldOrder && fieldOrder.length > 0) { const orderedColumns: typeof displayColumns = []; const remainingColumns = [...displayColumns]; // 지정된 순서대로 추가 fieldOrder.forEach((columnName) => { const column = remainingColumns.find((col) => col.columnName === columnName); if (column) { orderedColumns.push(column); const index = remainingColumns.indexOf(column); remainingColumns.splice(index, 1); } }); // 나머지 컬럼들 추가 orderedColumns.push(...remainingColumns); displayColumns = orderedColumns; } return displayColumns; }, [visibleColumns, component.addModalConfig]); // 자동 값 생성 const generateAutoValue = useCallback( (autoValueType: string): string => { const now = new Date(); switch (autoValueType) { case "current_datetime": return now.toISOString().slice(0, 19); // YYYY-MM-DDTHH:mm:ss case "current_date": return now.toISOString().slice(0, 10); // YYYY-MM-DD case "current_time": return now.toTimeString().slice(0, 8); // HH:mm:ss case "current_user": return currentUser?.userName || currentUser?.userId || "unknown_user"; case "uuid": return crypto.randomUUID(); case "sequence": return `SEQ_${Date.now()}`; default: return ""; } }, [currentUser], ); // 데이터 추가 핸들러 const handleAddData = useCallback(() => { // 폼 데이터 초기화 const initialData: Record = {}; const displayColumns = getDisplayColumns(); const advancedConfigs = component.addModalConfig?.advancedFieldConfigs || {}; displayColumns.forEach((col) => { const config = advancedConfigs[col.columnName]; if (config?.inputType === "auto") { // 자동 값 설정 if (config.autoValueType === "custom") { initialData[col.columnName] = config.customValue || ""; } else { initialData[col.columnName] = generateAutoValue(config.autoValueType); } } else if (config?.defaultValue) { // 기본값 설정 initialData[col.columnName] = config.defaultValue; } else { // 일반 빈 값 initialData[col.columnName] = ""; } }); setAddFormData(initialData); setShowAddModal(true); }, [getDisplayColumns, generateAutoValue, component.addModalConfig]); // 추가 폼 데이터 변경 핸들러 const handleAddFormChange = useCallback((columnName: string, value: any) => { setAddFormData((prev) => ({ ...prev, [columnName]: value, })); }, []); // 데이터 수정 핸들러 const handleEditData = useCallback(() => { if (selectedRows.size !== 1) return; const selectedIndex = Array.from(selectedRows)[0]; const selectedRowData = data[selectedIndex]; if (!selectedRowData) return; // 수정할 데이터로 폼 초기화 const initialData: Record = {}; const displayColumns = getDisplayColumns(); displayColumns.forEach((col) => { initialData[col.columnName] = selectedRowData[col.columnName] || ""; }); setEditFormData(initialData); setEditingRowData(selectedRowData); setShowEditModal(true); }, [selectedRows, data, getDisplayColumns]); // 수정 폼 데이터 변경 핸들러 const handleEditFormChange = useCallback((columnName: string, value: any) => { setEditFormData((prev) => ({ ...prev, [columnName]: value, })); }, []); // 파일 업로드 핸들러 const handleFileUpload = useCallback( async (columnName: string, files: FileList | null, isEdit: boolean = false) => { if (!files || files.length === 0) return; const detailSettings = getColumnDetailSettings(columnName); const maxSize = detailSettings?.maxSize || 10 * 1024 * 1024; // 기본 10MB const acceptedTypes = detailSettings?.accept ? detailSettings.accept.split(",").map((type: string) => type.trim()) : []; const multiple = detailSettings?.multiple || false; // 파일 검증 const validFiles: File[] = []; for (let i = 0; i < files.length; i++) { const file = files[i]; // 크기 체크 if (file.size > maxSize) { alert(`파일 크기가 너무 큽니다. 최대 ${Math.round(maxSize / 1024 / 1024)}MB까지 가능합니다.`); continue; } // 타입 체크 if ( acceptedTypes.length > 0 && !acceptedTypes.some((type: string) => { if (type.startsWith(".")) { return file.name.toLowerCase().endsWith(type.toLowerCase()); } else { return file.type.includes(type); } }) ) { alert(`지원하지 않는 파일 형식입니다. (${acceptedTypes.join(", ")})`); continue; } validFiles.push(file); if (!multiple) break; // 단일 파일만 허용 } if (validFiles.length === 0) return; try { setUploadingFiles((prev) => ({ ...prev, [columnName]: true })); // TODO: 실제 파일 업로드 API 호출 // const uploadPromises = validFiles.map(file => uploadFileToServer(file)); // const uploadResults = await Promise.all(uploadPromises); // 임시: 파일 정보를 로컬 상태에 저장 setUploadedFiles((prev) => ({ ...prev, [columnName]: multiple ? [...(prev[columnName] || []), ...validFiles] : validFiles, })); // 폼 데이터 업데이트 const fileNames = validFiles.map((file) => file.name).join(", "); if (isEdit) { handleEditFormChange(columnName, fileNames); } else { handleAddFormChange(columnName, fileNames); } } catch (error) { console.error("파일 업로드 실패:", error); alert("파일 업로드에 실패했습니다."); } finally { setUploadingFiles((prev) => ({ ...prev, [columnName]: false })); } }, [getColumnDetailSettings, handleAddFormChange, handleEditFormChange], ); // 파일 제거 핸들러 const handleFileRemove = useCallback( (columnName: string, fileIndex: number, isEdit: boolean = false) => { setUploadedFiles((prev) => { const currentFiles = prev[columnName] || []; const newFiles = currentFiles.filter((_, index) => index !== fileIndex); // 폼 데이터 업데이트 const fileNames = newFiles.map((file) => file.name).join(", "); if (isEdit) { handleEditFormChange(columnName, fileNames); } else { handleAddFormChange(columnName, fileNames); } return { ...prev, [columnName]: newFiles }; }); }, [handleAddFormChange, handleEditFormChange], ); // 파일 목록 렌더링 컴포넌트 const renderFileList = useCallback( (columnName: string, isEdit: boolean = false) => { const currentFiles = uploadedFiles[columnName] || []; const isUploading = uploadingFiles[columnName]; if (currentFiles.length === 0 && !isUploading) return null; return (
{currentFiles.map((file, index) => (
📄

{file.name}

{(file.size / 1024).toFixed(1)} KB

))} {isUploading && (
업로드 중...
)}
); }, [uploadedFiles, uploadingFiles, handleFileRemove], ); // 데이터 추가 제출 핸들러 const handleAddSubmit = useCallback(async () => { try { setIsAdding(true); // 실제 API 호출로 데이터 추가 console.log("🔥 추가할 데이터:", addFormData); await tableTypeApi.addTableData(component.tableName, addFormData); // 모달 닫기 및 폼 초기화 setShowAddModal(false); setAddFormData({}); // 첫 페이지로 이동하여 새 데이터 확인 loadData(1, searchValues); } catch (error) { console.error("데이터 추가 실패:", error); alert("데이터 추가에 실패했습니다."); } finally { setIsAdding(false); } }, [addFormData, loadData, searchValues]); // 데이터 수정 제출 핸들러 const handleEditSubmit = useCallback(async () => { try { setIsEditing(true); // 실제 API 호출로 데이터 수정 console.log("🔥 수정할 데이터:", editFormData); console.log("🔥 원본 데이터:", editingRowData); if (editingRowData) { await tableTypeApi.editTableData(component.tableName, editingRowData, editFormData); // 모달 닫기 및 폼 초기화 setShowEditModal(false); setEditFormData({}); setEditingRowData(null); setSelectedRows(new Set()); // 선택 해제 // 현재 페이지 데이터 새로고침 loadData(currentPage, searchValues); } } catch (error) { console.error("데이터 수정 실패:", error); alert("데이터 수정에 실패했습니다."); } finally { setIsEditing(false); } }, [editFormData, editingRowData, component.tableName, currentPage, searchValues, loadData]); // 추가 모달 닫기 핸들러 const handleAddModalClose = useCallback(() => { if (!isAdding) { setShowAddModal(false); setAddFormData({}); setUploadedFiles({}); // 파일 상태 초기화 } }, [isAdding]); // 데이터 삭제 핸들러 const handleDeleteData = useCallback(() => { if (selectedRows.size === 0) { alert("삭제할 데이터를 선택해주세요."); return; } setShowDeleteDialog(true); }, [selectedRows.size]); // 삭제 확인 핸들러 const handleDeleteConfirm = useCallback(async () => { try { setIsDeleting(true); // 선택된 행의 실제 데이터 가져오기 const selectedData = Array.from(selectedRows).map((index) => data[index]); // 실제 삭제 API 호출 console.log("🗑️ 삭제할 데이터:", selectedData); await tableTypeApi.deleteTableData(component.tableName, selectedData); // 선택 해제 및 다이얼로그 닫기 setSelectedRows(new Set()); setShowDeleteDialog(false); // 데이터 새로고침 loadData(currentPage, searchValues); } catch (error) { console.error("데이터 삭제 실패:", error); alert("데이터 삭제에 실패했습니다."); } finally { setIsDeleting(false); } }, [selectedRows, data, currentPage, searchValues, loadData]); // 삭제 다이얼로그 닫기 핸들러 const handleDeleteDialogClose = useCallback(() => { if (!isDeleting) { setShowDeleteDialog(false); } }, [isDeleting]); // 필수 필드 여부 확인 const isRequiredField = useCallback( (columnName: string) => { return component.addModalConfig?.requiredFields?.includes(columnName) || false; }, [component.addModalConfig], ); // 모달 크기 클래스 가져오기 const getModalSizeClass = useCallback(() => { const width = component.addModalConfig?.width || "lg"; const sizeMap = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", "2xl": "max-w-2xl", full: "max-w-full mx-4", }; return sizeMap[width]; }, [component.addModalConfig]); // 레이아웃 클래스 가져오기 const getLayoutClass = useCallback(() => { const layout = component.addModalConfig?.layout || "two-column"; const gridColumns = component.addModalConfig?.gridColumns || 2; switch (layout) { case "single": return "grid grid-cols-1 gap-4"; case "two-column": return "grid grid-cols-2 gap-4"; case "grid": return `grid grid-cols-${Math.min(gridColumns, 4)} gap-4`; default: return "grid grid-cols-2 gap-4"; } }, [component.addModalConfig]); // 수정 폼 입력 컴포넌트 렌더링 const renderEditFormInput = (column: DataTableColumn) => { const value = editFormData[column.columnName] || ""; const isRequired = isRequiredField(column.columnName); const advancedConfig = component.addModalConfig?.advancedFieldConfigs?.[column.columnName]; // 데이터베이스에서 실제 웹 타입 가져오기 const actualWebType = getColumnWebType(column.columnName); const detailSettings = getColumnDetailSettings(column.columnName); // 자동 생성 필드는 수정에서 읽기 전용으로 처리 if (advancedConfig?.inputType === "auto") { return (

자동 생성된 필드는 수정할 수 없습니다.

); } // 읽기 전용 필드 if (advancedConfig?.inputType === "readonly") { return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); } // 일반 입력 필드 렌더링 const commonProps = { value, onChange: (e: React.ChangeEvent) => handleEditFormChange(column.columnName, e.target.value), placeholder: advancedConfig?.placeholder || `${column.label} 입력...`, required: isRequired, className: isRequired && !value ? "border-orange-300 focus:border-orange-500" : "", }; // 실제 웹 타입에 따라 입력 컴포넌트 렌더링 switch (actualWebType) { case "text": case "email": case "tel": return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); case "number": case "decimal": return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); case "date": return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); case "datetime": return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); case "select": case "dropdown": // 상세 설정에서 옵션 목록 가져오기 const options = detailSettings?.options || []; if (options.length > 0) { return (
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); } else { return ; } case "radio": // 상세 설정에서 옵션 목록 가져오기 const radioOptions = detailSettings?.options || []; if (radioOptions.length > 0) { return (
{radioOptions.map((option: any, index: number) => (
handleEditFormChange(column.columnName, e.target.value)} className="text-primary focus:ring-primary" />
))}
{advancedConfig?.helpText &&

{advancedConfig.helpText}

}
); } else { return ; } case "textarea": return (