ERP-node/frontend/lib/registry/components/v2-table-grouped/hooks/useGroupedData.ts

390 lines
11 KiB
TypeScript

"use client";
import { useState, useCallback, useMemo, useEffect } from "react";
import {
TableGroupedConfig,
GroupState,
GroupSummary,
UseGroupedDataResult,
} from "../types";
import { apiClient } from "@/lib/api/client";
/**
* 그룹 요약 데이터 계산
*/
function calculateSummary(
items: any[],
config: TableGroupedConfig
): GroupSummary {
const summary: GroupSummary = {
count: items.length,
};
const summaryConfig = config.groupConfig?.summary;
if (!summaryConfig) return summary;
// 합계 계산
if (summaryConfig.sumColumns && summaryConfig.sumColumns.length > 0) {
summary.sum = {};
for (const col of summaryConfig.sumColumns) {
summary.sum[col] = items.reduce((acc, item) => {
const val = parseFloat(item[col]);
return acc + (isNaN(val) ? 0 : val);
}, 0);
}
}
// 평균 계산
if (summaryConfig.avgColumns && summaryConfig.avgColumns.length > 0) {
summary.avg = {};
for (const col of summaryConfig.avgColumns) {
const validItems = items.filter(
(item) => item[col] !== null && item[col] !== undefined
);
const sum = validItems.reduce((acc, item) => {
const val = parseFloat(item[col]);
return acc + (isNaN(val) ? 0 : val);
}, 0);
summary.avg[col] = validItems.length > 0 ? sum / validItems.length : 0;
}
}
// 최대값 계산
if (summaryConfig.maxColumns && summaryConfig.maxColumns.length > 0) {
summary.max = {};
for (const col of summaryConfig.maxColumns) {
const values = items
.map((item) => parseFloat(item[col]))
.filter((v) => !isNaN(v));
summary.max[col] = values.length > 0 ? Math.max(...values) : 0;
}
}
// 최소값 계산
if (summaryConfig.minColumns && summaryConfig.minColumns.length > 0) {
summary.min = {};
for (const col of summaryConfig.minColumns) {
const values = items
.map((item) => parseFloat(item[col]))
.filter((v) => !isNaN(v));
summary.min[col] = values.length > 0 ? Math.min(...values) : 0;
}
}
return summary;
}
/**
* 그룹 라벨 포맷팅
*/
function formatGroupLabel(
groupValue: any,
item: any,
format?: string
): string {
if (!format) {
return String(groupValue ?? "(빈 값)");
}
// {value}를 그룹 값으로 치환
let label = format.replace("{value}", String(groupValue ?? "(빈 값)"));
// {컬럼명} 패턴을 해당 컬럼 값으로 치환
const columnPattern = /\{([^}]+)\}/g;
label = label.replace(columnPattern, (match, columnName) => {
if (columnName === "value") return String(groupValue ?? "");
return String(item?.[columnName] ?? "");
});
return label;
}
/**
* 데이터를 그룹화하는 훅
*/
export function useGroupedData(
config: TableGroupedConfig,
externalData?: any[],
searchFilters?: Record<string, any>
): UseGroupedDataResult {
// 원본 데이터
const [rawData, setRawData] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
// 그룹 펼침 상태 관리
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
// 사용자가 수동으로 펼침/접기를 조작했는지 여부
const [isManuallyControlled, setIsManuallyControlled] = useState(false);
// 선택 상태 관리
const [selectedItemIds, setSelectedItemIds] = useState<Set<string>>(
new Set()
);
// 테이블명 결정
const tableName = config.useCustomTable
? config.customTableName
: config.selectedTable;
// 데이터 로드
const fetchData = useCallback(async () => {
if (externalData) {
setRawData(externalData);
return;
}
if (!tableName) {
setRawData([]);
return;
}
setIsLoading(true);
setError(null);
try {
const response = await apiClient.post(
`/table-management/tables/${tableName}/data`,
{
page: 1,
size: 10000, // 그룹화를 위해 전체 데이터 로드
autoFilter: true,
search: searchFilters || {},
}
);
const responseData = response.data?.data?.data || response.data?.data || [];
setRawData(Array.isArray(responseData) ? responseData : []);
} catch (err: any) {
setError(err.message || "데이터 로드 중 오류 발생");
setRawData([]);
} finally {
setIsLoading(false);
}
}, [tableName, externalData, searchFilters]);
// 초기 데이터 로드
useEffect(() => {
fetchData();
}, [fetchData]);
// 외부 데이터 변경 시 동기화
useEffect(() => {
if (externalData) {
setRawData(externalData);
}
}, [externalData]);
// 그룹화된 데이터 계산
const groups = useMemo((): GroupState[] => {
const groupByColumn = config.groupConfig?.groupByColumn;
if (!groupByColumn || rawData.length === 0) {
return [];
}
// 데이터를 그룹별로 분류
const groupMap = new Map<string, any[]>();
for (const item of rawData) {
const groupValue = item[groupByColumn];
const groupKey = String(groupValue ?? "__null__");
if (!groupMap.has(groupKey)) {
groupMap.set(groupKey, []);
}
groupMap.get(groupKey)!.push(item);
}
// 그룹 배열 생성
const groupArray: GroupState[] = [];
const defaultExpanded = config.groupConfig?.defaultExpanded ?? true;
for (const [groupKey, items] of groupMap.entries()) {
const firstItem = items[0];
const groupValue =
groupKey === "__null__" ? null : firstItem[groupByColumn];
// 펼침 상태 결정: 수동 조작 전에는 defaultExpanded, 수동 조작 후에는 expandedGroups 참조
const isExpanded = isManuallyControlled
? expandedGroups.has(groupKey)
: defaultExpanded;
groupArray.push({
groupKey,
groupLabel: formatGroupLabel(
groupValue,
firstItem,
config.groupConfig?.groupLabelFormat
),
expanded: isExpanded,
items,
summary: calculateSummary(items, config),
selected: items.every((item) =>
selectedItemIds.has(getItemId(item, config))
),
selectedItemIds: items
.filter((item) => selectedItemIds.has(getItemId(item, config)))
.map((item) => getItemId(item, config)),
});
}
// 정렬
const sortDirection = config.groupConfig?.sortDirection ?? "asc";
groupArray.sort((a, b) => {
const comparison = a.groupLabel.localeCompare(b.groupLabel, "ko");
return sortDirection === "asc" ? comparison : -comparison;
});
return groupArray;
}, [rawData, config, expandedGroups, selectedItemIds, isManuallyControlled]);
// 아이템 ID 추출
function getItemId(item: any, cfg: TableGroupedConfig): string {
// id 또는 첫 번째 컬럼을 ID로 사용
if (item.id !== undefined) return String(item.id);
const firstCol = cfg.columns?.[0]?.columnName;
if (firstCol && item[firstCol] !== undefined) return String(item[firstCol]);
return JSON.stringify(item);
}
// 그룹 토글
const toggleGroup = useCallback((groupKey: string) => {
setIsManuallyControlled(true);
setExpandedGroups((prev) => {
const next = new Set(prev);
if (next.has(groupKey)) {
next.delete(groupKey);
} else {
next.add(groupKey);
}
return next;
});
}, []);
// 전체 펼치기
const expandAll = useCallback(() => {
setIsManuallyControlled(true);
setExpandedGroups(new Set(groups.map((g) => g.groupKey)));
}, [groups]);
// 전체 접기
const collapseAll = useCallback(() => {
setIsManuallyControlled(true);
setExpandedGroups(new Set());
}, []);
// 아이템 선택 토글
const toggleItemSelection = useCallback(
(groupKey: string, itemId: string) => {
setSelectedItemIds((prev) => {
const next = new Set(prev);
if (next.has(itemId)) {
next.delete(itemId);
} else {
// 단일 선택 모드
if (config.checkboxMode === "single") {
next.clear();
}
next.add(itemId);
}
return next;
});
},
[config.checkboxMode]
);
// 그룹 전체 선택 토글
const toggleGroupSelection = useCallback(
(groupKey: string) => {
const group = groups.find((g) => g.groupKey === groupKey);
if (!group) return;
setSelectedItemIds((prev) => {
const next = new Set(prev);
const groupItemIds = group.items.map((item) => getItemId(item, config));
const allSelected = groupItemIds.every((id) => next.has(id));
if (allSelected) {
// 전체 해제
for (const id of groupItemIds) {
next.delete(id);
}
} else {
// 전체 선택
if (config.checkboxMode === "single") {
next.clear();
next.add(groupItemIds[0]);
} else {
for (const id of groupItemIds) {
next.add(id);
}
}
}
return next;
});
},
[groups, config]
);
// 전체 선택 토글
const toggleAllSelection = useCallback(() => {
const allItemIds = rawData.map((item) => getItemId(item, config));
const allSelected = allItemIds.every((id) => selectedItemIds.has(id));
if (allSelected) {
setSelectedItemIds(new Set());
} else {
if (config.checkboxMode === "single" && allItemIds.length > 0) {
setSelectedItemIds(new Set([allItemIds[0]]));
} else {
setSelectedItemIds(new Set(allItemIds));
}
}
}, [rawData, config, selectedItemIds]);
// 선택된 아이템 목록
const selectedItems = useMemo(() => {
return rawData.filter((item) =>
selectedItemIds.has(getItemId(item, config))
);
}, [rawData, selectedItemIds, config]);
// 모두 선택 여부
const isAllSelected = useMemo(() => {
if (rawData.length === 0) return false;
return rawData.every((item) =>
selectedItemIds.has(getItemId(item, config))
);
}, [rawData, selectedItemIds, config]);
// 일부 선택 여부
const isIndeterminate = useMemo(() => {
if (rawData.length === 0) return false;
const selectedCount = rawData.filter((item) =>
selectedItemIds.has(getItemId(item, config))
).length;
return selectedCount > 0 && selectedCount < rawData.length;
}, [rawData, selectedItemIds, config]);
return {
groups,
isLoading,
error,
toggleGroup,
expandAll,
collapseAll,
toggleItemSelection,
toggleGroupSelection,
toggleAllSelection,
selectedItems,
isAllSelected,
isIndeterminate,
refresh: fetchData,
rawData,
totalCount: rawData.length,
groupCount: groups.length,
};
}
export default useGroupedData;