ERP-node/frontend/components/dataflow/ConnectionSetupModal.tsx

1622 lines
76 KiB
TypeScript
Raw Normal View History

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 {
actions: Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
conditions?: Array<{
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: string;
2025-09-15 10:11:22 +09:00
logicalOperator?: "AND" | "OR";
}>;
fieldMappings: Array<{
sourceTable?: string;
2025-09-15 10:53:33 +09:00
sourceField: string;
targetTable?: string;
2025-09-15 10:53:33 +09:00
targetField: string;
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>({
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[]>([]);
// 필요시 로드하는 테이블 컬럼 캐시
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]);
// 테이블 컬럼 로드 함수 (캐시 활용)
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
const handleConfirm = () => {
2025-09-08 16:46:53 +09:00
if (!config.relationshipName || !connection) {
toast.error("필수 정보를 모두 입력해주세요.");
return;
}
// 연결 종류별 설정을 준비
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개 이상의 컬럼을 선택해주세요.");
return;
2025-09-05 16:19:31 +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-12 09:58:49 +09:00
conditionTree:
conditions.length > 0
? {
type: "group" as const,
operator: "AND" as const,
children: conditions,
}
: null,
},
category: {
type: config.connectionType,
},
plan: {
sourceTable: fromTableName,
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
},
}
: {};
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
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,
company_code: companyCode,
settings: {
...settings,
2025-09-12 09:58:49 +09:00
...conditionalSettings, // 조건부 연결 설정 추가
description: config.description,
multiColumnMapping: {
2025-09-10 17:25:41 +09:00
fromColumns: selectedFromColumns,
toColumns: selectedToColumns,
fromTable: fromTableName,
toTable: toTableName,
},
2025-09-10 17:25:41 +09:00
isMultiColumn: selectedFromColumns.length > 1 || selectedToColumns.length > 1,
columnCount: {
2025-09-10 17:25:41 +09:00
from: selectedFromColumns.length,
to: selectedToColumns.length,
},
},
};
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";
};
// 조건 관리 헬퍼 함수들
const addCondition = () => {
const newCondition: ConditionNode = {
type: "condition",
field: "",
operator_type: "=",
value: "",
dataType: "string",
2025-09-12 11:33:54 +09:00
operator: "AND", // 기본값으로 AND 설정
2025-09-12 09:58:49 +09:00
};
setConditions([...conditions, newCondition]);
};
const updateCondition = (index: number, field: keyof ConditionNode, value: string) => {
const updatedConditions = [...conditions];
updatedConditions[index] = { ...updatedConditions[index], [field]: value };
setConditions(updatedConditions);
};
const removeCondition = (index: number) => {
const updatedConditions = conditions.filter((_, i) => i !== index);
setConditions(updatedConditions);
};
// 조건부 연결 설정 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" />
<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>
<Button size="sm" variant="outline" onClick={() => addCondition()} className="h-7 text-xs">
<Plus className="mr-1 h-3 w-3" />
</Button>
</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>
) : (
conditions.map((condition, index) => (
2025-09-15 10:11:22 +09:00
<div key={index} className="flex items-center gap-2 rounded border bg-white p-2">
{/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */}
2025-09-12 11:33:54 +09:00
{index > 0 && (
<Select
2025-09-15 10:11:22 +09:00
value={conditions[index - 1]?.operator || "AND"}
onValueChange={(value: "AND" | "OR") => updateCondition(index - 1, "operator", value)}
2025-09-12 11:33:54 +09:00
>
2025-09-15 10:11:22 +09:00
<SelectTrigger className="h-8 w-24 border-blue-200 bg-blue-50 text-xs">
2025-09-12 11:33:54 +09:00
<SelectValue />
</SelectTrigger>
<SelectContent>
2025-09-15 10:11:22 +09:00
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
2025-09-12 11:33:54 +09:00
</SelectContent>
</Select>
2025-09-15 10:11:22 +09:00
)}
2025-09-12 11:33:54 +09:00
2025-09-15 10:11:22 +09:00
{/* 조건 필드들 */}
<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=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</SelectItem>
<SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</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>
2025-09-12 09:58:49 +09:00
</div>
))
)}
</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>
<div className="space-y-4">
{/* 액션 목록 */}
2025-09-15 10:53:33 +09:00
<div>
<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: "",
},
};
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 10:53:33 +09:00
<Input
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 10:53:33 +09:00
</div>
<div className="grid grid-cols-1 gap-3">
{/* 액션 타입 */}
2025-09-15 10:53:33 +09:00
<div>
<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 10:53:33 +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>
)}
</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();
const newActions = [...dataSaveSettings.actions];
2025-09-15 10:11:22 +09:00
newActions[actionIndex].conditions = [];
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-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">
<Button
size="sm"
variant="outline"
onClick={() => {
const newActions = [...dataSaveSettings.actions];
2025-09-15 10:11:22 +09:00
if (!newActions[actionIndex].conditions) {
newActions[actionIndex].conditions = [];
}
newActions[actionIndex].conditions = [
...(newActions[actionIndex].conditions || []),
{ field: "", operator: "=", value: "", logicalOperator: "AND" },
];
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 text-xs"
2025-09-15 10:11:22 +09:00
>
<Plus className="mr-1 h-2 w-2" />
</Button>
</div>
{action.conditions && action.conditions.length > 0 && (
<div className="space-y-2">
{action.conditions.map((condition, condIndex) => (
<div key={condIndex} className="flex items-center gap-2">
{/* 첫 번째 조건이 아닐 때 AND/OR 연산자를 앞에 표시 */}
{condIndex > 0 && (
<Select
value={action.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-20 border-green-200 bg-green-50 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
)}
{/* 조건 필드들 */}
<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}
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].conditions![condIndex].operator = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<SelectTrigger className="h-6 w-16 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</SelectItem>
<SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</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 (
2025-09-15 10:53:33 +09:00
<Input
2025-09-15 10:11:22 +09:00
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" />
2025-09-15 10:53:33 +09:00
</Button>
</div>
2025-09-15 10:11:22 +09:00
))}
2025-09-15 10:53:33 +09:00
</div>
2025-09-15 10:11:22 +09:00
)}
2025-09-15 10:53:33 +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>
)}
</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();
const newActions = [...dataSaveSettings.actions];
2025-09-15 10:11:22 +09:00
newActions[actionIndex].splitConfig = {
sourceField: "",
delimiter: ",",
targetField: "",
};
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 10:53:33 +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>
</div>
</div>
2025-09-15 10:11:22 +09:00
</details>
</div>
{/* 필드 매핑 */}
<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}>
{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}>
{column.columnName}
2025-09-15 10:11:22 +09:00
</div>
</SelectItem>
))}
2025-09-15 10:11:22 +09:00
</SelectContent>
</Select>
</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) => {
const newActions = [...dataSaveSettings.actions];
2025-09-15 10:11:22 +09:00
newActions[actionIndex].fieldMappings[mappingIndex].targetTable = value;
newActions[actionIndex].fieldMappings[mappingIndex].targetField = "";
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) => {
const newActions = [...dataSaveSettings.actions];
2025-09-15 10:11:22 +09:00
newActions[actionIndex].fieldMappings[mappingIndex].targetField = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
2025-09-15 10:11:22 +09:00
disabled={!mapping.targetTable}
>
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>
</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>
</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>
);
};