"use client"; import React, { useEffect, useState } from "react"; import { FlowComponent } from "@/types/screen-management"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { AlertCircle, Loader2, ChevronDown, ChevronUp, History } from "lucide-react"; import { getFlowById, getAllStepCounts, getStepDataList, moveBatchData, getFlowAuditLogs } from "@/lib/api/flow"; import type { FlowDefinition, FlowStep, FlowAuditLog } from "@/types/flow"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Checkbox } from "@/components/ui/checkbox"; import { toast } from "sonner"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Pagination, PaginationContent, PaginationItem, PaginationLink, PaginationNext, PaginationPrevious, } from "@/components/ui/pagination"; interface FlowWidgetProps { component: FlowComponent; onStepClick?: (stepId: number, stepName: string) => void; onSelectedDataChange?: (selectedData: any[], stepId: number | null) => void; } export function FlowWidget({ component, onStepClick, onSelectedDataChange }: FlowWidgetProps) { 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 [movingData, setMovingData] = useState(false); const [selectedNextStepId, setSelectedNextStepId] = useState(null); // 선택된 다음 단계 // 오딧 로그 상태 const [auditLogs, setAuditLogs] = useState([]); const [auditLogsLoading, setAuditLogsLoading] = useState(false); const [showAuditLogs, setShowAuditLogs] = useState(false); const [auditPage, setAuditPage] = useState(1); const [auditPageSize] = 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; console.log("🔍 FlowWidget 렌더링:", { component, componentConfig: config, flowId, flowName, displayMode, showStepCount, allowDataMove, }); useEffect(() => { console.log("🔍 FlowWidget useEffect 실행:", { flowId, hasFlowId: !!flowId, config, }); if (!flowId) { setLoading(false); return; } const loadFlowData = async () => { try { setLoading(true); setError(null); // 플로우 정보 조회 const flowResponse = await getFlowById(flowId!); if (!flowResponse.success || !flowResponse.data) { throw new Error("플로우를 찾을 수 없습니다"); } setFlowData(flowResponse.data); // 스텝 목록 조회 const stepsResponse = await fetch(`/api/flow/definitions/${flowId}/steps`); if (!stepsResponse.ok) { throw new Error("스텝 목록을 불러올 수 없습니다"); } const stepsData = await stepsResponse.json(); if (stepsData.success && stepsData.data) { const sortedSteps = stepsData.data.sort((a: FlowStep, b: FlowStep) => a.stepOrder - b.stepOrder); setSteps(sortedSteps); // 연결 정보 조회 const connectionsResponse = await fetch(`/api/flow/connections/${flowId}`); if (connectionsResponse.ok) { const connectionsData = await connectionsResponse.json(); if (connectionsData.success && connectionsData.data) { setConnections(connectionsData.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); } } } } catch (err: any) { console.error("Failed to load flow data:", err); setError(err.message || "플로우 데이터를 불러오는데 실패했습니다"); } finally { setLoading(false); } }; loadFlowData(); }, [flowId, showStepCount]); // 스텝 클릭 핸들러 const handleStepClick = async (stepId: number, stepName: string) => { if (onStepClick) { onStepClick(stepId, stepName); return; } // 같은 스텝을 다시 클릭하면 접기 if (selectedStepId === stepId) { setSelectedStepId(null); setStepData([]); setStepDataColumns([]); setSelectedRows(new Set()); // 선택 초기화 전달 onSelectedDataChange?.([], null); return; } // 새로운 스텝 선택 - 데이터 로드 setSelectedStepId(stepId); setStepDataLoading(true); setSelectedRows(new Set()); // 선택 초기화 전달 onSelectedDataChange?.([], stepId); try { 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) { setStepDataColumns(Object.keys(rows[0])); } else { setStepDataColumns([]); } } catch (err: any) { console.error("Failed to load step data:", err); toast.error(err.message || "데이터를 불러오는데 실패했습니다"); } finally { setStepDataLoading(false); } }; // 체크박스 토글 const toggleRowSelection = (rowIndex: number) => { 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); }; // 현재 단계에서 가능한 다음 단계들 찾기 const getNextSteps = (currentStepId: number) => { return connections .filter((conn) => conn.fromStepId === currentStepId) .map((conn) => steps.find((s) => s.id === conn.toStepId)) .filter((step) => step !== undefined); }; // 다음 단계로 이동 const handleMoveToNext = async (targetStepId?: number) => { if (!flowId || !selectedStepId || selectedRows.size === 0) return; // 다음 단계 결정 let nextStepId = targetStepId || selectedNextStepId; if (!nextStepId) { const nextSteps = getNextSteps(selectedStepId); if (nextSteps.length === 0) { toast.error("다음 단계가 없습니다"); return; } if (nextSteps.length === 1) { nextStepId = nextSteps[0].id; } else { toast.error("다음 단계를 선택해주세요"); return; } } const selectedData = Array.from(selectedRows).map((index) => stepData[index]); try { setMovingData(true); // Primary Key 컬럼 추출 (첫 번째 컬럼 가정) const primaryKeyColumn = stepDataColumns[0]; const dataIds = selectedData.map((data) => String(data[primaryKeyColumn])); // 배치 이동 API 호출 const response = await moveBatchData({ flowId, fromStepId: selectedStepId, toStepId: nextStepId, dataIds, }); if (!response.success) { throw new Error(response.message || "데이터 이동에 실패했습니다"); } const nextStepName = steps.find((s) => s.id === nextStepId)?.stepName; toast.success(`${selectedRows.size}건의 데이터를 "${nextStepName}"(으)로 이동했습니다`); // 선택 초기화 setSelectedNextStepId(null); setSelectedRows(new Set()); // 선택 초기화 전달 onSelectedDataChange?.([], selectedStepId); // 데이터 새로고침 await handleStepClick(selectedStepId, steps.find((s) => s.id === selectedStepId)?.stepName || ""); // 건수 새로고침 const countsResponse = await getAllStepCounts(flowId); if (countsResponse.success && countsResponse.data) { const countsMap: Record = {}; countsResponse.data.forEach((item: any) => { countsMap[item.stepId] = item.count; }); setStepCounts(countsMap); } } catch (err: any) { console.error("Failed to move data:", err); toast.error(err.message || "데이터 이동 중 오류가 발생했습니다"); } finally { setMovingData(false); } }; // 오딧 로그 로드 const loadAuditLogs = async () => { if (!flowId) return; try { setAuditLogsLoading(true); const response = await getFlowAuditLogs(flowId, 100); // 최근 100개 if (response.success && response.data) { setAuditLogs(response.data); } } catch (err: any) { console.error("Failed to load audit logs:", err); toast.error("이력 조회 중 오류가 발생했습니다"); } finally { setAuditLogsLoading(false); } }; // 오딧 로그 모달 열기 const handleOpenAuditLogs = () => { setShowAuditLogs(true); setAuditPage(1); // 페이지 초기화 loadAuditLogs(); }; // 페이지네이션된 오딧 로그 const paginatedAuditLogs = auditLogs.slice((auditPage - 1) * auditPageSize, auditPage * auditPageSize); const totalAuditPages = Math.ceil(auditLogs.length / auditPageSize); 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 (
{/* 플로우 제목 */}

{flowData.name}

{/* 오딧 로그 버튼 */} 플로우 변경 이력 데이터 이동 및 상태 변경 기록 (총 {auditLogs.length}건) {auditLogsLoading ? (
이력 로딩 중...
) : auditLogs.length === 0 ? (
변경 이력이 없습니다
) : (
{/* 테이블 */}
변경일시 타입 출발 단계 도착 단계 데이터 ID 상태 변경 변경자 DB 연결 테이블 {paginatedAuditLogs.map((log) => { const fromStep = steps.find((s) => s.id === log.fromStepId); const toStep = steps.find((s) => s.id === log.toStepId); return ( {new Date(log.changedAt).toLocaleString("ko-KR", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", })} {log.moveType === "status" ? "상태" : log.moveType === "table" ? "테이블" : "하이브리드"} {fromStep?.stepName || `Step ${log.fromStepId}`} {toStep?.stepName || `Step ${log.toStepId}`} {log.sourceDataId || "-"} {log.targetDataId && log.targetDataId !== log.sourceDataId && ( <>
→ {log.targetDataId} )}
{log.statusFrom && log.statusTo ? ( {log.statusFrom}
→ {log.statusTo}
) : ( - )}
{log.changedBy} {log.dbConnectionName ? ( {log.dbConnectionName} ) : ( - )} {log.sourceTable || "-"} {log.targetTable && log.targetTable !== log.sourceTable && ( <>
→ {log.targetTable} )}
); })}
{/* 페이지네이션 */} {totalAuditPages > 1 && (
{(auditPage - 1) * auditPageSize + 1}-{Math.min(auditPage * auditPageSize, auditLogs.length)} /{" "} {auditLogs.length}건
setAuditPage((p) => Math.max(1, p - 1))} className={auditPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} /> {Array.from({ length: totalAuditPages }, (_, i) => i + 1) .filter((page) => { // 현재 페이지 주변만 표시 return ( page === 1 || page === totalAuditPages || (page >= auditPage - 1 && page <= auditPage + 1) ); }) .map((page, idx, arr) => ( {idx > 0 && arr[idx - 1] !== page - 1 && ( ... )} setAuditPage(page)} isActive={auditPage === page} className="cursor-pointer" > {page} ))} setAuditPage((p) => Math.min(totalAuditPages, p + 1))} className={ auditPage === totalAuditPages ? "pointer-events-none opacity-50" : "cursor-pointer" } />
)}
)}
{flowData.description && (

{flowData.description}

)}
{/* 플로우 스텝 목록 */}
{steps.map((step, index) => ( {/* 스텝 카드 */}
handleStepClick(step.id, step.stepName)} > {/* 단계 번호 배지 */}
Step {step.stepOrder}
{/* 스텝 이름 */}

{step.stepName}

{/* 데이터 건수 */} {showStepCount && (
{stepCounts[step.id] || 0}
)} {/* 선택 인디케이터 */} {selectedStepId === step.id && (
)}
{/* 화살표 (마지막 스텝 제외) */} {index < steps.length - 1 && (
{displayMode === "horizontal" ? ( ) : ( )}
)}
))}
{/* 선택된 스텝의 데이터 리스트 */} {selectedStepId !== null && (
{/* 헤더 */}

{steps.find((s) => s.id === selectedStepId)?.stepName}

총 {stepData.length}건의 데이터

{allowDataMove && selectedRows.size > 0 && (() => { const nextSteps = getNextSteps(selectedStepId); return nextSteps.length > 1 ? ( // 다음 단계가 여러 개인 경우: 선택 UI 표시
) : ( // 다음 단계가 하나인 경우: 바로 이동 버튼만 표시 ); })()}
{/* 데이터 테이블 */} {stepDataLoading ? (
데이터 로딩 중...
) : stepData.length === 0 ? (
데이터가 없습니다
) : ( <> {/* 모바일: 카드 뷰 (컨테이너 640px 미만) */}
{stepData.map((row, index) => (
{/* 체크박스 헤더 */} {allowDataMove && (
선택 toggleRowSelection(index)} />
)} {/* 데이터 필드들 */}
{stepDataColumns.map((col) => (
{col}: {row[col] !== null && row[col] !== undefined ? ( String(row[col]) ) : ( - )}
))}
))}
{/* 데스크톱: 테이블 뷰 (컨테이너 640px 이상) */}
{allowDataMove && ( 0} onCheckedChange={toggleAllRows} /> )} {stepDataColumns.map((col) => ( {col} ))} {stepData.map((row, index) => ( {allowDataMove && ( toggleRowSelection(index)} /> )} {stepDataColumns.map((col) => ( {row[col] !== null && row[col] !== undefined ? ( String(row[col]) ) : ( - )} ))} ))}
)}
)}
); }