566 lines
20 KiB
TypeScript
566 lines
20 KiB
TypeScript
"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";
|
|
import { ArrowRight, Link, Key, Save, Globe, Plus } from "lucide-react";
|
|
import { DataFlowAPI, TableRelationship } from "@/lib/api/dataflow";
|
|
import toast from "react-hot-toast";
|
|
|
|
// 연결 정보 타입
|
|
interface ConnectionInfo {
|
|
fromNode: {
|
|
id: string;
|
|
tableName: string;
|
|
displayName: string;
|
|
};
|
|
toNode: {
|
|
id: string;
|
|
tableName: string;
|
|
displayName: string;
|
|
};
|
|
fromColumn?: string;
|
|
toColumn?: string;
|
|
selectedColumnsData?: {
|
|
[tableName: string]: {
|
|
displayName: string;
|
|
columns: string[];
|
|
};
|
|
};
|
|
}
|
|
|
|
// 연결 설정 타입
|
|
interface ConnectionConfig {
|
|
relationshipName: string;
|
|
relationshipType: "one-to-one" | "one-to-many" | "many-to-one" | "many-to-many";
|
|
connectionType: "simple-key" | "data-save" | "external-call";
|
|
fromColumnName: string;
|
|
toColumnName: string;
|
|
settings?: Record<string, unknown>;
|
|
description?: string;
|
|
}
|
|
|
|
// 단순 키값 연결 설정
|
|
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;
|
|
}
|
|
|
|
interface ConnectionSetupModalProps {
|
|
isOpen: boolean;
|
|
connection: ConnectionInfo | null;
|
|
companyCode: string;
|
|
diagramId?: number;
|
|
onConfirm: (relationship: TableRelationship) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
|
|
isOpen,
|
|
connection,
|
|
companyCode,
|
|
diagramId,
|
|
onConfirm,
|
|
onCancel,
|
|
}) => {
|
|
const [config, setConfig] = useState<ConnectionConfig>({
|
|
relationshipName: "",
|
|
relationshipType: "one-to-one",
|
|
connectionType: "simple-key",
|
|
fromColumnName: "",
|
|
toColumnName: "",
|
|
description: "",
|
|
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: "{}",
|
|
});
|
|
|
|
// 모달이 열릴 때 기본값 설정
|
|
useEffect(() => {
|
|
if (isOpen && connection) {
|
|
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} 간의 데이터 관계`,
|
|
settings: {},
|
|
});
|
|
|
|
// 단순 키값 연결 기본값 설정
|
|
setSimpleKeySettings({
|
|
notes: `${fromTableName}과 ${toTableName} 간의 키값 연결`,
|
|
});
|
|
|
|
// 데이터 저장 기본값 설정
|
|
setDataSaveSettings({
|
|
sourceField: "",
|
|
targetField: "",
|
|
saveConditions: "데이터 저장 조건을 입력하세요",
|
|
});
|
|
|
|
// 외부 호출 기본값 설정
|
|
setExternalCallSettings({
|
|
callType: "rest-api",
|
|
apiUrl: "https://api.example.com/webhook",
|
|
httpMethod: "POST",
|
|
headers: "{}",
|
|
bodyTemplate: "{}",
|
|
});
|
|
}
|
|
}, [isOpen, connection]);
|
|
|
|
const handleConfirm = () => {
|
|
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;
|
|
}
|
|
|
|
// 선택된 컬럼들 추출
|
|
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;
|
|
}
|
|
|
|
// 메모리 기반 시스템: 관계 데이터만 생성하여 부모로 전달
|
|
const relationshipData: TableRelationship = {
|
|
relationship_name: config.relationshipName,
|
|
from_table_name: connection.fromNode.tableName,
|
|
to_table_name: connection.toNode.tableName,
|
|
from_column_name: fromColumns.join(","), // 여러 컬럼을 콤마로 구분
|
|
to_column_name: toColumns.join(","), // 여러 컬럼을 콤마로 구분
|
|
relationship_type: config.relationshipType as any,
|
|
connection_type: config.connectionType as any,
|
|
company_code: companyCode,
|
|
settings: {
|
|
...settings,
|
|
description: config.description,
|
|
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,
|
|
},
|
|
},
|
|
};
|
|
|
|
toast.success("관계가 생성되었습니다!");
|
|
|
|
// 부모 컴포넌트로 관계 데이터 전달 (DB 저장 없이)
|
|
onConfirm(relationshipData);
|
|
handleCancel(); // 모달 닫기
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setConfig({
|
|
relationshipName: "",
|
|
relationshipType: "one-to-one",
|
|
connectionType: "simple-key",
|
|
fromColumnName: "",
|
|
toColumnName: "",
|
|
description: "",
|
|
});
|
|
onCancel();
|
|
};
|
|
|
|
if (!connection) return null;
|
|
|
|
// 선택된 컬럼 데이터 가져오기
|
|
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];
|
|
|
|
// 연결 종류별 설정 패널 렌더링
|
|
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>
|
|
</div>
|
|
<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>
|
|
);
|
|
|
|
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">
|
|
<div>
|
|
<Label htmlFor="callType" className="text-sm">
|
|
호출 유형
|
|
</Label>
|
|
<Select
|
|
value={externalCallSettings.callType}
|
|
onValueChange={(value: "rest-api" | "email" | "webhook" | "ftp" | "queue") =>
|
|
setExternalCallSettings({ ...externalCallSettings, callType: value })
|
|
}
|
|
>
|
|
<SelectTrigger className="text-sm">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="rest-api">REST API 호출</SelectItem>
|
|
<SelectItem value="email">이메일 전송</SelectItem>
|
|
<SelectItem value="webhook">웹훅</SelectItem>
|
|
<SelectItem value="ftp">FTP 업로드</SelectItem>
|
|
<SelectItem value="queue">메시지 큐</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
{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>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
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>
|
|
</div>
|
|
|
|
<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>
|
|
</div>
|
|
|
|
<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>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 연결 종류별 상세 설정 */}
|
|
{renderConnectionTypeSettings()}
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
취소
|
|
</Button>
|
|
<Button onClick={handleConfirm} disabled={!config.relationshipName}>
|
|
연결 생성
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
};
|