222 lines
7.8 KiB
TypeScript
222 lines
7.8 KiB
TypeScript
import React, { useState } 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 { ScrollArea } from "@/components/ui/scroll-area";
|
|
import { ArrowRight, GripVertical, X } from "lucide-react";
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
}
|
|
|
|
export const GroupingPanel: React.FC<Props> = ({
|
|
isOpen,
|
|
onClose,
|
|
}) => {
|
|
const { getTable, selectedTableId } = useTableOptions();
|
|
const table = selectedTableId ? getTable(selectedTableId) : undefined;
|
|
|
|
const [selectedColumns, setSelectedColumns] = useState<string[]>([]);
|
|
const [draggedIndex, setDraggedIndex] = useState<number | null>(null);
|
|
|
|
const toggleColumn = (columnName: string) => {
|
|
if (selectedColumns.includes(columnName)) {
|
|
setSelectedColumns(selectedColumns.filter((c) => c !== columnName));
|
|
} else {
|
|
setSelectedColumns([...selectedColumns, columnName]);
|
|
}
|
|
};
|
|
|
|
const removeColumn = (columnName: string) => {
|
|
setSelectedColumns(selectedColumns.filter((c) => c !== columnName));
|
|
};
|
|
|
|
const moveColumn = (fromIndex: number, toIndex: number) => {
|
|
const newColumns = [...selectedColumns];
|
|
const [movedItem] = newColumns.splice(fromIndex, 1);
|
|
newColumns.splice(toIndex, 0, movedItem);
|
|
setSelectedColumns(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 applyGrouping = () => {
|
|
table?.onGroupChange(selectedColumns);
|
|
onClose();
|
|
};
|
|
|
|
const clearGrouping = () => {
|
|
setSelectedColumns([]);
|
|
table?.onGroupChange([]);
|
|
};
|
|
|
|
return (
|
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-xl">
|
|
<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">
|
|
{/* 선택된 컬럼 (드래그 가능) */}
|
|
{selectedColumns.length > 0 && (
|
|
<div>
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<div className="text-xs font-medium sm:text-sm">
|
|
그룹화 순서 ({selectedColumns.length}개)
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={clearGrouping}
|
|
className="h-7 text-xs"
|
|
>
|
|
전체 해제
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{selectedColumns.map((colName, index) => {
|
|
const col = table?.columns.find(
|
|
(c) => c.columnName === colName
|
|
);
|
|
if (!col) return null;
|
|
|
|
return (
|
|
<div
|
|
key={colName}
|
|
draggable
|
|
onDragStart={() => handleDragStart(index)}
|
|
onDragOver={(e) => handleDragOver(e, index)}
|
|
onDragEnd={handleDragEnd}
|
|
className="flex items-center gap-2 rounded-lg border bg-primary/5 p-2 sm:p-3 transition-colors hover:bg-primary/10 cursor-move"
|
|
>
|
|
<GripVertical className="h-4 w-4 text-muted-foreground flex-shrink-0" />
|
|
|
|
<div className="flex h-5 w-5 sm:h-6 sm:w-6 items-center justify-center rounded-full bg-primary text-xs text-primary-foreground flex-shrink-0">
|
|
{index + 1}
|
|
</div>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-xs font-medium sm:text-sm truncate">
|
|
{col.columnLabel}
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeColumn(colName)}
|
|
className="h-6 w-6 p-0 flex-shrink-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 그룹화 순서 미리보기 */}
|
|
<div className="mt-2 rounded-lg border bg-muted/30 p-2">
|
|
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
{selectedColumns.map((colName, index) => {
|
|
const col = table?.columns.find(
|
|
(c) => c.columnName === colName
|
|
);
|
|
return (
|
|
<React.Fragment key={colName}>
|
|
<span className="font-medium">{col?.columnLabel}</span>
|
|
{index < selectedColumns.length - 1 && (
|
|
<ArrowRight className="h-3 w-3 text-muted-foreground" />
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 사용 가능한 컬럼 */}
|
|
<div>
|
|
<div className="mb-2 text-xs font-medium sm:text-sm">
|
|
사용 가능한 컬럼
|
|
</div>
|
|
<ScrollArea className={selectedColumns.length > 0 ? "h-[280px] sm:h-[320px]" : "h-[400px] sm:h-[450px]"}>
|
|
<div className="space-y-2 pr-4">
|
|
{table?.columns
|
|
.filter((col) => !selectedColumns.includes(col.columnName))
|
|
.map((col) => {
|
|
return (
|
|
<div
|
|
key={col.columnName}
|
|
className="flex items-center gap-3 rounded-lg border bg-background p-2 sm:p-3 transition-colors hover:bg-muted/50 cursor-pointer"
|
|
onClick={() => toggleColumn(col.columnName)}
|
|
>
|
|
<Checkbox
|
|
checked={false}
|
|
onCheckedChange={() => toggleColumn(col.columnName)}
|
|
className="flex-shrink-0"
|
|
/>
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-xs font-medium sm:text-sm truncate">
|
|
{col.columnLabel}
|
|
</div>
|
|
<div className="text-[10px] text-muted-foreground sm:text-xs truncate">
|
|
{col.columnName}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</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={applyGrouping}
|
|
className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm"
|
|
>
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|