컬럼 고정기능 구현
This commit is contained in:
parent
f5caa7127c
commit
964b6415f8
|
|
@ -58,25 +58,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
} as TableListConfig;
|
||||
|
||||
// 상태 관리
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
|
@ -150,7 +139,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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<string, string> = {};
|
||||
const meta: Record<string, { webType?: string; codeCategory?: string }> = {};
|
||||
|
||||
|
|
@ -463,6 +452,72 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
<div className="mt-1 text-xs text-gray-400">{error}</div>
|
||||
</div>
|
||||
</div>
|
||||
) : needsHorizontalScroll ? (
|
||||
// 가로 스크롤이 필요한 경우 - 고정 컬럼 지원 테이블
|
||||
<div className="relative flex h-full">
|
||||
{/* 왼쪽 고정 컬럼 */}
|
||||
{columnsByPosition.leftFixed.length > 0 && (
|
||||
<div className="flex-shrink-0 border-r bg-gray-50/50">
|
||||
<table className="table-auto">
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.leftFixed.map((column) => (
|
||||
<th
|
||||
key={`fixed-left-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columnsByPosition.leftFixed.length} className="py-8 text-center text-gray-500">
|
||||
데이터가 없습니다
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={`fixed-left-row-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer border-b",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{columnsByPosition.leftFixed.map((column) => (
|
||||
<td
|
||||
key={`fixed-left-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 스크롤 가능한 중앙 컬럼들 */}
|
||||
<div className="flex-1 overflow-x-auto">
|
||||
<table className="w-full table-auto">
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.normal.map((column) => (
|
||||
<th
|
||||
key={`normal-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columnsByPosition.normal.length} className="py-8 text-center text-gray-500">
|
||||
{columnsByPosition.leftFixed.length === 0 && columnsByPosition.rightFixed.length === 0
|
||||
? "데이터가 없습니다"
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={`normal-row-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer border-b",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{columnsByPosition.normal.map((column) => (
|
||||
<td
|
||||
key={`normal-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽 고정 컬럼 */}
|
||||
{columnsByPosition.rightFixed.length > 0 && (
|
||||
<div className="flex-shrink-0 border-l bg-gray-50/50">
|
||||
<table className="table-auto">
|
||||
<thead className={tableConfig.stickyHeader ? "sticky top-0 z-20 bg-white" : ""}>
|
||||
<tr>
|
||||
{columnsByPosition.rightFixed.map((column) => (
|
||||
<th
|
||||
key={`fixed-right-${column.columnName}`}
|
||||
style={{ minWidth: `${getColumnWidth(column)}px` }}
|
||||
className={cn(
|
||||
"cursor-pointer border-b px-4 py-3 text-left font-medium whitespace-nowrap text-gray-900 select-none",
|
||||
`text-${column.align}`,
|
||||
column.sortable && "hover:bg-gray-50",
|
||||
)}
|
||||
onClick={() => column.sortable && handleSort(column.columnName)}
|
||||
>
|
||||
<div className="flex items-center space-x-1">
|
||||
<span className="text-sm">{columnLabels[column.columnName] || column.displayName}</span>
|
||||
{column.sortable && (
|
||||
<div className="flex flex-col">
|
||||
{sortColumn === column.columnName ? (
|
||||
sortDirection === "asc" ? (
|
||||
<ArrowUp className="h-3 w-3" />
|
||||
) : (
|
||||
<ArrowDown className="h-3 w-3" />
|
||||
)
|
||||
) : (
|
||||
<ArrowUpDown className="h-3 w-3 text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={columnsByPosition.rightFixed.length} className="py-8 text-center text-gray-500">
|
||||
{columnsByPosition.leftFixed.length === 0 && columnsByPosition.normal.length === 0
|
||||
? "데이터가 없습니다"
|
||||
: ""}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={`fixed-right-row-${index}`}
|
||||
className={cn(
|
||||
"cursor-pointer border-b",
|
||||
tableConfig.tableStyle?.hoverEffect && "hover:bg-gray-50",
|
||||
tableConfig.tableStyle?.alternateRows && index % 2 === 1 && "bg-gray-50/50",
|
||||
)}
|
||||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{columnsByPosition.rightFixed.map((column) => (
|
||||
<td
|
||||
key={`fixed-right-cell-${column.columnName}`}
|
||||
className={cn("px-4 py-3 text-sm whitespace-nowrap", `text-${column.align}`)}
|
||||
>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
// 기존 테이블 (가로 스크롤이 필요 없는 경우)
|
||||
<Table>
|
||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||
<TableRow>
|
||||
|
|
@ -605,7 +879,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
|||
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<TableListComponentProps> = ({
|
|||
onClick={() => handleRowClick(row)}
|
||||
>
|
||||
{visibleColumns.map((column) => (
|
||||
<TableCell key={column.columnName} className={`text-${column.align}`}>
|
||||
<TableCell key={column.columnName} className={cn("whitespace-nowrap", `text-${column.align}`)}>
|
||||
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||
</TableCell>
|
||||
))}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue