/** * 플로우 조건 빌더 * 동적 조건 생성 UI */ import { useState, useEffect } from "react"; import { Plus, Trash2, Check, ChevronsUpDown } from "lucide-react"; import { Button } from "@/components/ui/button"; 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 { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Badge } from "@/components/ui/badge"; import { FlowConditionGroup, FlowCondition, ConditionOperator } from "@/types/flow"; import { getTableColumns } from "@/lib/api/tableManagement"; import { ExternalRestApiConnectionAPI } from "@/lib/api/externalRestApiConnection"; import { cn } from "@/lib/utils"; // 다중 REST API 연결 설정 interface RestApiConnectionConfig { connectionId: number; connectionName: string; endpoint: string; jsonPath: string; alias: string; } interface FlowConditionBuilderProps { flowId: number; tableName?: string; // 조회할 테이블명 dbSourceType?: "internal" | "external" | "restapi" | "multi_restapi"; // DB 소스 타입 dbConnectionId?: number; // 외부 DB 연결 ID restApiConnectionId?: number; // REST API 연결 ID (단일) restApiEndpoint?: string; // REST API 엔드포인트 (단일) restApiJsonPath?: string; // REST API JSON 경로 (단일) restApiConnections?: RestApiConnectionConfig[]; // 다중 REST API 설정 condition?: FlowConditionGroup; onChange: (condition: FlowConditionGroup | undefined) => void; } const OPERATORS: { value: ConditionOperator; label: string }[] = [ { value: "equals", label: "같음 (=)" }, { value: "not_equals", label: "같지 않음 (!=)" }, { value: "greater_than", label: "보다 큼 (>)" }, { value: "less_than", label: "보다 작음 (<)" }, { value: "greater_than_or_equal", label: "이상 (>=)" }, { value: "less_than_or_equal", label: "이하 (<=)" }, { value: "in", label: "포함 (IN)" }, { value: "not_in", label: "제외 (NOT IN)" }, { value: "like", label: "유사 (LIKE)" }, { value: "not_like", label: "유사하지 않음 (NOT LIKE)" }, { value: "is_null", label: "NULL" }, { value: "is_not_null", label: "NOT NULL" }, ]; export function FlowConditionBuilder({ flowId, tableName, dbSourceType = "internal", dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath, restApiConnections, condition, onChange, }: FlowConditionBuilderProps) { const [columns, setColumns] = useState([]); const [loadingColumns, setLoadingColumns] = useState(false); const [conditionType, setConditionType] = useState<"AND" | "OR">(condition?.type || "AND"); const [conditions, setConditions] = useState(condition?.conditions || []); const [columnComboboxOpen, setColumnComboboxOpen] = useState>({}); // condition prop이 변경될 때 상태 동기화 useEffect(() => { if (condition) { setConditionType(condition.type || "AND"); setConditions(condition.conditions || []); } else { setConditionType("AND"); setConditions([]); } }, [condition]); // 테이블 컬럼 로드 - 내부/외부 DB 및 REST API 모두 지원 useEffect(() => { // REST API인 경우 tableName이 없어도 진행 가능 if (!tableName && dbSourceType !== "restapi" && dbSourceType !== "multi_restapi") { setColumns([]); return; } const loadColumns = async () => { try { setLoadingColumns(true); console.log("🔍 [FlowConditionBuilder] Loading columns:", { tableName, dbSourceType, dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath, restApiConnections, }); // 다중 REST API인 경우 if (dbSourceType === "multi_restapi" && restApiConnections && restApiConnections.length > 0) { try { console.log("🌐 [FlowConditionBuilder] 다중 REST API 컬럼 로드 시작:", restApiConnections); // 각 API에서 컬럼 정보 수집 const allColumns: any[] = []; for (const config of restApiConnections) { 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})`, sourceApi: config.connectionName, })); allColumns.push(...prefixedColumns); } } catch (apiError) { console.warn(`API ${config.connectionId} 컬럼 로드 실패:`, apiError); } } console.log("✅ [FlowConditionBuilder] 다중 REST API 컬럼 로드 완료:", allColumns.length, "items"); setColumns(allColumns); } catch (multiApiError) { console.error("❌ 다중 REST API 컬럼 로드 실패:", multiApiError); setColumns([]); } return; } // 단일 REST API인 경우 (dbSourceType이 restapi이거나 tableName이 _restapi_로 시작) const isRestApi = dbSourceType === "restapi" || tableName?.startsWith("_restapi_"); // tableName에서 REST API 연결 ID 추출 (restApiConnectionId가 없는 경우) let effectiveRestApiConnectionId = restApiConnectionId; if (isRestApi && !effectiveRestApiConnectionId && tableName) { const match = tableName.match(/_restapi_(\d+)/); if (match) { effectiveRestApiConnectionId = parseInt(match[1]); console.log("🔍 tableName에서 REST API 연결 ID 추출:", effectiveRestApiConnectionId); } } if (isRestApi && effectiveRestApiConnectionId) { try { // jsonPath가 "data"이거나 없으면 "response"로 변경 (thiratis API 응답 구조에 맞춤) const effectiveJsonPath = (!restApiJsonPath || restApiJsonPath === "data") ? "response" : restApiJsonPath; console.log("🌐 [FlowConditionBuilder] REST API 컬럼 로드 시작:", { connectionId: effectiveRestApiConnectionId, endpoint: restApiEndpoint, jsonPath: restApiJsonPath, effectiveJsonPath, }); const restApiData = await ExternalRestApiConnectionAPI.fetchData( effectiveRestApiConnectionId, restApiEndpoint, effectiveJsonPath, ); console.log("✅ [FlowConditionBuilder] REST API columns response:", restApiData); 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("✅ Setting REST API columns:", columnList.length, "items", columnList); setColumns(columnList); } else { console.warn("❌ No columns in REST API response"); setColumns([]); } } catch (restApiError) { console.error("❌ REST API 컬럼 로드 실패:", restApiError); setColumns([]); } return; } // 외부 DB인 경우 if (dbSourceType === "external" && dbConnectionId) { const token = localStorage.getItem("authToken"); if (!token) { console.warn("토큰이 없습니다. 외부 DB 컬럼 목록을 조회할 수 없습니다."); setColumns([]); return; } const response = await fetch( `/api/multi-connection/connections/${dbConnectionId}/tables/${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("✅ [FlowConditionBuilder] External columns 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.length, "items"); 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(tableName); console.log("📦 [FlowConditionBuilder] Internal columns response:", response); if (response.success && response.data?.columns) { const columnArray = Array.isArray(response.data.columns) ? response.data.columns : []; console.log("✅ Setting internal columns:", columnArray.length, "items"); setColumns(columnArray); } else { console.error("❌ Failed to load internal columns:", response.message); setColumns([]); } } } catch (error) { console.error("❌ Exception loading columns:", error); setColumns([]); } finally { setLoadingColumns(false); } }; loadColumns(); }, [tableName, dbSourceType, dbConnectionId, restApiConnectionId, restApiEndpoint, restApiJsonPath]); // 조건 변경 시 부모에 전달 useEffect(() => { if (conditions.length === 0) { onChange(undefined); } else { onChange({ type: conditionType, conditions, }); } }, [conditionType, conditions]); // 조건 추가 const addCondition = () => { setConditions([ ...conditions, { column: "", operator: "equals", value: "", }, ]); }; // 조건 수정 const updateCondition = (index: number, field: keyof FlowCondition, value: any) => { const newConditions = [...conditions]; newConditions[index] = { ...newConditions[index], [field]: value, }; setConditions(newConditions); }; // 조건 삭제 const removeCondition = (index: number) => { setConditions(conditions.filter((_, i) => i !== index)); }; // value가 필요 없는 연산자 체크 const needsValue = (operator: ConditionOperator) => { return operator !== "is_null" && operator !== "is_not_null"; }; return (
{/* 조건 타입 선택 */}
{/* 조건 목록 */}
{conditions.length === 0 ? (
조건이 없습니다
조건을 추가하여 데이터를 필터링하세요
) : ( conditions.map((cond, index) => (
{/* 조건 번호 및 삭제 버튼 */}
조건 {index + 1}
{/* 컬럼 선택 */}
{loadingColumns ? ( ) : !Array.isArray(columns) || columns.length === 0 ? ( updateCondition(index, "column", e.target.value)} placeholder="테이블을 먼저 선택하세요" className="h-8" /> ) : ( setColumnComboboxOpen({ ...columnComboboxOpen, [index]: open })} > 컬럼을 찾을 수 없습니다. {columns.map((col, idx) => { const columnName = col.column_name || col.columnName || ""; const dataType = col.data_type || col.dataType || ""; const displayName = col.displayName || col.display_name || columnName; return ( { updateCondition(index, "column", currentValue); setColumnComboboxOpen({ ...columnComboboxOpen, [index]: false }); }} className="text-xs" >
{displayName} ({dataType})
); })}
)}
{/* 연산자 선택 */}
{/* 값 입력 */} {needsValue(cond.operator) && (
updateCondition(index, "value", e.target.value)} placeholder="값 입력" className="h-8" /> {(cond.operator === "in" || cond.operator === "not_in") && (

쉼표(,)로 구분하여 여러 값 입력

)}
)}
)) )}
{/* 조건 추가 버튼 */} {/* 조건 요약 */} {conditions.length > 0 && (
조건 요약:
{conditions.map((cond, index) => (
{cond.column} {OPERATORS.find((op) => op.value === cond.operator)?.label} {needsValue(cond.operator) && {cond.value}} {index < conditions.length - 1 && {conditionType}}
))}
)}
); }