컬럼 고정기능 구현

This commit is contained in:
kjs 2025-09-18 15:14:14 +09:00
parent f5caa7127c
commit 964b6415f8
4 changed files with 1030 additions and 613 deletions

View File

@ -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>
))}

View File

@ -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,

View File

@ -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;