410 lines
16 KiB
TypeScript
410 lines
16 KiB
TypeScript
"use client";
|
||
|
||
import React, { useState, useEffect } from "react";
|
||
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 { Plus, Trash2 } from "lucide-react";
|
||
import { TableInfo, ColumnInfo } from "@/lib/api/dataflow";
|
||
import { DataSaveSettings } from "@/types/connectionTypes";
|
||
import { InsertFieldMappingPanel } from "./InsertFieldMappingPanel";
|
||
import { ConnectionSelectionPanel } from "./ConnectionSelectionPanel";
|
||
import { TableSelectionPanel } from "./TableSelectionPanel";
|
||
import { UpdateFieldMappingPanel } from "./UpdateFieldMappingPanel";
|
||
import { DeleteConditionPanel } from "./DeleteConditionPanel";
|
||
import { getActiveConnections, ConnectionInfo } from "@/lib/api/multiConnection";
|
||
|
||
interface ActionFieldMappingsProps {
|
||
action: DataSaveSettings["actions"][0];
|
||
actionIndex: number;
|
||
settings: DataSaveSettings;
|
||
onSettingsChange: (settings: DataSaveSettings) => void;
|
||
availableTables: TableInfo[];
|
||
tableColumnsCache: { [tableName: string]: ColumnInfo[] };
|
||
fromTableColumns?: ColumnInfo[];
|
||
toTableColumns?: ColumnInfo[];
|
||
fromTableName?: string;
|
||
toTableName?: string;
|
||
// 🆕 다중 커넥션 지원을 위한 새로운 props
|
||
enableMultiConnection?: boolean;
|
||
}
|
||
|
||
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
||
action,
|
||
actionIndex,
|
||
settings,
|
||
onSettingsChange,
|
||
availableTables,
|
||
tableColumnsCache,
|
||
fromTableColumns = [],
|
||
toTableColumns = [],
|
||
fromTableName,
|
||
toTableName,
|
||
enableMultiConnection = false,
|
||
}) => {
|
||
// 🆕 다중 커넥션 상태 관리
|
||
const [fromConnectionId, setFromConnectionId] = useState<number | undefined>(action.fromConnection?.connectionId);
|
||
const [toConnectionId, setToConnectionId] = useState<number | undefined>(action.toConnection?.connectionId);
|
||
const [selectedFromTable, setSelectedFromTable] = useState<string | undefined>(action.fromTable || fromTableName);
|
||
const [selectedToTable, setSelectedToTable] = useState<string | undefined>(action.targetTable || toTableName);
|
||
|
||
// 다중 커넥션이 활성화된 경우 새로운 UI 렌더링
|
||
if (enableMultiConnection) {
|
||
return renderMultiConnectionUI();
|
||
}
|
||
|
||
// 기존 INSERT 액션 처리 (단일 커넥션)
|
||
if (action.actionType === "insert" && fromTableColumns.length > 0 && toTableColumns.length > 0) {
|
||
return (
|
||
<InsertFieldMappingPanel
|
||
action={action}
|
||
actionIndex={actionIndex}
|
||
settings={settings}
|
||
onSettingsChange={onSettingsChange}
|
||
fromTableColumns={fromTableColumns}
|
||
toTableColumns={toTableColumns}
|
||
fromTableName={fromTableName}
|
||
toTableName={toTableName}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 🆕 다중 커넥션 UI 렌더링 함수
|
||
function renderMultiConnectionUI() {
|
||
const hasConnectionsSelected = fromConnectionId !== undefined && toConnectionId !== undefined;
|
||
const hasTablesSelected = selectedFromTable && selectedToTable;
|
||
|
||
// 커넥션 변경 핸들러
|
||
const handleFromConnectionChange = (connectionId: number) => {
|
||
setFromConnectionId(connectionId);
|
||
setSelectedFromTable(undefined); // 테이블 선택 초기화
|
||
updateActionConnection("fromConnection", connectionId);
|
||
};
|
||
|
||
const handleToConnectionChange = (connectionId: number) => {
|
||
setToConnectionId(connectionId);
|
||
setSelectedToTable(undefined); // 테이블 선택 초기화
|
||
updateActionConnection("toConnection", connectionId);
|
||
};
|
||
|
||
// 테이블 변경 핸들러
|
||
const handleFromTableChange = (tableName: string) => {
|
||
setSelectedFromTable(tableName);
|
||
updateActionTable("fromTable", tableName);
|
||
};
|
||
|
||
const handleToTableChange = (tableName: string) => {
|
||
setSelectedToTable(tableName);
|
||
updateActionTable("targetTable", tableName);
|
||
};
|
||
|
||
// 액션 커넥션 정보 업데이트
|
||
const updateActionConnection = (type: "fromConnection" | "toConnection", connectionId: number) => {
|
||
const newActions = [...settings.actions];
|
||
if (!newActions[actionIndex][type]) {
|
||
newActions[actionIndex][type] = {};
|
||
}
|
||
newActions[actionIndex][type]!.connectionId = connectionId;
|
||
onSettingsChange({ ...settings, actions: newActions });
|
||
};
|
||
|
||
// 액션 테이블 정보 업데이트
|
||
const updateActionTable = (type: "fromTable" | "targetTable", tableName: string) => {
|
||
const newActions = [...settings.actions];
|
||
newActions[actionIndex][type] = tableName;
|
||
onSettingsChange({ ...settings, actions: newActions });
|
||
};
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* 1단계: 커넥션 선택 */}
|
||
<ConnectionSelectionPanel
|
||
fromConnectionId={fromConnectionId}
|
||
toConnectionId={toConnectionId}
|
||
onFromConnectionChange={handleFromConnectionChange}
|
||
onToConnectionChange={handleToConnectionChange}
|
||
actionType={action.actionType}
|
||
allowSameConnection={true}
|
||
/>
|
||
|
||
{/* 2단계: 테이블 선택 */}
|
||
{hasConnectionsSelected && (
|
||
<TableSelectionPanel
|
||
fromConnectionId={fromConnectionId}
|
||
toConnectionId={toConnectionId}
|
||
selectedFromTable={selectedFromTable}
|
||
selectedToTable={selectedToTable}
|
||
onFromTableChange={handleFromTableChange}
|
||
onToTableChange={handleToTableChange}
|
||
actionType={action.actionType}
|
||
allowSameTable={true}
|
||
showSameTableWarning={true}
|
||
/>
|
||
)}
|
||
|
||
{/* 3단계: 액션 타입별 매핑/조건 설정 */}
|
||
{hasTablesSelected && renderActionSpecificPanel()}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 액션 타입별 패널 렌더링
|
||
function renderActionSpecificPanel() {
|
||
switch (action.actionType) {
|
||
case "insert":
|
||
return (
|
||
<InsertFieldMappingPanel
|
||
action={action}
|
||
actionIndex={actionIndex}
|
||
settings={settings}
|
||
onSettingsChange={onSettingsChange}
|
||
fromConnectionId={fromConnectionId}
|
||
toConnectionId={toConnectionId}
|
||
fromTableName={selectedFromTable}
|
||
toTableName={selectedToTable}
|
||
/>
|
||
);
|
||
|
||
case "update":
|
||
return (
|
||
<UpdateFieldMappingPanel
|
||
action={action}
|
||
actionIndex={actionIndex}
|
||
settings={settings}
|
||
onSettingsChange={onSettingsChange}
|
||
fromConnectionId={fromConnectionId}
|
||
toConnectionId={toConnectionId}
|
||
fromTableName={selectedFromTable}
|
||
toTableName={selectedToTable}
|
||
/>
|
||
);
|
||
|
||
case "delete":
|
||
return (
|
||
<DeleteConditionPanel
|
||
action={action}
|
||
actionIndex={actionIndex}
|
||
settings={settings}
|
||
onSettingsChange={onSettingsChange}
|
||
fromConnectionId={fromConnectionId}
|
||
toConnectionId={toConnectionId}
|
||
fromTableName={selectedFromTable}
|
||
toTableName={selectedToTable}
|
||
/>
|
||
);
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
}
|
||
const addFieldMapping = () => {
|
||
const newActions = [...settings.actions];
|
||
newActions[actionIndex].fieldMappings.push({
|
||
sourceTable: "",
|
||
sourceField: "",
|
||
targetTable: "",
|
||
targetField: "",
|
||
defaultValue: "",
|
||
transformFunction: "",
|
||
});
|
||
onSettingsChange({ ...settings, actions: newActions });
|
||
};
|
||
|
||
const updateFieldMapping = (mappingIndex: number, field: string, value: string) => {
|
||
const newActions = [...settings.actions];
|
||
(newActions[actionIndex].fieldMappings[mappingIndex] as any)[field] = value;
|
||
onSettingsChange({ ...settings, actions: newActions });
|
||
};
|
||
|
||
const removeFieldMapping = (mappingIndex: number) => {
|
||
const newActions = [...settings.actions];
|
||
newActions[actionIndex].fieldMappings = newActions[actionIndex].fieldMappings.filter((_, i) => i !== mappingIndex);
|
||
onSettingsChange({ ...settings, actions: newActions });
|
||
};
|
||
|
||
return (
|
||
<div className="mt-3">
|
||
<div className="mb-2 flex items-center justify-between">
|
||
<div className="flex items-center gap-2">
|
||
<Label className="text-xs font-medium">필드 매핑</Label>
|
||
<span className="text-xs text-red-600">(필수)</span>
|
||
</div>
|
||
<Button size="sm" variant="outline" onClick={addFieldMapping} 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) => (
|
||
<div
|
||
key={`${action.id}-mapping-${mappingIndex}-${mapping.sourceField || "empty"}-${mapping.targetField || "empty"}`}
|
||
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 || "__EMPTY__"}
|
||
onValueChange={(value) => {
|
||
const actualValue = value === "__EMPTY__" ? "" : value;
|
||
updateFieldMapping(mappingIndex, "sourceTable", actualValue);
|
||
updateFieldMapping(mappingIndex, "sourceField", "");
|
||
if (actualValue) {
|
||
updateFieldMapping(mappingIndex, "defaultValue", "");
|
||
}
|
||
}}
|
||
disabled={!!(mapping.defaultValue && mapping.defaultValue.trim())}
|
||
>
|
||
<SelectTrigger className="h-6 w-24 border-0 bg-transparent p-0 text-xs">
|
||
<SelectValue placeholder="테이블" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="__EMPTY__">비워두기 (기본값 사용)</SelectItem>
|
||
{availableTables.map((table) => (
|
||
<SelectItem key={table.tableName} value={table.tableName}>
|
||
<div className="truncate" title={table.tableName}>
|
||
{table.tableName}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
{mapping.sourceTable && (
|
||
<button
|
||
onClick={() => {
|
||
updateFieldMapping(mappingIndex, "sourceTable", "");
|
||
updateFieldMapping(mappingIndex, "sourceField", "");
|
||
}}
|
||
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-gray-400 hover:bg-gray-200 hover:text-gray-600"
|
||
title="소스 테이블 지우기"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
<span className="text-gray-400">.</span>
|
||
<Select
|
||
value={mapping.sourceField}
|
||
onValueChange={(value) => {
|
||
updateFieldMapping(mappingIndex, "sourceField", value);
|
||
if (value) {
|
||
updateFieldMapping(mappingIndex, "defaultValue", "");
|
||
}
|
||
}}
|
||
disabled={!mapping.sourceTable || !!(mapping.defaultValue && mapping.defaultValue.trim())}
|
||
>
|
||
<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.displayName && column.displayName !== column.columnName
|
||
? column.displayName
|
||
: column.columnName}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
<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) => {
|
||
updateFieldMapping(mappingIndex, "targetTable", value);
|
||
updateFieldMapping(mappingIndex, "targetField", "");
|
||
}}
|
||
>
|
||
<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) => updateFieldMapping(mappingIndex, "targetField", value)}
|
||
disabled={!mapping.targetTable}
|
||
>
|
||
<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.displayName && column.displayName !== column.columnName
|
||
? column.displayName
|
||
: column.columnName}
|
||
</div>
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
|
||
{/* 기본값 (인라인) */}
|
||
<Input
|
||
value={mapping.defaultValue || ""}
|
||
onChange={(e) => {
|
||
updateFieldMapping(mappingIndex, "defaultValue", e.target.value);
|
||
if (e.target.value.trim()) {
|
||
updateFieldMapping(mappingIndex, "sourceTable", "");
|
||
updateFieldMapping(mappingIndex, "sourceField", "");
|
||
}
|
||
}}
|
||
disabled={!!mapping.sourceTable}
|
||
className="h-6 w-20 text-xs"
|
||
placeholder="기본값"
|
||
/>
|
||
|
||
{/* 삭제 버튼 */}
|
||
<Button
|
||
size="sm"
|
||
variant="ghost"
|
||
onClick={() => removeFieldMapping(mappingIndex)}
|
||
className="h-6 w-6 p-0 text-red-500 hover:text-red-700"
|
||
>
|
||
<Trash2 className="h-3 w-3" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
))}
|
||
|
||
{/* 필드 매핑이 없을 때 안내 메시지 */}
|
||
{action.fieldMappings.length === 0 && (
|
||
<div className="rounded border border-red-200 bg-red-50 p-3 text-xs text-red-700">
|
||
<div className="flex items-start gap-2">
|
||
<span className="text-red-500">⚠️</span>
|
||
<div>
|
||
<div className="font-medium">필드 매핑이 필요합니다</div>
|
||
<div className="mt-1">
|
||
{action.actionType.toUpperCase()} 액션은 어떤 데이터를 어떻게 처리할지 결정하는 필드 매핑이
|
||
필요합니다.
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|