/** * 플로우 단계 설정 패널 * 선택된 단계의 속성 편집 */ 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 { 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"; interface FlowStepPanelProps { step: FlowStep; flowId: number; flowTableName?: string; // 플로우 정의에서 선택한 테이블명 flowDbSourceType?: "internal" | "external"; // 플로우의 DB 소스 타입 flowDbConnectionId?: number; // 플로우의 외부 DB 연결 ID onClose: () => void; onUpdate: () => void; } export function FlowStepPanel({ step, flowId, flowTableName, flowDbSourceType = "internal", flowDbConnectionId, onClose, onUpdate, }: FlowStepPanelProps) { const { toast } = useToast(); console.log("🎯 FlowStepPanel Props:", { stepTableName: step.tableName, flowTableName, flowDbSourceType, flowDbConnectionId, 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, }); 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); // 테이블 목록 조회 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 fetch("/api/external-db-connections/control/active", { 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(); if (result.success && result.data) { // 메인 DB 제외하고 외부 DB만 필터링 const externalOnly = result.data.filter((conn: any) => conn.id !== 0); setExternalConnections(externalOnly); } else { setExternalConnections([]); } } else { // 401 오류 시 빈 배열로 처리 (리다이렉트 방지) console.warn("외부 DB 연결 목록 조회 실패:", response?.status || "네트워크 오류"); setExternalConnections([]); } } catch (error: any) { console.error("Failed to load external connections:", error); setExternalConnections([]); } finally { setLoadingConnections(false); } }; loadConnections(); }, []); // 외부 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, }; console.log("✅ Setting formData:", newFormData); setFormData(newFormData); }, [step.id, flowTableName]); // flowTableName도 의존성 추가 // 테이블 선택 시 컬럼 로드 - 내부/외부 DB 모두 지원 useEffect(() => { const loadColumns = async () => { if (!formData.tableName) { setColumns([]); return; } try { setLoadingColumns(true); console.log("🔍 Loading columns for status column selector:", { tableName: formData.tableName, flowDbSourceType, flowDbConnectionId, }); // 외부 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]); // 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: 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: 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) => ( { setFormData({ ...formData, statusColumn: column.columnName }); setOpenStatusColumnCombobox(false); }} >
{column.columnName}
({column.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" ? (