"use client"; import { useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { MoreHorizontal, Trash2, Copy, Plus, Search, Network, Calendar } from "lucide-react"; import { toast } from "sonner"; import { showErrorToast } from "@/lib/utils/toastUtils"; import { useAuth } from "@/hooks/useAuth"; import { apiClient } from "@/lib/api/client"; import { ResponsiveDataView, RDVColumn, RDVCardField } from "@/components/common/ResponsiveDataView"; interface NodeFlow { flowId: number; flowName: string; flowDescription: string; createdAt: string; updatedAt: string; } interface DataFlowListProps { onLoadFlow: (flowId: number | null) => void; } export default function DataFlowList({ onLoadFlow }: DataFlowListProps) { const { user } = useAuth(); const [flows, setFlows] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedFlow, setSelectedFlow] = useState(null); const loadFlows = useCallback(async () => { try { setLoading(true); const response = await apiClient.get("/dataflow/node-flows"); if (response.data.success) { setFlows(response.data.data); } else { throw new Error(response.data.message || "플로우 목록 조회 실패"); } } catch (error) { console.error("플로우 목록 조회 실패", error); showErrorToast("플로우 목록을 불러오는 데 실패했습니다", error, { guidance: "네트워크 연결을 확인해 주세요." }); } finally { setLoading(false); } }, []); useEffect(() => { loadFlows(); }, [loadFlows]); const handleDelete = (flow: NodeFlow) => { setSelectedFlow(flow); setShowDeleteModal(true); }; const handleCopy = async (flow: NodeFlow) => { try { setLoading(true); const response = await apiClient.get(`/dataflow/node-flows/${flow.flowId}`); if (!response.data.success) { throw new Error(response.data.message || "플로우 조회 실패"); } const originalFlow = response.data.data; const copyResponse = await apiClient.post("/dataflow/node-flows", { flowName: `${flow.flowName} (복사본)`, flowDescription: flow.flowDescription, flowData: originalFlow.flowData, }); if (copyResponse.data.success) { toast.success(`플로우가 성공적으로 복사되었습니다`); await loadFlows(); } else { throw new Error(copyResponse.data.message || "플로우 복사 실패"); } } catch (error) { console.error("플로우 복사 실패:", error); showErrorToast("플로우 복사에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); } }; const handleConfirmDelete = async () => { if (!selectedFlow) return; try { setLoading(true); const response = await apiClient.delete(`/dataflow/node-flows/${selectedFlow.flowId}`); if (response.data.success) { toast.success(`플로우가 삭제되었습니다: ${selectedFlow.flowName}`); await loadFlows(); } else { throw new Error(response.data.message || "플로우 삭제 실패"); } } catch (error) { console.error("플로우 삭제 실패:", error); showErrorToast("플로우 삭제에 실패했습니다", error, { guidance: "잠시 후 다시 시도해 주세요." }); } finally { setLoading(false); setShowDeleteModal(false); setSelectedFlow(null); } }; const filteredFlows = flows.filter( (flow) => flow.flowName.toLowerCase().includes(searchTerm.toLowerCase()) || flow.flowDescription.toLowerCase().includes(searchTerm.toLowerCase()), ); // DropdownMenu 렌더러 (테이블 + 카드 공통) const renderDropdownMenu = (flow: NodeFlow) => (
e.stopPropagation()}> onLoadFlow(flow.flowId)}> 불러오기 handleCopy(flow)}> 복사 handleDelete(flow)} className="text-destructive"> 삭제
); const columns: RDVColumn[] = [ { key: "flowName", label: "플로우명", render: (_val, flow) => (
{flow.flowName}
), }, { key: "flowDescription", label: "설명", render: (_val, flow) => ( {flow.flowDescription || "설명 없음"} ), }, { key: "createdAt", label: "생성일", render: (_val, flow) => ( {new Date(flow.createdAt).toLocaleDateString()} ), }, { key: "updatedAt", label: "최근 수정", hideOnMobile: true, render: (_val, flow) => ( {new Date(flow.updatedAt).toLocaleDateString()} ), }, ]; const cardFields: RDVCardField[] = [ { label: "생성일", render: (flow) => new Date(flow.createdAt).toLocaleDateString(), }, { label: "최근 수정", render: (flow) => new Date(flow.updatedAt).toLocaleDateString(), }, ]; return (
{/* 검색 및 액션 영역 */}
setSearchTerm(e.target.value)} className="h-10 pl-10 text-sm" />
{filteredFlows.length}
{/* 빈 상태: 커스텀 Empty UI */} {!loading && filteredFlows.length === 0 ? (

플로우가 없습니다

새 플로우를 생성하여 노드 기반 데이터 제어를 설계해보세요.

) : ( data={filteredFlows} columns={columns} keyExtractor={(flow) => String(flow.flowId)} isLoading={loading} skeletonCount={5} cardTitle={(flow) => ( {flow.flowName} )} cardSubtitle={(flow) => flow.flowDescription || "설명 없음"} cardHeaderRight={renderDropdownMenu} cardFields={cardFields} actionsLabel="작업" actionsWidth="80px" renderActions={renderDropdownMenu} onRowClick={(flow) => onLoadFlow(flow.flowId)} /> )} {/* 삭제 확인 모달 */} 플로우 삭제 “{selectedFlow?.flowName}” 플로우를 완전히 삭제하시겠습니까?
이 작업은 되돌릴 수 없으며, 모든 플로우 정보가 영구적으로 삭제됩니다.
); }