/** * 플로우 단계 설정 패널 * 선택된 단계의 속성 편집 */ import { useState, useEffect, useCallback, useRef } from "react"; import { X, Trash2, Save, Check, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useToast } from "@/hooks/use-toast"; import { updateFlowStep, deleteFlowStep } from "@/lib/api/flow"; import { FlowStep } from "@/types/flow"; import { FlowConditionBuilder } from "./FlowConditionBuilder"; import { tableManagementApi, getTableColumns } from "@/lib/api/tableManagement"; import { cn } from "@/lib/utils"; import { formatErrorMessage } from "@/lib/utils/errorUtils"; import { flowExternalDbApi } from "@/lib/api/flowExternalDb"; import { FlowExternalDbConnection, FlowExternalDbIntegrationConfig, INTEGRATION_TYPE_OPTIONS, OPERATION_OPTIONS, } from "@/types/flowExternalDb"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { ExternalDbConnectionAPI } from "@/lib/api/externalDbConnection"; // 다중 REST API 연결 설정 interface RestApiConnectionConfig { connectionId: number; connectionName: string; endpoint: string; jsonPath: string; alias: string; } interface FlowStepPanelProps { step: FlowStep; flowId: number; flowTableName?: string; // 플로우 정의에서 선택한 테이블명 flowDbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // 플로우의 DB 소스 타입 flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID flowRestApiConnectionId?: number; // 플로우의 REST API 연결 ID (단일) flowRestApiEndpoint?: string; // REST API 엔드포인트 (단일) flowRestApiJsonPath?: string; // REST API JSON 경로 (단일) flowRestApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 onClose: () => void; onUpdate: () => void; } export function FlowStepPanel({ step, flowId, flowTableName, flowDbSourceType = "internal", flowDbConnectionId, flowRestApiConnectionId, flowRestApiEndpoint, flowRestApiJsonPath, flowRestApiConnections, onClose, onUpdate, }: FlowStepPanelProps) { const { toast } = useToast(); console.log("🎯 FlowStepPanel Props:", { stepTableName: step.tableName, flowTableName, flowDbSourceType, flowDbConnectionId, flowRestApiConnectionId, flowRestApiEndpoint, flowRestApiJsonPath, final: step.tableName || flowTableName || "", }); const [formData, setFormData] = useState({ stepName: step.stepName, tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용 (신규 방식) conditionJson: step.conditionJson, // 하이브리드 모드 필드 moveType: step.moveType || "status", statusColumn: step.statusColumn || "", statusValue: step.statusValue || "", targetTable: step.targetTable || "", fieldMappings: step.fieldMappings || {}, // 외부 연동 필드 integrationType: step.integrationType || "internal", integrationConfig: step.integrationConfig, // 🆕 표시 설정 displayConfig: step.displayConfig || { visibleColumns: [] }, }); const [tableList, setTableList] = useState([]); const [loadingTables, setLoadingTables] = useState(true); const [openTableCombobox, setOpenTableCombobox] = useState(false); // 외부 DB 테이블 목록 const [externalTableList, setExternalTableList] = useState([]); const [loadingExternalTables, setLoadingExternalTables] = useState(false); const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID // 컬럼 목록 (상태 컬럼 선택용) const [columns, setColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [openStatusColumnCombobox, setOpenStatusColumnCombobox] = useState(false); // 외부 DB 연결 목록 const [externalConnections, setExternalConnections] = useState([]); const [loadingConnections, setLoadingConnections] = useState(false); // 🆕 표시 설정용 컬럼 목록 const [availableColumns, setAvailableColumns] = useState([]); const [loadingAvailableColumns, setLoadingAvailableColumns] = useState(false); // 테이블 목록 조회 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 연결 목록 조회 (JWT 토큰 사용) useEffect(() => { const loadConnections = async () => { try { setLoadingConnections(true); // localStorage에서 JWT 토큰 가져오기 const token = localStorage.getItem("authToken"); if (!token) { console.warn("토큰이 없습니다. 외부 DB 연결 목록을 조회할 수 없습니다."); setExternalConnections([]); return; } const response = await ExternalDbConnectionAPI.getActiveControlConnections(); if (response.success && response.data) { 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(); }, []); // 🆕 테이블이 선택되면 해당 테이블의 컬럼 목록 조회 (표시 설정용) useEffect(() => { const loadAvailableColumns = async () => { const tableName = formData.tableName || flowTableName; if (!tableName) { setAvailableColumns([]); return; } try { setLoadingAvailableColumns(true); const response = await getTableColumns(tableName); console.log("🎨 [FlowStepPanel] 컬럼 목록 API 응답:", { tableName, success: response.success, dataType: typeof response.data, dataKeys: response.data ? Object.keys(response.data) : [], isArray: Array.isArray(response.data), message: response.message, fullResponse: response, }); if (response.success && response.data) { // response.data가 객체일 경우 columns 배열 찾기 let columnsArray: any[] = []; if (Array.isArray(response.data)) { columnsArray = response.data; } else if (response.data.columns && Array.isArray(response.data.columns)) { columnsArray = response.data.columns; } else if (response.data.data && Array.isArray(response.data.data)) { columnsArray = response.data.data; } else { console.warn("⚠️ 예상치 못한 data 구조:", response.data); } const columnNames = columnsArray.map((col: any) => col.columnName || col.column_name); setAvailableColumns(columnNames); console.log("✅ [FlowStepPanel] 컬럼 목록 로드 성공:", { tableName, columns: columnNames, }); } else { console.warn("⚠️ [FlowStepPanel] 컬럼 목록 조회 실패:", { tableName, message: response.message, success: response.success, hasData: !!response.data, }); setAvailableColumns([]); } } catch (error) { console.error("❌ [FlowStepPanel] 컬럼 목록 로드 에러:", { tableName, error, }); setAvailableColumns([]); } finally { setLoadingAvailableColumns(false); } }; loadAvailableColumns(); }, [formData.tableName, flowTableName]); // 외부 DB 선택 시 해당 DB의 테이블 목록 조회 (JWT 토큰 사용) useEffect(() => { const loadExternalTables = async () => { console.log("🔍 loadExternalTables triggered, selectedDbSource:", selectedDbSource); if (selectedDbSource === "internal" || typeof selectedDbSource !== "number") { console.log("⚠️ Skipping external table load (internal or not a number)"); setExternalTableList([]); return; } console.log("📡 Loading external tables for connection ID:", selectedDbSource); try { setLoadingExternalTables(true); // localStorage에서 JWT 토큰 가져오기 const token = localStorage.getItem("authToken"); if (!token) { console.warn("토큰이 없습니다. 외부 DB 테이블 목록을 조회할 수 없습니다."); setExternalTableList([]); return; } // 기존 multi-connection API 사용 (JWT 토큰 포함) const response = await fetch(`/api/multi-connection/connections/${selectedDbSource}/tables`, { credentials: "include", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }).catch((err) => { console.warn("외부 DB 테이블 목록 fetch 실패:", err); return null; }); if (response && response.ok) { const result = await response.json(); console.log("✅ External tables API response:", result); console.log("📊 result.data type:", typeof result.data, "isArray:", Array.isArray(result.data)); console.log("📊 result.data:", JSON.stringify(result.data, null, 2)); if (result.success && result.data) { // 데이터 형식이 다를 수 있으므로 변환 const tableNames = result.data.map((t: any) => { console.log("🔍 Processing item:", t, "type:", typeof t); // tableName (camelCase), table_name, tablename, name 모두 지원 return typeof t === "string" ? t : t.tableName || t.table_name || t.tablename || t.name; }); console.log("📋 Processed table names:", tableNames); setExternalTableList(tableNames); } else { console.warn("❌ No data in response or success=false"); setExternalTableList([]); } } else { // 인증 오류 시에도 빈 배열로 처리 (리다이렉트 방지) console.warn(`외부 DB 테이블 목록 조회 실패: ${response?.status || "네트워크 오류"}`); setExternalTableList([]); } } catch (error) { console.error("외부 DB 테이블 목록 조회 오류:", error); setExternalTableList([]); } finally { setLoadingExternalTables(false); } }; loadExternalTables(); }, [selectedDbSource]); useEffect(() => { console.log("🔄 Initializing formData from step:", { id: step.id, stepName: step.stepName, statusColumn: step.statusColumn, statusValue: step.statusValue, flowTableName, // 플로우 정의의 테이블명 }); const newFormData = { stepName: step.stepName, tableName: step.tableName || flowTableName || "", // 플로우 테이블명 우선 사용 conditionJson: step.conditionJson, // 하이브리드 모드 필드 moveType: step.moveType || "status", statusColumn: step.statusColumn || "", statusValue: step.statusValue || "", targetTable: step.targetTable || "", fieldMappings: step.fieldMappings || {}, // 외부 연동 필드 integrationType: step.integrationType || "internal", integrationConfig: step.integrationConfig, // 표시 설정 (displayConfig 반드시 초기화) displayConfig: step.displayConfig || { visibleColumns: [] }, }; console.log("✅ Setting formData:", newFormData); setFormData(newFormData); }, [step.id, flowTableName]); // flowTableName도 의존성 추가 // 테이블 선택 시 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원 useEffect(() => { const loadColumns = async () => { // 다중 REST API인 경우 tableName 없이도 컬럼 로드 가능 if (!formData.tableName && flowDbSourceType !== "multi_restapi") { setColumns([]); return; } try { setLoadingColumns(true); console.log("🔍 Loading columns for status column selector:", { tableName: formData.tableName, flowDbSourceType, flowDbConnectionId, flowRestApiConnectionId, flowRestApiConnections, }); // 다중 REST API인 경우 if (flowDbSourceType === "multi_restapi" && flowRestApiConnections && flowRestApiConnections.length > 0) { console.log("🌐 다중 REST API 컬럼 로드 시작"); const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection"); const allColumns: any[] = []; for (const config of flowRestApiConnections) { try { const effectiveJsonPath = (!config.jsonPath || config.jsonPath === "data") ? "response" : config.jsonPath; const restApiData = await ExternalRestApiConnectionAPI.fetchData( config.connectionId, config.endpoint, effectiveJsonPath, ); if (restApiData.columns && restApiData.columns.length > 0) { const prefixedColumns = restApiData.columns.map((col) => ({ column_name: config.alias ? `${config.alias}${col.columnName}` : col.columnName, data_type: col.dataType || "varchar", displayName: `${col.columnLabel || col.columnName} (${config.connectionName})`, })); allColumns.push(...prefixedColumns); } } catch (apiError) { console.warn(`API ${config.connectionId} 컬럼 로드 실패:`, apiError); } } console.log("✅ 다중 REST API 컬럼 로드 완료:", allColumns.length, "items"); setColumns(allColumns); return; } // 단일 REST API인 경우 const isRestApi = flowDbSourceType === "restapi" || formData.tableName?.startsWith("_restapi_"); if (isRestApi && flowRestApiConnectionId) { console.log("🌐 단일 REST API 컬럼 로드 시작"); const { ExternalRestApiConnectionAPI } = await import("@/lib/api/externalRestApiConnection"); const effectiveJsonPath = (!flowRestApiJsonPath || flowRestApiJsonPath === "data") ? "response" : flowRestApiJsonPath; const restApiData = await ExternalRestApiConnectionAPI.fetchData( flowRestApiConnectionId, flowRestApiEndpoint, effectiveJsonPath, ); if (restApiData.columns && restApiData.columns.length > 0) { const columnList = restApiData.columns.map((col) => ({ column_name: col.columnName, data_type: col.dataType || "varchar", displayName: col.columnLabel || col.columnName, })); console.log("✅ REST API 컬럼 로드 완료:", columnList.length, "items"); setColumns(columnList); } else { setColumns([]); } return; } // 외부 DB인 경우 if (flowDbSourceType === "external" && flowDbConnectionId) { const token = localStorage.getItem("authToken"); if (!token) { console.warn("토큰이 없습니다. 외부 DB 컬럼 목록을 조회할 수 없습니다."); setColumns([]); return; } // 외부 DB 컬럼 조회 API const response = await fetch( `/api/multi-connection/connections/${flowDbConnectionId}/tables/${formData.tableName}/columns`, { credentials: "include", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, }, ).catch((err) => { console.warn("외부 DB 컬럼 목록 fetch 실패:", err); return null; }); if (response && response.ok) { const result = await response.json(); console.log("✅ External columns API response:", result); if (result.success && result.data) { // 컬럼 데이터 형식 통일 const columnList = Array.isArray(result.data) ? result.data.map((col: any) => ({ column_name: col.column_name || col.columnName || col.name, data_type: col.data_type || col.dataType || col.type, })) : []; console.log("✅ Setting external columns:", columnList); setColumns(columnList); } else { console.warn("❌ No data in external columns response"); setColumns([]); } } else { console.warn(`외부 DB 컬럼 목록 조회 실패: ${response?.status || "네트워크 오류"}`); setColumns([]); } } else { // 내부 DB인 경우 (기존 로직) const response = await getTableColumns(formData.tableName); console.log("📦 Internal columns response:", response); if (response.success && response.data && response.data.columns) { console.log("✅ Setting internal columns:", response.data.columns); setColumns(response.data.columns); } else { console.log("❌ No columns in response"); setColumns([]); } } } catch (error) { console.error("Failed to load columns:", error); setColumns([]); } finally { setLoadingColumns(false); } }; loadColumns(); }, [formData.tableName, flowDbSourceType, flowDbConnectionId, flowRestApiConnectionId, flowRestApiEndpoint, flowRestApiJsonPath, flowRestApiConnections]); // formData의 최신 값을 항상 참조하기 위한 ref const formDataRef = useRef(formData); // formData가 변경될 때마다 ref 업데이트 useEffect(() => { formDataRef.current = formData; }, [formData]); // 저장 const handleSave = useCallback(async () => { const currentFormData = formDataRef.current; console.log("🚀 handleSave called, formData:", JSON.stringify(currentFormData, null, 2)); // 상태 변경 방식일 때 필수 필드 검증 if (currentFormData.moveType === "status") { if (!currentFormData.statusColumn) { toast({ title: "입력 오류", description: "상태 변경 방식을 사용하려면 '상태 컬럼명'을 반드시 지정해야 합니다.", variant: "destructive", }); return; } if (!currentFormData.statusValue) { toast({ title: "입력 오류", description: "상태 변경 방식을 사용하려면 '이 단계의 상태값'을 반드시 지정해야 합니다.", variant: "destructive", }); return; } } try { const response = await updateFlowStep(step.id, currentFormData); console.log("📡 API response:", response); if (response.success) { toast({ title: "저장 완료", description: "단계가 수정되었습니다.", }); onUpdate(); onClose(); } else { toast({ title: "저장 실패", description: formatErrorMessage(response.error, "단계 저장 중 오류가 발생했습니다."), variant: "destructive", }); } } catch (error: any) { toast({ title: "오류 발생", description: error.message || "단계 저장 중 오류가 발생했습니다.", variant: "destructive", }); } }, [step.id, onUpdate, onClose, toast]); // 삭제 const handleDelete = async () => { if (!confirm(`"${step.stepName}" 단계를 삭제하시겠습니까?\n이 작업은 되돌릴 수 없습니다.`)) { return; } try { const response = await deleteFlowStep(step.id); if (response.success) { toast({ title: "삭제 완료", description: "단계가 삭제되었습니다.", }); onUpdate(); onClose(); } else { toast({ title: "삭제 실패", description: formatErrorMessage(response.error, "단계 삭제 중 오류가 발생했습니다."), variant: "destructive", }); } } catch (error: any) { toast({ title: "오류 발생", description: error.message || "단계 삭제 중 오류가 발생했습니다.", variant: "destructive", }); } }; return (
{/* 헤더 */}

단계 설정

{/* 기본 정보 */} 기본 정보
setFormData({ ...formData, stepName: e.target.value })} placeholder="단계 이름 입력" />
{/* ===== 구버전: 단계별 테이블 선택 방식 (주석처리) ===== */} {/* DB 소스 선택 */} {/*

조회할 데이터베이스를 선택합니다

*/} {/* 테이블 선택 */} {/*
테이블을 찾을 수 없습니다. {selectedDbSource === "internal" ? // 내부 DB 테이블 목록 tableList.map((table) => ( { setFormData({ ...formData, tableName: currentValue }); setOpenTableCombobox(false); }} >
{table.displayName || table.tableName} {table.description && ( {table.description} )}
)) : // 외부 DB 테이블 목록 (문자열 배열) externalTableList.map((tableName, index) => ( { setFormData({ ...formData, tableName: currentValue }); setOpenTableCombobox(false); }} >
{tableName}
))}

{selectedDbSource === "internal" ? "이 단계에서 조건을 적용할 테이블을 선택합니다" : "외부 데이터베이스의 테이블을 선택합니다"}

*/} {/* ===== 구버전 끝 ===== */} {/* ===== 신버전: 플로우에서 선택한 테이블 표시만 ===== */}

플로우 생성 시 선택한 테이블입니다 (수정 불가)

{/* ===== 신버전 끝 ===== */}
{/* 조건 설정 */} 조건 설정 이 단계에 포함될 데이터의 조건을 설정합니다 {!formData.tableName ? (
먼저 테이블을 선택해주세요
) : ( setFormData({ ...formData, conditionJson: condition })} /> )}
{/* 데이터 이동 설정 */} 데이터 이동 설정 다음 단계로 데이터를 이동할 때의 동작을 설정합니다 {/* 이동 방식 선택 */}
{/* 상태 변경 설정 (status 또는 both일 때) */} {(formData.moveType === "status" || formData.moveType === "both") && ( <>
컬럼을 찾을 수 없습니다. {columns.map((column, idx) => { const columnName = column.column_name || column.columnName || ""; const dataType = column.data_type || column.dataType || ""; return ( { setFormData({ ...formData, statusColumn: columnName }); setOpenStatusColumnCombobox(false); }} >
{columnName}
({dataType})
); })}

업데이트할 컬럼의 이름

{ const newValue = e.target.value; console.log("💡 statusValue onChange:", newValue); setFormData({ ...formData, statusValue: newValue }); console.log("✅ Updated formData:", { ...formData, statusValue: newValue }); }} placeholder="예: approved" />

이 단계에 있을 때의 상태값

)} {/* 테이블 이동 설정 (table 또는 both일 때) */} {(formData.moveType === "table" || formData.moveType === "both") && ( <>

다음 단계로 이동 시 데이터가 저장될 테이블

💡 필드 매핑은 향후 구현 예정입니다. 현재는 같은 이름의 컬럼만 자동 매핑됩니다.

)}
{/* 외부 DB 연동 설정 */} 외부 DB 연동 설정 데이터 이동 시 외부 시스템과의 연동을 설정합니다
{/* 외부 DB 연동 설정 */} {formData.integrationType === "external_db" && (
{externalConnections.length === 0 ? (

⚠️ 등록된 외부 DB 연결이 없습니다. 먼저 외부 DB 연결을 추가해주세요.

) : ( <>
{formData.integrationConfig?.connectionId && ( <>
setFormData({ ...formData, integrationConfig: { ...formData.integrationConfig!, tableName: e.target.value, }, }) } placeholder="예: orders" />
{formData.integrationConfig.operation === "custom" ? (