ERP-node/frontend/components/screen/table-options/ColumnVisibilityPanel.tsx

300 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) {
const updatedColumns = localColumns.map((col) => ({
columnName: col.columnName,
visible: col.visible,
}));
table.onFrozenColumnCountChange(frozenColumnCount, updatedColumns);
}
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>
);
};