플로우 외부db연결
This commit is contained in:
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user