"use client"; /** * 플로우 관리 메인 페이지 * - 플로우 정의 목록 * - 플로우 생성/수정/삭제 * - 플로우 편집기로 이동 */ import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { Plus, Edit2, Trash2, Workflow, Table, Calendar, User, Check, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useToast } from "@/hooks/use-toast"; import { getFlowDefinitions, createFlowDefinition, deleteFlowDefinition } from "@/lib/api/flow"; import { FlowDefinition } from "@/types/flow"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { cn } from "@/lib/utils"; import { formatErrorMessage } from "@/lib/utils/errorUtils"; import { tableManagementApi } from "@/lib/api/tableManagement"; import { ScrollToTop } from "@/components/common/ScrollToTop"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; import { ExternalRestApiConnectionAPI, ExternalRestApiConnection } from "@/lib/api/externalRestApiConnection"; export default function FlowManagementPage() { const router = useRouter(); const { toast } = useToast(); // 상태 const [flows, setFlows] = useState([]); const [loading, setLoading] = useState(true); const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); const [selectedFlow, setSelectedFlow] = useState(null); // 테이블 목록 관련 상태 const [tableList, setTableList] = useState>( [], ); const [loadingTables, setLoadingTables] = useState(false); const [openTableCombobox, setOpenTableCombobox] = useState(false); // 데이터 소스 타입: "internal" (내부 DB), "external_db_숫자" (외부 DB), "restapi_숫자" (REST API) const [selectedDbSource, setSelectedDbSource] = useState("internal"); const [externalConnections, setExternalConnections] = useState< Array<{ id: number; connection_name: string; db_type: string }> >([]); const [externalTableList, setExternalTableList] = useState([]); const [loadingExternalTables, setLoadingExternalTables] = useState(false); // REST API 연결 관련 상태 const [restApiConnections, setRestApiConnections] = useState([]); const [restApiEndpoint, setRestApiEndpoint] = useState(""); const [restApiJsonPath, setRestApiJsonPath] = useState("data"); // 생성 폼 상태 const [formData, setFormData] = useState({ name: "", description: "", tableName: "", }); // 플로우 목록 조회 const loadFlows = async () => { setLoading(true); try { const response = await getFlowDefinitions({ isActive: true }); if (response.success && response.data) { setFlows(response.data); } else { toast({ title: "조회 실패", description: formatErrorMessage(response.error, "플로우 목록을 불러올 수 없습니다."), variant: "destructive", }); } } catch (error) { toast({ title: "오류 발생", description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } finally { setLoading(false); } }; useEffect(() => { loadFlows(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 테이블 목록 로드 (내부 DB) useEffect(() => { const loadTables = async () => { try { setLoadingTables(true); const response = await tableManagementApi.getTableList(); if (response.success && response.data) { setTableList(response.data); } } catch (error) { console.error("Failed to load tables:", error); } finally { setLoadingTables(false); } }; loadTables(); }, []); // 외부 DB 연결 목록 로드 useEffect(() => { const loadConnections = async () => { try { const response = await ExternalDbConnectionAPI.getActiveControlConnections(); if (response.success && response.data) { // 메인 데이터베이스(현재 시스템) 제외 - connection_name에 "메인" 또는 "현재 시스템"이 포함된 것 필터링 const filtered = response.data.filter( (conn) => !conn.connection_name.includes("메인") && !conn.connection_name.includes("현재 시스템"), ); setExternalConnections(filtered); } } catch (error) { console.error("Failed to load external connections:", error); setExternalConnections([]); } }; loadConnections(); }, []); // REST API 연결 목록 로드 useEffect(() => { const loadRestApiConnections = async () => { try { const connections = await ExternalRestApiConnectionAPI.getConnections({ is_active: "Y" }); setRestApiConnections(connections); } catch (error) { console.error("Failed to load REST API connections:", error); setRestApiConnections([]); } }; loadRestApiConnections(); }, []); // 외부 DB 테이블 목록 로드 useEffect(() => { // REST API인 경우 테이블 목록 로드 불필요 if (selectedDbSource === "internal" || !selectedDbSource || selectedDbSource.startsWith("restapi_")) { setExternalTableList([]); return; } // 외부 DB인 경우 if (selectedDbSource.startsWith("external_db_")) { const connectionId = selectedDbSource.replace("external_db_", ""); const loadExternalTables = async () => { try { setLoadingExternalTables(true); const token = localStorage.getItem("authToken"); const response = await fetch(`/api/multi-connection/connections/${connectionId}/tables`, { headers: { Authorization: `Bearer ${token}`, }, }); if (response && response.ok) { const data = await response.json(); if (data.success && data.data) { const tables = Array.isArray(data.data) ? data.data : []; const tableNames = tables .map((t: string | { tableName?: string; table_name?: string; tablename?: string; name?: string }) => typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name, ) .filter(Boolean); setExternalTableList(tableNames); } else { setExternalTableList([]); } } else { setExternalTableList([]); } } catch (error) { console.error("외부 DB 테이블 목록 조회 오류:", error); setExternalTableList([]); } finally { setLoadingExternalTables(false); } }; loadExternalTables(); } }, [selectedDbSource]); // 플로우 생성 const handleCreate = async () => { console.log("🚀 handleCreate called with formData:", formData); // REST API인 경우 테이블 이름 검증 스킵 const isRestApi = selectedDbSource.startsWith("restapi_"); if (!formData.name || (!isRestApi && !formData.tableName)) { console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi }); toast({ title: "입력 오류", description: isRestApi ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.", variant: "destructive", }); return; } // REST API인 경우 엔드포인트 검증 if (isRestApi && !restApiEndpoint) { toast({ title: "입력 오류", description: "REST API 엔드포인트는 필수입니다.", variant: "destructive", }); return; } try { // 데이터 소스 타입 및 ID 파싱 let dbSourceType: "internal" | "external" | "restapi" = "internal"; let dbConnectionId: number | undefined = undefined; let restApiConnectionId: number | undefined = undefined; if (selectedDbSource === "internal") { dbSourceType = "internal"; } else if (selectedDbSource.startsWith("external_db_")) { dbSourceType = "external"; dbConnectionId = parseInt(selectedDbSource.replace("external_db_", "")); } else if (selectedDbSource.startsWith("restapi_")) { dbSourceType = "restapi"; restApiConnectionId = parseInt(selectedDbSource.replace("restapi_", "")); } // 요청 데이터 구성 const requestData: Record = { ...formData, dbSourceType, dbConnectionId, }; // REST API인 경우 추가 정보 if (dbSourceType === "restapi") { requestData.restApiConnectionId = restApiConnectionId; requestData.restApiEndpoint = restApiEndpoint; requestData.restApiJsonPath = restApiJsonPath || "data"; // REST API는 가상 테이블명 사용 requestData.tableName = `_restapi_${restApiConnectionId}`; } console.log("✅ Calling createFlowDefinition with:", requestData); const response = await createFlowDefinition(requestData as Parameters[0]); if (response.success && response.data) { toast({ title: "생성 완료", description: "플로우가 성공적으로 생성되었습니다.", }); setIsCreateDialogOpen(false); setFormData({ name: "", description: "", tableName: "" }); setSelectedDbSource("internal"); setRestApiEndpoint(""); setRestApiJsonPath("data"); loadFlows(); } else { toast({ title: "생성 실패", description: formatErrorMessage(response.error || response.message, "플로우 생성 중 오류가 발생했습니다."), variant: "destructive", }); } } catch (error) { toast({ title: "오류 발생", description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } }; // 플로우 삭제 const handleDelete = async () => { if (!selectedFlow) return; try { const response = await deleteFlowDefinition(selectedFlow.id); if (response.success) { toast({ title: "삭제 완료", description: "플로우가 삭제되었습니다.", }); setIsDeleteDialogOpen(false); setSelectedFlow(null); loadFlows(); } else { toast({ title: "삭제 실패", description: formatErrorMessage(response.error, "플로우 삭제 중 오류가 발생했습니다."), variant: "destructive", }); } } catch (error) { toast({ title: "오류 발생", description: error instanceof Error ? error.message : "알 수 없는 오류가 발생했습니다.", variant: "destructive", }); } }; // 플로우 편집기로 이동 const handleEdit = (flowId: number) => { router.push(`/admin/flow-management/${flowId}`); }; return (
{/* 페이지 헤더 */}

플로우 관리

업무 프로세스 플로우를 생성하고 관리합니다

{/* 액션 버튼 영역 */}
{/* 플로우 카드 목록 */} {loading ? (
{Array.from({ length: 6 }).map((_, index) => (
{Array.from({ length: 3 }).map((_, i) => (
))}
))}
) : flows.length === 0 ? (

생성된 플로우가 없습니다

새 플로우를 생성하여 업무 프로세스를 관리해보세요.

) : (
{flows.map((flow) => (
handleEdit(flow.id)} > {/* 헤더 */}

{flow.name}

{flow.isActive && ( 활성 )}

{flow.description || "설명 없음"}

{/* 정보 */}
{flow.tableName}
생성자: {flow.createdBy}
{new Date(flow.updatedAt).toLocaleDateString("ko-KR")}
{/* 액션 */}
))} )} {/* 생성 다이얼로그 */} 새 플로우 생성 새로운 업무 프로세스 플로우를 생성합니다
setFormData({ ...formData, name: e.target.value })} placeholder="예: 제품 수명주기 관리" className="h-8 text-xs sm:h-10 sm:text-sm" />
{/* 데이터 소스 선택 */}

플로우에서 사용할 데이터 소스를 선택합니다

{/* REST API인 경우 엔드포인트 설정 */} {selectedDbSource.startsWith("restapi_") ? ( <>
setRestApiEndpoint(e.target.value)} placeholder="예: /api/data/list" className="h-8 text-xs sm:h-10 sm:text-sm" />

데이터를 조회할 API 엔드포인트 경로입니다

setRestApiJsonPath(e.target.value)} placeholder="예: data 또는 result.items" className="h-8 text-xs sm:h-10 sm:text-sm" />

응답 JSON에서 데이터 배열의 경로입니다 (기본: data)

) : ( /* 테이블 선택 (내부 DB 또는 외부 DB) */
테이블을 찾을 수 없습니다. {selectedDbSource === "internal" ? // 내부 DB 테이블 목록 tableList.map((table) => ( { console.log("📝 Internal table selected:", { tableName: table.tableName, currentValue, }); setFormData({ ...formData, tableName: currentValue }); setOpenTableCombobox(false); }} className="text-xs sm:text-sm" >
{table.displayName || table.tableName} {table.description && ( {table.description} )}
)) : // 외부 DB 테이블 목록 externalTableList.map((tableName, index) => ( { setFormData({ ...formData, tableName: currentValue }); setOpenTableCombobox(false); }} className="text-xs sm:text-sm" >
{tableName}
))}

플로우의 모든 단계에서 사용할 기본 테이블입니다 (단계마다 상태 컬럼만 지정합니다)

)}