2025-09-16 15:43:18 +09:00
|
|
|
|
"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";
|
|
|
|
|
|
|
|
|
|
|
|
interface ActionFieldMappingsProps {
|
|
|
|
|
|
action: DataSaveSettings["actions"][0];
|
|
|
|
|
|
actionIndex: number;
|
|
|
|
|
|
settings: DataSaveSettings;
|
|
|
|
|
|
onSettingsChange: (settings: DataSaveSettings) => void;
|
|
|
|
|
|
availableTables: TableInfo[];
|
|
|
|
|
|
tableColumnsCache: { [tableName: string]: ColumnInfo[] };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
|
|
|
|
|
|
action,
|
|
|
|
|
|
actionIndex,
|
|
|
|
|
|
settings,
|
|
|
|
|
|
onSettingsChange,
|
|
|
|
|
|
availableTables,
|
|
|
|
|
|
tableColumnsCache,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
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>
|
|
|
|
|
|
<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.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.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>
|
|
|
|
|
|
))}
|
2025-09-18 13:26:42 +09:00
|
|
|
|
|
|
|
|
|
|
{/* 필드 매핑이 없을 때 안내 메시지 */}
|
|
|
|
|
|
{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>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|