diff --git a/frontend/components/screen/widgets/FlowWidget.tsx b/frontend/components/screen/widgets/FlowWidget.tsx index 9cb0a207..4bf23ba4 100644 --- a/frontend/components/screen/widgets/FlowWidget.tsx +++ b/frontend/components/screen/widgets/FlowWidget.tsx @@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { FlowComponent } from "@/types/screen-management"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { AlertCircle, Loader2, ChevronUp, Filter, X } from "lucide-react"; +import { AlertCircle, Loader2, ChevronUp, Filter, X, Layers, ChevronDown, ChevronRight } from "lucide-react"; import { getFlowById, getAllStepCounts, @@ -40,6 +40,14 @@ import { Input } from "@/components/ui/input"; import { useScreenPreview } from "@/contexts/ScreenPreviewContext"; import { useAuth } from "@/hooks/useAuth"; +// 그룹화된 데이터 인터페이스 +interface GroupedData { + groupKey: string; + groupValues: Record; + items: any[]; + count: number; +} + interface FlowWidgetProps { component: FlowComponent; onStepClick?: (stepId: number, stepName: string) => void; @@ -106,6 +114,11 @@ export function FlowWidget({ const [allAvailableColumns, setAllAvailableColumns] = useState([]); // 전체 컬럼 목록 const [filteredData, setFilteredData] = useState([]); // 필터링된 데이터 + // 🆕 그룹 설정 관련 상태 + const [isGroupSettingOpen, setIsGroupSettingOpen] = useState(false); // 그룹 설정 다이얼로그 + const [groupByColumns, setGroupByColumns] = useState([]); // 그룹화할 컬럼 목록 + const [collapsedGroups, setCollapsedGroups] = useState>(new Set()); // 접힌 그룹 + /** * 🆕 컬럼 표시 결정 함수 * 1순위: 플로우 스텝 기본 설정 (displayConfig) @@ -163,43 +176,30 @@ export function FlowWidget({ // 초기값: 빈 필터 (사용자가 선택해야 함) setSearchFilterColumns(new Set()); } - - // 이전 사용자의 필터 설정 정리 (사용자 ID가 다른 키들 제거) - if (typeof window !== "undefined") { - const currentUserId = user.userId; - const keysToRemove: string[] = []; - - // localStorage의 모든 키를 확인 - for (let i = 0; i < localStorage.length; i++) { - const key = localStorage.key(i); - if (key && key.startsWith("flowWidget_searchFilters_")) { - // 키 형식: flowWidget_searchFilters_${userId}_${flowId}_${stepId} - // split("_")를 하면 ["flowWidget", "searchFilters", "사용자ID", "플로우ID", "스텝ID"] - // 따라서 userId는 parts[2]입니다 - const parts = key.split("_"); - if (parts.length >= 3) { - const userIdFromKey = parts[2]; // flowWidget_searchFilters_ 다음이 userId - // 현재 사용자 ID와 다른 사용자의 설정은 제거 - if (userIdFromKey !== currentUserId) { - keysToRemove.push(key); - } - } - } - } - - // 이전 사용자의 설정 제거 - if (keysToRemove.length > 0) { - keysToRemove.forEach(key => { - localStorage.removeItem(key); - }); - } - } } catch (error) { console.error("필터 설정 불러오기 실패:", error); setSearchFilterColumns(new Set()); } }, [filterSettingKey, stepDataColumns, user?.userId]); + // 🆕 저장된 그룹 설정 불러오기 + useEffect(() => { + if (!groupSettingKey || stepDataColumns.length === 0) return; + + try { + const saved = localStorage.getItem(groupSettingKey); + if (saved) { + const savedGroups = JSON.parse(saved); + // 현재 단계에 표시되는 컬럼만 필터링 + const validGroups = savedGroups.filter((col: string) => stepDataColumns.includes(col)); + setGroupByColumns(validGroups); + } + } catch (error) { + console.error("그룹 설정 불러오기 실패:", error); + setGroupByColumns([]); + } + }, [groupSettingKey, stepDataColumns]); + // 🆕 필터 설정 저장 const saveFilterSettings = useCallback(() => { if (!filterSettingKey) return; @@ -247,6 +247,98 @@ export function FlowWidget({ setFilteredData([]); }, []); + // 🆕 그룹 설정 localStorage 키 생성 + const groupSettingKey = useMemo(() => { + if (!selectedStep) return null; + return `flowWidget_groupSettings_step_${selectedStep}`; + }, [selectedStep]); + + // 🆕 그룹 설정 저장 + const saveGroupSettings = useCallback(() => { + if (!groupSettingKey) return; + + try { + localStorage.setItem(groupSettingKey, JSON.stringify(groupByColumns)); + setIsGroupSettingOpen(false); + toast.success("그룹 설정이 저장되었습니다"); + } catch (error) { + console.error("그룹 설정 저장 실패:", error); + toast.error("설정 저장에 실패했습니다"); + } + }, [groupSettingKey, groupByColumns]); + + // 🆕 그룹 컬럼 토글 + const toggleGroupColumn = useCallback((columnName: string) => { + setGroupByColumns((prev) => { + if (prev.includes(columnName)) { + return prev.filter((col) => col !== columnName); + } else { + return [...prev, columnName]; + } + }); + }, []); + + // 🆕 그룹 펼치기/접기 토글 + const toggleGroupCollapse = useCallback((groupKey: string) => { + setCollapsedGroups((prev) => { + const newSet = new Set(prev); + if (newSet.has(groupKey)) { + newSet.delete(groupKey); + } else { + newSet.add(groupKey); + } + return newSet; + }); + }, []); + + // 🆕 그룹 해제 + const clearGrouping = useCallback(() => { + setGroupByColumns([]); + setCollapsedGroups(new Set()); + if (groupSettingKey) { + localStorage.removeItem(groupSettingKey); + } + toast.success("그룹이 해제되었습니다"); + }, [groupSettingKey]); + + // 🆕 데이터 그룹화 + const groupedData = useMemo((): GroupedData[] => { + const dataToGroup = filteredData.length > 0 ? filteredData : stepData; + + if (groupByColumns.length === 0 || dataToGroup.length === 0) return []; + + const grouped = new Map(); + + dataToGroup.forEach((item) => { + // 그룹 키 생성: "통화:KRW > 단위:EA" + const keyParts = groupByColumns.map((col) => { + const value = item[col]; + const label = columnLabels[col] || col; + return `${label}:${value !== null && value !== undefined ? value : "-"}`; + }); + const groupKey = keyParts.join(" > "); + + if (!grouped.has(groupKey)) { + grouped.set(groupKey, []); + } + grouped.get(groupKey)!.push(item); + }); + + return Array.from(grouped.entries()).map(([groupKey, items]) => { + const groupValues: Record = {}; + groupByColumns.forEach((col) => { + groupValues[col] = items[0]?.[col]; + }); + + return { + groupKey, + groupValues, + items, + count: items.length, + }; + }); + }, [filteredData, stepData, groupByColumns, columnLabels]); + // 🆕 검색 값이 변경될 때마다 자동 검색 (useEffect로 직접 처리) useEffect(() => { if (!stepData || stepData.length === 0) { @@ -796,29 +888,82 @@ export function FlowWidget({ {/* 🆕 필터 설정 버튼 */} {stepDataColumns.length > 0 && ( - +
+ + +
)} + {/* 🆕 그룹 표시 배지 */} + {groupByColumns.length > 0 && ( +
+
+ 그룹: +
+ {groupByColumns.map((col, idx) => ( + + {idx > 0 && } + + {columnLabels[col] || col} + + + ))} +
+ +
+
+ )} + {/* 🆕 검색 필터 입력 영역 */} {searchFilterColumns.size > 0 && (
@@ -940,29 +1085,87 @@ export function FlowWidget({ - {paginatedStepData.map((row, pageIndex) => { - const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; - return ( - - {allowDataMove && ( - - toggleRowSelection(actualIndex)} - /> + {groupByColumns.length > 0 && groupedData.length > 0 ? ( + // 그룹화된 렌더링 + groupedData.flatMap((group) => { + const isCollapsed = collapsedGroups.has(group.groupKey); + const groupRows = [ + + +
toggleGroupCollapse(group.groupKey)} + > + {isCollapsed ? ( + + ) : ( + + )} + {group.groupKey} + ({group.count}건) +
- )} - {stepDataColumns.map((col) => ( - - {formatValue(row[col])} - - ))} -
- ); - })} +
, + ]; + + if (!isCollapsed) { + const dataRows = group.items.map((row, itemIndex) => { + const actualIndex = displayData.indexOf(row); + return ( + + {allowDataMove && ( + + toggleRowSelection(actualIndex)} + /> + + )} + {stepDataColumns.map((col) => ( + + {formatValue(row[col])} + + ))} + + ); + }); + groupRows.push(...dataRows); + } + + return groupRows; + }) + ) : ( + // 일반 렌더링 (그룹 없음) + paginatedStepData.map((row, pageIndex) => { + const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; + return ( + + {allowDataMove && ( + + toggleRowSelection(actualIndex)} + /> + + )} + {stepDataColumns.map((col) => ( + + {formatValue(row[col])} + + ))} + + ); + }) + )}
@@ -1162,6 +1365,63 @@ export function FlowWidget({ + + {/* 🆕 그룹 설정 다이얼로그 */} + + + + 그룹 설정 + + 데이터를 그룹화할 컬럼을 선택하세요. 여러 컬럼을 선택하면 계층적으로 그룹화됩니다. + + + +
+ {/* 컬럼 목록 */} +
+ {stepDataColumns.map((col) => ( +
+ toggleGroupColumn(col)} + /> + +
+ ))} +
+ + {/* 선택된 그룹 안내 */} +
+ {groupByColumns.length === 0 ? ( + 그룹화할 컬럼을 선택하세요 + ) : ( + + 선택된 그룹:{" "} + + {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} + + + )} +
+
+ + + + + +
+
); }