feat: Enhance EDataTable with group collapsing functionality
- Added support for collapsing and expanding grouped rows in the EDataTable component. - Implemented a toggle mechanism for group headers, allowing users to hide or show group details. - Updated the data processing logic to filter out collapsed group rows, improving data visibility and organization. These changes aim to enhance the user experience by providing a more structured view of grouped data within the table.
This commit is contained in:
@@ -20,7 +20,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Filter, Check, Search, X, Loader2, Inbox, GripVertical,
|
||||
ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight, ArrowUp, ArrowDown,
|
||||
ChevronLeft, ChevronRight, ChevronDown, ChevronsLeft, ChevronsRight, ArrowUp, ArrowDown,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "sonner";
|
||||
@@ -290,6 +290,16 @@ export function EDataTable<T extends Record<string, any> = any>({
|
||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||
const [pageSizeInput, setPageSizeInput] = useState(String(defaultPageSize));
|
||||
|
||||
// 그룹 접기/펼치기
|
||||
const [collapsedGroups, setCollapsedGroups] = useState<Set<string>>(new Set());
|
||||
const toggleGroup = (groupValue: string) => {
|
||||
setCollapsedGroups((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupValue)) next.delete(groupValue); else next.add(groupValue);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// 인라인 편집
|
||||
const [editingCell, setEditingCell] = useState<{ rowId: string; colKey: string } | null>(null);
|
||||
const [editValue, setEditValue] = useState("");
|
||||
@@ -428,10 +438,27 @@ export function EDataTable<T extends Record<string, any> = any>({
|
||||
}, [currentPage, totalPages]);
|
||||
|
||||
const pageOffset = (safePage - 1) * pageSize;
|
||||
const paginatedData = showPagination
|
||||
const paginatedDataRaw = showPagination
|
||||
? processedData.slice(pageOffset, pageOffset + pageSize)
|
||||
: processedData;
|
||||
|
||||
// 접힌 그룹의 데이터 행 숨김
|
||||
const paginatedData = useMemo(() => {
|
||||
if (collapsedGroups.size === 0) return paginatedDataRaw;
|
||||
let currentGroup: string | null = null;
|
||||
return paginatedDataRaw.filter((row) => {
|
||||
if ((row as any)._isGroupHeader) {
|
||||
currentGroup = (row as any)._groupValue;
|
||||
return true; // 헤더는 항상 표시
|
||||
}
|
||||
if ((row as any)._isGroupSummary) {
|
||||
return !collapsedGroups.has((row as any)._groupValue);
|
||||
}
|
||||
// 일반 행: 현재 그룹이 접혀있으면 숨김
|
||||
return !currentGroup || !collapsedGroups.has(currentGroup);
|
||||
});
|
||||
}, [paginatedDataRaw, collapsedGroups]);
|
||||
|
||||
const applyPageSize = () => {
|
||||
const n = parseInt(pageSizeInput, 10);
|
||||
if (!isNaN(n) && n >= 1) {
|
||||
@@ -641,8 +668,29 @@ export function EDataTable<T extends Record<string, any> = any>({
|
||||
</TableRow>
|
||||
) : (
|
||||
paginatedData.map((row, rowIdx) => {
|
||||
// 그룹 헤더 행 처리
|
||||
if ((row as any)._isGroupHeader) {
|
||||
const gv = (row as any)._groupValue || "";
|
||||
const gc = (row as any)._groupCount || 0;
|
||||
const isCollapsed = collapsedGroups.has(gv);
|
||||
const totalCols = columns.length + (showCheckbox ? 1 : 0) + (showRowNumber ? 1 : 0);
|
||||
return (
|
||||
<TableRow key={`group-${gv}-${rowIdx}`} className="bg-primary/5 hover:bg-primary/10 cursor-pointer border-t-2 border-primary/20" onClick={() => toggleGroup(gv)}>
|
||||
<TableCell colSpan={totalCols} className="py-2 px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{isCollapsed ? <ChevronRight className="h-4 w-4 text-primary" /> : <ChevronDown className="h-4 w-4 text-primary" />}
|
||||
<span className="text-sm font-semibold text-primary">{gv}</span>
|
||||
<Badge variant="secondary" className="text-[10px] bg-primary/10 text-primary">{gc}건</Badge>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
// 그룹 소계 행 처리
|
||||
if ((row as any)._isGroupSummary) {
|
||||
const gv = (row as any)._groupValue || "";
|
||||
if (collapsedGroups.has(gv)) return null;
|
||||
return (
|
||||
<TableRow key={`summary-${rowIdx}`} className="bg-muted/60 font-semibold border-t border-primary/20">
|
||||
{showCheckbox && <TableCell />}
|
||||
|
||||
@@ -161,35 +161,39 @@ export function useTableSettings<T extends { key: string }>(
|
||||
* 소계 행은 _isGroupSummary: true, _groupKey, _groupValue 속성을 가집니다.
|
||||
*/
|
||||
const groupData = useCallback(
|
||||
<R extends Record<string, any>>(rows: R[]): (R & { _isGroupSummary?: boolean; _groupKey?: string; _groupValue?: string })[] => {
|
||||
<R extends Record<string, any>>(rows: R[]): (R & { _isGroupSummary?: boolean; _isGroupHeader?: boolean; _groupKey?: string; _groupValue?: string; _groupCount?: number })[] => {
|
||||
if (groupColumns.length === 0) return rows;
|
||||
|
||||
const groupCol = groupColumns[0]; // 첫 번째 그룹 컬럼 기준
|
||||
// 다중 그룹 컬럼 결합 키
|
||||
const makeKey = (row: R) => groupColumns.map((col) => String(row[col] ?? "(빈 값)")).join(" / ");
|
||||
const groups = new Map<string, R[]>();
|
||||
|
||||
for (const row of rows) {
|
||||
const key = String(row[groupCol] ?? "(빈 값)");
|
||||
const key = makeKey(row);
|
||||
if (!groups.has(key)) groups.set(key, []);
|
||||
groups.get(key)!.push(row);
|
||||
}
|
||||
|
||||
const result: (R & { _isGroupSummary?: boolean; _groupKey?: string; _groupValue?: string })[] = [];
|
||||
const result: (R & { _isGroupSummary?: boolean; _isGroupHeader?: boolean; _groupKey?: string; _groupValue?: string; _groupCount?: number })[] = [];
|
||||
|
||||
for (const [groupValue, groupRows] of groups) {
|
||||
// 그룹 헤더 행
|
||||
const headerRow: any = { _isGroupHeader: true, _groupKey: groupColumns.join(","), _groupValue: groupValue, _groupCount: groupRows.length };
|
||||
result.push(headerRow);
|
||||
|
||||
// 그룹 내 데이터 행
|
||||
result.push(...groupRows);
|
||||
|
||||
// 소계 행 (groupSumEnabled일 때만)
|
||||
if (groupSumEnabled) {
|
||||
const summaryRow: any = { _isGroupSummary: true, _groupKey: groupCol, _groupValue: groupValue };
|
||||
// 숫자 컬럼 합산
|
||||
const summaryRow: any = { _isGroupSummary: true, _groupKey: groupColumns.join(","), _groupValue: groupValue };
|
||||
for (const col of defaultColumns) {
|
||||
const values = groupRows.map((r) => Number(r[col.key])).filter((v) => !isNaN(v));
|
||||
if (values.length > 0 && values.some((v) => v !== 0)) {
|
||||
summaryRow[col.key] = values.reduce((a, b) => a + b, 0);
|
||||
}
|
||||
}
|
||||
summaryRow[groupCol] = `${groupValue} 소계 (${groupRows.length}건)`;
|
||||
summaryRow[groupColumns[0]] = `${groupValue} 소계 (${groupRows.length}건)`;
|
||||
result.push(summaryRow);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user