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

577 lines
21 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-08 16:46:53 +09:00
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow";
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
};
};
}
// 연결 설정 타입
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;
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,
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-05 18:00:18 +09:00
// 모달이 열릴 때 기본값 설정
2025-09-05 16:19:31 +09:00
useEffect(() => {
if (isOpen && connection) {
2025-09-05 18:00:18 +09:00
const fromTableName = connection.fromNode.displayName;
const toTableName = connection.toNode.displayName;
setConfig({
relationshipName: `${fromTableName}${toTableName}`,
relationshipType: "one-to-one",
connectionType: "simple-key",
fromColumnName: "",
toColumnName: "",
description: `${fromTableName}${toTableName} 간의 데이터 관계`,
2025-09-08 16:46:53 +09:00
settings: {},
});
// 단순 키값 연결 기본값 설정
setSimpleKeySettings({
notes: `${fromTableName}${toTableName} 간의 키값 연결`,
});
// 데이터 저장 기본값 설정
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-05 16:19:31 +09:00
}
}, [isOpen, connection]);
2025-09-08 16:46:53 +09:00
const handleConfirm = async () => {
if (!config.relationshipName || !connection) {
toast.error("필수 정보를 모두 입력해주세요.");
return;
}
try {
// 연결 종류별 설정을 준비
let settings = {};
switch (config.connectionType) {
case "simple-key":
settings = simpleKeySettings;
break;
case "data-save":
settings = dataSaveSettings;
break;
case "external-call":
settings = externalCallSettings;
break;
}
// 선택된 컬럼들 추출
const selectedColumnsData = connection.selectedColumnsData || {};
const tableNames = Object.keys(selectedColumnsData);
const fromTable = tableNames[0];
const toTable = tableNames[1];
const fromColumns = selectedColumnsData[fromTable]?.columns || [];
const toColumns = selectedColumnsData[toTable]?.columns || [];
if (fromColumns.length === 0 || toColumns.length === 0) {
toast.error("선택된 컬럼이 없습니다.");
return;
}
toast.loading("관계를 생성하고 있습니다...", { id: "create-relationship" });
// 단일 관계 데이터 준비 (모든 선택된 컬럼 정보 포함)
// API 요청용 데이터 (camelCase)
const apiRequestData = {
...(diagramId && diagramId > 0 ? { diagramId: diagramId } : {}), // diagramId가 유효할 때만 추가
relationshipName: config.relationshipName,
fromTableName: connection.fromNode.tableName,
fromColumnName: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
toTableName: connection.toNode.tableName,
toColumnName: toColumns.join(","), // 여러 컬럼을 콤마로 구분
relationshipType: config.relationshipType,
connectionType: config.connectionType,
companyCode: companyCode,
settings: {
...settings,
multiColumnMapping: {
fromColumns: fromColumns,
toColumns: toColumns,
fromTable: selectedColumnsData[fromTable]?.displayName || fromTable,
toTable: selectedColumnsData[toTable]?.displayName || toTable,
},
isMultiColumn: fromColumns.length > 1 || toColumns.length > 1,
columnCount: {
from: fromColumns.length,
to: toColumns.length,
},
},
2025-09-08 16:46:53 +09:00
};
// API 호출
const createdRelationship = await DataFlowAPI.createRelationship(apiRequestData as any);
2025-09-08 16:46:53 +09:00
toast.success("관계가 성공적으로 생성되었습니다!", { id: "create-relationship" });
// 성공 콜백 호출
onConfirm(createdRelationship);
2025-09-05 18:00:18 +09:00
handleCancel(); // 모달 닫기
2025-09-08 16:46:53 +09:00
} catch (error) {
console.error("관계 생성 오류:", error);
toast.error("관계 생성에 실패했습니다. 다시 시도해주세요.", { id: "create-relationship" });
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">
{/* 연결 정보 표시 */}
<div className="rounded-lg border bg-gray-50 p-3">
<div className="mb-2 text-sm font-medium"> </div>
<div className="flex items-center gap-2 text-sm">
<span className="font-medium">{fromTableData?.displayName || fromTable}</span>
<span className="text-xs text-gray-500">({fromTable})</span>
<ArrowRight className="h-4 w-4 text-gray-400" />
<span className="font-medium">{toTableData?.displayName || toTable}</span>
<span className="text-xs text-gray-500">({toTable})</span>
</div>
<div className="mt-2 flex flex-wrap gap-1">
{fromTableData?.columns.map((column, index) => (
<Badge key={`${fromTable}-${column}-${index}`} variant="outline" className="text-xs">
{column}
</Badge>
))}
{toTableData?.columns.map((column, index) => (
<Badge key={`${toTable}-${column}-${index}`} variant="secondary" className="text-xs">
{column}
</Badge>
))}
</div>
</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>
<Label htmlFor="relationshipType"> </Label>
<Select
value={config.relationshipType}
onValueChange={(value: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many") =>
setConfig({ ...config, relationshipType: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="one-to-one">1:1 (One to One)</SelectItem>
<SelectItem value="one-to-many">1:N (One to Many)</SelectItem>
<SelectItem value="many-to-one">N:1 (Many to One)</SelectItem>
<SelectItem value="many-to-many">N:N (Many to Many)</SelectItem>
</SelectContent>
</Select>
</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>
);
};