테이블 틀고정기능

This commit is contained in:
kjs 2025-12-18 10:15:33 +09:00
parent 061fd45bc8
commit c3f066f88f
3 changed files with 127 additions and 24 deletions

View File

@ -13,7 +13,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { ScrollArea } from "@/components/ui/scroll-area";
import { GripVertical, Eye, EyeOff } from "lucide-react";
import { GripVertical, Eye, EyeOff, Lock } from "lucide-react";
import { ColumnVisibility } from "@/types/table-options";
interface Props {
@ -30,6 +30,7 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
const [localColumns, setLocalColumns] = useState<ColumnVisibility[]>([]);
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
// 테이블 정보 로드
useEffect(() => {
@ -42,6 +43,8 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
order: 0,
}))
);
// 현재 틀고정 컬럼 수 로드
setFrozenColumnCount(table.frozenColumnCount ?? 0);
}
}, [table]);
@ -94,6 +97,11 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
table.onColumnOrderChange(newOrder);
}
// 틀고정 컬럼 수 변경 콜백 호출
if (table?.onFrozenColumnCountChange) {
table.onFrozenColumnCountChange(frozenColumnCount);
}
onClose();
};
@ -107,9 +115,18 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
order: 0,
}))
);
setFrozenColumnCount(0);
}
};
// 틀고정 컬럼 수 변경 핸들러
const handleFrozenColumnCountChange = (value: string) => {
const count = parseInt(value) || 0;
// 최대값은 표시 가능한 컬럼 수
const maxCount = localColumns.filter((col) => col.visible).length;
setFrozenColumnCount(Math.min(Math.max(0, count), maxCount));
};
const visibleCount = localColumns.filter((col) => col.visible).length;
return (
@ -126,11 +143,34 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
</DialogHeader>
<div className="space-y-3 sm:space-y-4">
{/* 상태 표시 */}
<div className="flex items-center justify-between rounded-lg border bg-muted/50 p-3">
<div className="text-xs text-muted-foreground sm:text-sm">
{visibleCount}/{localColumns.length}
{/* 상태 표시 및 틀고정 설정 */}
<div className="flex flex-col gap-3 rounded-lg border bg-muted/50 p-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-4">
<div className="text-xs text-muted-foreground sm:text-sm">
{visibleCount}/{localColumns.length}
</div>
{/* 틀고정 설정 */}
<div className="flex items-center gap-2">
<Lock className="h-4 w-4 text-muted-foreground" />
<Label className="text-xs text-muted-foreground whitespace-nowrap">
:
</Label>
<Input
type="number"
value={frozenColumnCount}
onChange={(e) => handleFrozenColumnCountChange(e.target.value)}
className="h-7 w-16 text-xs sm:h-8 sm:w-20 sm:text-sm"
min={0}
max={visibleCount}
placeholder="0"
/>
<span className="text-xs text-muted-foreground whitespace-nowrap">
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
@ -148,6 +188,12 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
const columnMeta = table?.columns.find(
(c) => c.columnName === col.columnName
);
// 표시 가능한 컬럼 중 몇 번째인지 계산 (틀고정 표시용)
const visibleIndex = localColumns
.slice(0, index + 1)
.filter((c) => c.visible).length;
const isFrozen = col.visible && visibleIndex <= frozenColumnCount;
return (
<div
key={col.columnName}
@ -155,7 +201,11 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
onDragStart={() => handleDragStart(index)}
onDragOver={(e) => handleDragOver(e, index)}
onDragEnd={handleDragEnd}
className="flex items-center gap-3 rounded-lg border bg-background p-3 transition-colors hover:bg-muted/50 cursor-move"
className={`flex items-center gap-3 rounded-lg border p-3 transition-colors cursor-move ${
isFrozen
? "bg-blue-50 border-blue-200 dark:bg-blue-950/30 dark:border-blue-800"
: "bg-background hover:bg-muted/50"
}`}
>
{/* 드래그 핸들 */}
<GripVertical className="h-4 w-4 shrink-0 text-muted-foreground" />
@ -171,8 +221,10 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
}
/>
{/* 가시성 아이콘 */}
{col.visible ? (
{/* 가시성/틀고정 아이콘 */}
{isFrozen ? (
<Lock className="h-4 w-4 shrink-0 text-blue-500" />
) : col.visible ? (
<Eye className="h-4 w-4 shrink-0 text-primary" />
) : (
<EyeOff className="h-4 w-4 shrink-0 text-muted-foreground" />
@ -180,8 +232,15 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
{/* 컬럼명 */}
<div className="flex-1">
<div className="text-xs font-medium sm:text-sm">
{columnMeta?.columnLabel}
<div className="flex items-center gap-2">
<span className="text-xs font-medium sm:text-sm">
{columnMeta?.columnLabel}
</span>
{isFrozen && (
<span className="text-[10px] text-blue-600 dark:text-blue-400 font-medium">
()
</span>
)}
</div>
<div className="text-[10px] text-muted-foreground sm:text-xs">
{col.columnName}
@ -217,7 +276,7 @@ export const ColumnVisibilityPanel: React.FC<Props> = ({
<DialogFooter className="gap-2 sm:gap-0">
<Button
variant="outline"
onClick={() => onOpenChange(false)}
onClick={onClose}
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
>

View File

@ -687,7 +687,11 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
const [isTableOptionsOpen, setIsTableOptionsOpen] = useState(false);
const [showGridLines, setShowGridLines] = useState(true);
const [viewMode, setViewMode] = useState<"table" | "card" | "grouped-card">("table");
const [frozenColumns, setFrozenColumns] = useState<string[]>([]);
// 체크박스 컬럼은 항상 기본 틀고정
const [frozenColumns, setFrozenColumns] = useState<string[]>(
(tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : []
);
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
// 🆕 Search Panel (통합 검색) 관련 상태
const [globalSearchTerm, setGlobalSearchTerm] = useState("");
@ -1022,6 +1026,19 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
onColumnVisibilityChange: setColumnVisibility,
getColumnUniqueValues, // 고유 값 조회 함수 등록
onGroupSumChange: setGroupSumConfig, // 그룹별 합산 설정
// 틀고정 컬럼 관련
frozenColumnCount, // 현재 틀고정 컬럼 수
onFrozenColumnCountChange: (count: number) => {
setFrozenColumnCount(count);
// 체크박스 컬럼은 항상 틀고정에 포함
const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? ["__checkbox__"] : [];
// 표시 가능한 컬럼 중 처음 N개를 틀고정 컬럼으로 설정
const visibleCols = columnsToRegister
.filter((col) => col.visible !== false)
.map((col) => col.columnName || col.field);
const newFrozenColumns = [...checkboxColumn, ...visibleCols.slice(0, count)];
setFrozenColumns(newFrozenColumns);
},
// 탭 관련 정보 (탭 내부의 테이블인 경우)
parentTabId,
parentTabsComponentId,
@ -2879,6 +2896,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortDirection,
groupByColumns,
frozenColumns,
frozenColumnCount, // 틀고정 컬럼 수 저장
showGridLines,
headerFilters: Object.fromEntries(
Object.entries(headerFilters).map(([key, set]) => [key, Array.from(set as Set<string>)]),
@ -2900,6 +2918,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
sortDirection,
groupByColumns,
frozenColumns,
frozenColumnCount,
showGridLines,
headerFilters,
localPageSize,
@ -2920,7 +2939,15 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (state.sortColumn !== undefined) setSortColumn(state.sortColumn);
if (state.sortDirection) setSortDirection(state.sortDirection);
if (state.groupByColumns) setGroupByColumns(state.groupByColumns);
if (state.frozenColumns) setFrozenColumns(state.frozenColumns);
if (state.frozenColumns) {
// 체크박스 컬럼이 항상 포함되도록 보장
const checkboxColumn = (tableConfig.checkbox?.enabled ?? true) ? "__checkbox__" : null;
const restoredFrozenColumns = checkboxColumn && !state.frozenColumns.includes(checkboxColumn)
? [checkboxColumn, ...state.frozenColumns]
: state.frozenColumns;
setFrozenColumns(restoredFrozenColumns);
}
if (state.frozenColumnCount !== undefined) setFrozenColumnCount(state.frozenColumnCount); // 틀고정 컬럼 수 복원
if (state.showGridLines !== undefined) setShowGridLines(state.showGridLines);
if (state.headerFilters) {
const filters: Record<string, Set<string>> = {};
@ -5590,7 +5617,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (isFrozen && frozenIndex > 0) {
for (let i = 0; i < frozenIndex; i++) {
const frozenCol = frozenColumns[i];
const frozenColWidth = columnWidths[frozenCol] || 150;
// 체크박스 컬럼은 48px 고정
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150);
leftPosition += frozenColWidth;
}
}
@ -5609,7 +5637,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
column.sortable !== false &&
column.columnName !== "__checkbox__" &&
"hover:bg-muted/70 cursor-pointer transition-colors",
isFrozen && "sticky z-60 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
isFrozen && "sticky z-40 shadow-[2px_0_4px_rgba(0,0,0,0.1)]",
// 🆕 Column Reordering 스타일
isColumnDragEnabled &&
column.columnName !== "__checkbox__" &&
@ -5901,7 +5929,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (isFrozen && frozenIndex > 0) {
for (let i = 0; i < frozenIndex; i++) {
const frozenCol = frozenColumns[i];
const frozenColWidth = columnWidths[frozenCol] || 150;
// 체크박스 컬럼은 48px 고정
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150);
leftPosition += frozenColWidth;
}
}
@ -5914,7 +5943,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
column.columnName === "__checkbox__"
? "px-0 py-1"
: "px-2 py-1 sm:px-4 sm:py-1.5",
isFrozen && "bg-background sticky z-10 shadow-[2px_0_4px_rgba(0,0,0,0.05)]",
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
)}
style={{
textAlign:
@ -5929,7 +5958,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
: `${100 / visibleColumns.length}%`,
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
...(isFrozen && { left: `${leftPosition}px` }),
...(isFrozen && {
left: `${leftPosition}px`,
backgroundColor: "hsl(var(--background))",
}),
}}
>
{column.columnName === "__checkbox__"
@ -6061,7 +6093,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (isFrozen && frozenIndex > 0) {
for (let i = 0; i < frozenIndex; i++) {
const frozenCol = frozenColumns[i];
const frozenColWidth = columnWidths[frozenCol] || 150;
// 체크박스 컬럼은 48px 고정
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150);
leftPosition += frozenColWidth;
}
}
@ -6074,7 +6107,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
className={cn(
"text-foreground overflow-hidden text-xs font-normal text-ellipsis whitespace-nowrap sm:text-sm",
column.columnName === "__checkbox__" ? "px-0 py-1" : "px-2 py-1 sm:px-4 sm:py-1.5",
isFrozen && "bg-background sticky z-10 shadow-[2px_0_4px_rgba(0,0,0,0.05)]",
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
// 🆕 포커스된 셀 스타일
isCellFocused && !editingCell && "ring-primary bg-primary/5 ring-2 ring-inset",
// 🆕 편집 중인 셀 스타일
@ -6101,7 +6134,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
column.columnName === "__checkbox__" ? "48px" : `${100 / visibleColumns.length}%`,
minWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
maxWidth: column.columnName === "__checkbox__" ? "48px" : undefined,
...(isFrozen && { left: `${leftPosition}px` }),
...(isFrozen && {
left: `${leftPosition}px`,
backgroundColor: "hsl(var(--background))",
}),
}}
onClick={(e) => handleCellClick(index, colIndex, e)}
onDoubleClick={() =>
@ -6222,7 +6258,8 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
if (isFrozen && frozenIndex > 0) {
for (let i = 0; i < frozenIndex; i++) {
const frozenCol = frozenColumns[i];
const frozenColWidth = columnWidths[frozenCol] || 150;
// 체크박스 컬럼은 48px 고정
const frozenColWidth = frozenCol === "__checkbox__" ? 48 : (columnWidths[frozenCol] || 150);
leftPosition += frozenColWidth;
}
}
@ -6237,7 +6274,7 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
className={cn(
"text-foreground text-xs font-semibold sm:text-sm",
column.columnName === "__checkbox__" ? "px-0 py-2" : "px-2 py-2 sm:px-4",
isFrozen && "bg-muted/80 sticky z-10 shadow-[2px_0_4px_rgba(0,0,0,0.05)]",
isFrozen && "sticky z-20 shadow-[2px_0_4px_rgba(0,0,0,0.08)]",
)}
style={{
textAlign: isNumeric ? "right" : column.align || "left",
@ -6247,7 +6284,10 @@ export const TableListComponent: React.FC<TableListComponentProps> = ({
: columnWidth
? `${columnWidth}px`
: undefined,
...(isFrozen && { left: `${leftPosition}px` }),
...(isFrozen && {
left: `${leftPosition}px`,
backgroundColor: "hsl(var(--muted) / 0.8)",
}),
}}
>
{summary ? (

View File

@ -66,6 +66,10 @@ export interface TableRegistration {
onGroupChange: (groups: string[]) => void;
onColumnVisibilityChange: (columns: ColumnVisibility[]) => void;
onGroupSumChange?: (config: GroupSumConfig | null) => void; // 그룹별 합산 설정 변경
onFrozenColumnCountChange?: (count: number) => void; // 틀고정 컬럼 수 변경
// 현재 설정 값 (읽기 전용)
frozenColumnCount?: number; // 현재 틀고정 컬럼 수
// 데이터 조회 함수 (선택 타입 필터용)
getColumnUniqueValues?: (columnName: string) => Promise<Array<{ label: string; value: string }>>;