컬럼 고정기능 구현
This commit is contained in:
parent
f5caa7127c
commit
964b6415f8
|
|
@ -58,25 +58,14 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
component,
|
component,
|
||||||
isDesignMode = false,
|
isDesignMode = false,
|
||||||
isSelected = false,
|
isSelected = false,
|
||||||
isInteractive = false,
|
|
||||||
onClick,
|
onClick,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
config,
|
config,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
formData,
|
|
||||||
onFormDataChange,
|
onFormDataChange,
|
||||||
screenId,
|
|
||||||
size,
|
|
||||||
position,
|
|
||||||
componentConfig,
|
componentConfig,
|
||||||
selectedScreen,
|
|
||||||
onZoneComponentDrop,
|
|
||||||
onZoneClick,
|
|
||||||
tableName,
|
|
||||||
onRefresh,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
}) => {
|
||||||
// 컴포넌트 설정
|
// 컴포넌트 설정
|
||||||
const tableConfig = {
|
const tableConfig = {
|
||||||
|
|
@ -86,7 +75,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
} as TableListConfig;
|
} as TableListConfig;
|
||||||
|
|
||||||
// 상태 관리
|
// 상태 관리
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<Record<string, any>[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
|
@ -150,7 +139,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
try {
|
try {
|
||||||
const response = await tableTypeApi.getColumns(tableConfig.selectedTable);
|
const response = await tableTypeApi.getColumns(tableConfig.selectedTable);
|
||||||
// API 응답 구조 확인 및 컬럼 배열 추출
|
// 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 labels: Record<string, string> = {};
|
||||||
const meta: Record<string, { webType?: string; codeCategory?: 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);
|
return displayColumns.filter((col) => col.visible).sort((a, b) => a.order - b.order);
|
||||||
}, [displayColumns, tableConfig.columns]);
|
}, [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(() => {
|
const formatCellValue = useMemo(() => {
|
||||||
return (value: any, format?: string, columnName?: string) => {
|
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 className="mt-1 text-xs text-gray-400">{error}</div>
|
||||||
</div>
|
</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>
|
<Table>
|
||||||
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
<TableHeader className={tableConfig.stickyHeader ? "sticky top-0 z-10 bg-white" : ""}>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|
@ -605,7 +879,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
style={{ width: column.width ? `${column.width}px` : undefined }}
|
style={{ width: column.width ? `${column.width}px` : undefined }}
|
||||||
className={cn(
|
className={cn(
|
||||||
"cursor-pointer select-none",
|
"cursor-pointer whitespace-nowrap select-none",
|
||||||
`text-${column.align}`,
|
`text-${column.align}`,
|
||||||
column.sortable && "hover:bg-gray-50",
|
column.sortable && "hover:bg-gray-50",
|
||||||
)}
|
)}
|
||||||
|
|
@ -650,7 +924,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
|
||||||
onClick={() => handleRowClick(row)}
|
onClick={() => handleRowClick(row)}
|
||||||
>
|
>
|
||||||
{visibleColumns.map((column) => (
|
{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)}
|
{formatCellValue(row[column.columnName], column.format, column.columnName)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -31,6 +31,14 @@ export const TableListDefinition = createComponentDefinition({
|
||||||
autoWidth: true,
|
autoWidth: true,
|
||||||
stickyHeader: false,
|
stickyHeader: false,
|
||||||
|
|
||||||
|
// 가로 스크롤 및 컬럼 고정 설정
|
||||||
|
horizontalScroll: {
|
||||||
|
enabled: true,
|
||||||
|
maxVisibleColumns: 8, // 8개 컬럼까지는 스크롤 없이 표시
|
||||||
|
minColumnWidth: 100,
|
||||||
|
maxColumnWidth: 300,
|
||||||
|
},
|
||||||
|
|
||||||
// 페이지네이션
|
// 페이지네이션
|
||||||
pagination: {
|
pagination: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ export interface ColumnConfig {
|
||||||
dataType?: string; // 컬럼 데이터 타입 (검색 컬럼 선택에 사용)
|
dataType?: string; // 컬럼 데이터 타입 (검색 컬럼 선택에 사용)
|
||||||
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
|
isEntityJoin?: boolean; // Entity 조인된 컬럼인지 여부
|
||||||
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
|
entityJoinInfo?: EntityJoinInfo; // Entity 조인 상세 정보
|
||||||
|
|
||||||
|
// 컬럼 고정 관련 속성
|
||||||
|
fixed?: "left" | "right" | false; // 컬럼 고정 위치 (왼쪽, 오른쪽, 고정 안함)
|
||||||
|
fixedOrder?: number; // 고정된 컬럼들 내에서의 순서
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -105,6 +109,14 @@ export interface TableListConfig extends ComponentConfig {
|
||||||
autoWidth: boolean;
|
autoWidth: boolean;
|
||||||
stickyHeader: boolean;
|
stickyHeader: boolean;
|
||||||
|
|
||||||
|
// 가로 스크롤 및 컬럼 고정 설정
|
||||||
|
horizontalScroll: {
|
||||||
|
enabled: boolean; // 가로 스크롤 활성화 여부
|
||||||
|
maxVisibleColumns?: number; // 스크롤 없이 표시할 최대 컬럼 수 (이 수를 넘으면 가로 스크롤)
|
||||||
|
minColumnWidth?: number; // 컬럼 최소 너비 (px)
|
||||||
|
maxColumnWidth?: number; // 컬럼 최대 너비 (px)
|
||||||
|
};
|
||||||
|
|
||||||
// 페이지네이션
|
// 페이지네이션
|
||||||
pagination: PaginationConfig;
|
pagination: PaginationConfig;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue