258 lines
10 KiB
TypeScript
258 lines
10 KiB
TypeScript
"use client";
|
||
|
||
import React 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";
|
||
|
||
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;
|
||
}
|
||
|
||
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
||
action,
|
||
actionIndex,
|
||
settings,
|
||
onSettingsChange,
|
||
availableTables,
|
||
tableColumnsCache,
|
||
fromTableColumns = [],
|
||
toTableColumns = [],
|
||
fromTableName,
|
||
toTableName,
|
||
}) => {
|
||
// 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}
|
||
/>
|
||
);
|
||
}
|
||
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.columnLabel || 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.columnLabel || 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>
|
||
);
|
||
};
|