2025-09-05 16:19:31 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect } from "react";
|
|
|
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
|
|
|
|
|
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 { Badge } from "@/components/ui/badge";
|
|
|
|
|
|
import { Textarea } from "@/components/ui/textarea";
|
2025-09-12 09:58:49 +09:00
|
|
|
|
import { Link, Key, Save, Globe, Plus, Zap, Trash2 } from "lucide-react";
|
|
|
|
|
|
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
|
2025-09-08 16:46:53 +09:00
|
|
|
|
import toast from "react-hot-toast";
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
|
|
|
|
|
// 연결 정보 타입
|
|
|
|
|
|
interface ConnectionInfo {
|
|
|
|
|
|
fromNode: {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
tableName: string;
|
2025-09-05 18:00:18 +09:00
|
|
|
|
displayName: string;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
toNode: {
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
tableName: string;
|
2025-09-05 18:00:18 +09:00
|
|
|
|
displayName: string;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
2025-09-05 18:00:18 +09:00
|
|
|
|
fromColumn?: string;
|
|
|
|
|
|
toColumn?: string;
|
|
|
|
|
|
selectedColumnsData?: {
|
|
|
|
|
|
[tableName: string]: {
|
|
|
|
|
|
displayName: string;
|
|
|
|
|
|
columns: string[];
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-09-11 10:45:16 +09:00
|
|
|
|
existingRelationship?: {
|
|
|
|
|
|
relationshipName: string;
|
|
|
|
|
|
relationshipType: string;
|
|
|
|
|
|
connectionType: string;
|
2025-09-12 09:58:49 +09:00
|
|
|
|
settings?: Record<string, unknown>;
|
2025-09-11 10:45:16 +09:00
|
|
|
|
};
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 연결 설정 타입
|
|
|
|
|
|
interface ConnectionConfig {
|
|
|
|
|
|
relationshipName: string;
|
|
|
|
|
|
relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
|
|
|
|
|
connectionType: "simple-key" | "data-save" | "external-call";
|
2025-09-05 18:00:18 +09:00
|
|
|
|
fromColumnName: string;
|
|
|
|
|
|
toColumnName: string;
|
2025-09-08 16:46:53 +09:00
|
|
|
|
settings?: Record<string, unknown>;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
description?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
// 단순 키값 연결 설정
|
|
|
|
|
|
interface SimpleKeySettings {
|
|
|
|
|
|
notes: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터 저장 설정
|
|
|
|
|
|
interface DataSaveSettings {
|
2025-09-12 16:15:36 +09:00
|
|
|
|
actions: Array<{
|
|
|
|
|
|
id: string;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
actionType: "insert" | "update" | "delete" | "upsert";
|
2025-09-15 11:17:46 +09:00
|
|
|
|
conditions?: ConditionNode[];
|
2025-09-12 16:15:36 +09:00
|
|
|
|
fieldMappings: Array<{
|
|
|
|
|
|
sourceTable?: string;
|
2025-09-15 11:17:46 +09:00
|
|
|
|
sourceField: string;
|
2025-09-12 16:15:36 +09:00
|
|
|
|
targetTable?: string;
|
2025-09-15 11:17:46 +09:00
|
|
|
|
targetField: string;
|
2025-09-12 16:15:36 +09:00
|
|
|
|
defaultValue?: string;
|
|
|
|
|
|
transformFunction?: string;
|
|
|
|
|
|
}>;
|
|
|
|
|
|
splitConfig?: {
|
|
|
|
|
|
sourceField: string; // 분할할 소스 필드
|
|
|
|
|
|
delimiter: string; // 구분자 (예: ",")
|
|
|
|
|
|
targetField: string; // 분할된 값이 들어갈 필드
|
|
|
|
|
|
};
|
|
|
|
|
|
}>;
|
2025-09-08 16:46:53 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 외부 호출 설정
|
|
|
|
|
|
interface ExternalCallSettings {
|
|
|
|
|
|
callType: "rest-api" | "email" | "webhook" | "ftp" | "queue";
|
|
|
|
|
|
apiUrl?: string;
|
|
|
|
|
|
httpMethod?: "GET" | "POST" | "PUT" | "DELETE";
|
|
|
|
|
|
headers?: string;
|
|
|
|
|
|
bodyTemplate?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-05 16:19:31 +09:00
|
|
|
|
interface ConnectionSetupModalProps {
|
|
|
|
|
|
isOpen: boolean;
|
|
|
|
|
|
connection: ConnectionInfo | null;
|
2025-09-08 16:46:53 +09:00
|
|
|
|
companyCode: string;
|
|
|
|
|
|
onConfirm: (relationship: TableRelationship) => void;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
onCancel: () => void;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|
|
|
|
|
isOpen,
|
|
|
|
|
|
connection,
|
2025-09-08 16:46:53 +09:00
|
|
|
|
companyCode,
|
2025-09-05 16:19:31 +09:00
|
|
|
|
onConfirm,
|
|
|
|
|
|
onCancel,
|
|
|
|
|
|
}) => {
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const [config, setConfig] = useState<ConnectionConfig>({
|
|
|
|
|
|
relationshipName: "",
|
|
|
|
|
|
relationshipType: "one-to-one",
|
|
|
|
|
|
connectionType: "simple-key",
|
|
|
|
|
|
fromColumnName: "",
|
|
|
|
|
|
toColumnName: "",
|
|
|
|
|
|
description: "",
|
2025-09-08 16:46:53 +09:00
|
|
|
|
settings: {},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 연결 종류별 설정 상태
|
|
|
|
|
|
const [simpleKeySettings, setSimpleKeySettings] = useState<SimpleKeySettings>({
|
|
|
|
|
|
notes: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [dataSaveSettings, setDataSaveSettings] = useState<DataSaveSettings>({
|
2025-09-12 16:15:36 +09:00
|
|
|
|
actions: [],
|
2025-09-08 16:46:53 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [externalCallSettings, setExternalCallSettings] = useState<ExternalCallSettings>({
|
|
|
|
|
|
callType: "rest-api",
|
|
|
|
|
|
apiUrl: "",
|
|
|
|
|
|
httpMethod: "POST",
|
|
|
|
|
|
headers: "{}",
|
|
|
|
|
|
bodyTemplate: "{}",
|
2025-09-05 18:00:18 +09:00
|
|
|
|
});
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 테이블 및 컬럼 선택을 위한 새로운 상태들
|
|
|
|
|
|
const [availableTables, setAvailableTables] = useState<TableInfo[]>([]);
|
|
|
|
|
|
const [selectedFromTable, setSelectedFromTable] = useState<string>("");
|
|
|
|
|
|
const [selectedToTable, setSelectedToTable] = useState<string>("");
|
|
|
|
|
|
const [fromTableColumns, setFromTableColumns] = useState<ColumnInfo[]>([]);
|
|
|
|
|
|
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
|
|
|
|
|
|
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
|
|
|
|
|
|
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
|
2025-09-12 16:15:36 +09:00
|
|
|
|
// 필요시 로드하는 테이블 컬럼 캐시
|
|
|
|
|
|
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
// 조건부 연결을 위한 새로운 상태들
|
|
|
|
|
|
const [conditions, setConditions] = useState<ConditionNode[]>([]);
|
|
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 테이블 목록 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadTables = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const tables = await DataFlowAPI.getTables();
|
|
|
|
|
|
setAvailableTables(tables);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("테이블 목록 로드 실패:", error);
|
|
|
|
|
|
toast.error("테이블 목록을 불러오는데 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isOpen) {
|
|
|
|
|
|
loadTables();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [isOpen]);
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
// 모달이 열릴 때 기본값 설정
|
2025-09-05 16:19:31 +09:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (isOpen && connection) {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
const fromTableName = connection.fromNode.tableName;
|
|
|
|
|
|
const toTableName = connection.toNode.tableName;
|
|
|
|
|
|
const fromDisplayName = connection.fromNode.displayName;
|
|
|
|
|
|
const toDisplayName = connection.toNode.displayName;
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 선택 설정
|
|
|
|
|
|
setSelectedFromTable(fromTableName);
|
|
|
|
|
|
setSelectedToTable(toTableName);
|
2025-09-05 18:00:18 +09:00
|
|
|
|
|
2025-09-11 10:45:16 +09:00
|
|
|
|
// 기존 관계 정보가 있으면 사용, 없으면 기본값 설정
|
|
|
|
|
|
const existingRel = connection.existingRelationship;
|
2025-09-05 18:00:18 +09:00
|
|
|
|
setConfig({
|
2025-09-11 10:45:16 +09:00
|
|
|
|
relationshipName: existingRel?.relationshipName || `${fromDisplayName} → ${toDisplayName}`,
|
|
|
|
|
|
relationshipType:
|
|
|
|
|
|
(existingRel?.relationshipType as "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many") ||
|
|
|
|
|
|
"one-to-one",
|
|
|
|
|
|
connectionType: (existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key",
|
2025-09-05 18:00:18 +09:00
|
|
|
|
fromColumnName: "",
|
|
|
|
|
|
toColumnName: "",
|
2025-09-12 09:58:49 +09:00
|
|
|
|
description:
|
|
|
|
|
|
(existingRel?.settings?.description as string) || `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
2025-09-11 10:45:16 +09:00
|
|
|
|
settings: existingRel?.settings || {},
|
2025-09-08 16:46:53 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 단순 키값 연결 기본값 설정
|
|
|
|
|
|
setSimpleKeySettings({
|
2025-09-10 17:25:41 +09:00
|
|
|
|
notes: `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`,
|
2025-09-08 16:46:53 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-15 10:11:22 +09:00
|
|
|
|
// 데이터 저장 기본값 설정 (빈 배열로 시작)
|
2025-09-08 16:46:53 +09:00
|
|
|
|
setDataSaveSettings({
|
2025-09-15 10:11:22 +09:00
|
|
|
|
actions: [],
|
2025-09-08 16:46:53 +09:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 외부 호출 기본값 설정
|
|
|
|
|
|
setExternalCallSettings({
|
|
|
|
|
|
callType: "rest-api",
|
|
|
|
|
|
apiUrl: "https://api.example.com/webhook",
|
|
|
|
|
|
httpMethod: "POST",
|
|
|
|
|
|
headers: "{}",
|
|
|
|
|
|
bodyTemplate: "{}",
|
2025-09-05 18:00:18 +09:00
|
|
|
|
});
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
|
|
|
|
|
// 선택된 컬럼 정보가 있다면 설정
|
|
|
|
|
|
if (connection.selectedColumnsData) {
|
|
|
|
|
|
const fromColumns = connection.selectedColumnsData[fromTableName]?.columns || [];
|
|
|
|
|
|
const toColumns = connection.selectedColumnsData[toTableName]?.columns || [];
|
|
|
|
|
|
|
|
|
|
|
|
setSelectedFromColumns(fromColumns);
|
|
|
|
|
|
setSelectedToColumns(toColumns);
|
|
|
|
|
|
|
|
|
|
|
|
setConfig((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
fromColumnName: fromColumns.join(", "),
|
|
|
|
|
|
toColumnName: toColumns.join(", "),
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}
|
|
|
|
|
|
}, [isOpen, connection]);
|
|
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// From 테이블 선택 시 컬럼 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadFromColumns = async () => {
|
|
|
|
|
|
if (selectedFromTable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const columns = await DataFlowAPI.getTableColumns(selectedFromTable);
|
|
|
|
|
|
setFromTableColumns(columns);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("From 테이블 컬럼 로드 실패:", error);
|
|
|
|
|
|
toast.error("From 테이블 컬럼을 불러오는데 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadFromColumns();
|
|
|
|
|
|
}, [selectedFromTable]);
|
|
|
|
|
|
|
|
|
|
|
|
// To 테이블 선택 시 컬럼 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadToColumns = async () => {
|
|
|
|
|
|
if (selectedToTable) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const columns = await DataFlowAPI.getTableColumns(selectedToTable);
|
|
|
|
|
|
setToTableColumns(columns);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error("To 테이블 컬럼 로드 실패:", error);
|
|
|
|
|
|
toast.error("To 테이블 컬럼을 불러오는데 실패했습니다.");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadToColumns();
|
|
|
|
|
|
}, [selectedToTable]);
|
|
|
|
|
|
|
|
|
|
|
|
// 선택된 컬럼들이 변경될 때 config 업데이트
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
setConfig((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
fromColumnName: selectedFromColumns.join(", "),
|
|
|
|
|
|
toColumnName: selectedToColumns.join(", "),
|
|
|
|
|
|
}));
|
|
|
|
|
|
}, [selectedFromColumns, selectedToColumns]);
|
|
|
|
|
|
|
2025-09-12 16:15:36 +09:00
|
|
|
|
// 테이블 컬럼 로드 함수 (캐시 활용)
|
|
|
|
|
|
const loadTableColumns = async (tableName: string): Promise<ColumnInfo[]> => {
|
|
|
|
|
|
if (tableColumnsCache[tableName]) {
|
|
|
|
|
|
return tableColumnsCache[tableName];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const columns = await DataFlowAPI.getTableColumns(tableName);
|
|
|
|
|
|
setTableColumnsCache((prev) => ({
|
|
|
|
|
|
...prev,
|
|
|
|
|
|
[tableName]: columns,
|
|
|
|
|
|
}));
|
|
|
|
|
|
return columns;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error(`${tableName} 컬럼 로드 실패:`, error);
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 테이블 선택 시 컬럼 로드
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
const loadColumns = async () => {
|
|
|
|
|
|
const tablesToLoad = new Set<string>();
|
|
|
|
|
|
|
|
|
|
|
|
// 필드 매핑에서 사용되는 모든 테이블 수집
|
|
|
|
|
|
dataSaveSettings.actions.forEach((action) => {
|
|
|
|
|
|
action.fieldMappings.forEach((mapping) => {
|
|
|
|
|
|
if (mapping.sourceTable && !tableColumnsCache[mapping.sourceTable]) {
|
|
|
|
|
|
tablesToLoad.add(mapping.sourceTable);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (mapping.targetTable && !tableColumnsCache[mapping.targetTable]) {
|
|
|
|
|
|
tablesToLoad.add(mapping.targetTable);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 필요한 테이블들의 컬럼만 로드
|
|
|
|
|
|
for (const tableName of tablesToLoad) {
|
|
|
|
|
|
await loadTableColumns(tableName);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
loadColumns();
|
|
|
|
|
|
}, [dataSaveSettings.actions, tableColumnsCache]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
const handleConfirm = () => {
|
2025-09-08 16:46:53 +09:00
|
|
|
|
if (!config.relationshipName || !connection) {
|
|
|
|
|
|
toast.error("필수 정보를 모두 입력해주세요.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 연결 종류별 설정을 준비
|
|
|
|
|
|
let settings = {};
|
|
|
|
|
|
|
|
|
|
|
|
switch (config.connectionType) {
|
|
|
|
|
|
case "simple-key":
|
|
|
|
|
|
settings = simpleKeySettings;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "data-save":
|
|
|
|
|
|
settings = dataSaveSettings;
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "external-call":
|
|
|
|
|
|
settings = externalCallSettings;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 선택된 컬럼들 검증
|
|
|
|
|
|
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
|
|
|
|
|
|
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
|
2025-09-10 15:30:14 +09:00
|
|
|
|
return;
|
2025-09-05 16:19:31 +09:00
|
|
|
|
}
|
2025-09-10 15:30:14 +09:00
|
|
|
|
|
2025-09-10 17:25:41 +09:00
|
|
|
|
// 선택된 테이블과 컬럼 정보 사용
|
|
|
|
|
|
const fromTableName = selectedFromTable || connection.fromNode.tableName;
|
|
|
|
|
|
const toTableName = selectedToTable || connection.toNode.tableName;
|
|
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
// 조건부 연결 설정 데이터 준비
|
|
|
|
|
|
const conditionalSettings = isConditionalConnection()
|
|
|
|
|
|
? {
|
|
|
|
|
|
control: {
|
2025-09-12 11:33:54 +09:00
|
|
|
|
triggerType: "insert",
|
2025-09-15 11:17:46 +09:00
|
|
|
|
conditionTree: conditions.length > 0 ? conditions : null,
|
2025-09-12 09:58:49 +09:00
|
|
|
|
},
|
|
|
|
|
|
category: {
|
|
|
|
|
|
type: config.connectionType,
|
|
|
|
|
|
},
|
|
|
|
|
|
plan: {
|
|
|
|
|
|
sourceTable: fromTableName,
|
2025-09-12 16:15:36 +09:00
|
|
|
|
targetActions:
|
|
|
|
|
|
config.connectionType === "data-save"
|
|
|
|
|
|
? dataSaveSettings.actions.map((action) => ({
|
|
|
|
|
|
id: action.id,
|
|
|
|
|
|
actionType: action.actionType,
|
|
|
|
|
|
enabled: true,
|
|
|
|
|
|
conditions: action.conditions,
|
|
|
|
|
|
fieldMappings: action.fieldMappings.map((mapping) => ({
|
|
|
|
|
|
sourceTable: mapping.sourceTable,
|
|
|
|
|
|
sourceField: mapping.sourceField,
|
|
|
|
|
|
targetTable: mapping.targetTable,
|
|
|
|
|
|
targetField: mapping.targetField,
|
|
|
|
|
|
defaultValue: mapping.defaultValue,
|
|
|
|
|
|
transformFunction: mapping.transformFunction,
|
|
|
|
|
|
})),
|
|
|
|
|
|
splitConfig: action.splitConfig,
|
|
|
|
|
|
}))
|
|
|
|
|
|
: [],
|
2025-09-12 09:58:49 +09:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
: {};
|
|
|
|
|
|
|
2025-09-10 15:30:14 +09:00
|
|
|
|
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
|
|
|
|
|
const relationshipData: TableRelationship = {
|
|
|
|
|
|
relationship_name: config.relationshipName,
|
2025-09-10 17:25:41 +09:00
|
|
|
|
from_table_name: fromTableName,
|
|
|
|
|
|
to_table_name: toTableName,
|
|
|
|
|
|
from_column_name: selectedFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
|
|
|
|
|
to_column_name: selectedToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
2025-09-12 09:58:49 +09:00
|
|
|
|
relationship_type: config.relationshipType,
|
|
|
|
|
|
connection_type: config.connectionType,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
company_code: companyCode,
|
|
|
|
|
|
settings: {
|
|
|
|
|
|
...settings,
|
2025-09-12 09:58:49 +09:00
|
|
|
|
...conditionalSettings, // 조건부 연결 설정 추가
|
2025-09-10 15:30:14 +09:00
|
|
|
|
description: config.description,
|
|
|
|
|
|
multiColumnMapping: {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
fromColumns: selectedFromColumns,
|
|
|
|
|
|
toColumns: selectedToColumns,
|
|
|
|
|
|
fromTable: fromTableName,
|
|
|
|
|
|
toTable: toTableName,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
},
|
2025-09-10 17:25:41 +09:00
|
|
|
|
isMultiColumn: selectedFromColumns.length > 1 || selectedToColumns.length > 1,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
columnCount: {
|
2025-09-10 17:25:41 +09:00
|
|
|
|
from: selectedFromColumns.length,
|
|
|
|
|
|
to: selectedToColumns.length,
|
2025-09-10 15:30:14 +09:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
toast.success("관계가 생성되었습니다!");
|
|
|
|
|
|
|
|
|
|
|
|
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이)
|
|
|
|
|
|
onConfirm(relationshipData);
|
|
|
|
|
|
handleCancel(); // 모달 닫기
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
|
const handleCancel = () => {
|
|
|
|
|
|
setConfig({
|
|
|
|
|
|
relationshipName: "",
|
|
|
|
|
|
relationshipType: "one-to-one",
|
|
|
|
|
|
connectionType: "simple-key",
|
|
|
|
|
|
fromColumnName: "",
|
|
|
|
|
|
toColumnName: "",
|
|
|
|
|
|
description: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
onCancel();
|
2025-09-05 16:19:31 +09:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (!connection) return null;
|
|
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
// 선택된 컬럼 데이터 가져오기 (현재 사용되지 않음 - 향후 확장을 위해 유지)
|
|
|
|
|
|
// const selectedColumnsData = connection.selectedColumnsData || {};
|
|
|
|
|
|
|
|
|
|
|
|
// 조건부 연결인지 확인하는 헬퍼 함수
|
|
|
|
|
|
const isConditionalConnection = () => {
|
|
|
|
|
|
return config.connectionType === "data-save" || config.connectionType === "external-call";
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:17:46 +09:00
|
|
|
|
// 고유 ID 생성 헬퍼
|
|
|
|
|
|
const generateId = () => `cond_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
// 조건 관리 헬퍼 함수들
|
|
|
|
|
|
const addCondition = () => {
|
|
|
|
|
|
const newCondition: ConditionNode = {
|
2025-09-15 11:17:46 +09:00
|
|
|
|
id: generateId(),
|
2025-09-12 09:58:49 +09:00
|
|
|
|
type: "condition",
|
|
|
|
|
|
field: "",
|
|
|
|
|
|
operator_type: "=",
|
|
|
|
|
|
value: "",
|
|
|
|
|
|
dataType: "string",
|
2025-09-15 11:17:46 +09:00
|
|
|
|
logicalOperator: "AND", // 기본값으로 AND 설정
|
2025-09-12 09:58:49 +09:00
|
|
|
|
};
|
|
|
|
|
|
setConditions([...conditions, newCondition]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:17:46 +09:00
|
|
|
|
// 그룹 시작 추가
|
|
|
|
|
|
const addGroupStart = () => {
|
|
|
|
|
|
const groupId = `group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
const groupLevel = getNextGroupLevel();
|
|
|
|
|
|
|
|
|
|
|
|
const groupStart: ConditionNode = {
|
|
|
|
|
|
id: generateId(),
|
|
|
|
|
|
type: "group-start",
|
|
|
|
|
|
groupId,
|
|
|
|
|
|
groupLevel,
|
|
|
|
|
|
logicalOperator: conditions.length > 0 ? "AND" : undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setConditions([...conditions, groupStart]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 끝 추가
|
|
|
|
|
|
const addGroupEnd = () => {
|
|
|
|
|
|
// 가장 최근에 열린 그룹 찾기
|
|
|
|
|
|
const openGroups = findOpenGroups();
|
|
|
|
|
|
if (openGroups.length === 0) {
|
|
|
|
|
|
toast.error("닫을 그룹이 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const lastOpenGroup = openGroups[openGroups.length - 1];
|
|
|
|
|
|
const groupEnd: ConditionNode = {
|
|
|
|
|
|
id: generateId(),
|
|
|
|
|
|
type: "group-end",
|
|
|
|
|
|
groupId: lastOpenGroup.groupId,
|
|
|
|
|
|
groupLevel: lastOpenGroup.groupLevel,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setConditions([...conditions, groupEnd]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 다음 그룹 레벨 계산
|
|
|
|
|
|
const getNextGroupLevel = (): number => {
|
|
|
|
|
|
const openGroups = findOpenGroups();
|
|
|
|
|
|
return openGroups.length;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 열린 그룹 찾기
|
|
|
|
|
|
const findOpenGroups = () => {
|
|
|
|
|
|
const openGroups: Array<{ groupId: string; groupLevel: number }> = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const condition of conditions) {
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
openGroups.push({
|
|
|
|
|
|
groupId: condition.groupId!,
|
|
|
|
|
|
groupLevel: condition.groupLevel!,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (condition.type === "group-end") {
|
|
|
|
|
|
// 해당 그룹 제거
|
|
|
|
|
|
const groupIndex = openGroups.findIndex((g) => g.groupId === condition.groupId);
|
|
|
|
|
|
if (groupIndex !== -1) {
|
|
|
|
|
|
openGroups.splice(groupIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return openGroups;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
const updateCondition = (index: number, field: keyof ConditionNode, value: string) => {
|
|
|
|
|
|
const updatedConditions = [...conditions];
|
|
|
|
|
|
updatedConditions[index] = { ...updatedConditions[index], [field]: value };
|
|
|
|
|
|
setConditions(updatedConditions);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeCondition = (index: number) => {
|
2025-09-15 11:17:46 +09:00
|
|
|
|
const conditionToRemove = conditions[index];
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 시작/끝을 삭제하는 경우 해당 그룹 전체 삭제
|
|
|
|
|
|
if (conditionToRemove.type === "group-start" || conditionToRemove.type === "group-end") {
|
|
|
|
|
|
removeGroup(conditionToRemove.groupId!);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const updatedConditions = conditions.filter((_, i) => i !== index);
|
|
|
|
|
|
setConditions(updatedConditions);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 전체 삭제
|
|
|
|
|
|
const removeGroup = (groupId: string) => {
|
|
|
|
|
|
const updatedConditions = conditions.filter((c) => c.groupId !== groupId);
|
2025-09-12 09:58:49 +09:00
|
|
|
|
setConditions(updatedConditions);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-15 11:17:46 +09:00
|
|
|
|
// 현재 조건의 그룹 레벨 계산
|
|
|
|
|
|
const getCurrentGroupLevel = (conditionIndex: number): number => {
|
|
|
|
|
|
let level = 0;
|
|
|
|
|
|
for (let i = 0; i < conditionIndex; i++) {
|
|
|
|
|
|
const condition = conditions[i];
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
level++;
|
|
|
|
|
|
} else if (condition.type === "group-end") {
|
|
|
|
|
|
level--;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return level;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 액션별 조건 그룹 관리 함수들
|
|
|
|
|
|
const addActionGroupStart = (actionIndex: number) => {
|
|
|
|
|
|
const groupId = `action_group_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
|
|
const currentConditions = dataSaveSettings.actions[actionIndex].conditions || [];
|
|
|
|
|
|
const groupLevel = getActionNextGroupLevel(currentConditions);
|
|
|
|
|
|
|
|
|
|
|
|
const groupStart: ConditionNode = {
|
|
|
|
|
|
id: generateId(),
|
|
|
|
|
|
type: "group-start",
|
|
|
|
|
|
groupId,
|
|
|
|
|
|
groupLevel,
|
|
|
|
|
|
logicalOperator: currentConditions.length > 0 ? "AND" : undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions = [...currentConditions, groupStart];
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addActionGroupEnd = (actionIndex: number) => {
|
|
|
|
|
|
const currentConditions = dataSaveSettings.actions[actionIndex].conditions || [];
|
|
|
|
|
|
const openGroups = findActionOpenGroups(currentConditions);
|
|
|
|
|
|
|
|
|
|
|
|
if (openGroups.length === 0) {
|
|
|
|
|
|
toast.error("닫을 그룹이 없습니다.");
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const lastOpenGroup = openGroups[openGroups.length - 1];
|
|
|
|
|
|
const groupEnd: ConditionNode = {
|
|
|
|
|
|
id: generateId(),
|
|
|
|
|
|
type: "group-end",
|
|
|
|
|
|
groupId: lastOpenGroup.groupId,
|
|
|
|
|
|
groupLevel: lastOpenGroup.groupLevel,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions = [...currentConditions, groupEnd];
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 액션별 다음 그룹 레벨 계산
|
|
|
|
|
|
const getActionNextGroupLevel = (conditions: ConditionNode[]): number => {
|
|
|
|
|
|
const openGroups = findActionOpenGroups(conditions);
|
|
|
|
|
|
return openGroups.length;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 액션별 열린 그룹 찾기
|
|
|
|
|
|
const findActionOpenGroups = (conditions: ConditionNode[]) => {
|
|
|
|
|
|
const openGroups: Array<{ groupId: string; groupLevel: number }> = [];
|
|
|
|
|
|
|
|
|
|
|
|
for (const condition of conditions) {
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
openGroups.push({
|
|
|
|
|
|
groupId: condition.groupId!,
|
|
|
|
|
|
groupLevel: condition.groupLevel!,
|
|
|
|
|
|
});
|
|
|
|
|
|
} else if (condition.type === "group-end") {
|
|
|
|
|
|
const groupIndex = openGroups.findIndex((g) => g.groupId === condition.groupId);
|
|
|
|
|
|
if (groupIndex !== -1) {
|
|
|
|
|
|
openGroups.splice(groupIndex, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return openGroups;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 액션별 현재 조건의 그룹 레벨 계산
|
|
|
|
|
|
const getActionCurrentGroupLevel = (conditions: ConditionNode[], conditionIndex: number): number => {
|
|
|
|
|
|
let level = 0;
|
|
|
|
|
|
for (let i = 0; i < conditionIndex; i++) {
|
|
|
|
|
|
const condition = conditions[i];
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
level++;
|
|
|
|
|
|
} else if (condition.type === "group-end") {
|
|
|
|
|
|
level--;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return level;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 액션별 조건 렌더링 함수
|
|
|
|
|
|
const renderActionCondition = (condition: ConditionNode, condIndex: number, actionIndex: number) => {
|
|
|
|
|
|
// 그룹 시작 렌더링
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
{condIndex > 0 && (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={dataSaveSettings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
|
|
|
|
|
onValueChange={(value: "AND" | "OR") => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex - 1].logicalOperator = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="AND">AND</SelectItem>
|
|
|
|
|
|
<SelectItem value="OR">OR</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
|
|
|
|
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="font-mono text-xs text-green-600">(</span>
|
|
|
|
|
|
<span className="text-xs text-green-600">그룹 시작</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
|
|
|
|
|
(c) => c.groupId !== condition.groupId,
|
|
|
|
|
|
);
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-4 w-4 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-2 w-2" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 끝 렌더링
|
|
|
|
|
|
if (condition.type === "group-end") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
|
|
|
|
|
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="font-mono text-xs text-green-600">)</span>
|
|
|
|
|
|
<span className="text-xs text-green-600">그룹 끝</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
|
|
|
|
|
(c) => c.groupId !== condition.groupId,
|
|
|
|
|
|
);
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-4 w-4 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-2 w-2" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 일반 조건 렌더링 (기존 로직 간소화)
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
{condIndex > 0 && (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={dataSaveSettings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
|
|
|
|
|
|
onValueChange={(value: "AND" | "OR") => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex - 1].logicalOperator = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="AND">AND</SelectItem>
|
|
|
|
|
|
<SelectItem value="OR">OR</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-1 items-center gap-2 rounded border bg-white p-1"
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginLeft: `${getActionCurrentGroupLevel(dataSaveSettings.actions[actionIndex].conditions || [], condIndex) * 15}px`,
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.field || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].field = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 flex-1 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="필드" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{fromTableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.operator_type || "="}
|
|
|
|
|
|
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].operator_type = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-20 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="=">=</SelectItem>
|
|
|
|
|
|
<SelectItem value="!=">!=</SelectItem>
|
|
|
|
|
|
<SelectItem value=">">></SelectItem>
|
|
|
|
|
|
<SelectItem value="<"><</SelectItem>
|
|
|
|
|
|
<SelectItem value=">=">>=</SelectItem>
|
|
|
|
|
|
<SelectItem value="<="><=</SelectItem>
|
|
|
|
|
|
<SelectItem value="LIKE">LIKE</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
{/* 데이터 타입에 따른 동적 입력 컴포넌트 */}
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
|
|
|
|
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
|
|
|
|
|
|
|
|
|
|
|
if (dataType.includes("timestamp") || dataType.includes("datetime") || dataType.includes("date")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("time")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="time"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("date")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (
|
|
|
|
|
|
dataType.includes("int") ||
|
|
|
|
|
|
dataType.includes("numeric") ||
|
|
|
|
|
|
dataType.includes("decimal") ||
|
|
|
|
|
|
dataType.includes("float") ||
|
|
|
|
|
|
dataType.includes("double")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
placeholder="숫자"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("bool")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 flex-1 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="true">TRUE</SelectItem>
|
|
|
|
|
|
<SelectItem value="false">FALSE</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="값"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions![condIndex].value = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
})()}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
|
|
|
|
|
|
(_, i) => i !== condIndex,
|
|
|
|
|
|
);
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 w-6 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-2 w-2" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
// 조건부 연결 설정 UI 렌더링
|
|
|
|
|
|
const renderConditionalSettings = () => {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4">
|
|
|
|
|
|
<div className="mb-4 flex items-center gap-2">
|
|
|
|
|
|
<Zap className="h-4 w-4 text-purple-500" />
|
2025-09-12 16:15:36 +09:00
|
|
|
|
<span className="text-sm font-medium">전체 실행 조건 (언제 이 연결이 동작할지)</span>
|
2025-09-12 09:58:49 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 실행 조건 설정 */}
|
|
|
|
|
|
<div className="mb-4">
|
|
|
|
|
|
<div className="mb-2 flex items-center justify-between">
|
|
|
|
|
|
<Label className="text-sm font-medium">실행 조건</Label>
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => addCondition()} className="h-7 text-xs">
|
|
|
|
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
|
|
|
|
조건 추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => addGroupStart()} className="h-7 text-xs">
|
|
|
|
|
|
그룹 시작 (
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button size="sm" variant="outline" onClick={() => addGroupEnd()} className="h-7 text-xs">
|
|
|
|
|
|
그룹 끝 )
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-09-12 09:58:49 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 조건 목록 */}
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
|
|
|
{conditions.length === 0 ? (
|
|
|
|
|
|
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
|
|
|
|
|
|
조건을 추가하면 해당 조건을 만족할 때만 실행됩니다.
|
|
|
|
|
|
<br />
|
|
|
|
|
|
조건이 없으면 항상 실행됩니다.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
2025-09-15 11:17:46 +09:00
|
|
|
|
conditions.map((condition, index) => {
|
|
|
|
|
|
// 그룹 시작 렌더링
|
|
|
|
|
|
if (condition.type === "group-start") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
{/* 이전 조건과의 논리 연산자 */}
|
|
|
|
|
|
{index > 0 && (
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<Select
|
2025-09-15 11:17:46 +09:00
|
|
|
|
value={conditions[index - 1]?.logicalOperator || "AND"}
|
|
|
|
|
|
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "logicalOperator", value)}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
>
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
|
|
|
|
|
<SelectValue />
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<SelectItem value="AND">AND</SelectItem>
|
|
|
|
|
|
<SelectItem value="OR">OR</SelectItem>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-15 11:17:46 +09:00
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 그룹 레벨에 따른 들여쓰기 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
|
|
|
|
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="font-mono text-sm text-blue-600">(</span>
|
|
|
|
|
|
<span className="text-xs text-blue-600">그룹 시작</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => removeCondition(index)}
|
|
|
|
|
|
className="h-6 w-6 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 그룹 끝 렌더링
|
|
|
|
|
|
if (condition.type === "group-end") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex items-center gap-2 rounded border-2 border-dashed border-blue-300 bg-blue-50/50 p-2"
|
|
|
|
|
|
style={{ marginLeft: `${(condition.groupLevel || 0) * 20}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span className="font-mono text-sm text-blue-600">)</span>
|
|
|
|
|
|
<span className="text-xs text-blue-600">그룹 끝</span>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => removeCondition(index)}
|
|
|
|
|
|
className="h-6 w-6 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 일반 조건 렌더링
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={condition.id} className="flex items-center gap-2">
|
|
|
|
|
|
{/* 이전 조건과의 논리 연산자 */}
|
|
|
|
|
|
{index > 0 && (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={conditions[index - 1]?.logicalOperator || "AND"}
|
|
|
|
|
|
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "logicalOperator", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="AND">AND</SelectItem>
|
|
|
|
|
|
<SelectItem value="OR">OR</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 그룹 레벨에 따른 들여쓰기와 조건 필드들 */}
|
|
|
|
|
|
<div
|
|
|
|
|
|
className="flex flex-1 items-center gap-2 rounded border bg-white p-2"
|
|
|
|
|
|
style={{ marginLeft: `${getCurrentGroupLevel(index) * 20}px` }}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 조건 필드 선택 */}
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.field || ""}
|
|
|
|
|
|
onValueChange={(value) => updateCondition(index, "field", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="필드 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{fromTableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 연산자 선택 */}
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.operator_type || "="}
|
|
|
|
|
|
onValueChange={(value) => updateCondition(index, "operator_type", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 w-20 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="=">=</SelectItem>
|
|
|
|
|
|
<SelectItem value="!=">!=</SelectItem>
|
|
|
|
|
|
<SelectItem value=">">></SelectItem>
|
|
|
|
|
|
<SelectItem value="<"><</SelectItem>
|
|
|
|
|
|
<SelectItem value=">=">>=</SelectItem>
|
|
|
|
|
|
<SelectItem value="<="><=</SelectItem>
|
|
|
|
|
|
<SelectItem value="LIKE">LIKE</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 데이터 타입에 따른 동적 입력 컴포넌트 */}
|
|
|
|
|
|
{(() => {
|
|
|
|
|
|
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
|
|
|
|
|
|
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
|
|
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
|
dataType.includes("timestamp") ||
|
|
|
|
|
|
dataType.includes("datetime") ||
|
|
|
|
|
|
dataType.includes("date")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="datetime-local"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
|
|
|
|
|
className="h-8 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("time")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="time"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
|
|
|
|
|
className="h-8 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("date")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="date"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
|
|
|
|
|
className="h-8 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (
|
|
|
|
|
|
dataType.includes("int") ||
|
|
|
|
|
|
dataType.includes("numeric") ||
|
|
|
|
|
|
dataType.includes("decimal") ||
|
|
|
|
|
|
dataType.includes("float") ||
|
|
|
|
|
|
dataType.includes("double")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
placeholder="숫자"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
|
|
|
|
|
className="h-8 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else if (dataType.includes("bool")) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onValueChange={(value) => updateCondition(index, "value", value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-8 flex-1 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="true">TRUE</SelectItem>
|
|
|
|
|
|
<SelectItem value="false">FALSE</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Input
|
|
|
|
|
|
placeholder="값"
|
|
|
|
|
|
value={condition.value || ""}
|
|
|
|
|
|
onChange={(e) => updateCondition(index, "value", e.target.value)}
|
|
|
|
|
|
className="h-8 flex-1 text-xs"
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
})()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* 삭제 버튼 */}
|
|
|
|
|
|
<Button size="sm" variant="ghost" onClick={() => removeCondition(index)} className="h-8 w-8 p-0">
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})
|
2025-09-12 09:58:49 +09:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|
2025-09-05 18:00:18 +09:00
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
// 연결 종류별 설정 패널 렌더링
|
|
|
|
|
|
const renderConnectionTypeSettings = () => {
|
|
|
|
|
|
switch (config.connectionType) {
|
|
|
|
|
|
case "simple-key":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-blue-50/30 p-4">
|
|
|
|
|
|
<div className="mb-3 flex items-center gap-2">
|
|
|
|
|
|
<Key className="h-4 w-4 text-blue-500" />
|
|
|
|
|
|
<span className="text-sm font-medium">단순 키값 연결 설정</span>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="notes" className="text-sm">
|
|
|
|
|
|
연결 설명
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
id="notes"
|
|
|
|
|
|
value={simpleKeySettings.notes}
|
|
|
|
|
|
onChange={(e) => setSimpleKeySettings({ ...simpleKeySettings, notes: e.target.value })}
|
|
|
|
|
|
placeholder="데이터 연결에 대한 설명을 입력하세요"
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
case "data-save":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-l-4 border-l-green-500 bg-green-50/30 p-4">
|
|
|
|
|
|
<div className="mb-3 flex items-center gap-2">
|
|
|
|
|
|
<Save className="h-4 w-4 text-green-500" />
|
|
|
|
|
|
<span className="text-sm font-medium">데이터 저장 설정</span>
|
|
|
|
|
|
</div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
<div className="space-y-4">
|
|
|
|
|
|
{/* 액션 목록 */}
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
<div className="mb-2 flex items-center justify-between">
|
|
|
|
|
|
<Label className="text-sm font-medium">저장 액션</Label>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newAction = {
|
|
|
|
|
|
id: `action_${dataSaveSettings.actions.length + 1}`,
|
|
|
|
|
|
name: `액션 ${dataSaveSettings.actions.length + 1}`,
|
|
|
|
|
|
actionType: "insert" as const,
|
|
|
|
|
|
fieldMappings: [],
|
2025-09-15 10:11:22 +09:00
|
|
|
|
conditions: [],
|
|
|
|
|
|
splitConfig: {
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
delimiter: ",",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
},
|
2025-09-12 16:15:36 +09:00
|
|
|
|
};
|
|
|
|
|
|
setDataSaveSettings({
|
|
|
|
|
|
...dataSaveSettings,
|
|
|
|
|
|
actions: [...dataSaveSettings.actions, newAction],
|
|
|
|
|
|
});
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-7 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-3 w-3" />
|
|
|
|
|
|
액션 추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{dataSaveSettings.actions.length === 0 ? (
|
|
|
|
|
|
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
|
|
|
|
|
|
저장 액션을 추가하여 데이터를 어떻게 저장할지 설정하세요.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{dataSaveSettings.actions.map((action, actionIndex) => (
|
|
|
|
|
|
<div key={action.id} className="rounded border bg-white p-3">
|
|
|
|
|
|
<div className="mb-3 flex items-center justify-between">
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<Input
|
2025-09-12 16:15:36 +09:00
|
|
|
|
value={action.name}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].name = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-7 flex-1 text-xs font-medium"
|
|
|
|
|
|
placeholder="액션 이름"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = dataSaveSettings.actions.filter((_, i) => i !== actionIndex);
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-7 w-7 p-0"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
2025-09-15 11:17:46 +09:00
|
|
|
|
</div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
|
|
|
|
|
|
<div className="grid grid-cols-1 gap-3">
|
|
|
|
|
|
{/* 액션 타입 */}
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
<Label className="text-xs">액션 타입</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={action.actionType}
|
|
|
|
|
|
onValueChange={(value: "insert" | "update" | "delete" | "upsert") => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].actionType = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-7 text-xs">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="insert">INSERT</SelectItem>
|
|
|
|
|
|
<SelectItem value="update">UPDATE</SelectItem>
|
|
|
|
|
|
<SelectItem value="delete">DELETE</SelectItem>
|
|
|
|
|
|
<SelectItem value="upsert">UPSERT</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 10:11:22 +09:00
|
|
|
|
{/* 액션별 개별 실행 조건 (선택사항) */}
|
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
|
<details className="group">
|
|
|
|
|
|
<summary className="flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900">
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div className="flex items-center gap-2">
|
2025-09-15 10:11:22 +09:00
|
|
|
|
🔍 이 액션의 실행 조건 (선택사항)
|
|
|
|
|
|
{action.conditions && action.conditions.length > 0 && (
|
|
|
|
|
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
|
|
|
|
|
|
{action.conditions.length}개
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
{action.conditions && action.conditions.length > 0 && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
2025-09-12 16:15:36 +09:00
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
2025-09-15 10:11:22 +09:00
|
|
|
|
newActions[actionIndex].conditions = [];
|
2025-09-12 16:15:36 +09:00
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
|
|
|
|
|
|
title="조건 모두 삭제"
|
2025-09-12 16:15:36 +09:00
|
|
|
|
>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
|
|
|
|
|
|
<div className="mb-2 flex items-center justify-between">
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div className="flex gap-1">
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
if (!newActions[actionIndex].conditions) {
|
|
|
|
|
|
newActions[actionIndex].conditions = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
newActions[actionIndex].conditions = [
|
|
|
|
|
|
...(newActions[actionIndex].conditions || []),
|
|
|
|
|
|
{
|
|
|
|
|
|
id: generateId(),
|
|
|
|
|
|
type: "condition",
|
|
|
|
|
|
field: "",
|
|
|
|
|
|
operator_type: "=",
|
|
|
|
|
|
value: "",
|
|
|
|
|
|
dataType: "string",
|
|
|
|
|
|
logicalOperator: "AND",
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-2 w-2" />
|
|
|
|
|
|
조건 추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => addActionGroupStart(actionIndex)}
|
|
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
그룹 시작 (
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => addActionGroupEnd(actionIndex)}
|
|
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
그룹 끝 )
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
{action.conditions && action.conditions.length > 0 && (
|
|
|
|
|
|
<div className="space-y-2">
|
2025-09-15 11:17:46 +09:00
|
|
|
|
{action.conditions.map((condition, condIndex) =>
|
|
|
|
|
|
renderActionCondition(condition, condIndex, actionIndex),
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
)}
|
2025-09-15 11:17:46 +09:00
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</details>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 데이터 분할 설정 (선택사항) */}
|
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
|
<details className="group">
|
|
|
|
|
|
<summary className="flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900">
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
✂️ 데이터 분할 설정 (선택사항)
|
|
|
|
|
|
{action.splitConfig && action.splitConfig.sourceField && (
|
|
|
|
|
|
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700">
|
|
|
|
|
|
설정됨
|
|
|
|
|
|
</span>
|
|
|
|
|
|
)}
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
{action.splitConfig && action.splitConfig.sourceField && (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
e.stopPropagation();
|
2025-09-12 16:15:36 +09:00
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
2025-09-15 10:11:22 +09:00
|
|
|
|
newActions[actionIndex].splitConfig = {
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
delimiter: ",",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
};
|
2025-09-12 16:15:36 +09:00
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
|
|
|
|
|
|
title="분할 설정 초기화"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
|
|
|
|
|
|
<Label className="text-xs font-medium">데이터 분할 설정</Label>
|
|
|
|
|
|
<div className="mt-1 grid grid-cols-3 gap-2">
|
2025-09-15 11:17:46 +09:00
|
|
|
|
<div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<Label className="text-xs text-gray-500">분할할 필드</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={action.splitConfig?.sourceField || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
if (!newActions[actionIndex].splitConfig) {
|
|
|
|
|
|
newActions[actionIndex].splitConfig = {
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
delimiter: ",",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
newActions[actionIndex].splitConfig!.sourceField = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="필드 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{fromTableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-gray-500">구분자</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={action.splitConfig?.delimiter || ","}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
if (!newActions[actionIndex].splitConfig) {
|
|
|
|
|
|
newActions[actionIndex].splitConfig = {
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
delimiter: ",",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
newActions[actionIndex].splitConfig!.delimiter = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
placeholder=","
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs text-gray-500">저장할 필드</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={action.splitConfig?.targetField || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
if (!newActions[actionIndex].splitConfig) {
|
|
|
|
|
|
newActions[actionIndex].splitConfig = {
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
delimiter: ",",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
newActions[actionIndex].splitConfig!.targetField = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="필드 선택" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{toTableColumns.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</details>
|
|
|
|
|
|
</div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 필드 매핑 */}
|
|
|
|
|
|
<div className="mt-3">
|
|
|
|
|
|
<div className="mb-2 flex items-center justify-between">
|
|
|
|
|
|
<Label className="text-xs font-medium">필드 매핑</Label>
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="outline"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].fieldMappings.push({
|
|
|
|
|
|
sourceTable: "",
|
|
|
|
|
|
sourceField: "",
|
|
|
|
|
|
targetTable: "",
|
|
|
|
|
|
targetField: "",
|
|
|
|
|
|
defaultValue: "",
|
|
|
|
|
|
transformFunction: "",
|
|
|
|
|
|
});
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 text-xs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Plus className="mr-1 h-2 w-2" />
|
|
|
|
|
|
매핑 추가
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{action.fieldMappings.map((mapping, mappingIndex) => (
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<div key={mappingIndex} className="rounded border bg-white p-2">
|
|
|
|
|
|
{/* 컴팩트한 매핑 표시 */}
|
|
|
|
|
|
<div className="flex items-center gap-2 text-xs">
|
|
|
|
|
|
{/* 소스 */}
|
|
|
|
|
|
<div className="flex items-center gap-1 rounded bg-blue-50 px-2 py-1">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={mapping.sourceTable || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = value;
|
|
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = "";
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="테이블" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{availableTables.map((table) => (
|
|
|
|
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
|
|
|
|
<div className="truncate" title={table.tableName}>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
{table.tableName}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<span className="text-gray-400">.</span>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={mapping.sourceField}
|
|
|
|
|
|
onValueChange={(value) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
disabled={!mapping.sourceTable}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="컬럼" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{mapping.sourceTable &&
|
|
|
|
|
|
tableColumnsCache[mapping.sourceTable]?.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
<div className="truncate" title={column.columnName}>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
{column.columnName}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</div>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<div className="text-gray-400">→</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 타겟 */}
|
|
|
|
|
|
<div className="flex items-center gap-1 rounded bg-green-50 px-2 py-1">
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={mapping.targetTable || ""}
|
|
|
|
|
|
onValueChange={(value) => {
|
2025-09-12 16:15:36 +09:00
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
2025-09-15 10:11:22 +09:00
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].targetTable = value;
|
|
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].targetField = "";
|
2025-09-12 16:15:36 +09:00
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="테이블" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{availableTables.map((table) => (
|
|
|
|
|
|
<SelectItem key={table.tableName} value={table.tableName}>
|
|
|
|
|
|
<div className="truncate" title={table.tableName}>
|
|
|
|
|
|
{table.tableName}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
<span className="text-gray-400">.</span>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={mapping.targetField}
|
|
|
|
|
|
onValueChange={(value) => {
|
2025-09-12 16:15:36 +09:00
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
2025-09-15 10:11:22 +09:00
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].targetField = value;
|
2025-09-12 16:15:36 +09:00
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
2025-09-15 10:11:22 +09:00
|
|
|
|
disabled={!mapping.targetTable}
|
2025-09-12 16:15:36 +09:00
|
|
|
|
>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
|
|
|
|
|
|
<SelectValue placeholder="컬럼" />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
{mapping.targetTable &&
|
|
|
|
|
|
tableColumnsCache[mapping.targetTable]?.map((column) => (
|
|
|
|
|
|
<SelectItem key={column.columnName} value={column.columnName}>
|
|
|
|
|
|
<div className="truncate" title={column.columnName}>
|
|
|
|
|
|
{column.columnName}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</SelectItem>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
2025-09-15 10:11:22 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 기본값 (인라인) */}
|
|
|
|
|
|
<Input
|
|
|
|
|
|
value={mapping.defaultValue || ""}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue = e.target.value;
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 w-20 text-xs"
|
|
|
|
|
|
placeholder="기본값"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 삭제 버튼 */}
|
|
|
|
|
|
<Button
|
|
|
|
|
|
size="sm"
|
|
|
|
|
|
variant="ghost"
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
const newActions = [...dataSaveSettings.actions];
|
|
|
|
|
|
newActions[actionIndex].fieldMappings = newActions[
|
|
|
|
|
|
actionIndex
|
|
|
|
|
|
].fieldMappings.filter((_, i) => i !== mappingIndex);
|
|
|
|
|
|
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="h-6 w-6 p-0 text-red-500 hover:text-red-700"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Trash2 className="h-3 w-3" />
|
|
|
|
|
|
</Button>
|
2025-09-12 16:15:36 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
case "external-call":
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="rounded-lg border border-l-4 border-l-orange-500 bg-orange-50/30 p-4">
|
|
|
|
|
|
<div className="mb-3 flex items-center gap-2">
|
|
|
|
|
|
<Globe className="h-4 w-4 text-orange-500" />
|
|
|
|
|
|
<span className="text-sm font-medium">외부 호출 설정</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-3">
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<Label htmlFor="callType" className="text-sm">
|
|
|
|
|
|
호출 유형
|
|
|
|
|
|
</Label>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<Select
|
2025-09-08 16:46:53 +09:00
|
|
|
|
value={externalCallSettings.callType}
|
|
|
|
|
|
onValueChange={(value: "rest-api" | "email" | "webhook" | "ftp" | "queue") =>
|
|
|
|
|
|
setExternalCallSettings({ ...externalCallSettings, callType: value })
|
|
|
|
|
|
}
|
2025-09-05 18:00:18 +09:00
|
|
|
|
>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<SelectTrigger className="text-sm">
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<SelectValue />
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<SelectItem value="rest-api">REST API 호출</SelectItem>
|
|
|
|
|
|
<SelectItem value="email">이메일 전송</SelectItem>
|
|
|
|
|
|
<SelectItem value="webhook">웹훅</SelectItem>
|
|
|
|
|
|
<SelectItem value="ftp">FTP 업로드</SelectItem>
|
|
|
|
|
|
<SelectItem value="queue">메시지 큐</SelectItem>
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
|
|
|
|
|
{externalCallSettings.callType === "rest-api" && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="apiUrl" className="text-sm">
|
|
|
|
|
|
API URL
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="apiUrl"
|
|
|
|
|
|
value={externalCallSettings.apiUrl}
|
|
|
|
|
|
onChange={(e) => setExternalCallSettings({ ...externalCallSettings, apiUrl: e.target.value })}
|
|
|
|
|
|
placeholder="https://api.example.com/webhook"
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="httpMethod" className="text-sm">
|
|
|
|
|
|
HTTP Method
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Select
|
|
|
|
|
|
value={externalCallSettings.httpMethod}
|
|
|
|
|
|
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
|
|
|
|
|
|
setExternalCallSettings({ ...externalCallSettings, httpMethod: value })
|
|
|
|
|
|
}
|
|
|
|
|
|
>
|
|
|
|
|
|
<SelectTrigger className="text-sm">
|
|
|
|
|
|
<SelectValue />
|
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
|
<SelectContent>
|
|
|
|
|
|
<SelectItem value="GET">GET</SelectItem>
|
|
|
|
|
|
<SelectItem value="POST">POST</SelectItem>
|
|
|
|
|
|
<SelectItem value="PUT">PUT</SelectItem>
|
|
|
|
|
|
<SelectItem value="DELETE">DELETE</SelectItem>
|
|
|
|
|
|
</SelectContent>
|
|
|
|
|
|
</Select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="headers" className="text-sm">
|
|
|
|
|
|
Headers
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
id="headers"
|
|
|
|
|
|
value={externalCallSettings.headers}
|
|
|
|
|
|
onChange={(e) => setExternalCallSettings({ ...externalCallSettings, headers: e.target.value })}
|
|
|
|
|
|
placeholder="{}"
|
|
|
|
|
|
rows={1}
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="bodyTemplate" className="text-sm">
|
|
|
|
|
|
Body Template
|
|
|
|
|
|
</Label>
|
|
|
|
|
|
<Textarea
|
|
|
|
|
|
id="bodyTemplate"
|
|
|
|
|
|
value={externalCallSettings.bodyTemplate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setExternalCallSettings({ ...externalCallSettings, bodyTemplate: e.target.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
placeholder="{}"
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
2025-09-05 18:00:18 +09:00
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-09-05 18:00:18 +09:00
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
default:
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog open={isOpen} onOpenChange={handleCancel}>
|
|
|
|
|
|
<DialogContent className="max-h-[80vh] max-w-3xl overflow-y-auto">
|
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
|
<DialogTitle className="flex items-center gap-2 text-lg">
|
|
|
|
|
|
<Link className="h-4 w-4" />
|
|
|
|
|
|
필드 연결 설정
|
|
|
|
|
|
</DialogTitle>
|
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
|
|
|
|
|
|
|
<div className="space-y-4">
|
2025-09-10 17:25:41 +09:00
|
|
|
|
{/* 테이블 및 컬럼 선택 */}
|
|
|
|
|
|
<div className="rounded-lg border bg-gray-50 p-4">
|
|
|
|
|
|
<div className="mb-4 text-sm font-medium">테이블 및 컬럼 선택</div>
|
|
|
|
|
|
|
2025-09-10 17:58:23 +09:00
|
|
|
|
{/* 현재 선택된 테이블 표시 */}
|
2025-09-10 17:25:41 +09:00
|
|
|
|
<div className="mb-4 grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">From 테이블</Label>
|
2025-09-10 17:58:23 +09:00
|
|
|
|
<div className="mt-1">
|
|
|
|
|
|
<span className="text-sm font-medium text-gray-800">
|
|
|
|
|
|
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="ml-2 text-xs text-gray-500">({selectedFromTable})</span>
|
|
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">To 테이블</Label>
|
2025-09-10 17:58:23 +09:00
|
|
|
|
<div className="mt-1">
|
|
|
|
|
|
<span className="text-sm font-medium text-gray-800">
|
|
|
|
|
|
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<span className="ml-2 text-xs text-gray-500">({selectedToTable})</span>
|
|
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 컬럼 선택 */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">From 컬럼</Label>
|
|
|
|
|
|
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
|
|
|
|
|
{fromTableColumns.map((column) => (
|
|
|
|
|
|
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={selectedFromColumns.includes(column.columnName)}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
if (e.target.checked) {
|
|
|
|
|
|
setSelectedFromColumns((prev) => [...prev, column.columnName]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedFromColumns((prev) => prev.filter((col) => col !== column.columnName));
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="rounded"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{column.columnName}</span>
|
|
|
|
|
|
<span className="text-xs text-gray-500">({column.dataType})</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{fromTableColumns.length === 0 && (
|
|
|
|
|
|
<div className="py-2 text-xs text-gray-500">
|
|
|
|
|
|
{selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">To 컬럼</Label>
|
|
|
|
|
|
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
|
|
|
|
|
|
{toTableColumns.map((column) => (
|
|
|
|
|
|
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={selectedToColumns.includes(column.columnName)}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
if (e.target.checked) {
|
|
|
|
|
|
setSelectedToColumns((prev) => [...prev, column.columnName]);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSelectedToColumns((prev) => prev.filter((col) => col !== column.columnName));
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="rounded"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span>{column.columnName}</span>
|
|
|
|
|
|
<span className="text-xs text-gray-500">({column.dataType})</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{toTableColumns.length === 0 && (
|
|
|
|
|
|
<div className="py-2 text-xs text-gray-500">
|
|
|
|
|
|
{selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 선택된 컬럼 미리보기 */}
|
|
|
|
|
|
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
|
2025-09-10 17:58:23 +09:00
|
|
|
|
<div className="mt-4 grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">선택된 From 컬럼</Label>
|
|
|
|
|
|
<div className="mt-1 flex flex-wrap gap-1">
|
|
|
|
|
|
{selectedFromColumns.length > 0 ? (
|
|
|
|
|
|
selectedFromColumns.map((column) => (
|
|
|
|
|
|
<Badge key={column} variant="outline" className="text-xs">
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-xs text-gray-400">선택된 컬럼 없음</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
</div>
|
2025-09-10 17:58:23 +09:00
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-xs font-medium text-gray-600">선택된 To 컬럼</Label>
|
|
|
|
|
|
<div className="mt-1 flex flex-wrap gap-1">
|
|
|
|
|
|
{selectedToColumns.length > 0 ? (
|
|
|
|
|
|
selectedToColumns.map((column) => (
|
|
|
|
|
|
<Badge key={column} variant="secondary" className="text-xs">
|
|
|
|
|
|
{column}
|
|
|
|
|
|
</Badge>
|
|
|
|
|
|
))
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<span className="text-xs text-gray-400">선택된 컬럼 없음</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
2025-09-10 17:25:41 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 기본 연결 설정 */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-4">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label htmlFor="relationshipName">연결 이름</Label>
|
|
|
|
|
|
<Input
|
|
|
|
|
|
id="relationshipName"
|
|
|
|
|
|
value={config.relationshipName}
|
|
|
|
|
|
onChange={(e) => setConfig({ ...config, relationshipName: e.target.value })}
|
|
|
|
|
|
placeholder="employee_id_department_id_연결"
|
|
|
|
|
|
className="text-sm"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* 연결 종류 선택 */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<Label className="text-sm font-medium">연결 종류</Label>
|
|
|
|
|
|
<div className="mt-2 grid grid-cols-3 gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
|
|
|
|
|
config.connectionType === "simple-key"
|
|
|
|
|
|
? "border-blue-500 bg-blue-50"
|
|
|
|
|
|
: "border-gray-200 hover:border-gray-300"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => setConfig({ ...config, connectionType: "simple-key" })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Key className="mx-auto h-6 w-6 text-blue-500" />
|
|
|
|
|
|
<div className="mt-1 text-xs font-medium">단순 키값 연결</div>
|
|
|
|
|
|
<div className="text-xs text-gray-600">중계 테이블 생성</div>
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<div
|
|
|
|
|
|
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
|
|
|
|
|
config.connectionType === "data-save"
|
|
|
|
|
|
? "border-green-500 bg-green-50"
|
|
|
|
|
|
: "border-gray-200 hover:border-gray-300"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => setConfig({ ...config, connectionType: "data-save" })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Save className="mx-auto h-6 w-6 text-green-500" />
|
|
|
|
|
|
<div className="mt-1 text-xs font-medium">데이터 저장</div>
|
|
|
|
|
|
<div className="text-xs text-gray-600">필드 매핑 저장</div>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<div
|
|
|
|
|
|
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
|
|
|
|
|
|
config.connectionType === "external-call"
|
|
|
|
|
|
? "border-orange-500 bg-orange-50"
|
|
|
|
|
|
: "border-gray-200 hover:border-gray-300"
|
|
|
|
|
|
}`}
|
|
|
|
|
|
onClick={() => setConfig({ ...config, connectionType: "external-call" })}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Globe className="mx-auto h-6 w-6 text-orange-500" />
|
|
|
|
|
|
<div className="mt-1 text-xs font-medium">외부 호출</div>
|
|
|
|
|
|
<div className="text-xs text-gray-600">API/이메일 호출</div>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
</div>
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
|
2025-09-12 09:58:49 +09:00
|
|
|
|
{/* 조건부 연결을 위한 조건 설정 */}
|
|
|
|
|
|
{isConditionalConnection() && renderConditionalSettings()}
|
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
|
{/* 연결 종류별 상세 설정 */}
|
|
|
|
|
|
{renderConnectionTypeSettings()}
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<DialogFooter>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
<Button variant="outline" onClick={handleCancel}>
|
2025-09-05 16:19:31 +09:00
|
|
|
|
취소
|
|
|
|
|
|
</Button>
|
2025-09-08 16:46:53 +09:00
|
|
|
|
<Button onClick={handleConfirm} disabled={!config.relationshipName}>
|
2025-09-05 18:00:18 +09:00
|
|
|
|
연결 생성
|
|
|
|
|
|
</Button>
|
2025-09-05 16:19:31 +09:00
|
|
|
|
</DialogFooter>
|
|
|
|
|
|
</DialogContent>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|