"use client"; 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, Layers, ChevronDown, ChevronRight } from "lucide-react"; import { getFlowById, getAllStepCounts, getStepDataList, getFlowSteps, getFlowConnections, getStepColumnLabels, } from "@/lib/api/flow"; import type { FlowDefinition, FlowStep } from "@/types/flow"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { toast } from "sonner"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; import { useFlowStepStore } from "@/stores/flowStepStore"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Label } from "@/components/ui/label"; 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; onSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; flowRefreshKey?: number; // 새로고침 키 onFlowRefresh?: () => void; // 새로고침 완료 콜백 } export function FlowWidget({ component, onStepClick, onSelectedDataChange, flowRefreshKey, onFlowRefresh, }: FlowWidgetProps) { const { isPreviewMode } = useScreenPreview(); // 프리뷰 모드 확인 const { user } = useAuth(); // 사용자 정보 가져오기 // 숫자 포맷팅 함수 const formatValue = (value: any): string => { if (value === null || value === undefined || value === "") { return "-"; } // 숫자 타입이거나 숫자로 변환 가능한 문자열인 경우 포맷팅 if (typeof value === "number") { return value.toLocaleString("ko-KR"); } if (typeof value === "string") { const numValue = parseFloat(value); // 숫자로 변환 가능하고, 변환 후 원래 값과 같은 경우에만 포맷팅 if (!isNaN(numValue) && numValue.toString() === value.trim()) { return numValue.toLocaleString("ko-KR"); } } return String(value); }; // 🆕 전역 상태 관리 const setSelectedStep = useFlowStepStore((state) => state.setSelectedStep); const resetFlow = useFlowStepStore((state) => state.resetFlow); const [flowData, setFlowData] = useState(null); const [steps, setSteps] = useState([]); const [stepCounts, setStepCounts] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [connections, setConnections] = useState([]); // 플로우 연결 정보 // 선택된 스텝의 데이터 리스트 상태 const [selectedStepId, setSelectedStepId] = useState(null); const [stepData, setStepData] = useState([]); const [stepDataColumns, setStepDataColumns] = useState([]); const [stepDataLoading, setStepDataLoading] = useState(false); const [selectedRows, setSelectedRows] = useState>(new Set()); const [columnLabels, setColumnLabels] = useState>({}); // 컬럼명 -> 라벨 매핑 // 🆕 검색 필터 관련 상태 const [searchFilterColumns, setSearchFilterColumns] = useState>(new Set()); // 검색 필터로 사용할 컬럼 const [isFilterSettingOpen, setIsFilterSettingOpen] = useState(false); // 필터 설정 다이얼로그 const [searchValues, setSearchValues] = useState>({}); // 검색 값 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) * 2순위: 모든 컬럼 표시 */ const getVisibleColumns = (stepId: number, allColumns: string[], stepsArray?: FlowStep[]): string[] => { // stepsArray가 제공되지 않으면 state의 steps 사용 const effectiveSteps = stepsArray || steps; // 1순위: 플로우 스텝 기본 설정 const currentStep = effectiveSteps.find((s) => s.id === stepId); if (currentStep?.displayConfig?.visibleColumns && currentStep.displayConfig.visibleColumns.length > 0) { return currentStep.displayConfig.visibleColumns; } // 2순위: 모든 컬럼 표시 return allColumns; }; // 🆕 스텝 데이터 페이지네이션 상태 const [stepDataPage, setStepDataPage] = useState(1); const [stepDataPageSize, setStepDataPageSize] = useState(10); // componentConfig에서 플로우 설정 추출 (DynamicComponentRenderer에서 전달됨) const config = (component as any).componentConfig || (component as any).config || {}; const flowId = config.flowId || component.flowId; const flowName = config.flowName || component.flowName; const displayMode = config.displayMode || component.displayMode || "horizontal"; const showStepCount = config.showStepCount !== false && component.showStepCount !== false; // 기본값 true const allowDataMove = config.allowDataMove || component.allowDataMove || false; // 🆕 플로우 컴포넌트 ID (버튼이 이 플로우를 참조할 때 사용) const flowComponentId = component.id; // 🆕 localStorage 키 생성 (사용자별로 저장) const filterSettingKey = useMemo(() => { if (!flowId || selectedStepId === null || !user?.userId) return null; return `flowWidget_searchFilters_${user.userId}_${flowId}_${selectedStepId}`; }, [flowId, selectedStepId, user?.userId]); // 🆕 그룹 설정 localStorage 키 생성 const groupSettingKey = useMemo(() => { if (!selectedStepId) return null; return `flowWidget_groupSettings_step_${selectedStepId}`; }, [selectedStepId]); // 🆕 저장된 필터 설정 불러오기 useEffect(() => { if (!filterSettingKey || stepDataColumns.length === 0 || !user?.userId) return; try { // 현재 사용자의 필터 설정만 불러오기 const saved = localStorage.getItem(filterSettingKey); if (saved) { const savedFilters = JSON.parse(saved); // 현재 단계에 표시되는 컬럼만 필터링 const validFilters = savedFilters.filter((col: string) => stepDataColumns.includes(col)); setSearchFilterColumns(new Set(validFilters)); } else { // 초기값: 빈 필터 (사용자가 선택해야 함) setSearchFilterColumns(new Set()); } } 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; try { localStorage.setItem(filterSettingKey, JSON.stringify(Array.from(searchFilterColumns))); setIsFilterSettingOpen(false); toast.success("검색 필터 설정이 저장되었습니다"); // 검색 값 초기화 setSearchValues({}); } catch (error) { console.error("필터 설정 저장 실패:", error); toast.error("설정 저장에 실패했습니다"); } }, [filterSettingKey, searchFilterColumns]); // 🆕 필터 컬럼 토글 const toggleFilterColumn = useCallback((columnName: string) => { setSearchFilterColumns((prev) => { const newSet = new Set(prev); if (newSet.has(columnName)) { newSet.delete(columnName); } else { newSet.add(columnName); } return newSet; }); }, []); // 🆕 전체 선택/해제 const toggleAllFilters = useCallback(() => { if (searchFilterColumns.size === stepDataColumns.length) { // 전체 해제 setSearchFilterColumns(new Set()); } else { // 전체 선택 setSearchFilterColumns(new Set(stepDataColumns)); } }, [searchFilterColumns, stepDataColumns]); // 🆕 검색 초기화 const handleClearSearch = useCallback(() => { setSearchValues({}); setFilteredData([]); }, []); // 🆕 그룹 설정 저장 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) { setFilteredData([]); return; } // 검색 값이 하나라도 있는지 확인 const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== ""); if (!hasSearchValue) { // 검색 값이 없으면 필터링 해제 setFilteredData([]); return; } // 필터링 실행 const filtered = stepData.filter((row) => { // 모든 검색 조건을 만족하는지 확인 return Object.entries(searchValues).every(([col, searchValue]) => { if (!searchValue || String(searchValue).trim() === "") return true; // 빈 값은 필터링하지 않음 const cellValue = row[col]; if (cellValue === null || cellValue === undefined) return false; // 문자열로 변환하여 대소문자 무시 검색 return String(cellValue).toLowerCase().includes(String(searchValue).toLowerCase()); }); }); setFilteredData(filtered); console.log("🔍 검색 실행:", { totalRows: stepData.length, filteredRows: filtered.length, searchValues, hasSearchValue, }); }, [searchValues, stepData]); // stepData와 searchValues가 변경될 때마다 실행 // 선택된 스텝의 데이터를 다시 로드하는 함수 const refreshStepData = async () => { if (!flowId) return; try { // 스텝 카운트는 항상 업데이트 (선택된 스텝 유무와 관계없이) const countsResponse = await getAllStepCounts(flowId); if (countsResponse.success && countsResponse.data) { // Record 형태로 변환 const countsMap: Record = {}; if (Array.isArray(countsResponse.data)) { countsResponse.data.forEach((item: any) => { countsMap[item.stepId] = item.count; }); } else if (typeof countsResponse.data === "object") { Object.assign(countsMap, countsResponse.data); } setStepCounts(countsMap); } // 선택된 스텝이 있으면 해당 스텝의 데이터도 새로고침 if (selectedStepId) { setStepDataLoading(true); // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId, selectedStepId); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } const response = await getStepDataList(flowId, selectedStepId, 1, 100); if (!response.success) { throw new Error(response.message || "데이터를 불러올 수 없습니다"); } const rows = response.data?.records || []; setStepData(rows); // 🆕 컬럼 추출 및 우선순위 적용 if (rows.length > 0) { const allColumns = Object.keys(rows[0]); setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 const visibleColumns = getVisibleColumns(selectedStepId, allColumns); setStepDataColumns(visibleColumns); } else { setAllAvailableColumns([]); setStepDataColumns([]); } // 선택 초기화 setSelectedRows(new Set()); setSearchValues({}); // 검색 값도 초기화 setFilteredData([]); // 필터링된 데이터 초기화 onSelectedDataChange?.([], selectedStepId); } } catch (err: any) { console.error("❌ 플로우 새로고침 실패:", err); toast.error(err.message || "데이터를 새로고치는데 실패했습니다"); } finally { if (selectedStepId) { setStepDataLoading(false); } } }; useEffect(() => { if (!flowId) { setLoading(false); return; } const loadFlowData = async () => { try { setLoading(true); setError(null); // 프리뷰 모드에서는 샘플 데이터만 표시 if (isPreviewMode) { console.log("🔒 프리뷰 모드: 플로우 데이터 로드 차단 - 샘플 데이터 표시"); setFlowData({ id: flowId || 0, flowName: flowName || "샘플 플로우", description: "프리뷰 모드 샘플", isActive: true, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), } as FlowDefinition); const sampleSteps: FlowStep[] = [ { id: 1, flowId: flowId || 0, stepName: "시작 단계", stepOrder: 1, stepType: "start", stepConfig: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { id: 2, flowId: flowId || 0, stepName: "진행 중", stepOrder: 2, stepType: "process", stepConfig: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, { id: 3, flowId: flowId || 0, stepName: "완료", stepOrder: 3, stepType: "end", stepConfig: {}, createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, ]; setSteps(sampleSteps); setStepCounts({ 1: 5, 2: 3, 3: 2 }); setConnections([]); setLoading(false); return; } // 플로우 정보 조회 const flowResponse = await getFlowById(flowId!); if (!flowResponse.success || !flowResponse.data) { throw new Error("플로우를 찾을 수 없습니다"); } setFlowData(flowResponse.data); // 스텝 목록 조회 const stepsResponse = await getFlowSteps(flowId); if (!stepsResponse.success) { throw new Error("스텝 목록을 불러올 수 없습니다"); } if (stepsResponse.data) { const sortedSteps = stepsResponse.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); setSteps(sortedSteps); // 연결 정보 조회 const connectionsResponse = await getFlowConnections(flowId); if (connectionsResponse.success && connectionsResponse.data) { setConnections(connectionsResponse.data); } // 스텝별 데이터 건수 조회 if (showStepCount) { const countsResponse = await getAllStepCounts(flowId!); if (countsResponse.success && countsResponse.data) { // 배열을 Record로 변환 const countsMap: Record = {}; countsResponse.data.forEach((item: any) => { countsMap[item.stepId] = item.count; }); setStepCounts(countsMap); } } // 🆕 플로우 로드 후 첫 번째 스텝 자동 선택 if (sortedSteps.length > 0) { const firstStep = sortedSteps[0]; setSelectedStepId(firstStep.id); setSelectedStep(flowComponentId, firstStep.id); // 첫 번째 스텝의 데이터 로드 try { // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId!, firstStep.id); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } const response = await getStepDataList(flowId!, firstStep.id, 1, 100); if (response.success) { const rows = response.data?.records || []; setStepData(rows); if (rows.length > 0) { const allColumns = Object.keys(rows[0]); setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 // sortedSteps를 직접 전달하여 타이밍 이슈 해결 const visibleColumns = getVisibleColumns(firstStep.id, allColumns, sortedSteps); setStepDataColumns(visibleColumns); } } } catch (err) { console.error("첫 번째 스텝 데이터 로드 실패:", err); } } } } catch (err: any) { console.error("Failed to load flow data:", err); setError(err.message || "플로우 데이터를 불러오는데 실패했습니다"); } finally { setLoading(false); } }; loadFlowData(); }, [flowId, showStepCount]); // flowRefreshKey가 변경될 때마다 스텝 데이터 새로고침 useEffect(() => { if (flowRefreshKey !== undefined && flowRefreshKey > 0 && flowId) { refreshStepData(); } }, [flowRefreshKey]); // 🆕 언마운트 시 전역 상태 초기화 useEffect(() => { return () => { resetFlow(flowComponentId); }; }, [flowComponentId, resetFlow]); // 🆕 스텝 클릭 핸들러 (전역 상태 업데이트 추가) const handleStepClick = async (stepId: number, stepName: string) => { // 프리뷰 모드에서는 스텝 클릭 차단 if (isPreviewMode) { return; } // 외부 콜백 실행 if (onStepClick) { onStepClick(stepId, stepName); } // 같은 스텝을 다시 클릭하면 접기 if (selectedStepId === stepId) { setSelectedStepId(null); setSelectedStep(flowComponentId, null); // 🆕 전역 상태 업데이트 setStepData([]); setStepDataColumns([]); setSelectedRows(new Set()); setStepDataPage(1); // 🆕 페이지 리셋 onSelectedDataChange?.([], null); return; } // 새로운 스텝 선택 - 데이터 로드 setSelectedStepId(stepId); setSelectedStep(flowComponentId, stepId); // 🆕 전역 상태 업데이트 setStepDataLoading(true); setSelectedRows(new Set()); setStepDataPage(1); // 🆕 페이지 리셋 onSelectedDataChange?.([], stepId); try { // 컬럼 라벨 조회 const labelsResponse = await getStepColumnLabels(flowId!, stepId); console.log("🏷️ 컬럼 라벨 조회 결과:", { stepId, success: labelsResponse.success, labelsCount: labelsResponse.data ? Object.keys(labelsResponse.data).length : 0, labels: labelsResponse.data, }); if (labelsResponse.success && labelsResponse.data) { setColumnLabels(labelsResponse.data); } else { console.warn("⚠️ 컬럼 라벨 조회 실패 또는 데이터 없음:", labelsResponse); setColumnLabels({}); } // 데이터 조회 const response = await getStepDataList(flowId!, stepId, 1, 100); if (!response.success) { throw new Error(response.message || "데이터를 불러올 수 없습니다"); } const rows = response.data?.records || []; setStepData(rows); // 🆕 컬럼 추출 및 우선순위 적용 if (rows.length > 0) { const allColumns = Object.keys(rows[0]); setAllAvailableColumns(allColumns); // 전체 컬럼 목록 저장 const visibleColumns = getVisibleColumns(stepId, allColumns); setStepDataColumns(visibleColumns); } else { setAllAvailableColumns([]); setStepDataColumns([]); } } catch (err: any) { console.error("Failed to load step data:", err); toast.error(err.message || "데이터를 불러오는데 실패했습니다"); } finally { setStepDataLoading(false); } }; // 체크박스 토글 const toggleRowSelection = (rowIndex: number) => { // 프리뷰 모드에서는 행 선택 차단 if (isPreviewMode) { return; } const newSelected = new Set(selectedRows); if (newSelected.has(rowIndex)) { newSelected.delete(rowIndex); } else { newSelected.add(rowIndex); } setSelectedRows(newSelected); // 선택된 데이터를 상위로 전달 const selectedData = Array.from(newSelected).map((index) => stepData[index]); console.log("🌊 FlowWidget - 체크박스 토글, 상위로 전달:", { rowIndex, newSelectedSize: newSelected.size, selectedData, selectedStepId, hasCallback: !!onSelectedDataChange, }); onSelectedDataChange?.(selectedData, selectedStepId); }; // 전체 선택/해제 const toggleAllRows = () => { let newSelected: Set; if (selectedRows.size === stepData.length) { newSelected = new Set(); } else { newSelected = new Set(stepData.map((_, index) => index)); } setSelectedRows(newSelected); // 선택된 데이터를 상위로 전달 const selectedData = Array.from(newSelected).map((index) => stepData[index]); onSelectedDataChange?.(selectedData, selectedStepId); }; // 🆕 표시할 데이터 결정 // - 검색 값이 있으면 → filteredData 사용 (결과가 0건이어도 filteredData 사용) // - 검색 값이 없으면 → stepData 사용 (전체 데이터) const hasSearchValue = Object.values(searchValues).some((val) => val && String(val).trim() !== ""); const displayData = hasSearchValue ? filteredData : stepData; // 🆕 페이지네이션된 스텝 데이터 const paginatedStepData = displayData.slice((stepDataPage - 1) * stepDataPageSize, stepDataPage * stepDataPageSize); const totalStepDataPages = Math.ceil(displayData.length / stepDataPageSize); if (loading) { return (
플로우 로딩 중...
); } if (error) { return (
{error}
); } if (!flowId || !flowData) { return (
플로우를 선택해주세요
); } if (steps.length === 0) { return (
플로우에 스텝이 없습니다
); } // 반응형 컨테이너 클래스 const containerClass = displayMode === "horizontal" ? "flex flex-col sm:flex-row sm:flex-wrap items-center justify-center gap-3 sm:gap-4" : "flex flex-col items-center gap-4"; return (
{/* 플로우 스텝 목록 */}
{steps.map((step, index) => ( {/* 스텝 카드 */}
handleStepClick(step.id, step.stepName)} > {/* 콘텐츠 */}
{/* 스텝 이름 */}

{step.stepName}

{/* 데이터 건수 */} {showStepCount && (
{(stepCounts[step.id] || 0).toLocaleString("ko-KR")}
)}
{/* 하단 선 */}
{/* 화살표 (마지막 스텝 제외) */} {index < steps.length - 1 && (
{displayMode === "horizontal" ? (
) : (
)}
)} ))}
{/* 선택된 스텝의 데이터 리스트 */} {selectedStepId !== null && (
{/* 필터 및 그룹 설정 */} {stepDataColumns.length > 0 && ( <>
{/* 검색 필터 입력 영역 */} {searchFilterColumns.size > 0 && ( <> {Array.from(searchFilterColumns).map((col) => ( setSearchValues((prev) => ({ ...prev, [col]: e.target.value, })) } placeholder={`${columnLabels[col] || col} 검색...`} className="h-8 text-xs w-40" /> ))} {Object.keys(searchValues).length > 0 && ( )} )} {/* 필터/그룹 설정 버튼 */}
{/* 🆕 그룹 표시 배지 */} {groupByColumns.length > 0 && (
그룹:
{groupByColumns.map((col, idx) => ( {idx > 0 && } {columnLabels[col] || col} ))}
)} )} {/* 데이터 영역 - 고정 높이 + 스크롤 */} {stepDataLoading ? (
데이터 로딩 중...
) : stepData.length === 0 ? (
데이터가 없습니다
) : ( <> {/* 모바일: 카드 뷰 - 고정 높이 + 스크롤 */}
{paginatedStepData.map((row, pageIndex) => { const actualIndex = (stepDataPage - 1) * stepDataPageSize + pageIndex; return (
{allowDataMove && (
선택 toggleRowSelection(actualIndex)} />
)}
{stepDataColumns.map((col) => (
{columnLabels[col] || col}: {formatValue(row[col])}
))}
); })}
{/* 데스크톱: 테이블 뷰 - 고정 높이 + 스크롤 */}
{allowDataMove && ( 0} onCheckedChange={toggleAllRows} /> )} {stepDataColumns.map((col) => ( {columnLabels[col] || col} ))} {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}건)
, ]; 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])} ))} ); }) )}
)} {/* 페이지네이션 - 항상 하단에 고정 */} {!stepDataLoading && stepData.length > 0 && (
{/* 왼쪽: 페이지 정보 + 페이지 크기 선택 */}
페이지 {stepDataPage} / {totalStepDataPages} (총 {stepData.length.toLocaleString("ko-KR")}건)
표시 개수:
{/* 오른쪽: 페이지네이션 */} {totalStepDataPages > 1 && ( { if (isPreviewMode) { return; } setStepDataPage((p) => Math.max(1, p - 1)); }} className={ stepDataPage === 1 || isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer" } /> {totalStepDataPages <= 7 ? ( Array.from({ length: totalStepDataPages }, (_, i) => i + 1).map((page) => ( { if (isPreviewMode) { return; } setStepDataPage(page); }} isActive={stepDataPage === page} className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"} > {page} )) ) : ( <> {Array.from({ length: totalStepDataPages }, (_, i) => i + 1) .filter((page) => { return ( page === 1 || page === totalStepDataPages || (page >= stepDataPage - 2 && page <= stepDataPage + 2) ); }) .map((page, idx, arr) => ( {idx > 0 && arr[idx - 1] !== page - 1 && ( ... )} { if (isPreviewMode) { return; } setStepDataPage(page); }} isActive={stepDataPage === page} className={isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer"} > {page} ))} )} { if (isPreviewMode) { return; } setStepDataPage((p) => Math.min(totalStepDataPages, p + 1)); }} className={ stepDataPage === totalStepDataPages || isPreviewMode ? "pointer-events-none opacity-50" : "cursor-pointer" } /> )}
)}
)} {/* 🆕 검색 필터 설정 다이얼로그 */} 검색 필터 설정 검색 필터로 사용할 컬럼을 선택하세요. 선택한 컬럼의 검색 입력 필드가 표시됩니다.
{/* 전체 선택/해제 */}
0} onCheckedChange={toggleAllFilters} /> {searchFilterColumns.size} / {stepDataColumns.length}개
{/* 컬럼 목록 */}
{stepDataColumns.map((col) => (
toggleFilterColumn(col)} />
))}
{/* 선택된 컬럼 개수 안내 */}
{searchFilterColumns.size === 0 ? ( 검색 필터를 사용하려면 최소 1개 이상의 컬럼을 선택하세요 ) : ( {searchFilterColumns.size}개의 검색 필터가 표시됩니다 )}
{/* 🆕 그룹 설정 다이얼로그 */} 그룹 설정 데이터를 그룹화할 컬럼을 선택하세요. 여러 컬럼을 선택하면 계층적으로 그룹화됩니다.
{/* 컬럼 목록 */}
{stepDataColumns.map((col) => (
toggleGroupColumn(col)} />
))}
{/* 선택된 그룹 안내 */}
{groupByColumns.length === 0 ? ( 그룹화할 컬럼을 선택하세요 ) : ( 선택된 그룹:{" "} {groupByColumns.map((col) => columnLabels[col] || col).join(" → ")} )}
); }