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-08 16:46:53 +09:00
|
|
|
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
2025-09-10 17:25:41 +09:00
|
|
|
import { DataFlowAPI, TableRelationship, TableInfo, ColumnInfo } 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;
|
|
|
|
|
settings?: any;
|
|
|
|
|
};
|
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 {
|
|
|
|
|
sourceField: string;
|
|
|
|
|
targetField: string;
|
|
|
|
|
saveConditions: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 외부 호출 설정
|
|
|
|
|
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;
|
2025-09-09 18:42:01 +09:00
|
|
|
diagramId?: number;
|
2025-09-08 16:46:53 +09:00
|
|
|
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-09 18:42:01 +09:00
|
|
|
diagramId,
|
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>({
|
|
|
|
|
sourceField: "",
|
|
|
|
|
targetField: "",
|
|
|
|
|
saveConditions: "",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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[]>([]);
|
|
|
|
|
|
|
|
|
|
// 테이블 목록 로드
|
|
|
|
|
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-11 10:45:16 +09:00
|
|
|
description: existingRel?.settings?.description || `${fromDisplayName}과 ${toDisplayName} 간의 데이터 관계`,
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 데이터 저장 기본값 설정
|
|
|
|
|
setDataSaveSettings({
|
|
|
|
|
sourceField: "",
|
|
|
|
|
targetField: "",
|
|
|
|
|
saveConditions: "데이터 저장 조건을 입력하세요",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// 외부 호출 기본값 설정
|
|
|
|
|
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-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-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-10 15:30:14 +09:00
|
|
|
relationship_type: config.relationshipType as any,
|
|
|
|
|
connection_type: config.connectionType as any,
|
|
|
|
|
company_code: companyCode,
|
|
|
|
|
settings: {
|
|
|
|
|
...settings,
|
|
|
|
|
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-05 18:00:18 +09:00
|
|
|
// 선택된 컬럼 데이터 가져오기
|
|
|
|
|
const selectedColumnsData = connection.selectedColumnsData || {};
|
|
|
|
|
const tableNames = Object.keys(selectedColumnsData);
|
|
|
|
|
const fromTable = tableNames[0];
|
|
|
|
|
const toTable = tableNames[1];
|
|
|
|
|
|
|
|
|
|
const fromTableData = selectedColumnsData[fromTable];
|
|
|
|
|
const toTableData = selectedColumnsData[toTable];
|
|
|
|
|
|
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-3">
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="sourceField" className="text-sm">
|
|
|
|
|
소스 필드
|
|
|
|
|
</Label>
|
|
|
|
|
<Input
|
|
|
|
|
id="sourceField"
|
|
|
|
|
value={dataSaveSettings.sourceField}
|
|
|
|
|
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, sourceField: e.target.value })}
|
|
|
|
|
placeholder="소스 필드"
|
|
|
|
|
className="text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="targetField" className="text-sm">
|
|
|
|
|
대상 필드
|
|
|
|
|
</Label>
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Input
|
|
|
|
|
id="targetField"
|
|
|
|
|
value={dataSaveSettings.targetField}
|
|
|
|
|
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, targetField: e.target.value })}
|
|
|
|
|
placeholder="대상 필드"
|
|
|
|
|
className="text-sm"
|
|
|
|
|
/>
|
|
|
|
|
<Button size="sm" variant="outline">
|
|
|
|
|
<Plus className="h-3 w-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<Label htmlFor="saveConditions" className="text-sm">
|
|
|
|
|
저장 조건
|
|
|
|
|
</Label>
|
|
|
|
|
<Textarea
|
|
|
|
|
id="saveConditions"
|
|
|
|
|
value={dataSaveSettings.saveConditions}
|
|
|
|
|
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, saveConditions: e.target.value })}
|
|
|
|
|
placeholder="데이터 저장 조건을 입력하세요"
|
|
|
|
|
rows={2}
|
|
|
|
|
className="text-sm"
|
|
|
|
|
/>
|
|
|
|
|
</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
|
|
|
|
|
|
|
|
{/* 연결 종류별 상세 설정 */}
|
|
|
|
|
{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>
|
|
|
|
|
);
|
|
|
|
|
};
|