"use client"; import React, { useState, useEffect, useMemo } from "react"; import { TableListConfig, ColumnConfig } from "./types"; import { WebType } from "@/types/common"; import { tableTypeApi } from "@/lib/api/screen"; import { entityJoinApi } from "@/lib/api/entityJoin"; import { codeCache } from "@/lib/caching/codeCache"; import { useEntityJoinOptimization } from "@/lib/hooks/useEntityJoinOptimization"; import { Button } from "@/components/ui/button"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, RefreshCw, ArrowUpDown, ArrowUp, ArrowDown, TableIcon, } from "lucide-react"; import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { SingleTableWithSticky } from "./SingleTableWithSticky"; export interface TableListComponentProps { component: any; isDesignMode?: boolean; isSelected?: boolean; isInteractive?: boolean; onClick?: () => void; onDragStart?: (e: React.DragEvent) => void; onDragEnd?: (e: React.DragEvent) => void; className?: string; style?: React.CSSProperties; formData?: Record; onFormDataChange?: (data: any) => void; config?: TableListConfig; // 추가 props (DOM에 전달되지 않음) size?: { width: number; height: number }; position?: { x: number; y: number; z?: number }; componentConfig?: any; selectedScreen?: any; onZoneComponentDrop?: any; onZoneClick?: any; tableName?: string; onRefresh?: () => void; onClose?: () => void; screenId?: string; // 선택된 행 정보 전달 핸들러 onSelectedRowsChange?: (selectedRows: any[], selectedRowsData: any[]) => void; // 설정 변경 핸들러 (상세설정과 연동) onConfigChange?: (config: any) => void; // 테이블 새로고침 키 refreshKey?: number; } /** * TableList 컴포넌트 * 데이터베이스 테이블의 데이터를 목록으로 표시하는 컴포넌트 */ export const TableListComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, onClick, onDragStart, onDragEnd, config, className, style, onFormDataChange, componentConfig, onSelectedRowsChange, onConfigChange, refreshKey, }) => { // 컴포넌트 설정 const tableConfig = { ...config, ...component.config, ...componentConfig, } as TableListConfig; // 🎨 동적 색상 설정 (속성편집 모달의 "색상" 필드와 연동) const buttonColor = component.style?.labelColor || '#3b83f6'; // 기본 파란색 const buttonTextColor = component.config?.buttonTextColor || '#ffffff'; const buttonStyle = { backgroundColor: buttonColor, color: buttonTextColor, borderColor: buttonColor }; // 디버깅 로그 제거 (성능상 이유로) // 상태 관리 const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(0); const [totalItems, setTotalItems] = useState(0); const [searchTerm, setSearchTerm] = useState(""); const [sortColumn, setSortColumn] = useState(null); const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc"); const [columnLabels, setColumnLabels] = useState>({}); const [tableLabel, setTableLabel] = useState(""); const [localPageSize, setLocalPageSize] = useState(tableConfig.pagination?.pageSize || 20); // 로컬 페이지 크기 상태 const [displayColumns, setDisplayColumns] = useState([]); // 🎯 표시할 컬럼 (Entity 조인 적용됨) // 🎯 조인 컬럼 매핑 상태 const [joinColumnMapping, setJoinColumnMapping] = useState>({}); const [columnMeta, setColumnMeta] = useState>({}); // 🎯 컬럼 메타정보 (웹타입, 코드카테고리) // 고급 필터 관련 state const [searchValues, setSearchValues] = useState>({}); // 체크박스 상태 관리 const [selectedRows, setSelectedRows] = useState>(new Set()); // 드래그 상태 관리 const [isDragging, setIsDragging] = useState(false); const [draggedRowIndex, setDraggedRowIndex] = useState(null); // 선택된 행들의 키 집합 const [isAllSelected, setIsAllSelected] = useState(false); // 전체 선택 상태 // 🎯 Entity 조인 최적화 훅 사용 const { optimizedConvertCode } = useEntityJoinOptimization(columnMeta, { enableBatchLoading: true, preloadCommonCodes: true, maxBatchSize: 5, }); // 높이 계산 함수 (메모이제이션) const optimalHeight = useMemo(() => { // 실제 데이터 개수에 맞춰서 높이 계산 (최소 5개, 최대 20개) const actualDataCount = data.length; const displayPageSize = Math.min(Math.max(actualDataCount, 5), 20); const headerHeight = 50; // 테이블 헤더 const rowHeight = 42; // 각 행 높이 const searchHeight = tableConfig.filter?.enabled ? 80 : 0; // 검색 영역 const footerHeight = tableConfig.showFooter ? 60 : 0; // 페이지네이션 const titleHeight = tableConfig.showHeader ? 60 : 0; // 제목 영역 const padding = 40; // 여백 const calculatedHeight = titleHeight + searchHeight + headerHeight + displayPageSize * rowHeight + footerHeight + padding; console.log("🔍 테이블 높이 계산:", { actualDataCount, localPageSize, displayPageSize, isDesignMode, titleHeight, searchHeight, headerHeight, rowHeight, footerHeight, padding, calculatedHeight, finalHeight: `${calculatedHeight}px`, }); // 추가 디버깅: 실제 데이터 상황 console.log("🔍 실제 데이터 상황:", { actualDataLength: data.length, localPageSize, currentPage, totalItems, totalPages, }); return calculatedHeight; }, []); // 🎯 강제로 그리드 컬럼수에 맞는 크기 적용 (디자인 모드에서는 더 큰 크기 허용) const gridColumns = component.gridColumns || 1; let calculatedWidth: string; if (isDesignMode) { // 디자인 모드에서는 더 큰 최소 크기 적용 if (gridColumns === 1) { calculatedWidth = "400px"; // 1컬럼일 때 400px (디자인 모드) } else if (gridColumns === 2) { calculatedWidth = "600px"; // 2컬럼일 때 600px (디자인 모드) } else if (gridColumns <= 6) { calculatedWidth = `${gridColumns * 250}px`; // 컬럼당 250px (디자인 모드) } else { calculatedWidth = "100%"; // 7컬럼 이상은 전체 } } else { // 일반 모드는 기존 크기 유지 if (gridColumns === 1) { calculatedWidth = "200px"; // 1컬럼일 때 200px 고정 } else if (gridColumns === 2) { calculatedWidth = "400px"; // 2컬럼일 때 400px } else if (gridColumns <= 6) { calculatedWidth = `${gridColumns * 200}px`; // 컬럼당 200px } else { calculatedWidth = "100%"; // 7컬럼 이상은 전체 } } // 디버깅 로그 제거 (성능상 이유로) // 스타일 계산 (컨테이너에 맞춤) const componentStyle: React.CSSProperties = { width: "100%", // 컨테이너 전체 너비 사용 maxWidth: "100%", // 최대 너비 제한 height: "auto", // 항상 자동 높이로 테이블 크기에 맞춤 minHeight: isDesignMode ? `${Math.min(optimalHeight, 400)}px` : `${optimalHeight}px`, // 최소 높이 보장 ...component.style, ...style, display: "flex", flexDirection: "column", boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 // overflow는 CSS 클래스로 처리 }; // 🎯 tableContainerStyle 제거 - componentStyle만 사용 // 디자인 모드 스타일 if (isDesignMode) { componentStyle.border = "2px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; componentStyle.borderRadius = "8px"; componentStyle.padding = "4px"; // 약간의 패딩으로 구분감 확보 componentStyle.margin = "2px"; // 외부 여백으로 레이아웃과 구분 // 🎯 컨테이너에 맞춤 componentStyle.width = "calc(100% - 12px)"; // margin + padding 보정 componentStyle.maxWidth = "calc(100% - 12px)"; componentStyle.minWidth = "calc(100% - 12px)"; componentStyle.overflow = "hidden !important"; // 넘치는 부분 숨김 (강제) componentStyle.boxSizing = "border-box"; // 패딩 포함 크기 계산 componentStyle.position = "relative"; // 위치 고정 // 자동 높이로 테이블 전체를 감쌈 } // 컬럼 라벨 정보 가져오기 const fetchColumnLabels = async () => { if (!tableConfig.selectedTable) return; try { const response = await tableTypeApi.getColumns(tableConfig.selectedTable); // API 응답 구조 확인 및 컬럼 배열 추출 const columns = Array.isArray(response) ? response : (response as any).columns || []; const labels: Record = {}; const meta: Record = {}; columns.forEach((column: any) => { // 🎯 Entity 조인된 컬럼의 경우 표시 컬럼명 사용 let displayLabel = column.displayName || column.columnName; // Entity 타입인 경우 if (column.webType === "entity") { // 우선 기준 테이블의 컬럼 라벨을 사용 displayLabel = column.displayName || column.columnName; console.log( `🎯 Entity 조인 컬럼 라벨 설정: ${column.columnName} → "${displayLabel}" (기준 테이블 라벨 사용)`, ); } labels[column.columnName] = displayLabel; // 🎯 웹타입과 코드카테고리 정보 저장 meta[column.columnName] = { webType: column.webType, codeCategory: column.codeCategory, }; }); setColumnLabels(labels); setColumnMeta(meta); console.log("🔍 컬럼 라벨 설정 완료:", labels); console.log("🔍 컬럼 메타정보 설정 완료:", meta); } catch (error) { console.log("컬럼 라벨 정보를 가져올 수 없습니다:", error); } }; // 🎯 전역 코드 캐시 사용으로 함수 제거 (codeCache.convertCodeToName 사용) // 테이블 라벨명 가져오기 const fetchTableLabel = async () => { if (!tableConfig.selectedTable) return; try { const tables = await tableTypeApi.getTables(); const table = tables.find((t: any) => t.tableName === tableConfig.selectedTable); if (table && table.displayName && table.displayName !== table.tableName) { setTableLabel(table.displayName); } else { setTableLabel(tableConfig.selectedTable); } } catch (error) { console.log("테이블 라벨 정보를 가져올 수 없습니다:", error); setTableLabel(tableConfig.selectedTable); } }; // 테이블 데이터 가져오기 const fetchTableData = async () => { if (!tableConfig.selectedTable) { setData([]); return; } setLoading(true); setError(null); try { // 🎯 Entity 조인 API 사용 - Entity 조인이 포함된 데이터 조회 console.log("🔗 Entity 조인 데이터 조회 시작:", tableConfig.selectedTable); // Entity 조인 컬럼 추출 (isEntityJoin === true인 컬럼들) const entityJoinColumns = tableConfig.columns?.filter((col) => col.isEntityJoin && col.entityJoinInfo) || []; // 🎯 조인 탭에서 추가한 컬럼들도 포함 (실제로 존재하는 컬럼만) const joinTabColumns = tableConfig.columns?.filter( (col) => !col.isEntityJoin && col.columnName.includes("_") && (col.columnName.includes("dept_code_") || col.columnName.includes("_dept_code") || col.columnName.includes("_company_") || col.columnName.includes("_user_")), // 조인 탭에서 추가한 컬럼 패턴들 ) || []; console.log( "🔍 조인 탭 컬럼들:", joinTabColumns.map((c) => c.columnName), ); const additionalJoinColumns = [ ...entityJoinColumns.map((col) => ({ sourceTable: col.entityJoinInfo!.sourceTable, sourceColumn: col.entityJoinInfo!.sourceColumn, joinAlias: col.entityJoinInfo!.joinAlias, })), // 🎯 조인 탭에서 추가한 컬럼들도 추가 (실제로 존재하는 컬럼만) ...joinTabColumns .filter((col) => { // 실제 API 응답에 존재하는 컬럼만 필터링 const validJoinColumns = ["dept_code_name", "dept_name"]; const isValid = validJoinColumns.includes(col.columnName); if (!isValid) { console.log(`🔍 조인 탭 컬럼 제외: ${col.columnName} (유효하지 않음)`); } return isValid; }) .map((col) => { // 실제 존재하는 조인 컬럼만 처리 let sourceTable = tableConfig.selectedTable; let sourceColumn = col.columnName; if (col.columnName === "dept_code_name" || col.columnName === "dept_name") { sourceTable = "dept_info"; sourceColumn = "dept_code"; } console.log(`🔍 조인 탭 컬럼 처리: ${col.columnName} -> ${sourceTable}.${sourceColumn}`); return { sourceTable: sourceTable || tableConfig.selectedTable || "", sourceColumn: sourceColumn, joinAlias: col.columnName, }; }), ]; // 🎯 화면별 엔티티 표시 설정 생성 const screenEntityConfigs: Record = {}; entityJoinColumns.forEach((col) => { if (col.entityDisplayConfig) { const sourceColumn = col.entityJoinInfo!.sourceColumn; screenEntityConfigs[sourceColumn] = { displayColumns: col.entityDisplayConfig.displayColumns, separator: col.entityDisplayConfig.separator || " - ", }; } }); console.log("🔗 Entity 조인 컬럼:", entityJoinColumns); console.log("🔗 조인 탭 컬럼:", joinTabColumns); console.log("🔗 추가 Entity 조인 컬럼:", additionalJoinColumns); console.log("🎯 화면별 엔티티 설정:", screenEntityConfigs); const result = await entityJoinApi.getTableDataWithJoins(tableConfig.selectedTable, { page: currentPage, size: localPageSize, search: (() => { // 고급 필터 값이 있으면 우선 사용 const hasAdvancedFilters = Object.values(searchValues).some((value) => { if (typeof value === "string") return value.trim() !== ""; if (typeof value === "object" && value !== null) { return Object.values(value).some((v) => v !== "" && v !== null && v !== undefined); } return value !== null && value !== undefined && value !== ""; }); if (hasAdvancedFilters) { console.log("🔍 고급 검색 필터 사용:", searchValues); console.log("🔍 고급 검색 필터 상세:", JSON.stringify(searchValues, null, 2)); return searchValues; } // 고급 필터가 없으면 기존 단순 검색 사용 if (searchTerm?.trim()) { // 스마트한 단일 컬럼 선택 로직 (서버가 OR 검색을 지원하지 않음) let searchColumn = sortColumn; // 정렬된 컬럼 우선 if (!searchColumn) { // 1순위: name 관련 컬럼 (가장 검색에 적합) const nameColumns = visibleColumns.filter( (col) => col.columnName.toLowerCase().includes("name") || col.columnName.toLowerCase().includes("title") || col.columnName.toLowerCase().includes("subject"), ); // 2순위: text/varchar 타입 컬럼 const textColumns = visibleColumns.filter((col) => col.dataType === "text" || col.dataType === "varchar"); // 3순위: description 관련 컬럼 const descColumns = visibleColumns.filter( (col) => col.columnName.toLowerCase().includes("desc") || col.columnName.toLowerCase().includes("comment") || col.columnName.toLowerCase().includes("memo"), ); // 우선순위에 따라 선택 if (nameColumns.length > 0) { searchColumn = nameColumns[0].columnName; } else if (textColumns.length > 0) { searchColumn = textColumns[0].columnName; } else if (descColumns.length > 0) { searchColumn = descColumns[0].columnName; } else { // 마지막 대안: 첫 번째 컬럼 searchColumn = visibleColumns[0]?.columnName || "id"; } } console.log("🔍 기존 검색 방식 사용:", { [searchColumn]: searchTerm }); return { [searchColumn]: searchTerm }; } return undefined; })(), sortBy: sortColumn || undefined, sortOrder: sortDirection, enableEntityJoin: true, // 🎯 Entity 조인 활성화 additionalJoinColumns: additionalJoinColumns.length > 0 ? additionalJoinColumns : undefined, // 추가 조인 컬럼 screenEntityConfigs: Object.keys(screenEntityConfigs).length > 0 ? screenEntityConfigs : undefined, // 🎯 화면별 엔티티 설정 }); if (result) { console.log("🎯 API 응답 결과:", result); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 페이지:", result.totalPages); console.log("🎯 총 아이템:", result.total); setData(result.data || []); setTotalPages(result.totalPages || 1); setTotalItems(result.total || 0); // 🎯 Entity 조인 정보 로깅 if (result.entityJoinInfo) { console.log("🔗 Entity 조인 적용됨:", { strategy: result.entityJoinInfo.strategy, joinConfigs: result.entityJoinInfo.joinConfigs, performance: result.entityJoinInfo.performance, }); } else { console.log("🔗 Entity 조인 없음"); } // 🎯 코드 컬럼들의 캐시 미리 로드 (전역 캐시 사용) const codeColumns = Object.entries(columnMeta).filter( ([_, meta]) => meta.webType === "code" && meta.codeCategory, ); if (codeColumns.length > 0) { console.log( "📋 코드 컬럼 감지:", codeColumns.map(([col, meta]) => `${col}(${meta.codeCategory})`), ); // 필요한 코드 카테고리들을 추출하여 배치 로드 const categoryList = codeColumns.map(([, meta]) => meta.codeCategory).filter(Boolean) as string[]; try { await codeCache.preloadCodes(categoryList); console.log("📋 모든 코드 캐시 로드 완료 (전역 캐시)"); } catch (error) { console.error("❌ 코드 캐시 로드 중 오류:", error); } } // 🎯 Entity 조인된 컬럼 처리 - 사용자가 설정한 컬럼들만 사용 let processedColumns = [...(tableConfig.columns || [])]; // 초기 컬럼이 있으면 먼저 설정 if (processedColumns.length > 0) { console.log( "🔍 사용자 설정 컬럼들:", processedColumns.map((c) => c.columnName), ); // 🎯 API 응답과 비교하여 존재하지 않는 컬럼 필터링 if (result.data.length > 0) { const actualApiColumns = Object.keys(result.data[0]); console.log("🔍 API 응답의 실제 컬럼들:", actualApiColumns); // 🎯 조인 컬럼 매핑 테이블 (사용자 설정 → API 응답) // 실제 API 응답에 존재하는 컬럼만 매핑 const newJoinColumnMapping: Record = { dept_code_dept_code: "dept_code", // user_info.dept_code dept_code_status: "status", // user_info.status (dept_info.status가 조인되지 않음) dept_code_company_name: "dept_name", // dept_info.dept_name (company_name이 조인되지 않음) dept_code_name: "dept_code_name", // dept_info.dept_name dept_name: "dept_name", // dept_info.dept_name status: "status", // user_info.status }; // 🎯 조인 컬럼 매핑 상태 업데이트 setJoinColumnMapping(newJoinColumnMapping); console.log("🔍 조인 컬럼 매핑 테이블:", newJoinColumnMapping); console.log("🔍 실제 API 응답 컬럼들:", actualApiColumns); // 🎯 컬럼명 매핑 및 유효성 검사 const validColumns = processedColumns .map((col) => { // 체크박스는 그대로 유지 if (col.columnName === "__checkbox__") return col; // 조인 컬럼 매핑 적용 const mappedColumnName = newJoinColumnMapping[col.columnName] || col.columnName; console.log(`🔍 컬럼 매핑 처리: ${col.columnName} → ${mappedColumnName}`); // API 응답에 존재하는지 확인 const existsInApi = actualApiColumns.includes(mappedColumnName); if (!existsInApi) { console.log(`🔍 제거될 컬럼: ${col.columnName} → ${mappedColumnName} (API에 존재하지 않음)`); return null; } // 컬럼명이 변경된 경우 업데이트 if (mappedColumnName !== col.columnName) { console.log(`🔄 컬럼명 매핑: ${col.columnName} → ${mappedColumnName}`); return { ...col, columnName: mappedColumnName, }; } console.log(`✅ 컬럼 유지: ${col.columnName}`); return col; }) .filter((col) => col !== null) as ColumnConfig[]; if (validColumns.length !== processedColumns.length) { console.log( "🔍 필터링된 컬럼들:", validColumns.map((c) => c.columnName), ); console.log( "🔍 제거된 컬럼들:", processedColumns .filter((col) => { const mappedName = newJoinColumnMapping[col.columnName] || col.columnName; return !actualApiColumns.includes(mappedName) && col.columnName !== "__checkbox__"; }) .map((c) => c.columnName), ); processedColumns = validColumns; } } } if (result.entityJoinInfo?.joinConfigs) { result.entityJoinInfo.joinConfigs.forEach((joinConfig) => { // 원본 컬럼을 조인된 컬럼으로 교체 const originalColumnIndex = processedColumns.findIndex((col) => col.columnName === joinConfig.sourceColumn); if (originalColumnIndex !== -1) { console.log(`🔄 컬럼 교체: ${joinConfig.sourceColumn} → ${joinConfig.aliasColumn}`); const originalColumn = processedColumns[originalColumnIndex]; processedColumns[originalColumnIndex] = { ...originalColumn, columnName: joinConfig.aliasColumn, // dept_code → dept_code_name displayName: columnLabels[originalColumn.columnName] || originalColumn.displayName || originalColumn.columnName, // 올바른 라벨 사용 // isEntityJoined: true, // 🎯 임시 주석 처리 (타입 에러 해결 후 복원) } as ColumnConfig; console.log( `✅ 조인 컬럼 라벨 유지: ${joinConfig.sourceColumn} → "${columnLabels[originalColumn.columnName] || originalColumn.displayName || originalColumn.columnName}"`, ); } }); } // 🎯 컬럼 설정이 없으면 API 응답 기반으로 생성 if ((!processedColumns || processedColumns.length === 0) && result.data.length > 0) { const autoColumns: ColumnConfig[] = Object.keys(result.data[0]).map((key, index) => ({ columnName: key, displayName: columnLabels[key] || key, visible: true, sortable: true, searchable: true, align: "left", format: "text", order: index, })); console.log( "🎯 자동 생성된 컬럼들:", autoColumns.map((c) => c.columnName), ); // 컴포넌트 설정 업데이트 (부모 컴포넌트에 알림) if (onFormDataChange) { onFormDataChange({ ...component, config: { ...tableConfig, columns: autoColumns, }, }); } processedColumns = autoColumns; } // 🎯 표시할 컬럼 상태 업데이트 setDisplayColumns(processedColumns); console.log("🎯 displayColumns 업데이트됨:", processedColumns); console.log("🎯 데이터 개수:", result.data?.length || 0); console.log("🎯 전체 데이터:", result.data); } } catch (err) { console.error("테이블 데이터 로딩 오류:", err); setError(err instanceof Error ? err.message : "데이터를 불러오는 중 오류가 발생했습니다."); setData([]); } finally { setLoading(false); } }; // 페이지 변경 const handlePageChange = (newPage: number) => { setCurrentPage(newPage); // 상세설정에 현재 페이지 정보 알림 (필요한 경우) if (onConfigChange && tableConfig.pagination) { console.log("📤 테이블에서 페이지 변경을 상세설정에 알림:", newPage); onConfigChange({ ...tableConfig, pagination: { ...tableConfig.pagination, currentPage: newPage, // 현재 페이지 정보 추가 }, }); } else if (!onConfigChange) { console.warn("⚠️ onConfigChange가 정의되지 않음 - 페이지 변경 상세설정과 연동 불가"); } }; // 정렬 변경 const handleSort = (column: string) => { if (sortColumn === column) { setSortDirection(sortDirection === "asc" ? "desc" : "asc"); } else { setSortColumn(column); setSortDirection("asc"); } }; // 고급 필터 핸들러 const handleSearchValueChange = (columnName: string, value: any) => { setSearchValues((prev) => ({ ...prev, [columnName]: value, })); }; const handleAdvancedSearch = () => { setCurrentPage(1); fetchTableData(); }; const handleClearAdvancedFilters = () => { setSearchValues({}); setCurrentPage(1); fetchTableData(); }; // 새로고침 const handleRefresh = () => { fetchTableData(); }; // 체크박스 핸들러들 const getRowKey = (row: any, index: number) => { // 기본키가 있으면 사용, 없으면 인덱스 사용 return row.id || row.objid || row.pk || index.toString(); }; const handleRowSelection = (rowKey: string, checked: boolean) => { const newSelectedRows = new Set(selectedRows); if (checked) { newSelectedRows.add(rowKey); } else { newSelectedRows.delete(rowKey); } setSelectedRows(newSelectedRows); setIsAllSelected(newSelectedRows.size === data.length && data.length > 0); // 선택된 실제 데이터를 상위 컴포넌트로 전달 const selectedKeys = Array.from(newSelectedRows); const selectedData = selectedKeys .map((key) => { // rowKey를 사용하여 데이터 찾기 (ID 기반 또는 인덱스 기반) return data.find((row, index) => { const currentRowKey = getRowKey(row, index); return currentRowKey === key; }); }) .filter(Boolean); console.log("🔍 handleRowSelection 디버그:", { rowKey, checked, selectedKeys, selectedData, dataCount: data.length, }); onSelectedRowsChange?.(selectedKeys, selectedData); if (tableConfig.onSelectionChange) { tableConfig.onSelectionChange(selectedData); } }; const handleSelectAll = (checked: boolean) => { if (checked) { const allKeys = data.map((row, index) => getRowKey(row, index)); setSelectedRows(new Set(allKeys)); setIsAllSelected(true); // 선택된 실제 데이터를 상위 컴포넌트로 전달 onSelectedRowsChange?.(allKeys, data); if (tableConfig.onSelectionChange) { tableConfig.onSelectionChange(data); } } else { setSelectedRows(new Set()); setIsAllSelected(false); // 빈 선택을 상위 컴포넌트로 전달 onSelectedRowsChange?.([], []); if (tableConfig.onSelectionChange) { tableConfig.onSelectionChange([]); } } }; // 효과 useEffect(() => { if (tableConfig.selectedTable) { fetchColumnLabels(); fetchTableLabel(); } }, [tableConfig.selectedTable]); // 컬럼 라벨이 로드되면 기존 컬럼의 displayName을 업데이트 useEffect(() => { if (Object.keys(columnLabels).length > 0 && tableConfig.columns && tableConfig.columns.length > 0) { const updatedColumns = tableConfig.columns.map((col) => ({ ...col, displayName: columnLabels[col.columnName] || col.displayName, })); // 부모 컴포넌트에 업데이트된 컬럼 정보 전달 if (onFormDataChange) { onFormDataChange({ ...component, componentConfig: { ...tableConfig, columns: updatedColumns, }, }); } } }, [columnLabels]); useEffect(() => { if (tableConfig.autoLoad && !isDesignMode) { fetchTableData(); } }, [ tableConfig.selectedTable, localPageSize, currentPage, searchTerm, sortColumn, sortDirection, columnLabels, searchValues, ]); // refreshKey 변경 시 테이블 데이터 새로고침 useEffect(() => { if (refreshKey && refreshKey > 0 && !isDesignMode) { console.log("🔄 refreshKey 변경 감지, 테이블 데이터 새로고침:", refreshKey); // 선택된 행 상태 초기화 setSelectedRows(new Set()); setIsAllSelected(false); // 부모 컴포넌트에 빈 선택 상태 전달 console.log("🔄 선택 상태 초기화 - 빈 배열 전달"); onSelectedRowsChange?.([], []); // 테이블 데이터 새로고침 fetchTableData(); } }, [refreshKey]); // 상세설정에서 페이지네이션 설정 변경 시 로컬 상태 동기화 useEffect(() => { // 페이지 크기 동기화 if (tableConfig.pagination?.pageSize && tableConfig.pagination.pageSize !== localPageSize) { console.log("🔄 상세설정에서 페이지 크기 변경 감지:", tableConfig.pagination.pageSize); setLocalPageSize(tableConfig.pagination.pageSize); setCurrentPage(1); // 페이지를 1로 리셋 } // 현재 페이지 동기화 (상세설정에서 페이지를 직접 변경한 경우) if (tableConfig.pagination?.currentPage && tableConfig.pagination.currentPage !== currentPage) { console.log("🔄 상세설정에서 현재 페이지 변경 감지:", tableConfig.pagination.currentPage); setCurrentPage(tableConfig.pagination.currentPage); } }, [tableConfig.pagination?.pageSize, tableConfig.pagination?.currentPage]); // 표시할 컬럼 계산 (Entity 조인 적용됨 + 체크박스 컬럼 추가 + 숨김 기능) const visibleColumns = useMemo(() => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 const checkboxConfig = tableConfig.checkbox || { enabled: true, multiple: true, position: "left", selectAll: true, }; let columns: ColumnConfig[] = []; // displayColumns가 있으면 우선 사용 (Entity 조인 적용된 컬럼들) if (displayColumns && displayColumns.length > 0) { // 디버깅 로그 제거 (성능상 이유로) const filteredColumns = displayColumns.filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 if (isDesignMode) { return col.visible; // 디자인 모드에서는 visible만 체크 } else { return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 } }); // 디버깅 로그 제거 (성능상 이유로) columns = filteredColumns.sort((a, b) => a.order - b.order); } else if (tableConfig.columns && tableConfig.columns.length > 0) { // displayColumns가 없으면 기본 컬럼 사용 // 디버깅 로그 제거 (성능상 이유로) columns = tableConfig.columns .filter((col) => { // 디자인 모드에서는 숨김 컬럼도 표시 (연하게), 실제 화면에서는 완전히 숨김 if (isDesignMode) { return col.visible; // 디자인 모드에서는 visible만 체크 } else { return col.visible && !col.hidden; // 실제 화면에서는 visible이면서 hidden이 아닌 것만 } }) .sort((a, b) => a.order - b.order); } else { console.log("🎯 사용할 컬럼이 없음"); return []; } // 체크박스가 활성화되고 실제 데이터 컬럼이 있는 경우에만 체크박스 컬럼을 추가 if (checkboxConfig.enabled && columns.length > 0) { const checkboxColumn: ColumnConfig = { columnName: "__checkbox__", displayName: "", visible: true, sortable: false, searchable: false, width: 50, align: "center", order: -1, // 가장 앞에 위치 fixed: checkboxConfig.position === "left" ? "left" : false, fixedOrder: 0, // 가장 앞에 고정 }; // 체크박스 위치에 따라 추가 if (checkboxConfig.position === "left") { columns.unshift(checkboxColumn); } else { columns.push(checkboxColumn); } } // 디버깅 로그 제거 (성능상 이유로) return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox, isDesignMode]); // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 // 기존 테이블에서만 필요한 경우 다시 추가 가능 // 가로 스크롤이 필요한지 계산 const needsHorizontalScroll = useMemo(() => { if (!tableConfig.horizontalScroll?.enabled) { console.log("🚫 가로 스크롤 비활성화됨"); return false; } const maxVisible = tableConfig.horizontalScroll.maxVisibleColumns || 8; const totalColumns = visibleColumns.length; const result = totalColumns > maxVisible; console.log( `🔍 가로 스크롤 계산: ${totalColumns}개 컬럼 > ${maxVisible}개 최대 = ${result ? "스크롤 필요" : "스크롤 불필요"}`, ); console.log("📊 가로 스크롤 설정:", tableConfig.horizontalScroll); console.log( "📋 현재 컬럼들:", visibleColumns.map((c) => c.columnName), ); return result; }, [visibleColumns.length, tableConfig.horizontalScroll]); // 컬럼 너비 계산 - 내용 길이에 맞게 자동 조정 const getColumnWidth = (column: ColumnConfig) => { if (column.width) return column.width; // 체크박스 컬럼인 경우 고정 너비 if (column.columnName === "__checkbox__") { return 50; } // 컬럼 헤더 텍스트 길이 기반으로 계산 const headerText = columnLabels[column.columnName] || column.displayName || column.columnName; const headerLength = headerText.length; // 데이터 셀의 최대 길이 추정 (실제 데이터가 있다면 더 정확하게 계산 가능) const estimatedContentLength = Math.max(headerLength, 10); // 최소 10자 // 문자당 약 8px 정도로 계산하고, 패딩 및 여백 고려 const calculatedWidth = estimatedContentLength * 8 + 40; // 40px는 패딩과 여백 // 최소 너비만 보장하고, 최대 너비 제한은 제거 const minWidth = 80; return Math.max(minWidth, calculatedWidth); }; // 체크박스 헤더 렌더링 const renderCheckboxHeader = () => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 const checkboxConfig = tableConfig.checkbox || { enabled: true, multiple: true, position: "left", selectAll: true, }; if (!checkboxConfig.enabled || !checkboxConfig.selectAll) { return null; } return ; }; // 체크박스 셀 렌더링 const renderCheckboxCell = (row: any, index: number) => { // 기본값 처리: checkbox 설정이 없으면 기본값 사용 const checkboxConfig = tableConfig.checkbox || { enabled: true, multiple: true, position: "left", selectAll: true, }; if (!checkboxConfig.enabled) { return null; } const rowKey = getRowKey(row, index); const isSelected = selectedRows.has(rowKey); return ( handleRowSelection(rowKey, checked as boolean)} aria-label={`행 ${index + 1} 선택`} style={{ zIndex: 1 }} /> ); }; // 🎯 값 포맷팅 (전역 코드 캐시 사용) const formatCellValue = useMemo(() => { return (value: any, format?: string, columnName?: string) => { if (value === null || value === undefined) return ""; // 디버깅 로그 제거 (성능상 이유로) // 🎯 코드 컬럼인 경우 최적화된 코드명 변환 사용 if (columnName && columnMeta[columnName]?.webType === "code" && columnMeta[columnName]?.codeCategory) { const categoryCode = columnMeta[columnName].codeCategory!; const convertedValue = optimizedConvertCode(categoryCode, String(value)); // 코드 변환 로그 제거 (성능상 이유로) value = convertedValue; } switch (format) { case "number": return typeof value === "number" ? value.toLocaleString() : value; case "currency": return typeof value === "number" ? `₩${value.toLocaleString()}` : value; case "date": return value instanceof Date ? value.toLocaleDateString() : value; case "boolean": return value ? "예" : "아니오"; default: return String(value); } }; }, [columnMeta, optimizedConvertCode]); // 최적화된 변환 함수 의존성 추가 // 이벤트 핸들러 const handleClick = (e: React.MouseEvent) => { e.stopPropagation(); onClick?.(); }; // 행 클릭 핸들러 const handleRowClick = (row: any) => { if (tableConfig.onRowClick) { tableConfig.onRowClick(row); } }; // 드래그 핸들러 (그리드 스냅 지원) const handleRowDragStart = (e: React.DragEvent, row: any, index: number) => { setIsDragging(true); setDraggedRowIndex(index); // 드래그 데이터에 그리드 정보 포함 const dragData = { ...row, _dragType: 'table-row', _gridSize: { width: 4, height: 1 }, // 기본 그리드 크기 (4칸 너비, 1칸 높이) _snapToGrid: true }; e.dataTransfer.setData('application/json', JSON.stringify(dragData)); e.dataTransfer.effectAllowed = 'copy'; // move 대신 copy로 변경 // 드래그 이미지를 더 깔끔하게 const dragElement = e.currentTarget as HTMLElement; // 커스텀 드래그 이미지 생성 (저장 버튼과 어울리는 스타일) const dragImage = document.createElement('div'); dragImage.style.position = 'absolute'; dragImage.style.top = '-1000px'; dragImage.style.left = '-1000px'; dragImage.style.background = 'linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%)'; dragImage.style.color = 'white'; dragImage.style.padding = '12px 16px'; dragImage.style.borderRadius = '8px'; dragImage.style.fontSize = '14px'; dragImage.style.fontWeight = '600'; dragImage.style.boxShadow = '0 4px 12px rgba(59, 130, 246, 0.4)'; dragImage.style.display = 'flex'; dragImage.style.alignItems = 'center'; dragImage.style.gap = '8px'; dragImage.style.minWidth = '200px'; dragImage.style.whiteSpace = 'nowrap'; // 아이콘과 텍스트 추가 const firstValue = Object.values(row)[0] || 'Row'; dragImage.innerHTML = `
📋
${firstValue}
4×1
`; document.body.appendChild(dragImage); e.dataTransfer.setDragImage(dragImage, 20, 20); // 정리 setTimeout(() => { if (document.body.contains(dragImage)) { document.body.removeChild(dragImage); } }, 0); }; const handleRowDragEnd = (e: React.DragEvent) => { setIsDragging(false); setDraggedRowIndex(null); }; // DOM에 전달할 수 있는 기본 props만 정의 const domProps = { onClick: handleClick, onDragStart, onDragEnd, }; // 디자인 모드에서의 플레이스홀더 if (isDesignMode && !tableConfig.selectedTable) { return (
테이블 리스트
설정 패널에서 테이블을 선택해주세요
); } return (
{/* 헤더 */} {tableConfig.showHeader && (
{(tableConfig.title || tableLabel) && (

{tableConfig.title || tableLabel}

)}
{/* 선택된 항목 정보 표시 */} {selectedRows.size > 0 && (
{selectedRows.size}개 선택됨
)} {/* 새로고침 */}
)} {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( <>
({ columnName: col.columnName, widgetType: (columnMeta[col.columnName]?.webType || "text") as WebType, displayName: columnLabels[col.columnName] || col.displayName || col.columnName, codeCategory: columnMeta[col.columnName]?.codeCategory, isVisible: col.visible, // 추가 메타데이터 전달 (필터 자동 생성용) web_type: (columnMeta[col.columnName]?.webType || "text") as WebType, column_name: col.columnName, column_label: columnLabels[col.columnName] || col.displayName || col.columnName, code_category: columnMeta[col.columnName]?.codeCategory, }))} tableName={tableConfig.selectedTable} />
)} {/* 테이블 컨텐츠 */}
= 50 ? "flex-1" : ""}`} style={{ width: "100%", maxWidth: "100%", boxSizing: "border-box" }} > {loading ? (
데이터를 불러오는 중...
잠시만 기다려주세요
) : error ? (
!
오류가 발생했습니다
{error}
) : needsHorizontalScroll ? ( // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용
) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우)
{visibleColumns.map((column, colIndex) => ( column.sortable && handleSort(column.columnName)} > {column.columnName === "__checkbox__" ? ( renderCheckboxHeader() ) : (
{columnLabels[column.columnName] || column.displayName} {column.sortable && (
{sortColumn === column.columnName ? ( sortDirection === "asc" ? ( ) : ( ) ) : ( )}
)}
)}
))}
{data.length === 0 ? (
데이터가 없습니다
조건을 변경하거나 새로운 데이터를 추가해보세요
) : ( data.map((row, index) => ( handleRowDragStart(e, row, index)} onDragEnd={handleRowDragEnd} className={cn( "group relative h-12 cursor-pointer transition-all duration-200 border-b border-gray-100", // 기본 스타일 tableConfig.tableStyle?.hoverEffect && "hover:bg-gradient-to-r hover:from-orange-200 hover:to-orange-300/90 hover:shadow-sm", tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-100/80", // 드래그 상태 스타일 (미묘하게) draggedRowIndex === index && "bg-gradient-to-r from-blue-50 to-blue-100/40 shadow-sm border-blue-200", isDragging && draggedRowIndex !== index && "opacity-70", // 드래그 가능 표시 !isDesignMode && "hover:cursor-grab active:cursor-grabbing" )} style={{ minHeight: "48px", height: "48px", lineHeight: "1", width: "100%", maxWidth: "100%" }} onClick={() => handleRowClick(row)} > {visibleColumns.map((column, colIndex) => ( {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index) : (() => { // 🎯 매핑된 컬럼명으로 데이터 찾기 const mappedColumnName = joinColumnMapping[column.columnName] || column.columnName; const cellValue = row[mappedColumnName]; if (index === 0) { // 디버깅 로그 제거 (성능상 이유로) } const formattedValue = formatCellValue(cellValue, column.format, column.columnName) || "\u00A0"; // 첫 번째 컬럼에 드래그 핸들과 아바타 추가 const isFirstColumn = colIndex === (visibleColumns[0]?.columnName === "__checkbox__" ? 1 : 0); return (
{isFirstColumn && !isDesignMode && (
{/* 그리드 스냅 가이드 아이콘 */}
)} {formattedValue}
); })()}
))}
)) )}
)}
{/* 푸터/페이지네이션 */} {tableConfig.showFooter && tableConfig.pagination?.enabled && (
{/* 페이지 정보 - 가운데 정렬 */} {tableConfig.pagination?.showPageInfo && (
전체 {totalItems.toLocaleString()}건 중{" "} {(currentPage - 1) * localPageSize + 1}-{Math.min(currentPage * localPageSize, totalItems)} {" "} 표시
)} {/* 페이지 크기 선택과 페이지네이션 버튼 - 가운데 정렬 */}
{/* 페이지 크기 선택 - 임시로 항상 표시 (테스트용) */} {true && ( )} {/* 페이지네이션 버튼 */}
{currentPage} / {totalPages}
)}
); }; /** * TableList 래퍼 컴포넌트 * 추가적인 로직이나 상태 관리가 필요한 경우 사용 */ export const TableListWrapper: React.FC = (props) => { return ; };