296 lines
10 KiB
TypeScript
296 lines
10 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { useTableOptions } from "@/contexts/TableOptionsContext";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import { Button } from "@/components/ui/button";
|
|
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, Lock } from "lucide-react";
|
|
import { ColumnVisibility } from "@/types/table-options";
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const ColumnVisibilityPanel: React.FC<Props> = ({
|
|
isOpen,
|
|
onClose,
|
|
}) => {
|
|
const { getTable, selectedTableId } = useTableOptions();
|
|
const table = selectedTableId ? getTable(selectedTableId) : undefined;
|
|
|
|
const [localColumns, setLocalColumns] = useState<ColumnVisibility[]>([]);
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
|
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
|
|
|
|
// 테이블 정보 로드
|
|
useEffect(() => {
|
|
if (table) {
|
|
setLocalColumns(
|
|
table.columns.map((col) => ({
|
|
columnName: col.columnName,
|
|
visible: col.visible,
|
|
width: col.width,
|
|
order: 0,
|
|
}))
|
|
);
|
|
// 현재 틀고정 컬럼 수 로드
|
|
setFrozenColumnCount(table.frozenColumnCount ?? 0);
|
|
}
|
|
}, [table]);
|
|
|
|
const handleVisibilityChange = (columnName: string, visible: boolean) => {
|
|
setLocalColumns((prev) =>
|
|
prev.map((col) =>
|
|
col.columnName === columnName ? { ...col, visible } : col
|
|
)
|
|
);
|
|
};
|
|
|
|
const handleWidthChange = (columnName: string, width: number) => {
|
|
setLocalColumns((prev) =>
|
|
prev.map((col) =>
|
|
col.columnName === columnName ? { ...col, width } : col
|
|
)
|
|
);
|
|
};
|
|
|
|
const moveColumn = (fromIndex: number, toIndex: number) => {
|
|
const newColumns = [...localColumns];
|
|
const [movedItem] = newColumns.splice(fromIndex, 1);
|
|
newColumns.splice(toIndex, 0, movedItem);
|
|
setLocalColumns(newColumns);
|
|
};
|
|
|
|
const handleDragStart = (index: number) => {
|
|
setDraggedIndex(index);
|
|
};
|
|
|
|
const handleDragOver = (e: React.DragEvent, index: number) => {
|
|
e.preventDefault();
|
|
if (draggedIndex === null || draggedIndex === index) return;
|
|
moveColumn(draggedIndex, index);
|
|
setDraggedIndex(index);
|
|
};
|
|
|
|
const handleDragEnd = () => {
|
|
setDraggedIndex(null);
|
|
};
|
|
|
|
const handleApply = () => {
|
|
table?.onColumnVisibilityChange(localColumns);
|
|
|
|
// 컬럼 순서 변경 콜백 호출
|
|
if (table?.onColumnOrderChange) {
|
|
const newOrder = localColumns
|
|
.map((col) => col.columnName)
|
|
.filter((name) => name !== "__checkbox__");
|
|
table.onColumnOrderChange(newOrder);
|
|
}
|
|
|
|
// 틀고정 컬럼 수 변경 콜백 호출
|
|
if (table?.onFrozenColumnCountChange) {
|
|
table.onFrozenColumnCountChange(frozenColumnCount);
|
|
}
|
|
|
|
onClose();
|
|
};
|
|
|
|
const handleReset = () => {
|
|
if (table) {
|
|
setLocalColumns(
|
|
table.columns.map((col) => ({
|
|
columnName: col.columnName,
|
|
visible: true,
|
|
width: 150,
|
|
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 (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-base sm:text-lg">
|
|
테이블 옵션
|
|
</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
컬럼 표시/숨기기, 순서 변경, 너비 등을 설정할 수 있습니다. 모든
|
|
테두리를 드래그하여 크기를 조정할 수 있습니다.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
<div className="space-y-3 sm:space-y-4">
|
|
{/* 상태 표시 및 틀고정 설정 */}
|
|
<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"
|
|
onClick={handleReset}
|
|
className="h-7 text-xs"
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 컬럼 리스트 */}
|
|
<ScrollArea className="h-[300px] sm:h-[400px]">
|
|
<div className="space-y-2 pr-4">
|
|
{localColumns.map((col, index) => {
|
|
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}
|
|
draggable
|
|
onDragStart={() => handleDragStart(index)}
|
|
onDragOver={(e) => handleDragOver(e, index)}
|
|
onDragEnd={handleDragEnd}
|
|
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" />
|
|
|
|
{/* 체크박스 */}
|
|
<Checkbox
|
|
checked={col.visible}
|
|
onCheckedChange={(checked) =>
|
|
handleVisibilityChange(
|
|
col.columnName,
|
|
checked as boolean
|
|
)
|
|
}
|
|
/>
|
|
|
|
{/* 가시성/틀고정 아이콘 */}
|
|
{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" />
|
|
)}
|
|
|
|
{/* 컬럼명 */}
|
|
<div className="flex-1">
|
|
<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}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 너비 설정 */}
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-xs text-muted-foreground">
|
|
너비:
|
|
</Label>
|
|
<Input
|
|
type="number"
|
|
value={col.width || 150}
|
|
onChange={(e) =>
|
|
handleWidthChange(
|
|
col.columnName,
|
|
parseInt(e.target.value) || 150
|
|
)
|
|
}
|
|
className="h-7 w-16 text-xs sm:h-8 sm:w-20 sm:text-sm"
|
|
min={50}
|
|
max={500}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
|
|
<DialogFooter className="gap-2 sm:gap-0">
|
|
<Button
|
|
variant="outline"
|
|
onClick={onClose}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
취소
|
|
</Button>
|
|
<Button
|
|
onClick={handleApply}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|