- 카테고리 값 관리 컴포넌트에 체크박스를 통한 다중 선택 기능을 추가하였습니다. - 선택된 카테고리를 일괄 삭제할 수 있는 다이얼로그를 구현하였습니다. - 테이블 관리 서비스에서 다중 선택 처리 로직을 추가하여, 파이프(|)로 구분된 값을 처리하도록 개선하였습니다. - 관련된 로그 메시지를 추가하여 다중 선택 및 삭제 과정에서의 정보를 기록하도록 하였습니다.
669 lines
27 KiB
TypeScript
669 lines
27 KiB
TypeScript
"use client";
|
|
|
|
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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { GripVertical, Eye, EyeOff, Lock, ArrowRight, X, Settings, Filter, Layers } from "lucide-react";
|
|
import { ColumnVisibility, TableFilter, GroupSumConfig } from "@/types/table-options";
|
|
|
|
interface Props {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onFiltersApplied?: (filters: TableFilter[]) => void;
|
|
screenId?: number;
|
|
}
|
|
|
|
// 컬럼 필터 설정 인터페이스
|
|
interface ColumnFilterConfig {
|
|
columnName: string;
|
|
columnLabel: string;
|
|
inputType: string;
|
|
enabled: boolean;
|
|
filterType: "text" | "number" | "date" | "select";
|
|
width?: number;
|
|
selectOptions?: Array<{ label: string; value: string }>;
|
|
}
|
|
|
|
export const TableSettingsModal: React.FC<Props> = ({ isOpen, onClose, onFiltersApplied, screenId }) => {
|
|
const { getTable, selectedTableId } = useTableOptions();
|
|
const table = selectedTableId ? getTable(selectedTableId) : undefined;
|
|
|
|
const [activeTab, setActiveTab] = useState("columns");
|
|
|
|
// 컬럼 가시성 상태
|
|
const [localColumns, setLocalColumns] = useState<ColumnVisibility[]>([]);
|
|
const [draggedColumnIndex, setDraggedColumnIndex] = useState<number | null>(null);
|
|
const [frozenColumnCount, setFrozenColumnCount] = useState<number>(0);
|
|
|
|
// 필터 상태
|
|
const [columnFilters, setColumnFilters] = useState<ColumnFilterConfig[]>([]);
|
|
const [selectAllFilters, setSelectAllFilters] = useState(false);
|
|
const [groupSumEnabled, setGroupSumEnabled] = useState(false);
|
|
const [groupByColumn, setGroupByColumn] = useState<string>("");
|
|
|
|
// 그룹화 상태
|
|
const [selectedGroupColumns, setSelectedGroupColumns] = useState<string[]>([]);
|
|
const [draggedGroupIndex, setDraggedGroupIndex] = useState<number | null>(null);
|
|
|
|
// 테이블 정보 로드 - 컬럼 가시성
|
|
useEffect(() => {
|
|
if (table) {
|
|
setLocalColumns(
|
|
table.columns.map((col) => ({
|
|
columnName: col.columnName,
|
|
visible: col.visible,
|
|
width: col.width,
|
|
order: 0,
|
|
}))
|
|
);
|
|
setFrozenColumnCount(table.frozenColumnCount ?? 0);
|
|
}
|
|
}, [table]);
|
|
|
|
// 테이블 정보 로드 - 필터
|
|
useEffect(() => {
|
|
if (table?.columns && table?.tableName) {
|
|
const storageKey = screenId
|
|
? `table_filters_${table.tableName}_screen_${screenId}`
|
|
: `table_filters_${table.tableName}`;
|
|
const savedFilters = localStorage.getItem(storageKey);
|
|
|
|
const groupSumKey = screenId
|
|
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
|
: `table_groupsum_${table.tableName}`;
|
|
const savedGroupSum = localStorage.getItem(groupSumKey);
|
|
|
|
if (savedGroupSum) {
|
|
try {
|
|
const parsed = JSON.parse(savedGroupSum) as GroupSumConfig;
|
|
setGroupSumEnabled(parsed.enabled);
|
|
setGroupByColumn(parsed.groupByColumn || "");
|
|
} catch {
|
|
setGroupSumEnabled(false);
|
|
setGroupByColumn("");
|
|
}
|
|
}
|
|
|
|
if (savedFilters) {
|
|
try {
|
|
const parsed = JSON.parse(savedFilters);
|
|
setColumnFilters(parsed);
|
|
setSelectAllFilters(parsed.every((f: ColumnFilterConfig) => f.enabled));
|
|
} catch {
|
|
initializeFilters();
|
|
}
|
|
} else {
|
|
initializeFilters();
|
|
}
|
|
}
|
|
}, [table?.columns, table?.tableName, screenId]);
|
|
|
|
const initializeFilters = () => {
|
|
if (!table?.columns) return;
|
|
|
|
const filters: ColumnFilterConfig[] = table.columns
|
|
.filter((col) => col.columnName !== "__checkbox__")
|
|
.map((col) => {
|
|
let filterType: "text" | "number" | "date" | "select" = "text";
|
|
const inputType = col.inputType || "";
|
|
|
|
if (["number", "decimal", "currency", "integer"].includes(inputType)) {
|
|
filterType = "number";
|
|
} else if (["date", "datetime", "time"].includes(inputType)) {
|
|
filterType = "date";
|
|
} else if (["select", "dropdown", "code", "category", "entity"].includes(inputType)) {
|
|
filterType = "select";
|
|
}
|
|
|
|
return {
|
|
columnName: col.columnName,
|
|
columnLabel: col.columnLabel,
|
|
inputType,
|
|
enabled: false,
|
|
filterType,
|
|
width: 200,
|
|
};
|
|
});
|
|
|
|
setColumnFilters(filters);
|
|
setSelectAllFilters(false);
|
|
};
|
|
|
|
// 컬럼 가시성 핸들러
|
|
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 handleFilterEnabledChange = (columnName: string, enabled: boolean) => {
|
|
setColumnFilters((prev) =>
|
|
prev.map((f) => (f.columnName === columnName ? { ...f, enabled } : f))
|
|
);
|
|
};
|
|
|
|
const handleFilterTypeChange = (columnName: string, filterType: "text" | "number" | "date" | "select") => {
|
|
setColumnFilters((prev) =>
|
|
prev.map((f) => (f.columnName === columnName ? { ...f, filterType } : f))
|
|
);
|
|
};
|
|
|
|
const handleFilterWidthChange = (columnName: string, width: number) => {
|
|
setColumnFilters((prev) =>
|
|
prev.map((f) => (f.columnName === columnName ? { ...f, width } : f))
|
|
);
|
|
};
|
|
|
|
const handleSelectAll = (checked: boolean) => {
|
|
setSelectAllFilters(checked);
|
|
setColumnFilters((prev) => prev.map((f) => ({ ...f, enabled: checked })));
|
|
};
|
|
|
|
// 그룹화 핸들러
|
|
const toggleGroupColumn = (columnName: string) => {
|
|
if (selectedGroupColumns.includes(columnName)) {
|
|
setSelectedGroupColumns(selectedGroupColumns.filter((c) => c !== columnName));
|
|
} else {
|
|
setSelectedGroupColumns([...selectedGroupColumns, columnName]);
|
|
}
|
|
};
|
|
|
|
const removeGroupColumn = (columnName: string) => {
|
|
setSelectedGroupColumns(selectedGroupColumns.filter((c) => c !== columnName));
|
|
};
|
|
|
|
const moveGroupColumn = (fromIndex: number, toIndex: number) => {
|
|
const newColumns = [...selectedGroupColumns];
|
|
const [movedItem] = newColumns.splice(fromIndex, 1);
|
|
newColumns.splice(toIndex, 0, movedItem);
|
|
setSelectedGroupColumns(newColumns);
|
|
};
|
|
|
|
const clearGrouping = () => {
|
|
setSelectedGroupColumns([]);
|
|
table?.onGroupChange([]);
|
|
};
|
|
|
|
// 틀고정 컬럼 수 변경 핸들러
|
|
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;
|
|
|
|
// 저장
|
|
const handleSave = () => {
|
|
if (!table) return;
|
|
|
|
// 1. 컬럼 가시성 저장
|
|
table.onColumnVisibilityChange(localColumns);
|
|
|
|
// 2. 컬럼 순서 변경 콜백 호출
|
|
if (table.onColumnOrderChange) {
|
|
const newOrder = localColumns
|
|
.map((col) => col.columnName)
|
|
.filter((name) => name !== "__checkbox__");
|
|
table.onColumnOrderChange(newOrder);
|
|
}
|
|
|
|
// 3. 틀고정 컬럼 수 변경 콜백 호출 (현재 컬럼 상태도 함께 전달)
|
|
if (table.onFrozenColumnCountChange) {
|
|
const updatedColumns = localColumns.map((col) => ({
|
|
columnName: col.columnName,
|
|
visible: col.visible,
|
|
}));
|
|
table.onFrozenColumnCountChange(frozenColumnCount, updatedColumns);
|
|
}
|
|
|
|
// 2. 필터 설정 저장
|
|
const storageKey = screenId
|
|
? `table_filters_${table.tableName}_screen_${screenId}`
|
|
: `table_filters_${table.tableName}`;
|
|
localStorage.setItem(storageKey, JSON.stringify(columnFilters));
|
|
|
|
// 그룹별 합산 설정 저장
|
|
const groupSumKey = screenId
|
|
? `table_groupsum_${table.tableName}_screen_${screenId}`
|
|
: `table_groupsum_${table.tableName}`;
|
|
const groupSumConfig: GroupSumConfig = {
|
|
enabled: groupSumEnabled,
|
|
groupByColumn: groupByColumn || undefined,
|
|
};
|
|
localStorage.setItem(groupSumKey, JSON.stringify(groupSumConfig));
|
|
|
|
// 활성화된 필터만 콜백
|
|
const activeFilters: TableFilter[] = columnFilters
|
|
.filter((f) => f.enabled)
|
|
.map((f) => ({
|
|
columnName: f.columnName,
|
|
operator: "contains",
|
|
value: "",
|
|
filterType: f.filterType,
|
|
width: f.width || 200,
|
|
}));
|
|
onFiltersApplied?.(activeFilters);
|
|
|
|
// 3. 그룹화 저장
|
|
table.onGroupChange(selectedGroupColumns);
|
|
|
|
onClose();
|
|
};
|
|
|
|
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>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
|
<TabsList className="grid w-full grid-cols-3">
|
|
<TabsTrigger value="columns" className="gap-1.5 text-xs sm:text-sm">
|
|
<Settings className="h-3.5 w-3.5" />
|
|
컬럼 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="filters" className="gap-1.5 text-xs sm:text-sm">
|
|
<Filter className="h-3.5 w-3.5" />
|
|
필터 설정
|
|
</TabsTrigger>
|
|
<TabsTrigger value="grouping" className="gap-1.5 text-xs sm:text-sm">
|
|
<Layers className="h-3.5 w-3.5" />
|
|
그룹 설정
|
|
</TabsTrigger>
|
|
</TabsList>
|
|
|
|
{/* 컬럼 설정 탭 */}
|
|
<TabsContent value="columns" className="mt-4">
|
|
<div className="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-muted-foreground text-xs sm:text-sm">
|
|
{visibleCount}/{localColumns.length}개 컬럼 표시 중
|
|
</div>
|
|
|
|
{/* 틀고정 설정 */}
|
|
<div className="flex items-center gap-2">
|
|
<Lock className="text-muted-foreground h-4 w-4" />
|
|
<Label className="text-muted-foreground whitespace-nowrap text-xs">
|
|
틀고정:
|
|
</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-muted-foreground whitespace-nowrap text-xs">
|
|
개 컬럼
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (table) {
|
|
setLocalColumns(
|
|
table.columns.map((col) => ({
|
|
columnName: col.columnName,
|
|
visible: true,
|
|
width: 150,
|
|
order: 0,
|
|
}))
|
|
);
|
|
setFrozenColumnCount(0);
|
|
}
|
|
}}
|
|
className="h-7 text-xs"
|
|
>
|
|
초기화
|
|
</Button>
|
|
</div>
|
|
|
|
{/* 컬럼 목록 */}
|
|
<ScrollArea className="h-[300px]">
|
|
<div className="space-y-2 pr-4">
|
|
{localColumns.map((col, index) => {
|
|
const originalCol = table?.columns.find((c) => c.columnName === col.columnName);
|
|
if (!originalCol) return null;
|
|
|
|
// 표시 가능한 컬럼 중 몇 번째인지 계산 (틀고정 표시용)
|
|
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={() => setDraggedColumnIndex(index)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
if (draggedColumnIndex !== null && draggedColumnIndex !== index) {
|
|
moveColumn(draggedColumnIndex, index);
|
|
setDraggedColumnIndex(index);
|
|
}
|
|
}}
|
|
onDragEnd={() => setDraggedColumnIndex(null)}
|
|
className={`flex cursor-move items-center gap-3 rounded-lg border p-3 transition-colors ${
|
|
isFrozen
|
|
? "border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/30"
|
|
: "bg-background hover:bg-muted/50"
|
|
}`}
|
|
>
|
|
{/* 드래그 핸들 */}
|
|
<GripVertical className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
|
|
{/* 체크박스 */}
|
|
<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="text-primary h-4 w-4 shrink-0" />
|
|
) : (
|
|
<EyeOff className="text-muted-foreground h-4 w-4 shrink-0" />
|
|
)}
|
|
|
|
{/* 컬럼명 */}
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2">
|
|
<span className="truncate text-xs font-medium sm:text-sm">
|
|
{originalCol.columnLabel}
|
|
</span>
|
|
{isFrozen && (
|
|
<span className="text-[10px] font-medium text-blue-600 dark:text-blue-400">
|
|
(고정)
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-muted-foreground truncate text-[10px] sm:text-xs">
|
|
{col.columnName}
|
|
</div>
|
|
</div>
|
|
|
|
{/* 너비 설정 */}
|
|
<div className="flex items-center gap-2">
|
|
<Label className="text-muted-foreground text-xs">너비:</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>
|
|
</TabsContent>
|
|
|
|
{/* 필터 설정 탭 */}
|
|
<TabsContent value="filters" className="mt-4">
|
|
<div className="space-y-4">
|
|
{/* 전체 선택 */}
|
|
<div className="flex items-center gap-2">
|
|
<Checkbox
|
|
checked={selectAllFilters}
|
|
onCheckedChange={(checked) => handleSelectAll(checked as boolean)}
|
|
/>
|
|
<Label className="text-xs sm:text-sm">전체 선택</Label>
|
|
</div>
|
|
|
|
{/* 필터 목록 */}
|
|
<ScrollArea className="h-[300px]">
|
|
<div className="space-y-2 pr-4">
|
|
{columnFilters.map((filter) => (
|
|
<div
|
|
key={filter.columnName}
|
|
className="flex items-center gap-2 rounded-lg border bg-background p-2"
|
|
>
|
|
<Checkbox
|
|
checked={filter.enabled}
|
|
onCheckedChange={(checked) =>
|
|
handleFilterEnabledChange(filter.columnName, checked as boolean)
|
|
}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-xs font-medium sm:text-sm">
|
|
{filter.columnLabel}
|
|
</div>
|
|
</div>
|
|
<Select
|
|
value={filter.filterType}
|
|
onValueChange={(v) =>
|
|
handleFilterTypeChange(filter.columnName, v as "text" | "number" | "date" | "select")
|
|
}
|
|
>
|
|
<SelectTrigger className="h-7 w-20 text-xs">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="text">텍스트</SelectItem>
|
|
<SelectItem value="number">숫자</SelectItem>
|
|
<SelectItem value="date">날짜</SelectItem>
|
|
<SelectItem value="select">선택</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<Input
|
|
type="number"
|
|
min={100}
|
|
max={400}
|
|
value={filter.width || 200}
|
|
onChange={(e) =>
|
|
handleFilterWidthChange(filter.columnName, parseInt(e.target.value) || 200)
|
|
}
|
|
className="h-7 w-16 text-center text-xs"
|
|
/>
|
|
<span className="text-muted-foreground text-xs">px</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
|
|
{/* 그룹별 합산 설정 */}
|
|
<div className="rounded-lg border bg-muted/30 p-3">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="text-xs font-medium sm:text-sm">그룹별 합산</div>
|
|
<div className="text-muted-foreground text-[10px] sm:text-xs">
|
|
같은 값끼리 그룹핑하여 합산
|
|
</div>
|
|
</div>
|
|
<Switch checked={groupSumEnabled} onCheckedChange={setGroupSumEnabled} />
|
|
</div>
|
|
{groupSumEnabled && (
|
|
<div className="mt-3">
|
|
<Select value={groupByColumn} onValueChange={setGroupByColumn}>
|
|
<SelectTrigger className="h-8 text-xs sm:text-sm">
|
|
<SelectValue placeholder="그룹화 기준 컬럼 선택" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{columnFilters.map((f) => (
|
|
<SelectItem key={f.columnName} value={f.columnName}>
|
|
{f.columnLabel}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
|
|
{/* 그룹 설정 탭 */}
|
|
<TabsContent value="grouping" className="mt-4">
|
|
<div className="space-y-4">
|
|
{/* 선택된 그룹화 컬럼 */}
|
|
{selectedGroupColumns.length > 0 && (
|
|
<div>
|
|
<div className="mb-2 flex items-center justify-between">
|
|
<div className="text-xs font-medium sm:text-sm">
|
|
그룹화 순서 ({selectedGroupColumns.length}개)
|
|
</div>
|
|
<Button variant="ghost" size="sm" onClick={clearGrouping} className="h-7 text-xs">
|
|
전체 해제
|
|
</Button>
|
|
</div>
|
|
<div className="space-y-2">
|
|
{selectedGroupColumns.map((colName, index) => {
|
|
const col = table?.columns.find((c) => c.columnName === colName);
|
|
if (!col) return null;
|
|
|
|
return (
|
|
<div
|
|
key={colName}
|
|
draggable
|
|
onDragStart={() => setDraggedGroupIndex(index)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
if (draggedGroupIndex !== null && draggedGroupIndex !== index) {
|
|
moveGroupColumn(draggedGroupIndex, index);
|
|
setDraggedGroupIndex(index);
|
|
}
|
|
}}
|
|
onDragEnd={() => setDraggedGroupIndex(null)}
|
|
className="hover:bg-primary/10 bg-primary/5 flex cursor-move items-center gap-2 rounded-lg border p-2 transition-colors"
|
|
>
|
|
<GripVertical className="text-muted-foreground h-4 w-4 flex-shrink-0" />
|
|
<div className="bg-primary text-primary-foreground flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full text-xs">
|
|
{index + 1}
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-xs font-medium sm:text-sm">{col.columnLabel}</div>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => removeGroupColumn(colName)}
|
|
className="h-6 w-6 flex-shrink-0 p-0"
|
|
>
|
|
<X className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* 그룹화 순서 미리보기 */}
|
|
<div className="bg-muted/30 mt-2 rounded-lg border p-2">
|
|
<div className="flex flex-wrap items-center gap-2 text-xs">
|
|
{selectedGroupColumns.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 < selectedGroupColumns.length - 1 && (
|
|
<ArrowRight className="text-muted-foreground h-3 w-3" />
|
|
)}
|
|
</React.Fragment>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* 사용 가능한 컬럼 */}
|
|
<div>
|
|
<div className="mb-2 text-xs font-medium sm:text-sm">사용 가능한 컬럼</div>
|
|
<ScrollArea className={selectedGroupColumns.length > 0 ? "h-[200px]" : "h-[320px]"}>
|
|
<div className="space-y-2 pr-4">
|
|
{table?.columns
|
|
.filter((col) => !selectedGroupColumns.includes(col.columnName))
|
|
.map((col) => (
|
|
<div
|
|
key={col.columnName}
|
|
className="hover:bg-muted/50 flex cursor-pointer items-center gap-3 rounded-lg border bg-background p-2 transition-colors"
|
|
onClick={() => toggleGroupColumn(col.columnName)}
|
|
>
|
|
<Checkbox
|
|
checked={false}
|
|
onCheckedChange={() => toggleGroupColumn(col.columnName)}
|
|
className="flex-shrink-0"
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="truncate text-xs font-medium sm:text-sm">{col.columnLabel}</div>
|
|
<div className="text-muted-foreground truncate text-[10px] sm:text-xs">
|
|
{col.columnName}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</ScrollArea>
|
|
</div>
|
|
</div>
|
|
</TabsContent>
|
|
</Tabs>
|
|
|
|
<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={handleSave} className="h-8 flex-1 text-xs sm:h-10 sm:flex-none sm:text-sm">
|
|
저장
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|
|
|