diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 0d834bef..e42aebeb 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -58,25 +58,14 @@ export const TableListComponent: React.FC = ({ component, isDesignMode = false, isSelected = false, - isInteractive = false, onClick, onDragStart, onDragEnd, config, className, style, - formData, onFormDataChange, - screenId, - size, - position, componentConfig, - selectedScreen, - onZoneComponentDrop, - onZoneClick, - tableName, - onRefresh, - onClose, }) => { // 컴포넌트 설정 const tableConfig = { @@ -86,7 +75,7 @@ export const TableListComponent: React.FC = ({ } as TableListConfig; // 상태 관리 - const [data, setData] = useState([]); + const [data, setData] = useState[]>([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [currentPage, setCurrentPage] = useState(1); @@ -150,7 +139,7 @@ export const TableListComponent: React.FC = ({ try { const response = await tableTypeApi.getColumns(tableConfig.selectedTable); // API 응답 구조 확인 및 컬럼 배열 추출 - const columns = Array.isArray(response) ? response : response.columns || []; + const columns = Array.isArray(response) ? response : (response as any).columns || []; const labels: Record = {}; const meta: Record = {}; @@ -463,6 +452,72 @@ export const TableListComponent: React.FC = ({ return displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order); }, [displayColumns, tableConfig.columns]); + // 컬럼을 고정 위치별로 분류 + const columnsByPosition = useMemo(() => { + const leftFixed: ColumnConfig[] = []; + const rightFixed: ColumnConfig[] = []; + const normal: ColumnConfig[] = []; + + visibleColumns.forEach((col) => { + if (col.fixed === "left") { + leftFixed.push(col); + } else if (col.fixed === "right") { + rightFixed.push(col); + } else { + normal.push(col); + } + }); + + // 고정 컬럼들은 fixedOrder로 정렬 + leftFixed.sort((a, b) => (a.fixedOrder || 0) - (b.fixedOrder || 0)); + rightFixed.sort((a, b) => (a.fixedOrder || 0) - (b.fixedOrder || 0)); + + return { leftFixed, rightFixed, normal }; + }, [visibleColumns]); + + // 가로 스크롤이 필요한지 계산 + 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; + + // 컬럼 헤더 텍스트 길이 기반으로 계산 + 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 formatCellValue = useMemo(() => { return (value: any, format?: string, columnName?: string) => { @@ -596,7 +651,226 @@ export const TableListComponent: React.FC = ({
{error}
+ ) : needsHorizontalScroll ? ( + // 가로 스크롤이 필요한 경우 - 고정 컬럼 지원 테이블 +
+ {/* 왼쪽 고정 컬럼 */} + {columnsByPosition.leftFixed.length > 0 && ( +
+ + + + {columnsByPosition.leftFixed.map((column) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, index) => ( + handleRowClick(row)} + > + {columnsByPosition.leftFixed.map((column) => ( + + ))} + + )) + )} + +
column.sortable && handleSort(column.columnName)} + > +
+ {columnLabels[column.columnName] || column.displayName} + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) + ) : ( + + )} +
+ )} +
+
+ 데이터가 없습니다 +
+ {formatCellValue(row[column.columnName], column.format, column.columnName)} +
+
+ )} + + {/* 스크롤 가능한 중앙 컬럼들 */} +
+ + + + {columnsByPosition.normal.map((column) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, index) => ( + handleRowClick(row)} + > + {columnsByPosition.normal.map((column) => ( + + ))} + + )) + )} + +
column.sortable && handleSort(column.columnName)} + > +
+ {columnLabels[column.columnName] || column.displayName} + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) + ) : ( + + )} +
+ )} +
+
+ {columnsByPosition.leftFixed.length === 0 && columnsByPosition.rightFixed.length === 0 + ? "데이터가 없습니다" + : ""} +
+ {formatCellValue(row[column.columnName], column.format, column.columnName)} +
+
+ + {/* 오른쪽 고정 컬럼 */} + {columnsByPosition.rightFixed.length > 0 && ( +
+ + + + {columnsByPosition.rightFixed.map((column) => ( + + ))} + + + + {data.length === 0 ? ( + + + + ) : ( + data.map((row, index) => ( + handleRowClick(row)} + > + {columnsByPosition.rightFixed.map((column) => ( + + ))} + + )) + )} + +
column.sortable && handleSort(column.columnName)} + > +
+ {columnLabels[column.columnName] || column.displayName} + {column.sortable && ( +
+ {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) + ) : ( + + )} +
+ )} +
+
+ {columnsByPosition.leftFixed.length === 0 && columnsByPosition.normal.length === 0 + ? "데이터가 없습니다" + : ""} +
+ {formatCellValue(row[column.columnName], column.format, column.columnName)} +
+
+ )} +
) : ( + // 기존 테이블 (가로 스크롤이 필요 없는 경우) @@ -605,7 +879,7 @@ export const TableListComponent: React.FC = ({ key={column.columnName} style={{ width: column.width ? `${column.width}px` : undefined }} className={cn( - "cursor-pointer select-none", + "cursor-pointer whitespace-nowrap select-none", `text-${column.align}`, column.sortable && "hover:bg-gray-50", )} @@ -650,7 +924,7 @@ export const TableListComponent: React.FC = ({ onClick={() => handleRowClick(row)} > {visibleColumns.map((column) => ( - + {formatCellValue(row[column.columnName], column.format, column.columnName)} ))} diff --git a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx index 817f6f34..665b2f40 100644 --- a/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx +++ b/frontend/lib/registry/components/table-list/TableListConfigPanel.tsx @@ -10,22 +10,9 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Separator } from "@/components/ui/separator"; import { TableListConfig, ColumnConfig } from "./types"; import { entityJoinApi } from "@/lib/api/entityJoin"; -import { - Plus, - Trash2, - ArrowUp, - ArrowDown, - Eye, - EyeOff, - Settings, - Columns, - Filter, - Palette, - MousePointer, -} from "lucide-react"; +import { Plus, Trash2, ArrowUp, ArrowDown, Settings, Columns, Filter, Palette, MousePointer } from "lucide-react"; export interface TableListConfigPanelProps { config: TableListConfig; @@ -321,652 +308,788 @@ export const TableListConfigPanel: React.FC = ({ {/* 기본 설정 탭 */} - - - 연결된 테이블 - 화면에 연결된 테이블 정보가 자동으로 매핑됩니다 - - -
- -
-
- {screenTableName ? ( - {screenTableName} - ) : ( - 테이블이 연결되지 않았습니다 + + + + 연결된 테이블 + 화면에 연결된 테이블 정보가 자동으로 매핑됩니다 + + +
+ +
+
+ {screenTableName ? ( + {screenTableName} + ) : ( + 테이블이 연결되지 않았습니다 + )} +
+ {screenTableName && ( +
화면 설정에서 자동으로 연결된 테이블입니다
)}
- {screenTableName && ( -
화면 설정에서 자동으로 연결된 테이블입니다
- )}
-
-
- - handleChange("title", e.target.value)} - placeholder="테이블 제목 (선택사항)" - /> -
- - - - - - 표시 설정 - - -
- handleChange("showHeader", checked)} - /> - -
- -
- handleChange("showFooter", checked)} - /> - -
- -
- handleChange("autoLoad", checked)} - /> - -
-
-
- - - - 높이 설정 - - -
- - -
- - {config.height === "fixed" && (
- + handleChange("fixedHeight", parseInt(e.target.value) || 400)} - min={200} - max={1000} + id="title" + value={config.title || ""} + onChange={(e) => handleChange("title", e.target.value)} + placeholder="테이블 제목 (선택사항)" />
- )} -
-
+ + - - - 페이지네이션 - - -
- handleNestedChange("pagination", "enabled", checked)} - /> - -
+ + + 표시 설정 + + +
+ handleChange("showHeader", checked)} + /> + +
- {config.pagination?.enabled && ( - <> +
+ handleChange("showFooter", checked)} + /> + +
+ +
+ handleChange("autoLoad", checked)} + /> + +
+
+
+ + + + 높이 설정 + + +
+ + +
+ + {config.height === "fixed" && (
- - -
- -
- handleNestedChange("pagination", "showSizeSelector", checked)} + + handleChange("fixedHeight", parseInt(e.target.value) || 400)} + min={200} + max={1000} /> -
+ )} +
+
-
- handleNestedChange("pagination", "showPageInfo", checked)} - /> - + + + 페이지네이션 + + +
+ handleNestedChange("pagination", "enabled", checked)} + /> + +
+ + {config.pagination?.enabled && ( + <> +
+ + +
+ +
+ handleNestedChange("pagination", "showSizeSelector", checked)} + /> + +
+ +
+ handleNestedChange("pagination", "showPageInfo", checked)} + /> + +
+ + )} +
+
+ + + + 가로 스크롤 및 컬럼 고정 + 컬럼이 많을 때 가로 스크롤과 컬럼 고정 기능을 설정하세요 + + +
+ handleNestedChange("horizontalScroll", "enabled", checked)} + /> + +
+ + {config.horizontalScroll?.enabled && ( +
+
+ + + handleNestedChange("horizontalScroll", "maxVisibleColumns", parseInt(e.target.value) || 8) + } + min={3} + max={20} + placeholder="8" + className="h-8" + /> +
이 수를 넘는 컬럼이 있으면 가로 스크롤이 생성됩니다
+
+ +
+
+ + + handleNestedChange("horizontalScroll", "minColumnWidth", parseInt(e.target.value) || 100) + } + min={50} + max={500} + placeholder="100" + className="h-8" + /> +
+ +
+ + + handleNestedChange("horizontalScroll", "maxColumnWidth", parseInt(e.target.value) || 300) + } + min={100} + max={800} + placeholder="300" + className="h-8" + /> +
+
- - )} -
-
+ )} + + + {/* 컬럼 설정 탭 */} - {!screenTableName ? ( - - -
-

테이블이 연결되지 않았습니다.

-

화면에 테이블을 연결한 후 컬럼을 설정할 수 있습니다.

-
-
-
- ) : ( - <> + + {!screenTableName ? ( - - 컬럼 추가 - {screenTableName} - - {availableColumns.length > 0 - ? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요` - : "컬럼 정보를 불러오는 중..."} - - - - {availableColumns.length > 0 ? ( -
- {availableColumns - .filter((col) => !config.columns?.find((c) => c.columnName === col.columnName)) - .map((column) => ( - - ))} -
- ) : ( -
-

컬럼 정보를 불러오는 중입니다...

-
- )} + +
+

테이블이 연결되지 않았습니다.

+

화면에 테이블을 연결한 후 컬럼을 설정할 수 있습니다.

+
- - )} - - {screenTableName && ( - - - 컬럼 설정 - 선택된 컬럼들의 표시 옵션을 설정하세요 - - - -
- {config.columns?.map((column, index) => ( -
-
-
- - updateColumn(column.columnName, { visible: checked as boolean }) - } - /> - - {availableColumns.find((col) => col.columnName === column.columnName)?.label || - column.displayName || - column.columnName} - -
- -
+ ) : ( + <> + + + 컬럼 추가 - {screenTableName} + + {availableColumns.length > 0 + ? `${availableColumns.length}개의 사용 가능한 컬럼에서 선택하세요` + : "컬럼 정보를 불러오는 중..."} + + + + {availableColumns.length > 0 ? ( +
+ {availableColumns + .filter((col) => !config.columns?.find((c) => c.columnName === col.columnName)) + .map((column) => ( - - -
-
- - {column.visible && ( -
-
- - col.columnName === column.columnName)?.label || - column.displayName || - column.columnName - } - onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })} - className="h-8" - /> -
- -
- - -
- -
- - -
- -
- - - updateColumn(column.columnName, { - width: e.target.value ? parseInt(e.target.value) : undefined, - }) - } - placeholder="자동" - className="h-8" - /> -
- -
-
- - updateColumn(column.columnName, { sortable: checked as boolean }) - } - /> - -
-
- - updateColumn(column.columnName, { searchable: checked as boolean }) - } - /> - -
-
-
- )} + ))}
- ))} -
- - - - )} + ) : ( +
+

컬럼 정보를 불러오는 중입니다...

+
+ )} + + + + )} + + {screenTableName && ( + + + 컬럼 설정 + 선택된 컬럼들의 표시 옵션을 설정하세요 + + + +
+ {config.columns?.map((column, index) => ( +
+
+
+ + updateColumn(column.columnName, { visible: checked as boolean }) + } + /> + + {availableColumns.find((col) => col.columnName === column.columnName)?.label || + column.displayName || + column.columnName} + +
+ +
+ + + +
+
+ + {column.visible && ( +
+
+ + col.columnName === column.columnName)?.label || + column.displayName || + column.columnName + } + onChange={(e) => updateColumn(column.columnName, { displayName: e.target.value })} + className="h-8" + /> +
+ +
+ + +
+ +
+ + +
+ +
+ + + updateColumn(column.columnName, { + width: e.target.value ? parseInt(e.target.value) : undefined, + }) + } + placeholder="자동" + className="h-8" + /> +
+ +
+ + +
+ + {(column.fixed === "left" || column.fixed === "right") && ( +
+ + + updateColumn(column.columnName, { + fixedOrder: parseInt(e.target.value) || 0, + }) + } + placeholder="0" + className="h-8" + min="0" + /> +
+ )} + +
+
+ + updateColumn(column.columnName, { sortable: checked as boolean }) + } + /> + +
+
+ + updateColumn(column.columnName, { searchable: checked as boolean }) + } + /> + +
+
+
+ )} +
+ ))} +
+
+
+
+ )} + {/* Entity 조인 컬럼 추가 탭 */} - - - Entity 조인 컬럼 추가 - Entity 조인된 테이블의 다른 컬럼들을 추가로 표시할 수 있습니다. - - - - {loadingEntityJoins ? ( -
조인 정보를 가져오는 중...
- ) : entityJoinColumns.joinTables.length === 0 ? ( -
-
Entity 조인이 설정된 컬럼이 없습니다.
-
- 먼저 컬럼의 웹타입을 'entity'로 설정하고 참조 테이블을 지정해주세요. + + + + Entity 조인 컬럼 추가 + Entity 조인된 테이블의 다른 컬럼들을 추가로 표시할 수 있습니다. + + + + {loadingEntityJoins ? ( +
조인 정보를 가져오는 중...
+ ) : entityJoinColumns.joinTables.length === 0 ? ( +
+
Entity 조인이 설정된 컬럼이 없습니다.
+
+ 먼저 컬럼의 웹타입을 'entity'로 설정하고 참조 테이블을 지정해주세요. +
-
- ) : ( -
- {/* 조인 테이블별 그룹 */} - {entityJoinColumns.joinTables.map((joinTable, tableIndex) => ( - - - - 📊 {joinTable.tableName} - - 현재: {joinTable.currentDisplayColumn} - - - - - {joinTable.availableColumns.length === 0 ? ( -
추가할 수 있는 컬럼이 없습니다.
- ) : ( -
- {joinTable.availableColumns.map((column, colIndex) => { - const matchingJoinColumn = entityJoinColumns.availableColumns.find( - (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, - ); + ) : ( +
+ {/* 조인 테이블별 그룹 */} + {entityJoinColumns.joinTables.map((joinTable, tableIndex) => ( + + + + 📊 {joinTable.tableName} + + 현재: {joinTable.currentDisplayColumn} + + + + + {joinTable.availableColumns.length === 0 ? ( +
추가할 수 있는 컬럼이 없습니다.
+ ) : ( +
+ {joinTable.availableColumns.map((column, colIndex) => { + const matchingJoinColumn = entityJoinColumns.availableColumns.find( + (jc) => jc.tableName === joinTable.tableName && jc.columnName === column.columnName, + ); + const isAlreadyAdded = config.columns?.some( + (col) => col.columnName === matchingJoinColumn?.joinAlias, + ); + + return ( +
+
+
{column.columnLabel}
+
+ {column.columnName} ({column.dataType}) +
+ {column.description && ( +
{column.description}
+ )} +
+
+ {isAlreadyAdded ? ( + + 추가됨 + + ) : ( + matchingJoinColumn && ( + + ) + )} +
+
+ ); + })} +
+ )} +
+
+ ))} + + {/* 전체 사용 가능한 컬럼 요약 */} + {entityJoinColumns.availableColumns.length > 0 && ( + + + 📋 추가 가능한 컬럼 요약 + + +
+ 총 {entityJoinColumns.availableColumns.length}개의 컬럼을 추가할 수 있습니다. +
+
+ {entityJoinColumns.availableColumns.map((column, index) => { const isAlreadyAdded = config.columns?.some( - (col) => col.columnName === matchingJoinColumn?.joinAlias, + (col) => col.columnName === column.joinAlias, ); return ( -
-
-
{column.columnLabel}
-
- {column.columnName} ({column.dataType}) -
- {column.description && ( -
{column.description}
- )} -
-
- {isAlreadyAdded ? ( - - 추가됨 - - ) : ( - matchingJoinColumn && ( - - ) - )} -
-
+ !isAlreadyAdded && addEntityJoinColumn(column)} + > + {column.columnLabel} + {!isAlreadyAdded && } + ); })}
- )} -
-
- ))} - - {/* 전체 사용 가능한 컬럼 요약 */} - {entityJoinColumns.availableColumns.length > 0 && ( - - - 📋 추가 가능한 컬럼 요약 - - -
- 총 {entityJoinColumns.availableColumns.length}개의 컬럼을 추가할 수 있습니다. -
-
- {entityJoinColumns.availableColumns.map((column, index) => { - const isAlreadyAdded = config.columns?.some((col) => col.columnName === column.joinAlias); - - return ( - !isAlreadyAdded && addEntityJoinColumn(column)} - > - {column.columnLabel} - {!isAlreadyAdded && } - - ); - })} -
-
-
- )} -
- )} - - - + + + )} +
+ )} + +
+
+ {/* 필터 설정 탭 */} - - - 검색 및 필터 - - -
- handleNestedChange("filter", "enabled", checked)} - /> - -
+ + + + 검색 및 필터 + + +
+ handleNestedChange("filter", "enabled", checked)} + /> + +
- {config.filter?.enabled && ( - <> -
- handleNestedChange("filter", "quickSearch", checked)} - /> - -
- - {config.filter?.quickSearch && ( -
+ {config.filter?.enabled && ( + <> +
handleNestedChange("filter", "showColumnSelector", checked)} + id="quickSearch" + checked={config.filter?.quickSearch} + onCheckedChange={(checked) => handleNestedChange("filter", "quickSearch", checked)} /> - +
- )} -
- handleNestedChange("filter", "advancedFilter", checked)} - /> - -
- - )} - - + {config.filter?.quickSearch && ( +
+ handleNestedChange("filter", "showColumnSelector", checked)} + /> + +
+ )} + +
+ handleNestedChange("filter", "advancedFilter", checked)} + /> + +
+ + )} + + + {/* 액션 설정 탭 */} - - - 행 액션 - - -
- handleNestedChange("actions", "showActions", checked)} - /> - -
+ + + + 행 액션 + + +
+ handleNestedChange("actions", "showActions", checked)} + /> + +
-
- handleNestedChange("actions", "bulkActions", checked)} - /> - -
-
-
+
+ handleNestedChange("actions", "bulkActions", checked)} + /> + +
+
+
+
{/* 스타일 설정 탭 */} - - - 테이블 스타일 - - -
- - -
+ + + + 테이블 스타일 + + +
+ + +
-
- - -
+
+ + +
-
- handleNestedChange("tableStyle", "alternateRows", checked)} - /> - -
+
+ handleNestedChange("tableStyle", "alternateRows", checked)} + /> + +
-
- handleNestedChange("tableStyle", "hoverEffect", checked)} - /> - -
+
+ handleNestedChange("tableStyle", "hoverEffect", checked)} + /> + +
-
- handleChange("stickyHeader", checked)} - /> - -
-
-
+
+ handleChange("stickyHeader", checked)} + /> + +
+
+
+
diff --git a/frontend/lib/registry/components/table-list/index.ts b/frontend/lib/registry/components/table-list/index.ts index 5bcc2b97..770dddaf 100644 --- a/frontend/lib/registry/components/table-list/index.ts +++ b/frontend/lib/registry/components/table-list/index.ts @@ -31,6 +31,14 @@ export const TableListDefinition = createComponentDefinition({ autoWidth: true, stickyHeader: false, + // 가로 스크롤 및 컬럼 고정 설정 + horizontalScroll: { + enabled: true, + maxVisibleColumns: 8, // 8개 컬럼까지는 스크롤 없이 표시 + minColumnWidth: 100, + maxColumnWidth: 300, + }, + // 페이지네이션 pagination: { enabled: true, diff --git a/frontend/lib/registry/components/table-list/types.ts b/frontend/lib/registry/components/table-list/types.ts index 993fe9ec..b341eb65 100644 --- a/frontend/lib/registry/components/table-list/types.ts +++ b/frontend/lib/registry/components/table-list/types.ts @@ -27,6 +27,10 @@ export interface ColumnConfig { dataType?: string; // 컬럼 데이터 타입 (검색 컬럼 선택에 사용) isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부 entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보 + + // 컬럼 고정 관련 속성 + fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함) + fixedOrder?: number; // 고정된 컬럼들 내에서의 순서 } /** @@ -105,6 +109,14 @@ export interface TableListConfig extends ComponentConfig { autoWidth: boolean; stickyHeader: boolean; + // 가로 스크롤 및 컬럼 고정 설정 + horizontalScroll: { + enabled: boolean; // 가로 스크롤 활성화 여부 + maxVisibleColumns?: number; // 스크롤 없이 표시할 최대 컬럼 수 (이 수를 넘으면 가로 스크롤) + minColumnWidth?: number; // 컬럼 최소 너비 (px) + maxColumnWidth?: number; // 컬럼 최대 너비 (px) + }; + // 페이지네이션 pagination: PaginationConfig;