플로우 외부db연결
This commit is contained in:
384
frontend/app/(main)/admin/flow-external-db/page.tsx
Normal file
384
frontend/app/(main)/admin/flow-external-db/page.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { flowExternalDbApi } from "@/lib/api/flowExternalDb";
|
||||
import {
|
||||
FlowExternalDbConnection,
|
||||
CreateFlowExternalDbConnectionRequest,
|
||||
UpdateFlowExternalDbConnectionRequest,
|
||||
DB_TYPE_OPTIONS,
|
||||
getDbTypeLabel,
|
||||
} from "@/types/flowExternalDb";
|
||||
import { Plus, Pencil, Trash2, TestTube, Loader2 } from "lucide-react";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
export default function FlowExternalDbPage() {
|
||||
const { toast } = useToast();
|
||||
const [connections, setConnections] = useState<FlowExternalDbConnection[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showDialog, setShowDialog] = useState(false);
|
||||
const [editingConnection, setEditingConnection] = useState<FlowExternalDbConnection | null>(null);
|
||||
const [testingId, setTestingId] = useState<number | null>(null);
|
||||
|
||||
// 폼 상태
|
||||
const [formData, setFormData] = useState<
|
||||
CreateFlowExternalDbConnectionRequest | UpdateFlowExternalDbConnectionRequest
|
||||
>({
|
||||
name: "",
|
||||
description: "",
|
||||
dbType: "postgresql",
|
||||
host: "",
|
||||
port: 5432,
|
||||
databaseName: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslEnabled: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadConnections();
|
||||
}, []);
|
||||
|
||||
const loadConnections = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await flowExternalDbApi.getAll();
|
||||
if (response.success) {
|
||||
setConnections(response.data);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message || "외부 DB 연결 목록 조회 실패",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setEditingConnection(null);
|
||||
setFormData({
|
||||
name: "",
|
||||
description: "",
|
||||
dbType: "postgresql",
|
||||
host: "",
|
||||
port: 5432,
|
||||
databaseName: "",
|
||||
username: "",
|
||||
password: "",
|
||||
sslEnabled: false,
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleEdit = (connection: FlowExternalDbConnection) => {
|
||||
setEditingConnection(connection);
|
||||
setFormData({
|
||||
name: connection.name,
|
||||
description: connection.description,
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
databaseName: connection.databaseName,
|
||||
username: connection.username,
|
||||
password: "", // 비밀번호는 비워둠
|
||||
sslEnabled: connection.sslEnabled,
|
||||
isActive: connection.isActive,
|
||||
});
|
||||
setShowDialog(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
if (editingConnection) {
|
||||
// 수정
|
||||
await flowExternalDbApi.update(editingConnection.id, formData);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 수정되었습니다" });
|
||||
} else {
|
||||
// 생성
|
||||
await flowExternalDbApi.create(formData as CreateFlowExternalDbConnectionRequest);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 생성되었습니다" });
|
||||
}
|
||||
setShowDialog(false);
|
||||
loadConnections();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number, name: string) => {
|
||||
if (!confirm(`"${name}" 연결을 삭제하시겠습니까?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await flowExternalDbApi.delete(id);
|
||||
toast({ title: "성공", description: "외부 DB 연결이 삭제되었습니다" });
|
||||
loadConnections();
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestConnection = async (id: number, name: string) => {
|
||||
setTestingId(id);
|
||||
try {
|
||||
const result = await flowExternalDbApi.testConnection(id);
|
||||
toast({
|
||||
title: result.success ? "연결 성공" : "연결 실패",
|
||||
description: result.message,
|
||||
variant: result.success ? "default" : "destructive",
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast({
|
||||
title: "오류",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
setTestingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold">외부 DB 연결 관리</h1>
|
||||
<p className="text-muted-foreground mt-1 text-sm">플로우에서 사용할 외부 데이터베이스 연결을 관리합니다</p>
|
||||
</div>
|
||||
<Button onClick={handleCreate}>
|
||||
<Plus className="mr-2 h-4 w-4" />새 연결 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : connections.length === 0 ? (
|
||||
<div className="bg-muted/50 rounded-lg border py-12 text-center">
|
||||
<p className="text-muted-foreground">등록된 외부 DB 연결이 없습니다</p>
|
||||
<Button onClick={handleCreate} className="mt-4">
|
||||
<Plus className="mr-2 h-4 w-4" />첫 연결 추가하기
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rounded-lg border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>이름</TableHead>
|
||||
<TableHead>DB 타입</TableHead>
|
||||
<TableHead>호스트</TableHead>
|
||||
<TableHead>데이터베이스</TableHead>
|
||||
<TableHead>상태</TableHead>
|
||||
<TableHead className="text-right">작업</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{connections.map((conn) => (
|
||||
<TableRow key={conn.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div>
|
||||
<div>{conn.name}</div>
|
||||
{conn.description && <div className="text-muted-foreground text-xs">{conn.description}</div>}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{getDbTypeLabel(conn.dbType)}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm">
|
||||
{conn.host}:{conn.port}
|
||||
</TableCell>
|
||||
<TableCell className="font-mono text-sm">{conn.databaseName}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={conn.isActive ? "default" : "secondary"}>{conn.isActive ? "활성" : "비활성"}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleTestConnection(conn.id, conn.name)}
|
||||
disabled={testingId === conn.id}
|
||||
>
|
||||
{testingId === conn.id ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<TestTube className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleEdit(conn)}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => handleDelete(conn.id, conn.name)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 생성/수정 다이얼로그 */}
|
||||
<Dialog open={showDialog} onOpenChange={setShowDialog}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingConnection ? "외부 DB 연결 수정" : "새 외부 DB 연결 추가"}</DialogTitle>
|
||||
<DialogDescription>외부 데이터베이스 연결 정보를 입력하세요</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="name">연결 이름 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
placeholder="예: 운영_PostgreSQL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="description">설명</Label>
|
||||
<Input
|
||||
id="description"
|
||||
value={formData.description || ""}
|
||||
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
|
||||
placeholder="연결에 대한 설명"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="dbType">DB 타입 *</Label>
|
||||
<Select
|
||||
value={formData.dbType}
|
||||
onValueChange={(value: any) => setFormData({ ...formData, dbType: value })}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DB_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<Label htmlFor="host">호스트 *</Label>
|
||||
<Input
|
||||
id="host"
|
||||
value={formData.host}
|
||||
onChange={(e) => setFormData({ ...formData, host: e.target.value })}
|
||||
placeholder="localhost"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<Label htmlFor="port">포트 *</Label>
|
||||
<Input
|
||||
id="port"
|
||||
type="number"
|
||||
value={formData.port}
|
||||
onChange={(e) => setFormData({ ...formData, port: parseInt(e.target.value) || 0 })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2">
|
||||
<Label htmlFor="databaseName">데이터베이스명 *</Label>
|
||||
<Input
|
||||
id="databaseName"
|
||||
value={formData.databaseName}
|
||||
onChange={(e) => setFormData({ ...formData, databaseName: e.target.value })}
|
||||
placeholder="mydb"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="username">사용자명 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username}
|
||||
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
|
||||
placeholder="dbuser"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">비밀번호 {editingConnection && "(변경 시에만 입력)"}</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={formData.password || ""}
|
||||
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
|
||||
placeholder={editingConnection ? "변경하지 않으려면 비워두세요" : "비밀번호"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Switch
|
||||
id="sslEnabled"
|
||||
checked={formData.sslEnabled}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, sslEnabled: checked })}
|
||||
/>
|
||||
<Label htmlFor="sslEnabled">SSL 사용</Label>
|
||||
</div>
|
||||
|
||||
{editingConnection && (
|
||||
<div className="col-span-2 flex items-center gap-2">
|
||||
<Switch
|
||||
id="isActive"
|
||||
checked={(formData as UpdateFlowExternalDbConnectionRequest).isActive ?? true}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
||||
/>
|
||||
<Label htmlFor="isActive">활성화</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button variant="outline" onClick={() => setShowDialog(false)}>
|
||||
취소
|
||||
</Button>
|
||||
<Button onClick={handleSave}>{editingConnection ? "수정" : "생성"}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* 선택된 단계의 속성 편집
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
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";
|
||||
@@ -18,6 +18,15 @@ 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;
|
||||
@@ -39,17 +48,29 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
statusValue: step.statusValue || "",
|
||||
targetTable: step.targetTable || "",
|
||||
fieldMappings: step.fieldMappings || {},
|
||||
// 외부 연동 필드
|
||||
integrationType: step.integrationType || "internal",
|
||||
integrationConfig: step.integrationConfig,
|
||||
});
|
||||
|
||||
const [tableList, setTableList] = useState<any[]>([]);
|
||||
const [loadingTables, setLoadingTables] = useState(true);
|
||||
const [openTableCombobox, setOpenTableCombobox] = useState(false);
|
||||
|
||||
// 외부 DB 테이블 목록
|
||||
const [externalTableList, setExternalTableList] = useState<string[]>([]);
|
||||
const [loadingExternalTables, setLoadingExternalTables] = useState(false);
|
||||
const [selectedDbSource, setSelectedDbSource] = useState<"internal" | number>("internal"); // "internal" 또는 외부 DB connection ID
|
||||
|
||||
// 컬럼 목록 (상태 컬럼 선택용)
|
||||
const [columns, setColumns] = useState<any[]>([]);
|
||||
const [loadingColumns, setLoadingColumns] = useState(false);
|
||||
const [openStatusColumnCombobox, setOpenStatusColumnCombobox] = useState(false);
|
||||
|
||||
// 외부 DB 연결 목록
|
||||
const [externalConnections, setExternalConnections] = useState<FlowExternalDbConnection[]>([]);
|
||||
const [loadingConnections, setLoadingConnections] = useState(false);
|
||||
|
||||
// 테이블 목록 조회
|
||||
useEffect(() => {
|
||||
const loadTables = async () => {
|
||||
@@ -68,8 +89,135 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
loadTables();
|
||||
}, []);
|
||||
|
||||
// 외부 DB 연결 목록 조회 (JWT 토큰 사용)
|
||||
useEffect(() => {
|
||||
setFormData({
|
||||
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,
|
||||
});
|
||||
|
||||
const newFormData = {
|
||||
stepName: step.stepName,
|
||||
tableName: step.tableName || "",
|
||||
conditionJson: step.conditionJson,
|
||||
@@ -79,8 +227,14 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
statusValue: step.statusValue || "",
|
||||
targetTable: step.targetTable || "",
|
||||
fieldMappings: step.fieldMappings || {},
|
||||
});
|
||||
}, [step]);
|
||||
// 외부 연동 필드
|
||||
integrationType: step.integrationType || "internal",
|
||||
integrationConfig: step.integrationConfig,
|
||||
};
|
||||
|
||||
console.log("✅ Setting formData:", newFormData);
|
||||
setFormData(newFormData);
|
||||
}, [step.id]); // step 전체가 아닌 step.id만 의존성으로 설정
|
||||
|
||||
// 테이블 선택 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
@@ -114,10 +268,21 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
loadColumns();
|
||||
}, [formData.tableName]);
|
||||
|
||||
// formData의 최신 값을 항상 참조하기 위한 ref
|
||||
const formDataRef = useRef(formData);
|
||||
|
||||
// formData가 변경될 때마다 ref 업데이트
|
||||
useEffect(() => {
|
||||
formDataRef.current = formData;
|
||||
}, [formData]);
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
const handleSave = useCallback(async () => {
|
||||
const currentFormData = formDataRef.current;
|
||||
console.log("🚀 handleSave called, formData:", JSON.stringify(currentFormData, null, 2));
|
||||
try {
|
||||
const response = await updateFlowStep(step.id, formData);
|
||||
const response = await updateFlowStep(step.id, currentFormData);
|
||||
console.log("📡 API response:", response);
|
||||
if (response.success) {
|
||||
toast({
|
||||
title: "저장 완료",
|
||||
@@ -139,7 +304,7 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [step.id, onUpdate, onClose, toast]);
|
||||
|
||||
// 삭제
|
||||
const handleDelete = async () => {
|
||||
@@ -203,6 +368,34 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
<Input value={step.stepOrder} disabled />
|
||||
</div>
|
||||
|
||||
{/* DB 소스 선택 */}
|
||||
<div>
|
||||
<Label>데이터베이스 소스</Label>
|
||||
<Select
|
||||
value={selectedDbSource.toString()}
|
||||
onValueChange={(value) => {
|
||||
const dbSource = value === "internal" ? "internal" : parseInt(value);
|
||||
setSelectedDbSource(dbSource);
|
||||
// DB 소스 변경 시 테이블 선택 초기화
|
||||
setFormData({ ...formData, tableName: "" });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="데이터베이스 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="internal">내부 데이터베이스</SelectItem>
|
||||
{externalConnections.map((conn: any) => (
|
||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||||
{conn.connection_name} ({conn.db_type?.toUpperCase()})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p className="mt-1 text-xs text-gray-500">조회할 데이터베이스를 선택합니다</p>
|
||||
</div>
|
||||
|
||||
{/* 테이블 선택 */}
|
||||
<div>
|
||||
<Label>조회할 테이블</Label>
|
||||
<Popover open={openTableCombobox} onOpenChange={setOpenTableCombobox}>
|
||||
@@ -212,50 +405,79 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
role="combobox"
|
||||
aria-expanded={openTableCombobox}
|
||||
className="w-full justify-between"
|
||||
disabled={loadingTables}
|
||||
disabled={loadingTables || (selectedDbSource !== "internal" && loadingExternalTables)}
|
||||
>
|
||||
{formData.tableName
|
||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||||
formData.tableName
|
||||
: loadingTables
|
||||
? selectedDbSource === "internal"
|
||||
? tableList.find((table) => table.tableName === formData.tableName)?.displayName ||
|
||||
formData.tableName
|
||||
: formData.tableName
|
||||
: loadingTables || loadingExternalTables
|
||||
? "로딩 중..."
|
||||
: "테이블 선택"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[400px] p-0">
|
||||
<PopoverContent className="p-0" style={{ width: "var(--radix-popover-trigger-width)" }} align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="테이블 검색..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>테이블을 찾을 수 없습니다.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{tableList.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
onSelect={(currentValue) => {
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||
{table.description && <span className="text-xs text-gray-500">{table.description}</span>}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
{selectedDbSource === "internal"
|
||||
? // 내부 DB 테이블 목록
|
||||
tableList.map((table) => (
|
||||
<CommandItem
|
||||
key={table.tableName}
|
||||
value={table.tableName}
|
||||
onSelect={(currentValue) => {
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === table.tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{table.displayName || table.tableName}</span>
|
||||
{table.description && (
|
||||
<span className="text-xs text-gray-500">{table.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
))
|
||||
: // 외부 DB 테이블 목록 (문자열 배열)
|
||||
externalTableList.map((tableName, index) => (
|
||||
<CommandItem
|
||||
key={`external-${selectedDbSource}-${tableName}-${index}`}
|
||||
value={tableName}
|
||||
onSelect={(currentValue) => {
|
||||
setFormData({ ...formData, tableName: currentValue });
|
||||
setOpenTableCombobox(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
formData.tableName === tableName ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div>{tableName}</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<p className="mt-1 text-xs text-gray-500">이 단계에서 조건을 적용할 테이블을 선택합니다</p>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
{selectedDbSource === "internal"
|
||||
? "이 단계에서 조건을 적용할 테이블을 선택합니다"
|
||||
: "외부 데이터베이스의 테이블을 선택합니다"}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -382,7 +604,12 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
<Label>이 단계의 상태값</Label>
|
||||
<Input
|
||||
value={formData.statusValue}
|
||||
onChange={(e) => setFormData({ ...formData, statusValue: e.target.value })}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
console.log("💡 statusValue onChange:", newValue);
|
||||
setFormData({ ...formData, statusValue: newValue });
|
||||
console.log("✅ Updated formData:", { ...formData, statusValue: newValue });
|
||||
}}
|
||||
placeholder="예: approved"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">이 단계에 있을 때의 상태값</p>
|
||||
@@ -423,6 +650,228 @@ export function FlowStepPanel({ step, flowId, onClose, onUpdate }: FlowStepPanel
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 외부 DB 연동 설정 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>외부 DB 연동 설정</CardTitle>
|
||||
<CardDescription>데이터 이동 시 외부 시스템과의 연동을 설정합니다</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label>연동 타입</Label>
|
||||
<Select
|
||||
value={formData.integrationType}
|
||||
onValueChange={(value: any) => {
|
||||
setFormData({ ...formData, integrationType: value });
|
||||
// 타입 변경 시 config 초기화
|
||||
if (value === "internal") {
|
||||
setFormData((prev) => ({ ...prev, integrationConfig: undefined }));
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{INTEGRATION_TYPE_OPTIONS.map((opt) => (
|
||||
<SelectItem
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
disabled={opt.value !== "internal" && opt.value !== "external_db"}
|
||||
>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 외부 DB 연동 설정 */}
|
||||
{formData.integrationType === "external_db" && (
|
||||
<div className="space-y-4 rounded-lg border p-4">
|
||||
{externalConnections.length === 0 ? (
|
||||
<div className="rounded-md bg-yellow-50 p-3">
|
||||
<p className="text-sm text-yellow-900">
|
||||
⚠️ 등록된 외부 DB 연결이 없습니다. 먼저 외부 DB 연결을 추가해주세요.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<Label>외부 DB 연결</Label>
|
||||
<Select
|
||||
value={formData.integrationConfig?.connectionId?.toString() || ""}
|
||||
onValueChange={(value) => {
|
||||
const connectionId = parseInt(value);
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
type: "external_db",
|
||||
connectionId,
|
||||
operation: "update",
|
||||
tableName: "",
|
||||
updateFields: {},
|
||||
whereCondition: {},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="연결 선택" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{externalConnections.map((conn) => (
|
||||
<SelectItem key={conn.id} value={conn.id.toString()}>
|
||||
{conn.name} ({conn.dbType})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{formData.integrationConfig?.connectionId && (
|
||||
<>
|
||||
<div>
|
||||
<Label>작업 타입</Label>
|
||||
<Select
|
||||
value={formData.integrationConfig.operation}
|
||||
onValueChange={(value: any) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
...formData.integrationConfig!,
|
||||
operation: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OPERATION_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>테이블명</Label>
|
||||
<Input
|
||||
value={formData.integrationConfig.tableName}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
...formData.integrationConfig!,
|
||||
tableName: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="예: orders"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.integrationConfig.operation === "custom" ? (
|
||||
<div>
|
||||
<Label>커스텀 쿼리</Label>
|
||||
<Textarea
|
||||
value={formData.integrationConfig.customQuery || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
...formData.integrationConfig!,
|
||||
customQuery: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
placeholder="UPDATE orders SET status = 'approved' WHERE id = {{dataId}}"
|
||||
rows={4}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
템플릿 변수: {`{{dataId}}, {{currentUser}}, {{currentTimestamp}}`}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{(formData.integrationConfig.operation === "update" ||
|
||||
formData.integrationConfig.operation === "insert") && (
|
||||
<div>
|
||||
<Label>업데이트할 필드 (JSON)</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(formData.integrationConfig.updateFields || {}, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
...formData.integrationConfig!,
|
||||
updateFields: parsed,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// JSON 파싱 실패 시 무시
|
||||
}
|
||||
}}
|
||||
placeholder='{"status": "approved", "updated_by": "{{currentUser}}"}'
|
||||
rows={4}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(formData.integrationConfig.operation === "update" ||
|
||||
formData.integrationConfig.operation === "delete") && (
|
||||
<div>
|
||||
<Label>WHERE 조건 (JSON)</Label>
|
||||
<Textarea
|
||||
value={JSON.stringify(formData.integrationConfig.whereCondition || {}, null, 2)}
|
||||
onChange={(e) => {
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value);
|
||||
setFormData({
|
||||
...formData,
|
||||
integrationConfig: {
|
||||
...formData.integrationConfig!,
|
||||
whereCondition: parsed,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
// JSON 파싱 실패 시 무시
|
||||
}
|
||||
}}
|
||||
placeholder='{"id": "{{dataId}}"}'
|
||||
rows={3}
|
||||
className="font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="rounded-md bg-blue-50 p-3">
|
||||
<p className="text-sm text-blue-900">
|
||||
💡 템플릿 변수를 사용하여 동적 값을 삽입할 수 있습니다:
|
||||
<br />• {`{{dataId}}`} - 이동하는 데이터의 ID
|
||||
<br />• {`{{currentUser}}`} - 현재 사용자
|
||||
<br />• {`{{currentTimestamp}}`} - 현재 시간
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 액션 버튼 */}
|
||||
<div className="flex gap-2">
|
||||
<Button className="flex-1" onClick={handleSave}>
|
||||
|
||||
139
frontend/lib/api/flowExternalDb.ts
Normal file
139
frontend/lib/api/flowExternalDb.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 플로우 전용 외부 DB 연결 API
|
||||
*/
|
||||
|
||||
import {
|
||||
FlowExternalDbConnection,
|
||||
CreateFlowExternalDbConnectionRequest,
|
||||
UpdateFlowExternalDbConnectionRequest,
|
||||
FlowExternalDbConnectionListResponse,
|
||||
FlowExternalDbConnectionResponse,
|
||||
FlowExternalDbConnectionTestResponse,
|
||||
} from "@/types/flowExternalDb";
|
||||
|
||||
const API_BASE = "/api/flow-external-db";
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 API
|
||||
*/
|
||||
export const flowExternalDbApi = {
|
||||
/**
|
||||
* 모든 외부 DB 연결 조회
|
||||
*/
|
||||
async getAll(activeOnly: boolean = false): Promise<FlowExternalDbConnectionListResponse> {
|
||||
const query = activeOnly ? "?activeOnly=true" : "";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}${query}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
// 오류 발생 시 빈 목록 반환 (조용히 실패)
|
||||
console.warn(`외부 DB 연결 목록 조회 실패: ${response.status} ${response.statusText}`);
|
||||
return { success: false, data: [] };
|
||||
}
|
||||
|
||||
return response.json();
|
||||
} catch (error) {
|
||||
// 네트워크 오류 등 예외 발생 시에도 빈 목록 반환
|
||||
console.error("외부 DB 연결 목록 조회 오류:", error);
|
||||
return { success: false, data: [] };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 특정 외부 DB 연결 조회
|
||||
*/
|
||||
async getById(id: number): Promise<FlowExternalDbConnectionResponse> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`외부 DB 연결 조회 실패: ${response.statusText}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 새 외부 DB 연결 생성
|
||||
*/
|
||||
async create(request: CreateFlowExternalDbConnectionRequest): Promise<FlowExternalDbConnectionResponse> {
|
||||
const response = await fetch(API_BASE, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || "외부 DB 연결 생성 실패");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 수정
|
||||
*/
|
||||
async update(id: number, request: UpdateFlowExternalDbConnectionRequest): Promise<FlowExternalDbConnectionResponse> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(request),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || "외부 DB 연결 수정 실패");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 삭제
|
||||
*/
|
||||
async delete(id: number): Promise<{ success: boolean; message: string }> {
|
||||
const response = await fetch(`${API_BASE}/${id}`, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || "외부 DB 연결 삭제 실패");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
/**
|
||||
* 외부 DB 연결 테스트
|
||||
*/
|
||||
async testConnection(id: number): Promise<FlowExternalDbConnectionTestResponse> {
|
||||
const response = await fetch(`${API_BASE}/${id}/test`, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
success: false,
|
||||
message: result.message || "연결 테스트 실패",
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
149
frontend/types/flowExternalDb.ts
Normal file
149
frontend/types/flowExternalDb.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* 플로우 전용 외부 DB 연동 타입 정의
|
||||
* (기존 제어관리 외부 DB와 별도)
|
||||
*/
|
||||
|
||||
// ==================== 연동 타입 ====================
|
||||
|
||||
export type FlowIntegrationType = "internal" | "external_db" | "rest_api" | "webhook" | "hybrid";
|
||||
|
||||
// ==================== 외부 DB 연결 ====================
|
||||
|
||||
export interface FlowExternalDbConnection {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
dbType: "postgresql" | "mysql" | "mssql" | "oracle";
|
||||
host: string;
|
||||
port: number;
|
||||
databaseName: string;
|
||||
username: string;
|
||||
passwordEncrypted: string; // 암호화된 비밀번호 (화면에는 표시하지 않음)
|
||||
sslEnabled: boolean;
|
||||
connectionOptions?: Record<string, any>;
|
||||
isActive: boolean;
|
||||
createdBy?: string;
|
||||
updatedBy?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreateFlowExternalDbConnectionRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
dbType: "postgresql" | "mysql" | "mssql" | "oracle";
|
||||
host: string;
|
||||
port: number;
|
||||
databaseName: string;
|
||||
username: string;
|
||||
password: string; // 평문 비밀번호 (생성 시에만 사용)
|
||||
sslEnabled?: boolean;
|
||||
connectionOptions?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface UpdateFlowExternalDbConnectionRequest {
|
||||
name?: string;
|
||||
description?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
databaseName?: string;
|
||||
username?: string;
|
||||
password?: string; // 평문 비밀번호 (변경 시에만)
|
||||
sslEnabled?: boolean;
|
||||
connectionOptions?: Record<string, any>;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
// ==================== 외부 DB 연동 설정 ====================
|
||||
|
||||
export interface FlowExternalDbIntegrationConfig {
|
||||
type: "external_db";
|
||||
connectionId: number; // 연결 ID
|
||||
operation: "update" | "insert" | "delete" | "custom";
|
||||
tableName: string;
|
||||
updateFields?: Record<string, any>; // 업데이트할 필드
|
||||
whereCondition?: Record<string, any>; // WHERE 조건
|
||||
customQuery?: string; // 커스텀 쿼리
|
||||
}
|
||||
|
||||
// 연동 설정 통합 타입
|
||||
export type FlowIntegrationConfig = FlowExternalDbIntegrationConfig;
|
||||
|
||||
// ==================== 연동 로그 ====================
|
||||
|
||||
export interface FlowIntegrationLog {
|
||||
id: number;
|
||||
flowDefinitionId: number;
|
||||
stepId: number;
|
||||
dataId?: string;
|
||||
integrationType: string;
|
||||
connectionId?: number;
|
||||
requestPayload?: Record<string, any>;
|
||||
responsePayload?: Record<string, any>;
|
||||
status: "success" | "failed" | "timeout" | "rollback";
|
||||
errorMessage?: string;
|
||||
executionTimeMs?: number;
|
||||
executedBy?: string;
|
||||
executedAt: string;
|
||||
}
|
||||
|
||||
// ==================== API 응답 ====================
|
||||
|
||||
export interface FlowExternalDbConnectionListResponse {
|
||||
success: boolean;
|
||||
data: FlowExternalDbConnection[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface FlowExternalDbConnectionResponse {
|
||||
success: boolean;
|
||||
data?: FlowExternalDbConnection;
|
||||
message?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface FlowExternalDbConnectionTestResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// ==================== UI 관련 ====================
|
||||
|
||||
export const DB_TYPE_OPTIONS = [
|
||||
{ value: "postgresql", label: "PostgreSQL" },
|
||||
{ value: "mysql", label: "MySQL" },
|
||||
{ value: "mssql", label: "MS SQL Server" },
|
||||
{ value: "oracle", label: "Oracle" },
|
||||
] as const;
|
||||
|
||||
export const OPERATION_OPTIONS = [
|
||||
{ value: "update", label: "업데이트 (UPDATE)" },
|
||||
{ value: "insert", label: "삽입 (INSERT)" },
|
||||
{ value: "delete", label: "삭제 (DELETE)" },
|
||||
{ value: "custom", label: "커스텀 쿼리" },
|
||||
] as const;
|
||||
|
||||
export const INTEGRATION_TYPE_OPTIONS = [
|
||||
{ value: "internal", label: "내부 DB (기본)" },
|
||||
{ value: "external_db", label: "외부 DB 연동" },
|
||||
{ value: "rest_api", label: "REST API (추후 지원)" },
|
||||
{ value: "webhook", label: "Webhook (추후 지원)" },
|
||||
{ value: "hybrid", label: "복합 연동 (추후 지원)" },
|
||||
] as const;
|
||||
|
||||
// ==================== 헬퍼 함수 ====================
|
||||
|
||||
export function getDbTypeLabel(dbType: string): string {
|
||||
const option = DB_TYPE_OPTIONS.find((opt) => opt.value === dbType);
|
||||
return option?.label || dbType;
|
||||
}
|
||||
|
||||
export function getOperationLabel(operation: string): string {
|
||||
const option = OPERATION_OPTIONS.find((opt) => opt.value === operation);
|
||||
return option?.label || operation;
|
||||
}
|
||||
|
||||
export function getIntegrationTypeLabel(type: string): string {
|
||||
const option = INTEGRATION_TYPE_OPTIONS.find((opt) => opt.value === type);
|
||||
return option?.label || type;
|
||||
}
|
||||
Reference in New Issue
Block a user