"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("response"); // 다중 REST API 선택 상태 interface RestApiConfig { connectionId: number; connectionName: string; endpoint: string; jsonPath: string; alias: string; // 컬럼 접두어 (예: "api1_") } const [selectedRestApis, setSelectedRestApis] = useState([]); const [isMultiRestApi, setIsMultiRestApi] = useState(false); // 다중 REST API 모드 // 다중 외부 DB 선택 상태 interface ExternalDbConfig { connectionId: number; connectionName: string; dbType: string; tableName: string; alias: string; // 컬럼 접두어 (예: "db1_") } const [selectedExternalDbs, setSelectedExternalDbs] = useState([]); const [isMultiExternalDb, setIsMultiExternalDb] = useState(false); // 다중 외부 DB 모드 const [multiDbTableLists, setMultiDbTableLists] = useState>({}); // 각 DB별 테이블 목록 // 생성 폼 상태 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]); // 다중 외부 DB 추가 const addExternalDbConfig = async (connectionId: number) => { const connection = externalConnections.find(c => c.id === connectionId); if (!connection) return; // 이미 추가된 경우 스킵 if (selectedExternalDbs.some(db => db.connectionId === connectionId)) { toast({ title: "이미 추가됨", description: "해당 외부 DB가 이미 추가되어 있습니다.", variant: "destructive", }); return; } // 해당 DB의 테이블 목록 로드 try { const data = await ExternalDbConnectionAPI.getTables(connectionId); 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); setMultiDbTableLists(prev => ({ ...prev, [connectionId]: tableNames })); } } catch (error) { console.error("외부 DB 테이블 목록 조회 오류:", error); } const newConfig: ExternalDbConfig = { connectionId, connectionName: connection.connection_name, dbType: connection.db_type, tableName: "", alias: `db${selectedExternalDbs.length + 1}_`, // 자동 별칭 생성 }; setSelectedExternalDbs([...selectedExternalDbs, newConfig]); }; // 다중 외부 DB 삭제 const removeExternalDbConfig = (connectionId: number) => { setSelectedExternalDbs(selectedExternalDbs.filter(db => db.connectionId !== connectionId)); }; // 다중 외부 DB 설정 업데이트 const updateExternalDbConfig = (connectionId: number, field: keyof ExternalDbConfig, value: string) => { setSelectedExternalDbs(selectedExternalDbs.map(db => db.connectionId === connectionId ? { ...db, [field]: value } : db )); }; // 다중 REST API 추가 const addRestApiConfig = (connectionId: number) => { const connection = restApiConnections.find(c => c.id === connectionId); if (!connection) return; // 이미 추가된 경우 스킵 if (selectedRestApis.some(api => api.connectionId === connectionId)) { toast({ title: "이미 추가됨", description: "해당 REST API가 이미 추가되어 있습니다.", variant: "destructive", }); return; } // 연결 테이블의 기본값 사용 const newConfig: RestApiConfig = { connectionId, connectionName: connection.connection_name, endpoint: connection.endpoint_path || "", // 연결 테이블의 기본 엔드포인트 jsonPath: "response", // 기본값 alias: `api${selectedRestApis.length + 1}_`, // 자동 별칭 생성 }; setSelectedRestApis([...selectedRestApis, newConfig]); }; // 다중 REST API 삭제 const removeRestApiConfig = (connectionId: number) => { setSelectedRestApis(selectedRestApis.filter(api => api.connectionId !== connectionId)); }; // 다중 REST API 설정 업데이트 const updateRestApiConfig = (connectionId: number, field: keyof RestApiConfig, value: string) => { setSelectedRestApis(selectedRestApis.map(api => api.connectionId === connectionId ? { ...api, [field]: value } : api )); }; // 플로우 생성 const handleCreate = async () => { console.log("🚀 handleCreate called with formData:", formData); // REST API 또는 다중 선택인 경우 테이블 이름 검증 스킵 const isRestApi = selectedDbSource.startsWith("restapi_") || isMultiRestApi; const isMultiMode = isMultiRestApi || isMultiExternalDb; if (!formData.name || (!isRestApi && !isMultiMode && !formData.tableName)) { console.log("❌ Validation failed:", { name: formData.name, tableName: formData.tableName, isRestApi, isMultiMode }); toast({ title: "입력 오류", description: (isRestApi || isMultiMode) ? "플로우 이름은 필수입니다." : "플로우 이름과 테이블 이름은 필수입니다.", variant: "destructive", }); return; } // 다중 REST API 모드인 경우 검증 if (isMultiRestApi) { if (selectedRestApis.length === 0) { toast({ title: "입력 오류", description: "최소 하나의 REST API를 추가해주세요.", variant: "destructive", }); return; } // 각 API의 엔드포인트 검증 const missingEndpoint = selectedRestApis.find(api => !api.endpoint); if (missingEndpoint) { toast({ title: "입력 오류", description: `${missingEndpoint.connectionName}의 엔드포인트를 입력해주세요.`, variant: "destructive", }); return; } } else if (isMultiExternalDb) { // 다중 외부 DB 모드인 경우 검증 if (selectedExternalDbs.length === 0) { toast({ title: "입력 오류", description: "최소 하나의 외부 DB를 추가해주세요.", variant: "destructive", }); return; } // 각 DB의 테이블 선택 검증 const missingTable = selectedExternalDbs.find(db => !db.tableName); if (missingTable) { toast({ title: "입력 오류", description: `${missingTable.connectionName}의 테이블을 선택해주세요.`, variant: "destructive", }); return; } } else if (isRestApi && !restApiEndpoint) { // 단일 REST API인 경우 엔드포인트 검증 toast({ title: "입력 오류", description: "REST API 엔드포인트는 필수입니다.", variant: "destructive", }); return; } try { // 데이터 소스 타입 및 ID 파싱 let dbSourceType: "internal" | "external" | "restapi" | "multi_restapi" | "multi_external_db" = "internal"; let dbConnectionId: number | undefined = undefined; let restApiConnectionId: number | undefined = undefined; if (isMultiRestApi) { dbSourceType = "multi_restapi"; } else if (isMultiExternalDb) { dbSourceType = "multi_external_db"; } else 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 === "multi_restapi") { requestData.restApiConnections = selectedRestApis; // 다중 REST API는 첫 번째 API의 ID를 기본으로 사용 requestData.restApiConnectionId = selectedRestApis[0]?.connectionId; requestData.restApiEndpoint = selectedRestApis[0]?.endpoint; requestData.restApiJsonPath = selectedRestApis[0]?.jsonPath || "response"; // 가상 테이블명: 모든 연결 ID를 조합 requestData.tableName = `_multi_restapi_${selectedRestApis.map(a => a.connectionId).join("_")}`; } else if (dbSourceType === "multi_external_db") { // 다중 외부 DB인 경우 requestData.externalDbConnections = selectedExternalDbs; // 첫 번째 DB의 ID를 기본으로 사용 requestData.dbConnectionId = selectedExternalDbs[0]?.connectionId; // 가상 테이블명: 모든 연결 ID와 테이블명 조합 requestData.tableName = `_multi_external_db_${selectedExternalDbs.map(db => `${db.connectionId}_${db.tableName}`).join("_")}`; } else if (dbSourceType === "restapi") { // 단일 REST API인 경우 requestData.restApiConnectionId = restApiConnectionId; requestData.restApiEndpoint = restApiEndpoint; requestData.restApiJsonPath = restApiJsonPath || "response"; // 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("response"); setSelectedRestApis([]); setSelectedExternalDbs([]); setIsMultiRestApi(false); setIsMultiExternalDb(false); 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 선택 UI */} {isMultiRestApi && (
{selectedRestApis.length === 0 ? (

위에서 REST API를 추가해주세요

) : (
{selectedRestApis.map((api) => (
{api.connectionName} ({api.endpoint || "기본 엔드포인트"})
))}
)}

선택한 REST API들의 데이터가 자동으로 병합됩니다.

)} {/* 다중 외부 DB 선택 UI */} {isMultiExternalDb && (
{selectedExternalDbs.length === 0 ? (

위에서 외부 DB를 추가해주세요

) : (
{selectedExternalDbs.map((db) => (
{db.connectionName} ({db.dbType?.toUpperCase()})
updateExternalDbConfig(db.connectionId, "alias", e.target.value)} placeholder="db1_" className="h-7 text-xs" />
))}
)}

선택한 외부 DB들의 데이터가 자동으로 병합됩니다. 각 DB별 테이블을 선택해주세요.

)} {/* 단일 REST API인 경우 엔드포인트 설정 */} {!isMultiRestApi && 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 - 다중 선택 모드가 아닌 경우만) */} {!isMultiRestApi && !isMultiExternalDb && !selectedDbSource.startsWith("restapi_") && (
테이블을 찾을 수 없습니다. {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}
))}

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

)}