ERP-node/frontend/components/dataflow/connection/ActionFieldMappings.tsx

410 lines
16 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
};