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

410 lines
16 KiB
TypeScript
Raw Normal View History

2025-09-16 15:43:18 +09:00
"use client";
import React, { useState, useEffect } from "react";
2025-09-16 15:43:18 +09:00
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";
2025-09-16 15:43:18 +09:00
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;
2025-09-16 15:43:18 +09:00
}
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
availableTables,
tableColumnsCache,
fromTableColumns = [],
toTableColumns = [],
fromTableName,
toTableName,
enableMultiConnection = false,
2025-09-16 15:43:18 +09:00
}) => {
// 🆕 다중 커넥션 상태 관리
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;
}
}
2025-09-16 15:43:18 +09:00
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>
2025-09-16 15:43:18 +09:00
<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}
2025-09-16 15:43:18 +09:00
</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}
2025-09-16 15:43:18 +09:00
</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>
)}
2025-09-16 15:43:18 +09:00
</div>
</div>
);
};