2025-09-05 16:19:31 +09:00
|
|
|
"use client";
|
|
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
import React, { useState, useEffect, useCallback } from "react";
|
2025-09-05 16:19:31 +09:00
|
|
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
2025-09-19 15:47:35 +09:00
|
|
|
import {
|
|
|
|
|
AlertDialog,
|
|
|
|
|
AlertDialogAction,
|
|
|
|
|
AlertDialogContent,
|
|
|
|
|
AlertDialogDescription,
|
|
|
|
|
AlertDialogFooter,
|
|
|
|
|
AlertDialogHeader,
|
|
|
|
|
AlertDialogTitle,
|
|
|
|
|
} from "@/components/ui/alert-dialog";
|
2025-09-05 16:19:31 +09:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { Input } from "@/components/ui/input";
|
|
|
|
|
import { Label } from "@/components/ui/label";
|
2025-09-19 15:47:35 +09:00
|
|
|
import { Link, CheckCircle } from "lucide-react";
|
2025-09-12 09:58:49 +09:00
|
|
|
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo, ConditionNode } from "@/lib/api/dataflow";
|
2025-09-16 15:43:18 +09:00
|
|
|
import {
|
|
|
|
|
ConnectionConfig,
|
|
|
|
|
SimpleKeySettings,
|
|
|
|
|
DataSaveSettings,
|
2025-09-17 17:14:59 +09:00
|
|
|
SimpleExternalCallSettings,
|
2025-09-16 15:43:18 +09:00
|
|
|
ConnectionSetupModalProps,
|
|
|
|
|
} from "@/types/connectionTypes";
|
2025-09-26 17:11:18 +09:00
|
|
|
import { ExternalCallConfig } from "@/types/external-call/ExternalCallTypes";
|
2025-09-16 15:43:18 +09:00
|
|
|
import { isConditionalConnection } from "@/utils/connectionUtils";
|
|
|
|
|
import { useConditionManager } from "@/hooks/useConditionManager";
|
|
|
|
|
import { ConditionalSettings } from "./condition/ConditionalSettings";
|
|
|
|
|
import { ConnectionTypeSelector } from "./connection/ConnectionTypeSelector";
|
|
|
|
|
import { SimpleKeySettings as SimpleKeySettingsComponent } from "./connection/SimpleKeySettings";
|
|
|
|
|
import { DataSaveSettings as DataSaveSettingsComponent } from "./connection/DataSaveSettings";
|
2025-09-17 17:14:59 +09:00
|
|
|
import { SimpleExternalCallSettings as ExternalCallSettingsComponent } from "./connection/SimpleExternalCallSettings";
|
2025-09-26 17:11:18 +09:00
|
|
|
import ExternalCallPanel from "./external-call/ExternalCallPanel";
|
2025-09-19 15:47:35 +09:00
|
|
|
import { toast } from "sonner";
|
2025-09-05 16:19:31 +09:00
|
|
|
|
|
|
|
|
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: "",
|
|
|
|
|
connectionType: "simple-key",
|
|
|
|
|
fromColumnName: "",
|
|
|
|
|
toColumnName: "",
|
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
|
|
|
});
|
|
|
|
|
|
2025-09-17 17:14:59 +09:00
|
|
|
const [externalCallSettings, setExternalCallSettings] = useState<SimpleExternalCallSettings>({
|
|
|
|
|
message: "",
|
2025-09-05 18:00:18 +09:00
|
|
|
});
|
2025-09-05 16:19:31 +09:00
|
|
|
|
2025-09-26 17:11:18 +09:00
|
|
|
// 새로운 외부호출 설정 상태 (분리된 컴포넌트용)
|
|
|
|
|
const [externalCallConfig, setExternalCallConfig] = useState<ExternalCallConfig | null>(null);
|
|
|
|
|
|
2025-09-16 15:43:18 +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-19 15:47:35 +09:00
|
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
|
|
|
const [createdConnectionName, setCreatedConnectionName] = useState("");
|
|
|
|
|
const [pendingRelationshipData, setPendingRelationshipData] = useState<TableRelationship | null>(null);
|
2025-09-10 17:25:41 +09:00
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
// 조건 관리 훅 사용
|
|
|
|
|
const {
|
|
|
|
|
conditions,
|
|
|
|
|
setConditions,
|
|
|
|
|
addCondition,
|
|
|
|
|
addGroupStart,
|
|
|
|
|
addGroupEnd,
|
|
|
|
|
updateCondition,
|
|
|
|
|
removeCondition,
|
|
|
|
|
getCurrentGroupLevel,
|
|
|
|
|
} = useConditionManager();
|
|
|
|
|
|
|
|
|
|
// 기존 설정 로드 함수
|
|
|
|
|
const loadExistingSettings = useCallback(
|
|
|
|
|
(settings: Record<string, unknown>, connectionType: string) => {
|
|
|
|
|
if (connectionType === "simple-key" && settings.notes) {
|
|
|
|
|
setSimpleKeySettings({
|
|
|
|
|
notes: settings.notes as string,
|
|
|
|
|
});
|
2025-09-16 18:22:06 +09:00
|
|
|
} else if (connectionType === "data-save") {
|
|
|
|
|
// data-save 설정 로드 - 안전하게 처리 (다양한 구조 지원)
|
|
|
|
|
let actionsData: Record<string, unknown>[] = [];
|
2025-09-17 10:08:20 +09:00
|
|
|
const settingsRecord = settings as Record<string, unknown>;
|
2025-09-16 18:22:06 +09:00
|
|
|
|
2025-09-17 10:08:20 +09:00
|
|
|
if (Array.isArray(settingsRecord.actions)) {
|
2025-09-16 18:22:06 +09:00
|
|
|
// 직접 actions 배열이 있는 경우
|
2025-09-17 10:08:20 +09:00
|
|
|
actionsData = settingsRecord.actions as Record<string, unknown>[];
|
|
|
|
|
} else if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) {
|
2025-09-16 18:22:06 +09:00
|
|
|
// plan 객체 안에 actions가 있는 경우
|
2025-09-17 10:08:20 +09:00
|
|
|
const planRecord = settingsRecord.plan as Record<string, unknown>;
|
|
|
|
|
if (Array.isArray(planRecord.actions)) {
|
|
|
|
|
actionsData = planRecord.actions as Record<string, unknown>[];
|
|
|
|
|
}
|
2025-09-16 18:22:06 +09:00
|
|
|
} else if (Array.isArray(settings)) {
|
|
|
|
|
// settings 자체가 actions 배열인 경우
|
|
|
|
|
actionsData = settings as Record<string, unknown>[];
|
|
|
|
|
}
|
2025-09-16 15:43:18 +09:00
|
|
|
setDataSaveSettings({
|
|
|
|
|
actions: actionsData.map((action: Record<string, unknown>) => ({
|
|
|
|
|
id: (action.id as string) || `action-${Date.now()}`,
|
|
|
|
|
name: (action.name as string) || "새 액션",
|
|
|
|
|
actionType: (action.actionType as "insert" | "update" | "delete" | "upsert") || "insert",
|
|
|
|
|
conditions: Array.isArray(action.conditions)
|
|
|
|
|
? (action.conditions as ConditionNode[]).map((condition) => ({
|
|
|
|
|
...condition,
|
|
|
|
|
operator: condition.operator || "=", // 기본값 보장
|
|
|
|
|
}))
|
|
|
|
|
: [],
|
|
|
|
|
fieldMappings: Array.isArray(action.fieldMappings)
|
|
|
|
|
? action.fieldMappings.map((mapping: Record<string, unknown>) => ({
|
|
|
|
|
sourceTable: (mapping.sourceTable as string) || "",
|
|
|
|
|
sourceField: (mapping.sourceField as string) || "",
|
|
|
|
|
targetTable: (mapping.targetTable as string) || "",
|
|
|
|
|
targetField: (mapping.targetField as string) || "",
|
|
|
|
|
defaultValue: (mapping.defaultValue as string) || "",
|
|
|
|
|
transformFunction: (mapping.transformFunction as string) || "",
|
|
|
|
|
}))
|
|
|
|
|
: [],
|
|
|
|
|
splitConfig: action.splitConfig
|
|
|
|
|
? {
|
|
|
|
|
sourceField: ((action.splitConfig as Record<string, unknown>).sourceField as string) || "",
|
|
|
|
|
delimiter: ((action.splitConfig as Record<string, unknown>).delimiter as string) || ",",
|
|
|
|
|
targetField: ((action.splitConfig as Record<string, unknown>).targetField as string) || "",
|
|
|
|
|
}
|
|
|
|
|
: undefined,
|
|
|
|
|
})),
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-16 18:22:06 +09:00
|
|
|
// control 설정도 로드 (전체 실행 조건)
|
2025-09-17 10:08:20 +09:00
|
|
|
if (settingsRecord.control && typeof settingsRecord.control === "object" && settingsRecord.control !== null) {
|
|
|
|
|
const controlRecord = settingsRecord.control as Record<string, unknown>;
|
|
|
|
|
if (Array.isArray(controlRecord.conditionTree)) {
|
|
|
|
|
const conditionTree = controlRecord.conditionTree as ConditionNode[];
|
|
|
|
|
setConditions(
|
|
|
|
|
conditionTree.map((condition) => ({
|
|
|
|
|
...condition,
|
|
|
|
|
operator: condition.operator || "=", // 기본값 보장
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-09-16 15:43:18 +09:00
|
|
|
}
|
|
|
|
|
} else if (connectionType === "external-call") {
|
2025-09-17 17:14:59 +09:00
|
|
|
// 외부 호출 설정은 plan에서 로드
|
|
|
|
|
const settingsRecord = settings as Record<string, unknown>;
|
|
|
|
|
let externalCallData: Record<string, unknown> = {};
|
|
|
|
|
|
|
|
|
|
if (settingsRecord.plan && typeof settingsRecord.plan === "object" && settingsRecord.plan !== null) {
|
|
|
|
|
const planRecord = settingsRecord.plan as Record<string, unknown>;
|
|
|
|
|
if (planRecord.externalCall && typeof planRecord.externalCall === "object") {
|
|
|
|
|
externalCallData = planRecord.externalCall as Record<string, unknown>;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
setExternalCallSettings({
|
2025-09-17 17:14:59 +09:00
|
|
|
configId: (externalCallData.configId as number) || undefined,
|
|
|
|
|
configName: (externalCallData.configName as string) || undefined,
|
|
|
|
|
message: (externalCallData.message as string) || "",
|
2025-09-16 15:43:18 +09:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
[setConditions, setSimpleKeySettings, setDataSaveSettings, setExternalCallSettings],
|
|
|
|
|
);
|
2025-09-12 09:58:49 +09:00
|
|
|
|
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-19 10:45:09 +09:00
|
|
|
// 모달이 열릴 때마다 캐시 초기화 (라벨 업데이트 반영)
|
|
|
|
|
setTableColumnsCache({});
|
|
|
|
|
|
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-16 12:37:57 +09:00
|
|
|
const connectionType =
|
|
|
|
|
(existingRel?.connectionType as "simple-key" | "data-save" | "external-call") || "simple-key";
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
setConfig({
|
2025-09-11 10:45:16 +09:00
|
|
|
relationshipName: existingRel?.relationshipName || `${fromDisplayName} → ${toDisplayName}`,
|
2025-09-16 12:37:57 +09:00
|
|
|
connectionType,
|
2025-09-05 18:00:18 +09:00
|
|
|
fromColumnName: "",
|
|
|
|
|
toColumnName: "",
|
2025-09-11 10:45:16 +09:00
|
|
|
settings: existingRel?.settings || {},
|
2025-09-08 16:46:53 +09:00
|
|
|
});
|
|
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
// 기존 설정 데이터 로드
|
2025-09-16 12:37:57 +09:00
|
|
|
if (existingRel?.settings) {
|
2025-09-16 15:43:18 +09:00
|
|
|
loadExistingSettings(existingRel.settings, connectionType);
|
2025-09-16 12:37:57 +09:00
|
|
|
} else {
|
|
|
|
|
// 기본값 설정
|
|
|
|
|
setSimpleKeySettings({
|
2025-09-16 18:15:54 +09:00
|
|
|
notes: existingRel?.note || `${fromDisplayName}과 ${toDisplayName} 간의 키값 연결`,
|
2025-09-16 12:37:57 +09:00
|
|
|
});
|
2025-09-16 15:43:18 +09:00
|
|
|
setDataSaveSettings({ actions: [] });
|
2025-09-16 12:37:57 +09:00
|
|
|
}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
// 필드 선택 상태 초기화
|
2025-09-15 20:07:28 +09:00
|
|
|
setSelectedFromColumns([]);
|
|
|
|
|
setSelectedToColumns([]);
|
|
|
|
|
|
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
|
|
|
}
|
2025-09-16 15:43:18 +09:00
|
|
|
}, [isOpen, connection, setConditions, loadExistingSettings]);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
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
|
|
|
// 테이블 컬럼 로드 함수 (캐시 활용)
|
2025-09-19 10:45:09 +09:00
|
|
|
const loadTableColumns = async (tableName: string, forceReload = false): Promise<ColumnInfo[]> => {
|
|
|
|
|
if (tableColumnsCache[tableName] && !forceReload) {
|
2025-09-12 16:15:36 +09:00
|
|
|
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>();
|
|
|
|
|
|
|
|
|
|
// 필드 매핑에서 사용되는 모든 테이블 수집
|
2025-09-16 15:43:18 +09:00
|
|
|
dataSaveSettings.actions?.forEach((action) => {
|
|
|
|
|
action.fieldMappings?.forEach((mapping) => {
|
2025-09-12 16:15:36 +09:00
|
|
|
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 = {};
|
2025-09-17 17:14:59 +09:00
|
|
|
let plan = {}; // plan 변수 선언
|
2025-09-10 15:30:14 +09:00
|
|
|
|
|
|
|
|
switch (config.connectionType) {
|
|
|
|
|
case "simple-key":
|
|
|
|
|
settings = simpleKeySettings;
|
|
|
|
|
break;
|
|
|
|
|
case "data-save":
|
|
|
|
|
settings = dataSaveSettings;
|
2025-09-18 13:26:42 +09:00
|
|
|
|
|
|
|
|
// INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증
|
|
|
|
|
for (const action of dataSaveSettings.actions) {
|
|
|
|
|
if (action.actionType !== "insert") {
|
|
|
|
|
if (!action.conditions || action.conditions.length === 0) {
|
|
|
|
|
toast.error(
|
|
|
|
|
`${action.actionType.toUpperCase()} 액션은 실행조건이 필수입니다. '${action.name}' 액션에 실행조건을 추가해주세요.`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 실제 조건이 있는지 확인 (group-start, group-end만 있는 경우 제외)
|
|
|
|
|
const hasValidConditions = action.conditions.some((condition) => {
|
|
|
|
|
if (condition.type !== "condition") return false;
|
|
|
|
|
if (!condition.field || !condition.operator) return false;
|
|
|
|
|
|
|
|
|
|
// value가 null, undefined, 빈 문자열이면 유효하지 않음
|
|
|
|
|
const value = condition.value;
|
|
|
|
|
if (value === null || value === undefined || value === "") return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!hasValidConditions) {
|
|
|
|
|
toast.error(
|
|
|
|
|
`${action.actionType.toUpperCase()} 액션은 완전한 실행조건이 필요합니다. '${action.name}' 액션에 필드, 연산자, 값을 모두 설정해주세요.`,
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-10 15:30:14 +09:00
|
|
|
break;
|
|
|
|
|
case "external-call":
|
2025-09-26 17:11:18 +09:00
|
|
|
// 새로운 외부호출 설정을 plan에 저장
|
|
|
|
|
if (externalCallConfig) {
|
|
|
|
|
plan = {
|
|
|
|
|
externalCall: externalCallConfig,
|
|
|
|
|
};
|
|
|
|
|
} else {
|
|
|
|
|
// 기존 설정 호환성 유지
|
|
|
|
|
plan = {
|
|
|
|
|
externalCall: {
|
|
|
|
|
configId: externalCallSettings.configId,
|
|
|
|
|
configName: externalCallSettings.configName,
|
|
|
|
|
message: externalCallSettings.message,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
2025-09-17 17:14:59 +09:00
|
|
|
settings = {}; // 외부 호출은 settings에 저장하지 않음
|
2025-09-10 15:30:14 +09:00
|
|
|
break;
|
|
|
|
|
}
|
2025-09-08 16:46:53 +09:00
|
|
|
|
2025-09-15 15:12:02 +09:00
|
|
|
// 단순 키값 연결일 때만 컬럼 선택 검증
|
|
|
|
|
if (config.connectionType === "simple-key") {
|
2025-09-16 12:37:57 +09:00
|
|
|
if (selectedFromColumns.length === 0 || selectedToColumns.length === 0) {
|
|
|
|
|
toast.error("선택된 컬럼이 없습니다. From과 To 테이블에서 각각 최소 1개 이상의 컬럼을 선택해주세요.");
|
|
|
|
|
return;
|
2025-09-15 15:12:02 +09:00
|
|
|
}
|
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
|
|
|
// 조건부 연결 설정 데이터 준비
|
2025-09-16 15:43:18 +09:00
|
|
|
const conditionalSettings = isConditionalConnection(config.connectionType)
|
2025-09-12 09:58:49 +09:00
|
|
|
? {
|
|
|
|
|
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,
|
2025-09-16 14:44:41 +09:00
|
|
|
conditions:
|
|
|
|
|
action.conditions?.map((condition) => {
|
|
|
|
|
// 모든 조건 타입에 대해 operator 필드 보장
|
|
|
|
|
const baseCondition = { ...condition };
|
|
|
|
|
if (condition.type === "condition") {
|
|
|
|
|
baseCondition.operator = condition.operator || "=";
|
|
|
|
|
}
|
|
|
|
|
return baseCondition;
|
|
|
|
|
}) || [],
|
2025-09-12 16:15:36 +09:00
|
|
|
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-15 15:12:02 +09:00
|
|
|
// 컬럼 정보는 단순 키값 연결일 때만 사용
|
|
|
|
|
const finalFromColumns = config.connectionType === "simple-key" ? selectedFromColumns : [];
|
|
|
|
|
const finalToColumns = config.connectionType === "simple-key" ? selectedToColumns : [];
|
|
|
|
|
|
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,
|
2025-09-15 15:12:02 +09:00
|
|
|
from_column_name: finalFromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
|
|
|
|
to_column_name: finalToColumns.join(","), // 여러 컬럼을 콤마로 구분
|
2025-09-12 09:58:49 +09:00
|
|
|
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-17 17:14:59 +09:00
|
|
|
...plan, // 외부 호출 plan 추가
|
2025-09-10 15:30:14 +09:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
// 성공 모달 표시를 위한 상태 설정
|
|
|
|
|
setCreatedConnectionName(config.relationshipName);
|
|
|
|
|
setPendingRelationshipData(relationshipData);
|
|
|
|
|
setShowSuccessModal(true);
|
2025-09-05 16:19:31 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-05 18:00:18 +09:00
|
|
|
const handleCancel = () => {
|
|
|
|
|
setConfig({
|
|
|
|
|
relationshipName: "",
|
|
|
|
|
connectionType: "simple-key",
|
|
|
|
|
fromColumnName: "",
|
|
|
|
|
toColumnName: "",
|
|
|
|
|
});
|
|
|
|
|
onCancel();
|
2025-09-05 16:19:31 +09:00
|
|
|
};
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
const handleSuccessModalClose = () => {
|
|
|
|
|
setShowSuccessModal(false);
|
|
|
|
|
setCreatedConnectionName("");
|
|
|
|
|
|
|
|
|
|
// 저장된 관계 데이터를 부모에게 전달
|
|
|
|
|
if (pendingRelationshipData) {
|
|
|
|
|
onConfirm(pendingRelationshipData);
|
|
|
|
|
setPendingRelationshipData(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
handleCancel(); // 원래 모달도 닫기
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
// 연결 종류별 설정 패널 렌더링
|
|
|
|
|
const renderConnectionTypeSettings = () => {
|
2025-09-26 17:11:18 +09:00
|
|
|
console.log("🔍 [ConnectionSetupModal] renderConnectionTypeSettings - connectionType:", config.connectionType);
|
|
|
|
|
console.log("🔍 [ConnectionSetupModal] externalCallConfig:", externalCallConfig);
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
switch (config.connectionType) {
|
|
|
|
|
case "simple-key":
|
2025-09-16 12:37:57 +09:00
|
|
|
return (
|
2025-09-16 15:43:18 +09:00
|
|
|
<SimpleKeySettingsComponent
|
|
|
|
|
settings={simpleKeySettings}
|
|
|
|
|
onSettingsChange={setSimpleKeySettings}
|
|
|
|
|
availableTables={availableTables}
|
|
|
|
|
selectedFromTable={selectedFromTable}
|
|
|
|
|
selectedToTable={selectedToTable}
|
|
|
|
|
fromTableColumns={fromTableColumns}
|
|
|
|
|
toTableColumns={toTableColumns}
|
|
|
|
|
selectedFromColumns={selectedFromColumns}
|
|
|
|
|
selectedToColumns={selectedToColumns}
|
|
|
|
|
onFromColumnsChange={setSelectedFromColumns}
|
|
|
|
|
onToColumnsChange={setSelectedToColumns}
|
|
|
|
|
/>
|
2025-09-08 16:46:53 +09:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
case "data-save":
|
|
|
|
|
return (
|
2025-09-16 15:43:18 +09:00
|
|
|
<DataSaveSettingsComponent
|
|
|
|
|
settings={dataSaveSettings}
|
|
|
|
|
onSettingsChange={setDataSaveSettings}
|
|
|
|
|
availableTables={availableTables}
|
|
|
|
|
fromTableColumns={fromTableColumns}
|
|
|
|
|
toTableColumns={toTableColumns}
|
2025-09-18 10:05:28 +09:00
|
|
|
fromTableName={selectedFromTable}
|
|
|
|
|
toTableName={selectedToTable}
|
2025-09-16 15:43:18 +09:00
|
|
|
tableColumnsCache={tableColumnsCache}
|
|
|
|
|
/>
|
2025-09-08 16:46:53 +09:00
|
|
|
);
|
2025-09-05 16:19:31 +09:00
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
case "external-call":
|
2025-09-26 17:11:18 +09:00
|
|
|
console.log("🚀 [ConnectionSetupModal] Rendering ExternalCallPanel");
|
2025-09-08 16:46:53 +09:00
|
|
|
return (
|
2025-09-26 17:11:18 +09:00
|
|
|
<ExternalCallPanel
|
|
|
|
|
relationshipId={connection?.id || `temp-${Date.now()}`}
|
|
|
|
|
initialSettings={externalCallConfig}
|
|
|
|
|
onSettingsChange={setExternalCallConfig}
|
|
|
|
|
/>
|
2025-09-08 16:46:53 +09:00
|
|
|
);
|
2025-09-05 18:00:18 +09:00
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
default:
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-16 15:43:18 +09:00
|
|
|
const isButtonDisabled = () => {
|
2025-09-16 17:50:01 +09:00
|
|
|
// 공통 검증: 관계 이름은 필수
|
|
|
|
|
const hasRelationshipName = !!config.relationshipName?.trim();
|
|
|
|
|
if (!hasRelationshipName) return true;
|
|
|
|
|
|
|
|
|
|
// 연결 타입별 검증
|
|
|
|
|
switch (config.connectionType) {
|
|
|
|
|
case "simple-key":
|
|
|
|
|
// 단순 키값 연결: From과 To 컬럼이 모두 선택되어야 함
|
|
|
|
|
const hasFromColumns = selectedFromColumns.length > 0;
|
|
|
|
|
const hasToColumns = selectedToColumns.length > 0;
|
|
|
|
|
return !hasFromColumns || !hasToColumns;
|
2025-09-16 15:43:18 +09:00
|
|
|
|
2025-09-16 17:50:01 +09:00
|
|
|
case "data-save":
|
|
|
|
|
// 데이터 저장: 액션과 필드 매핑이 완성되어야 함
|
|
|
|
|
const hasActions = dataSaveSettings.actions.length > 0;
|
2025-09-18 13:26:42 +09:00
|
|
|
|
|
|
|
|
// DELETE 액션은 필드 매핑이 필요 없음
|
|
|
|
|
const allActionsHaveMappings = dataSaveSettings.actions.every((action) => {
|
|
|
|
|
if (action.actionType === "delete") {
|
|
|
|
|
return true; // DELETE는 필드 매핑 불필요
|
|
|
|
|
}
|
|
|
|
|
return action.fieldMappings.length > 0;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const allMappingsComplete = dataSaveSettings.actions.every((action) => {
|
|
|
|
|
if (action.actionType === "delete") {
|
|
|
|
|
return true; // DELETE는 필드 매핑 검증 생략
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-19 11:27:16 +09:00
|
|
|
// INSERT 액션의 경우 최소 하나의 매핑이 있으면 됨 (모든 컬럼 매핑 필수 조건 제거)
|
2025-09-18 17:17:06 +09:00
|
|
|
if (action.actionType === "insert") {
|
2025-09-19 11:27:16 +09:00
|
|
|
return true; // 필드 매핑이 있으면 충분함
|
2025-09-18 17:17:06 +09:00
|
|
|
}
|
|
|
|
|
|
2025-09-18 13:26:42 +09:00
|
|
|
return action.fieldMappings.every((mapping) => {
|
2025-09-16 17:50:01 +09:00
|
|
|
// 타겟은 항상 필요
|
|
|
|
|
if (!mapping.targetTable || !mapping.targetField) return false;
|
|
|
|
|
|
|
|
|
|
// 소스와 기본값 중 하나는 있어야 함
|
|
|
|
|
const hasSource = mapping.sourceTable && mapping.sourceField;
|
|
|
|
|
const hasDefault = mapping.defaultValue && mapping.defaultValue.trim();
|
|
|
|
|
|
|
|
|
|
// FROM 테이블이 비어있으면 기본값이 필요
|
|
|
|
|
if (!mapping.sourceTable) {
|
|
|
|
|
return !!hasDefault;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FROM 테이블이 있으면 소스 매핑 완성 또는 기본값 필요
|
|
|
|
|
return hasSource || hasDefault;
|
2025-09-18 13:26:42 +09:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// INSERT가 아닌 액션 타입에 대한 실행조건 필수 검증
|
|
|
|
|
const allRequiredConditionsMet = dataSaveSettings.actions.every((action) => {
|
|
|
|
|
if (action.actionType === "insert") {
|
|
|
|
|
return true; // INSERT는 조건 불필요
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// INSERT가 아닌 액션은 유효한 조건이 있어야 함
|
|
|
|
|
if (!action.conditions || action.conditions.length === 0) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 실제 조건이 있는지 확인 (group-start, group-end만 있는 경우 제외)
|
|
|
|
|
const hasValidConditions = action.conditions.some((condition) => {
|
|
|
|
|
if (condition.type !== "condition") return false;
|
|
|
|
|
if (!condition.field || !condition.operator) return false;
|
|
|
|
|
|
|
|
|
|
// value가 null, undefined, 빈 문자열이면 유효하지 않음
|
|
|
|
|
const value = condition.value;
|
|
|
|
|
if (value === null || value === undefined || value === "") return false;
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return hasValidConditions;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return !hasActions || !allActionsHaveMappings || !allMappingsComplete || !allRequiredConditionsMet;
|
2025-09-16 15:43:18 +09:00
|
|
|
|
2025-09-16 17:50:01 +09:00
|
|
|
case "external-call":
|
2025-09-26 17:11:18 +09:00
|
|
|
// 외부 호출: 새로운 설정이 있으면 API URL 검증, 없으면 기존 설정 검증
|
|
|
|
|
if (externalCallConfig) {
|
|
|
|
|
return !externalCallConfig.restApiSettings?.apiUrl?.trim();
|
|
|
|
|
} else {
|
|
|
|
|
return !externalCallSettings.configId || !externalCallSettings.message?.trim();
|
|
|
|
|
}
|
2025-09-16 17:50:01 +09:00
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-09-16 15:43:18 +09:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!connection) return null;
|
|
|
|
|
|
2025-09-08 16:46:53 +09:00
|
|
|
return (
|
2025-09-19 15:47:35 +09:00
|
|
|
<>
|
|
|
|
|
<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">
|
|
|
|
|
{/* 기본 연결 설정 */}
|
|
|
|
|
<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>
|
2025-09-08 16:46:53 +09:00
|
|
|
</div>
|
2025-09-19 15:47:35 +09:00
|
|
|
|
|
|
|
|
{/* 연결 종류 선택 */}
|
|
|
|
|
<ConnectionTypeSelector config={config} onConfigChange={setConfig} />
|
|
|
|
|
|
|
|
|
|
{/* 조건부 연결을 위한 조건 설정 */}
|
|
|
|
|
{isConditionalConnection(config.connectionType) && (
|
|
|
|
|
<ConditionalSettings
|
|
|
|
|
conditions={conditions}
|
|
|
|
|
fromTableColumns={fromTableColumns}
|
2025-09-21 09:53:05 +09:00
|
|
|
fromTableName={selectedFromTable || connection.fromNode.tableName}
|
2025-09-19 15:47:35 +09:00
|
|
|
onAddCondition={addCondition}
|
|
|
|
|
onAddGroupStart={addGroupStart}
|
|
|
|
|
onAddGroupEnd={addGroupEnd}
|
|
|
|
|
onUpdateCondition={updateCondition}
|
|
|
|
|
onRemoveCondition={removeCondition}
|
|
|
|
|
getCurrentGroupLevel={getCurrentGroupLevel}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 연결 종류별 상세 설정 */}
|
|
|
|
|
{renderConnectionTypeSettings()}
|
2025-09-08 16:46:53 +09:00
|
|
|
</div>
|
|
|
|
|
|
2025-09-19 15:47:35 +09:00
|
|
|
<DialogFooter>
|
|
|
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
|
|
|
취소
|
|
|
|
|
</Button>
|
|
|
|
|
<Button onClick={handleConfirm} disabled={isButtonDisabled()}>
|
|
|
|
|
연결 생성
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
<AlertDialog open={showSuccessModal} onOpenChange={setShowSuccessModal}>
|
|
|
|
|
<AlertDialogContent>
|
|
|
|
|
<AlertDialogHeader>
|
|
|
|
|
<AlertDialogTitle className="flex items-center gap-2">
|
|
|
|
|
<CheckCircle className="h-5 w-5 text-green-500" />
|
|
|
|
|
연결 생성 완료
|
|
|
|
|
</AlertDialogTitle>
|
|
|
|
|
<AlertDialogDescription className="text-base">
|
|
|
|
|
<span className="font-medium text-green-600">{createdConnectionName}</span> 연결이 생성되었습니다.
|
|
|
|
|
<br />
|
|
|
|
|
<span className="mt-2 block text-sm text-gray-500">
|
|
|
|
|
생성된 연결은 데이터플로우 다이어그램에서 확인할 수 있습니다.
|
|
|
|
|
</span>
|
|
|
|
|
</AlertDialogDescription>
|
|
|
|
|
</AlertDialogHeader>
|
|
|
|
|
<AlertDialogFooter>
|
|
|
|
|
<AlertDialogAction onClick={handleSuccessModalClose}>확인</AlertDialogAction>
|
|
|
|
|
</AlertDialogFooter>
|
|
|
|
|
</AlertDialogContent>
|
|
|
|
|
</AlertDialog>
|
|
|
|
|
</>
|
2025-09-05 16:19:31 +09:00
|
|
|
);
|
|
|
|
|
};
|