diff --git a/frontend/components/screen/ResponsiveDesignerContainer.tsx b/frontend/components/screen/ResponsiveDesignerContainer.tsx index ea55b30e..b014172b 100644 --- a/frontend/components/screen/ResponsiveDesignerContainer.tsx +++ b/frontend/components/screen/ResponsiveDesignerContainer.tsx @@ -25,7 +25,7 @@ export const ResponsiveDesignerContainer: React.FC { const containerRef = useRef(null); - const [viewMode, setViewMode] = useState("fit"); + const [viewMode, setViewMode] = useState("original"); const [customScale, setCustomScale] = useState(1); const containerSize = useContainerSize(containerRef); diff --git a/frontend/components/screen/filters/AdvancedSearchFilters.tsx b/frontend/components/screen/filters/AdvancedSearchFilters.tsx index a509cd5d..8ce1baaf 100644 --- a/frontend/components/screen/filters/AdvancedSearchFilters.tsx +++ b/frontend/components/screen/filters/AdvancedSearchFilters.tsx @@ -317,7 +317,7 @@ export const AdvancedSearchFilters: React.FC = ({ }).length; return ( -
+
{/* 필터 헤더 */}
@@ -339,7 +339,7 @@ export const AdvancedSearchFilters: React.FC = ({ }; return ( -
+
{renderFilter(filter)}
diff --git a/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx new file mode 100644 index 00000000..51cfdb6b --- /dev/null +++ b/frontend/lib/registry/components/table-list/SingleTableWithSticky.tsx @@ -0,0 +1,188 @@ +"use client"; + +import React from "react"; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; +import { Checkbox } from "@/components/ui/checkbox"; +import { ArrowUp, ArrowDown, ArrowUpDown } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ColumnConfig } from "./types"; + +interface SingleTableWithStickyProps { + visibleColumns: ColumnConfig[]; + data: Record[]; + columnLabels: Record; + sortColumn: string | null; + sortDirection: "asc" | "desc"; + tableConfig: any; + isDesignMode: boolean; + isAllSelected: boolean; + handleSort: (columnName: string) => void; + handleSelectAll: (checked: boolean) => void; + handleRowClick: (row: any) => void; + renderCheckboxCell: (row: any, index: number) => React.ReactNode; + formatCellValue: (value: any, format?: string, columnName?: string) => string; + getColumnWidth: (column: ColumnConfig) => number; +} + +export const SingleTableWithSticky: React.FC = ({ + visibleColumns, + data, + columnLabels, + sortColumn, + sortDirection, + tableConfig, + isDesignMode, + isAllSelected, + handleSort, + handleSelectAll, + handleRowClick, + renderCheckboxCell, + formatCellValue, + getColumnWidth, +}) => { + const checkboxConfig = tableConfig.checkbox || {}; + + return ( +
+ + + + {visibleColumns.map((column, colIndex) => { + // 왼쪽 고정 컬럼들의 누적 너비 계산 + const leftFixedWidth = visibleColumns + .slice(0, colIndex) + .filter((col) => col.fixed === "left") + .reduce((sum, col) => sum + getColumnWidth(col), 0); + + // 오른쪽 고정 컬럼들의 누적 너비 계산 + const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right"); + const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName); + const rightFixedWidth = + rightFixedIndex >= 0 + ? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0) + : 0; + + return ( + column.sortable && handleSort(column.columnName)} + > +
+ {column.columnName === "__checkbox__" ? ( + checkboxConfig.selectAll && ( + + ) + ) : ( + <> + + {columnLabels[column.columnName] || column.displayName || column.columnName} + + {column.sortable && ( + + {sortColumn === column.columnName ? ( + sortDirection === "asc" ? ( + + ) : ( + + ) + ) : ( + + )} + + )} + + )} +
+
+ ); + })} +
+
+ + + {data.length === 0 ? ( + + + 데이터가 없습니다 + + + ) : ( + data.map((row, index) => ( + handleRowClick(row)} + > + {visibleColumns.map((column, colIndex) => { + // 왼쪽 고정 컬럼들의 누적 너비 계산 + const leftFixedWidth = visibleColumns + .slice(0, colIndex) + .filter((col) => col.fixed === "left") + .reduce((sum, col) => sum + getColumnWidth(col), 0); + + // 오른쪽 고정 컬럼들의 누적 너비 계산 + const rightFixedColumns = visibleColumns.filter((col) => col.fixed === "right"); + const rightFixedIndex = rightFixedColumns.findIndex((col) => col.columnName === column.columnName); + const rightFixedWidth = + rightFixedIndex >= 0 + ? rightFixedColumns.slice(rightFixedIndex + 1).reduce((sum, col) => sum + getColumnWidth(col), 0) + : 0; + + return ( + + {column.columnName === "__checkbox__" + ? renderCheckboxCell(row, index) + : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} + + ); + })} + + )) + )} + +
+
+ ); +}; diff --git a/frontend/lib/registry/components/table-list/TableListComponent.tsx b/frontend/lib/registry/components/table-list/TableListComponent.tsx index 785d089c..813d36d2 100644 --- a/frontend/lib/registry/components/table-list/TableListComponent.tsx +++ b/frontend/lib/registry/components/table-list/TableListComponent.tsx @@ -23,6 +23,7 @@ import { Checkbox } from "@/components/ui/checkbox"; import { cn } from "@/lib/utils"; import { AdvancedSearchFilters } from "@/components/screen/filters/AdvancedSearchFilters"; import { Separator } from "@/components/ui/separator"; +import { SingleTableWithSticky } from "./SingleTableWithSticky"; export interface TableListComponentProps { component: any; @@ -113,39 +114,67 @@ export const TableListComponent: React.FC = ({ maxBatchSize: 5, }); - // 높이 계산 함수 - const calculateOptimalHeight = () => { - // 50개 이상일 때는 20개 기준으로 높이 고정 - const displayPageSize = localPageSize >= 50 ? 20 : localPageSize; - const headerHeight = 48; // 테이블 헤더 - const rowHeight = 40; // 각 행 높이 (normal) - const searchHeight = tableConfig.filter?.enabled ? 48 : 0; // 검색 영역 - const footerHeight = tableConfig.showFooter ? 56 : 0; // 페이지네이션 - const padding = 8; // 여백 + // 높이 계산 함수 (메모이제이션) + const optimalHeight = useMemo(() => { + // 50개 이상일 때는 20개 기준으로 높이 고정하고 스크롤 생성 + // 50개 미만일 때는 실제 데이터 개수에 맞춰서 스크롤 없이 표시 + const actualDataCount = Math.min(data.length, localPageSize); + const displayPageSize = localPageSize >= 50 ? 20 : Math.max(actualDataCount, 5); - return headerHeight + displayPageSize * rowHeight + searchHeight + footerHeight + padding; - }; + 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, + willHaveScroll: localPageSize >= 50, + titleHeight, + searchHeight, + headerHeight, + rowHeight, + footerHeight, + padding, + calculatedHeight, + finalHeight: `${calculatedHeight}px`, + }); + + // 추가 디버깅: 실제 데이터 상황 + console.log("🔍 실제 데이터 상황:", { + actualDataLength: data.length, + localPageSize, + currentPage, + totalItems, + totalPages, + }); + + return calculatedHeight; + }, [data.length, localPageSize, tableConfig.filter?.enabled, tableConfig.showFooter, tableConfig.showHeader]); // 스타일 계산 const componentStyle: React.CSSProperties = { width: "100%", - height: - tableConfig.height === "fixed" - ? `${tableConfig.fixedHeight || calculateOptimalHeight()}px` - : tableConfig.height === "auto" - ? `${calculateOptimalHeight()}px` - : "100%", + height: `${optimalHeight}px`, // 20개 데이터를 모두 보여주는 높이 + minHeight: `${optimalHeight}px`, // 최소 높이 보장 ...component.style, ...style, display: "flex", flexDirection: "column", + boxSizing: "border-box", // 패딩/보더 포함한 크기 계산 }; // 디자인 모드 스타일 if (isDesignMode) { componentStyle.border = "1px dashed #cbd5e1"; componentStyle.borderColor = isSelected ? "#3b82f6" : "#cbd5e1"; - componentStyle.minHeight = "200px"; + // minHeight 제거 - 실제 데이터에 맞는 높이 사용 } // 컬럼 라벨 정보 가져오기 @@ -637,28 +666,8 @@ export const TableListComponent: React.FC = ({ return columns; }, [displayColumns, tableConfig.columns, tableConfig.checkbox]); - // 컬럼을 고정 위치별로 분류 - 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]); + // columnsByPosition은 SingleTableWithSticky에서 사용하지 않으므로 제거 + // 기존 테이블에서만 필요한 경우 다시 추가 가능 // 가로 스크롤이 필요한지 계산 const needsHorizontalScroll = useMemo(() => { @@ -893,7 +902,7 @@ export const TableListComponent: React.FC = ({ {/* 고급 검색 필터 - 항상 표시 (컬럼 정보 기반 자동 생성) */} {tableConfig.filter?.enabled && visibleColumns && visibleColumns.length > 0 && ( <> - + = ({ )} {/* 테이블 컨텐츠 */} -
= 50 ? "overflow-auto" : "overflow-hidden"}`}> +
= 50 ? "flex-1 overflow-auto" : ""}`}> {loading ? (
@@ -934,317 +943,43 @@ export const TableListComponent: React.FC = ({
) : 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)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - - ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- 데이터가 없습니다 -
- {column.columnName === "__checkbox__" - ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} -
-
- )} - - {/* 스크롤 가능한 중앙 컬럼들 */} -
- - - - {columnsByPosition.normal.map((column) => ( - - ))} - - - - {data.length === 0 ? ( - - - - ) : ( - data.map((row, index) => ( - handleRowClick(row)} - > - {columnsByPosition.normal.map((column) => ( - - ))} - - )) - )} - -
column.sortable && handleSort(column.columnName)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - - ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- {columnsByPosition.leftFixed.length === 0 && columnsByPosition.rightFixed.length === 0 - ? "데이터가 없습니다" - : ""} -
- {column.columnName === "__checkbox__" - ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} -
-
- - {/* 오른쪽 고정 컬럼 */} - {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)} - > - {column.columnName === "__checkbox__" ? ( - renderCheckboxHeader() - ) : ( -
- {columnLabels[column.columnName] || column.displayName} - {column.sortable && ( -
- {sortColumn === column.columnName ? ( - sortDirection === "asc" ? ( - - ) : ( - - ) - ) : ( - - )} -
- )} -
- )} -
- {columnsByPosition.leftFixed.length === 0 && columnsByPosition.normal.length === 0 - ? "데이터가 없습니다" - : ""} -
- {column.columnName === "__checkbox__" - ? renderCheckboxCell(row, index) - : formatCellValue(row[column.columnName], column.format, column.columnName) || "\u00A0"} -
-
- )} -
+ // 가로 스크롤이 필요한 경우 - 단일 테이블에서 sticky 컬럼 사용 + ) : ( // 기존 테이블 (가로 스크롤이 필요 없는 경우) - + {visibleColumns.map((column) => ( = ({ handleRowClick(row)} > {visibleColumns.map((column) => ( {column.columnName === "__checkbox__" ? renderCheckboxCell(row, index)