2025-09-16 15:43:18 +09:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
2025-09-24 18:23:57 +09:00
|
|
|
|
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";
|
2025-09-18 17:17:06 +09:00
|
|
|
|
import { InsertFieldMappingPanel } from "./InsertFieldMappingPanel";
|
2025-09-24 18:23:57 +09:00
|
|
|
|
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[] };
|
2025-09-18 17:17:06 +09:00
|
|
|
|
fromTableColumns?: ColumnInfo[];
|
|
|
|
|
|
toTableColumns?: ColumnInfo[];
|
|
|
|
|
|
fromTableName?: string;
|
|
|
|
|
|
toTableName?: string;
|
2025-09-24 18:23:57 +09:00
|
|
|
|
// 🆕 다중 커넥션 지원을 위한 새로운 props
|
|
|
|
|
|
enableMultiConnection?: boolean;
|
2025-09-16 15:43:18 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
|
|
|
|
|
action,
|
|
|
|
|
|
actionIndex,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
onSettingsChange,
|
|
|
|
|
|
availableTables,
|
|
|
|
|
|
tableColumnsCache,
|
2025-09-18 17:17:06 +09:00
|
|
|
|
fromTableColumns = [],
|
|
|
|
|
|
toTableColumns = [],
|
|
|
|
|
|
fromTableName,
|
|
|
|
|
|
toTableName,
|
2025-09-24 18:23:57 +09:00
|
|
|
|
enableMultiConnection = false,
|
2025-09-16 15:43:18 +09:00
|
|
|
|
}) => {
|
2025-09-24 18:23:57 +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 액션 처리 (단일 커넥션)
|
2025-09-18 17:17:06 +09:00
|
|
|
|
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}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2025-09-24 18:23:57 +09:00
|
|
|
|
|
|
|
|
|
|
// 🆕 다중 커넥션 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">
|
2025-09-18 13:26:42 +09:00
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
<Label className="text-xs font-medium">필드 매핑</Label>
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<span className="text-xs text-destructive">(필수)</span>
|
2025-09-18 13:26:42 +09:00
|
|
|
|
</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">
|
|
|
|
|
|
{/* 소스 */}
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="flex items-center gap-1 rounded bg-accent px-2 py-1">
|
2025-09-16 15:43:18 +09:00
|
|
|
|
<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", "");
|
|
|
|
|
|
}}
|
2025-10-02 14:34:15 +09:00
|
|
|
|
className="ml-1 flex h-4 w-4 items-center justify-center rounded-full text-gray-400 hover:bg-gray-200 hover:text-muted-foreground"
|
2025-09-16 15:43:18 +09:00
|
|
|
|
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}>
|
2025-09-24 18:23:57 +09:00
|
|
|
|
{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}>
|
2025-09-24 18:23:57 +09:00
|
|
|
|
{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>
|
|
|
|
|
|
))}
|
2025-09-18 13:26:42 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 필드 매핑이 없을 때 안내 메시지 */}
|
|
|
|
|
|
{action.fieldMappings.length === 0 && (
|
2025-10-02 14:34:15 +09:00
|
|
|
|
<div className="rounded border border-destructive/20 bg-destructive/10 p-3 text-xs text-red-700">
|
2025-09-18 13:26:42 +09:00
|
|
|
|
<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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|