- 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.
239 lines
8.3 KiB
TypeScript
239 lines
8.3 KiB
TypeScript
"use client";
|
|
|
|
/**
|
|
* useTableSettings — 날코딩 페이지용 테이블 설정 훅
|
|
*
|
|
* TableSettingsModal과 함께 사용하여 컬럼 표시/숨김, 순서, 너비를 관리합니다.
|
|
* 설정은 localStorage에 자동 저장/복원됩니다.
|
|
*
|
|
* @example
|
|
* const ts = useTableSettings("item-info", TABLE_NAME, GRID_COLUMNS);
|
|
*
|
|
* // 툴바 버튼
|
|
* <Button variant="ghost" size="sm" onClick={() => ts.setOpen(true)}>
|
|
* <Settings2 className="h-4 w-4" />
|
|
* </Button>
|
|
*
|
|
* // 테이블 헤더 — GRID_COLUMNS 대신 ts.visibleColumns 사용
|
|
* {ts.visibleColumns.map(col => <TableHead key={col.key}>{col.label}</TableHead>)}
|
|
*
|
|
* // 모달 (JSX 하단)
|
|
* <TableSettingsModal
|
|
* open={ts.open}
|
|
* onOpenChange={ts.setOpen}
|
|
* tableName={ts.tableName}
|
|
* settingsId={ts.settingsId}
|
|
* onSave={ts.applySettings}
|
|
* />
|
|
*/
|
|
|
|
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import { loadTableSettings, type TableSettings, type BaseFilter } from "@/components/common/TableSettingsModal";
|
|
|
|
export function useTableSettings<T extends { key: string }>(
|
|
settingsId: string,
|
|
tableName: string,
|
|
defaultColumns: T[],
|
|
/** 초기 표시 컬럼 키 (미지정 시 defaultColumns 전체) */
|
|
initialVisibleKeys?: string[],
|
|
) {
|
|
const [open, setOpen] = useState(false);
|
|
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(
|
|
() => new Set(initialVisibleKeys || defaultColumns.map((c) => c.key)),
|
|
);
|
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
const [orderedKeys, setOrderedKeys] = useState<string[]>(
|
|
() => initialVisibleKeys || defaultColumns.map((c) => c.key),
|
|
);
|
|
const [baseFilter, setBaseFilter] = useState<BaseFilter | undefined>();
|
|
const [groupColumns, setGroupColumns] = useState<string[]>([]);
|
|
const [groupSumEnabled, setGroupSumEnabled] = useState(false);
|
|
|
|
// 초기 filterConfig: GRID_COLUMNS에 있는 컬럼만 필터 가능 (전부 비활성)
|
|
const [filterConfig, setFilterConfig] = useState<TableSettings["filters"]>(
|
|
() =>
|
|
defaultColumns.map((c) => ({
|
|
columnName: c.key,
|
|
displayName: (c as any).label || c.key,
|
|
enabled: false,
|
|
filterType: "text" as const,
|
|
width: 25,
|
|
})),
|
|
);
|
|
|
|
/** TableSettingsModal onSave에 전달할 콜백 */
|
|
const applySettings = useCallback(
|
|
(settings: TableSettings) => {
|
|
const visible = new Set<string>();
|
|
const widths: Record<string, number> = {};
|
|
const order: string[] = [];
|
|
|
|
for (const cs of settings.columns) {
|
|
if (cs.visible) {
|
|
visible.add(cs.columnName);
|
|
widths[cs.columnName] = cs.width;
|
|
order.push(cs.columnName);
|
|
}
|
|
}
|
|
|
|
// settings에 없는 새 컬럼은 초기 표시 목록에 있을 때만 보이도록 추가
|
|
const initKeys = initialVisibleKeys
|
|
? new Set(initialVisibleKeys)
|
|
: new Set(defaultColumns.map((c) => c.key));
|
|
for (const col of defaultColumns) {
|
|
if (!settings.columns.find((c) => c.columnName === col.key) && initKeys.has(col.key)) {
|
|
visible.add(col.key);
|
|
order.push(col.key);
|
|
}
|
|
}
|
|
|
|
setVisibleKeys(visible);
|
|
setColumnWidths(widths);
|
|
setOrderedKeys(order);
|
|
|
|
// 화면에 표시된 컬럼만 필터 가능하도록 제한
|
|
setFilterConfig(
|
|
settings.filters?.filter((f) => visible.has(f.columnName)),
|
|
);
|
|
|
|
// 기본 데이터 필터
|
|
setBaseFilter(settings.baseFilter);
|
|
|
|
// 그룹 설정
|
|
const enabledGroups = (settings.groups || []).filter((g) => g.enabled).map((g) => g.columnName);
|
|
setGroupColumns(enabledGroups);
|
|
setGroupSumEnabled(settings.groupSumEnabled || false);
|
|
},
|
|
[defaultColumns, initialVisibleKeys],
|
|
);
|
|
|
|
// 마운트 시 저장된 설정 복원
|
|
useEffect(() => {
|
|
const saved = loadTableSettings(settingsId);
|
|
if (saved) applySettings(saved);
|
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
/** 설정이 적용된 컬럼 목록 (순서 + 표시 필터 적용) */
|
|
const visibleColumns = useMemo((): T[] => {
|
|
const colMap = new Map(defaultColumns.map((c) => [c.key, c]));
|
|
const result: T[] = [];
|
|
|
|
// 저장된 순서대로
|
|
for (const key of orderedKeys) {
|
|
if (visibleKeys.has(key)) {
|
|
const col = colMap.get(key);
|
|
if (col) result.push(col);
|
|
}
|
|
}
|
|
|
|
// orderedKeys에 없는 컬럼 (새로 추가된 것)
|
|
for (const col of defaultColumns) {
|
|
if (!orderedKeys.includes(col.key) && visibleKeys.has(col.key)) {
|
|
result.push(col);
|
|
}
|
|
}
|
|
|
|
return result.length > 0 ? result : defaultColumns;
|
|
}, [defaultColumns, orderedKeys, visibleKeys]);
|
|
|
|
/** 컬럼 표시 여부 확인 */
|
|
const isVisible = useCallback((key: string) => visibleKeys.has(key), [visibleKeys]);
|
|
|
|
/** 컬럼 너비 가져오기 (설정값 or undefined) */
|
|
const getWidth = useCallback(
|
|
(key: string): number | undefined => columnWidths[key],
|
|
[columnWidths],
|
|
);
|
|
|
|
/** TableHead/TableCell에 적용할 style 객체 (0 = 자동, 값 있으면 고정) */
|
|
const thStyle = useCallback(
|
|
(key: string): React.CSSProperties | undefined => {
|
|
const w = columnWidths[key];
|
|
if (!w || w <= 0) return undefined; // 0이면 브라우저 자동
|
|
return { width: `${w}px`, minWidth: `${w}px`, maxWidth: `${w}px` };
|
|
},
|
|
[columnWidths],
|
|
);
|
|
|
|
/**
|
|
* 데이터를 그룹핑하고 소계 행을 삽입한 배열을 반환합니다.
|
|
* groupColumns가 비어있으면 원본 배열을 그대로 반환합니다.
|
|
* 소계 행은 _isGroupSummary: true, _groupKey, _groupValue 속성을 가집니다.
|
|
*/
|
|
const groupData = useCallback(
|
|
<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 makeKey = (row: R) => groupColumns.map((col) => String(row[col] ?? "(빈 값)")).join(" / ");
|
|
const groups = new Map<string, R[]>();
|
|
|
|
for (const row of rows) {
|
|
const key = makeKey(row);
|
|
if (!groups.has(key)) groups.set(key, []);
|
|
groups.get(key)!.push(row);
|
|
}
|
|
|
|
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: 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[groupColumns[0]] = `${groupValue} 소계 (${groupRows.length}건)`;
|
|
result.push(summaryRow);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
},
|
|
[groupColumns, groupSumEnabled, defaultColumns],
|
|
);
|
|
|
|
return {
|
|
/** 모달 open 상태 */
|
|
open,
|
|
/** 모달 open 상태 setter */
|
|
setOpen,
|
|
/** web-types API 호출용 테이블명 */
|
|
tableName,
|
|
/** localStorage 키 */
|
|
settingsId,
|
|
/** TableSettingsModal onSave 콜백 */
|
|
applySettings,
|
|
/** 설정 적용된 컬럼 배열 (순서 + 표시 필터) */
|
|
visibleColumns,
|
|
/** 특정 컬럼 표시 여부 */
|
|
isVisible,
|
|
/** 특정 컬럼 너비 (px) */
|
|
getWidth,
|
|
/** TableHead/TableCell style 객체 반환 */
|
|
thStyle,
|
|
/** 필터 설정 */
|
|
filterConfig,
|
|
/** 기본 데이터 필터 (예: division = '판매') */
|
|
baseFilter,
|
|
/** 데이터 그룹핑 + 소계 삽입 함수 */
|
|
groupData,
|
|
/** 그룹 컬럼 목록 */
|
|
groupColumns,
|
|
/** 그룹별 합산 활성 여부 */
|
|
groupSumEnabled,
|
|
/** GRID_COLUMNS 기본 컬럼 키 목록 (TableSettingsModal defaultVisibleKeys용) */
|
|
defaultVisibleKeys: initialVisibleKeys || defaultColumns.map((c) => c.key),
|
|
};
|
|
}
|