ConnectionSetupModal 리팩터링

This commit is contained in:
hyeonsu
2025-09-16 15:43:18 +09:00
parent 4ccce97eef
commit 7acea0b272
15 changed files with 2146 additions and 1811 deletions

View File

@@ -0,0 +1,206 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Trash2 } from "lucide-react";
import { ConditionNode, ColumnInfo } from "@/lib/api/dataflow";
import { DataSaveSettings } from "@/types/connectionTypes";
import { getInputTypeForDataType } from "@/utils/connectionUtils";
interface ActionConditionRendererProps {
condition: ConditionNode;
condIndex: number;
actionIndex: number;
settings: DataSaveSettings;
onSettingsChange: (settings: DataSaveSettings) => void;
fromTableColumns: ColumnInfo[];
getActionCurrentGroupLevel: (conditions: ConditionNode[], conditionIndex: number) => number;
}
export const ActionConditionRenderer: React.FC<ActionConditionRendererProps> = ({
condition,
condIndex,
actionIndex,
settings,
onSettingsChange,
fromTableColumns,
getActionCurrentGroupLevel,
}) => {
const removeConditionGroup = (groupId: string) => {
const newActions = [...settings.actions];
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter((c) => c.groupId !== groupId);
onSettingsChange({ ...settings, actions: newActions });
};
const removeCondition = () => {
const newActions = [...settings.actions];
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter((_, i) => i !== condIndex);
onSettingsChange({ ...settings, actions: newActions });
};
const updateCondition = (field: string, value: any) => {
const newActions = [...settings.actions];
(newActions[actionIndex].conditions![condIndex] as any)[field] = value;
onSettingsChange({ ...settings, actions: newActions });
};
const updateLogicalOperator = (targetIndex: number, value: "AND" | "OR") => {
const newActions = [...settings.actions];
newActions[actionIndex].conditions![targetIndex].logicalOperator = value;
onSettingsChange({ ...settings, actions: newActions });
};
const renderConditionValue = () => {
const selectedColumn = fromTableColumns.find((col) => col.columnName === condition.field);
const dataType = selectedColumn?.dataType?.toLowerCase() || "string";
const inputType = getInputTypeForDataType(dataType);
if (dataType.includes("bool")) {
return (
<Select value={String(condition.value || "")} onValueChange={(value) => updateCondition("value", value)}>
<SelectTrigger className="h-6 flex-1 text-xs">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="true">TRUE</SelectItem>
<SelectItem value="false">FALSE</SelectItem>
</SelectContent>
</Select>
);
} else {
return (
<Input
type={inputType}
placeholder={inputType === "number" ? "숫자" : "값"}
value={String(condition.value || "")}
onChange={(e) => updateCondition("value", e.target.value)}
className="h-6 flex-1 text-xs"
/>
);
}
};
// 그룹 시작 렌더링
if (condition.type === "group-start") {
return (
<div className="flex items-center gap-2">
{/* 그룹 시작 앞의 논리 연산자 */}
{condIndex > 0 && (
<Select
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
>
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
)}
<div
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
>
<span className="font-mono text-xs text-green-600">(</span>
<span className="text-xs text-green-600"> </span>
<Button
size="sm"
variant="ghost"
onClick={() => removeConditionGroup(condition.groupId!)}
className="h-4 w-4 p-0"
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
);
}
// 그룹 끝 렌더링
if (condition.type === "group-end") {
return (
<div className="flex items-center gap-2">
<div
className="flex items-center gap-2 rounded border-2 border-dashed border-green-300 bg-green-50/50 p-1"
style={{ marginLeft: `${(condition.groupLevel || 0) * 15}px` }}
>
<span className="font-mono text-xs text-green-600">)</span>
<span className="text-xs text-green-600"> </span>
<Button
size="sm"
variant="ghost"
onClick={() => removeConditionGroup(condition.groupId!)}
className="h-4 w-4 p-0"
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
);
}
// 일반 조건 렌더링
return (
<div className="flex items-center gap-2">
{/* 그룹 내 첫 번째 조건이 아닐 때만 논리 연산자 표시 */}
{condIndex > 0 && settings.actions[actionIndex].conditions![condIndex - 1]?.type !== "group-start" && (
<Select
value={settings.actions[actionIndex].conditions![condIndex - 1]?.logicalOperator || "AND"}
onValueChange={(value: "AND" | "OR") => updateLogicalOperator(condIndex - 1, value)}
>
<SelectTrigger className="h-6 w-24 border-green-200 bg-green-50 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="AND">AND</SelectItem>
<SelectItem value="OR">OR</SelectItem>
</SelectContent>
</Select>
)}
<div
className="flex flex-1 items-center gap-2 rounded border bg-white p-1"
style={{
marginLeft: `${getActionCurrentGroupLevel(settings.actions[actionIndex].conditions || [], condIndex) * 15}px`,
}}
>
<Select value={condition.field || ""} onValueChange={(value) => updateCondition("field", value)}>
<SelectTrigger className="h-6 flex-1 text-xs">
<SelectValue placeholder="필드" />
</SelectTrigger>
<SelectContent>
{fromTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
<Select
value={condition.operator || "="}
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => updateCondition("operator", value)}
>
<SelectTrigger className="h-6 w-20 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="=">=</SelectItem>
<SelectItem value="!=">!=</SelectItem>
<SelectItem value=">">&gt;</SelectItem>
<SelectItem value="<">&lt;</SelectItem>
<SelectItem value=">=">&gt;=</SelectItem>
<SelectItem value="<=">&lt;=</SelectItem>
<SelectItem value="LIKE">LIKE</SelectItem>
</SelectContent>
</Select>
{renderConditionValue()}
<Button size="sm" variant="ghost" onClick={removeCondition} className="h-6 w-6 p-0">
<Trash2 className="h-2 w-2" />
</Button>
</div>
</div>
);
};

View File

@@ -0,0 +1,132 @@
"use client";
import React from "react";
import { Button } from "@/components/ui/button";
import { Plus, Trash2 } from "lucide-react";
import { ColumnInfo } from "@/lib/api/dataflow";
import { DataSaveSettings } from "@/types/connectionTypes";
import { generateConditionId } from "@/utils/connectionUtils";
import { useActionConditionHelpers } from "@/hooks/useConditionManager";
import { ActionConditionRenderer } from "./ActionConditionRenderer";
interface ActionConditionsSectionProps {
action: DataSaveSettings["actions"][0];
actionIndex: number;
settings: DataSaveSettings;
onSettingsChange: (settings: DataSaveSettings) => void;
fromTableColumns: ColumnInfo[];
}
export const ActionConditionsSection: React.FC<ActionConditionsSectionProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
fromTableColumns,
}) => {
const { addActionGroupStart, addActionGroupEnd, getActionCurrentGroupLevel } = useActionConditionHelpers();
const addActionCondition = () => {
const newActions = [...settings.actions];
if (!newActions[actionIndex].conditions) {
newActions[actionIndex].conditions = [];
}
const currentConditions = newActions[actionIndex].conditions || [];
const newCondition = {
id: generateConditionId(),
type: "condition" as const,
field: "",
operator: "=" as const,
value: "",
dataType: "string",
// 첫 번째 조건이 아니고, 바로 앞이 group-start가 아니면 logicalOperator 추가
...(currentConditions.length > 0 &&
currentConditions[currentConditions.length - 1]?.type !== "group-start" && {
logicalOperator: "AND" as const,
}),
};
newActions[actionIndex].conditions = [...currentConditions, newCondition];
onSettingsChange({ ...settings, actions: newActions });
};
const clearAllConditions = () => {
const newActions = [...settings.actions];
newActions[actionIndex].conditions = [];
onSettingsChange({ ...settings, actions: newActions });
};
return (
<div className="mt-3">
<details className="group">
<summary className="flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900">
<div className="flex items-center gap-2">
🔍 ()
{action.conditions && action.conditions.length > 0 && (
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs text-blue-700">
{action.conditions.length}
</span>
)}
</div>
{action.conditions && action.conditions.length > 0 && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
clearAllConditions();
}}
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
title="조건 모두 삭제"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</summary>
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
<div className="mb-2 flex items-center justify-between">
<div className="flex gap-1">
<Button size="sm" variant="outline" onClick={addActionCondition} className="h-6 text-xs">
<Plus className="mr-1 h-2 w-2" />
</Button>
<Button
size="sm"
variant="outline"
onClick={() => addActionGroupStart(actionIndex, settings, onSettingsChange)}
className="h-6 text-xs"
>
(
</Button>
<Button
size="sm"
variant="outline"
onClick={() => addActionGroupEnd(actionIndex, settings, onSettingsChange)}
className="h-6 text-xs"
>
)
</Button>
</div>
</div>
{action.conditions && action.conditions.length > 0 && (
<div className="space-y-2">
{action.conditions.map((condition, condIndex) => (
<div key={`action-${actionIndex}-condition-${condition.id}`}>
<ActionConditionRenderer
condition={condition}
condIndex={condIndex}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
getActionCurrentGroupLevel={getActionCurrentGroupLevel}
/>
</div>
))}
</div>
)}
</div>
</details>
</div>
);
};

View File

@@ -0,0 +1,214 @@
"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">
<Label className="text-xs font-medium"> </Label>
<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>
))}
</div>
</div>
);
};

View File

@@ -0,0 +1,131 @@
"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 { Trash2 } from "lucide-react";
import { ColumnInfo } from "@/lib/api/dataflow";
import { DataSaveSettings } from "@/types/connectionTypes";
interface ActionSplitConfigProps {
action: DataSaveSettings["actions"][0];
actionIndex: number;
settings: DataSaveSettings;
onSettingsChange: (settings: DataSaveSettings) => void;
fromTableColumns: ColumnInfo[];
toTableColumns: ColumnInfo[];
}
export const ActionSplitConfig: React.FC<ActionSplitConfigProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
fromTableColumns,
toTableColumns,
}) => {
const updateSplitConfig = (field: string, value: string) => {
const newActions = [...settings.actions];
if (!newActions[actionIndex].splitConfig) {
newActions[actionIndex].splitConfig = {
sourceField: "",
delimiter: ",",
targetField: "",
};
}
(newActions[actionIndex].splitConfig as any)[field] = value;
onSettingsChange({ ...settings, actions: newActions });
};
const clearSplitConfig = () => {
const newActions = [...settings.actions];
newActions[actionIndex].splitConfig = {
sourceField: "",
delimiter: ",",
targetField: "",
};
onSettingsChange({ ...settings, actions: newActions });
};
return (
<div className="mt-3">
<details className="group">
<summary className="flex cursor-pointer items-center justify-between rounded border p-2 text-xs font-medium text-gray-700 hover:bg-gray-50 hover:text-gray-900">
<div className="flex items-center gap-2">
()
{action.splitConfig && action.splitConfig.sourceField && (
<span className="rounded-full bg-green-100 px-2 py-0.5 text-xs text-green-700"></span>
)}
</div>
{action.splitConfig && action.splitConfig.sourceField && (
<Button
size="sm"
variant="ghost"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
clearSplitConfig();
}}
className="h-5 w-5 p-0 text-red-500 hover:text-red-700"
title="분할 설정 초기화"
>
<Trash2 className="h-3 w-3" />
</Button>
)}
</summary>
<div className="mt-2 space-y-2 border-l-2 border-gray-100 pl-4">
<Label className="text-xs font-medium"> </Label>
<div className="mt-1 grid grid-cols-3 gap-2">
<div>
<Label className="text-xs text-gray-500"> </Label>
<Select
value={action.splitConfig?.sourceField || ""}
onValueChange={(value) => updateSplitConfig("sourceField", value)}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{fromTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-gray-500"></Label>
<Input
value={action.splitConfig?.delimiter || ""}
onChange={(e) => updateSplitConfig("delimiter", e.target.value)}
className="h-6 text-xs"
placeholder=","
/>
</div>
<div>
<Label className="text-xs text-gray-500"> </Label>
<Select
value={action.splitConfig?.targetField || ""}
onValueChange={(value) => updateSplitConfig("targetField", value)}
>
<SelectTrigger className="h-6 text-xs">
<SelectValue placeholder="필드 선택" />
</SelectTrigger>
<SelectContent>
{toTableColumns.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</details>
</div>
);
};

View File

@@ -0,0 +1,59 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Key, Save, Globe } from "lucide-react";
import { ConnectionConfig } from "@/types/connectionTypes";
interface ConnectionTypeSelectorProps {
config: ConnectionConfig;
onConfigChange: (config: ConnectionConfig) => void;
}
export const ConnectionTypeSelector: React.FC<ConnectionTypeSelectorProps> = ({ config, onConfigChange }) => {
return (
<div>
<Label className="text-sm font-medium"> </Label>
<div className="mt-2 grid grid-cols-3 gap-2">
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "simple-key"
? "border-blue-500 bg-blue-50"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => onConfigChange({ ...config, connectionType: "simple-key" })}
>
<Key className="mx-auto h-6 w-6 text-blue-500" />
<div className="mt-1 text-xs font-medium"> </div>
<div className="text-xs text-gray-600"> </div>
</div>
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "data-save"
? "border-green-500 bg-green-50"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => onConfigChange({ ...config, connectionType: "data-save" })}
>
<Save className="mx-auto h-6 w-6 text-green-500" />
<div className="mt-1 text-xs font-medium"> </div>
<div className="text-xs text-gray-600"> </div>
</div>
<div
className={`cursor-pointer rounded-lg border-2 p-3 text-center transition-colors ${
config.connectionType === "external-call"
? "border-orange-500 bg-orange-50"
: "border-gray-200 hover:border-gray-300"
}`}
onClick={() => onConfigChange({ ...config, connectionType: "external-call" })}
>
<Globe className="mx-auto h-6 w-6 text-orange-500" />
<div className="mt-1 text-xs font-medium"> </div>
<div className="text-xs text-gray-600">API/ </div>
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,158 @@
"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, Save, Trash2 } from "lucide-react";
import { TableInfo, ColumnInfo } from "@/lib/api/dataflow";
import { DataSaveSettings as DataSaveSettingsType } from "@/types/connectionTypes";
import { ActionConditionsSection } from "./ActionConditionsSection";
import { ActionFieldMappings } from "./ActionFieldMappings";
import { ActionSplitConfig } from "./ActionSplitConfig";
interface DataSaveSettingsProps {
settings: DataSaveSettingsType;
onSettingsChange: (settings: DataSaveSettingsType) => void;
availableTables: TableInfo[];
fromTableColumns: ColumnInfo[];
toTableColumns: ColumnInfo[];
tableColumnsCache: { [tableName: string]: ColumnInfo[] };
}
export const DataSaveSettings: React.FC<DataSaveSettingsProps> = ({
settings,
onSettingsChange,
availableTables,
fromTableColumns,
toTableColumns,
tableColumnsCache,
}) => {
const addAction = () => {
const newAction = {
id: `action_${settings.actions.length + 1}`,
name: `액션 ${settings.actions.length + 1}`,
actionType: "insert" as const,
fieldMappings: [],
conditions: [],
splitConfig: {
sourceField: "",
delimiter: "",
targetField: "",
},
};
onSettingsChange({
...settings,
actions: [...settings.actions, newAction],
});
};
const updateAction = (actionIndex: number, field: string, value: any) => {
const newActions = [...settings.actions];
(newActions[actionIndex] as any)[field] = value;
onSettingsChange({ ...settings, actions: newActions });
};
const removeAction = (actionIndex: number) => {
const newActions = settings.actions.filter((_, i) => i !== actionIndex);
onSettingsChange({ ...settings, actions: newActions });
};
return (
<div className="rounded-lg border border-l-4 border-l-green-500 bg-green-50/30 p-4">
<div className="mb-3 flex items-center gap-2">
<Save className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="space-y-4">
{/* 액션 목록 */}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium"> </Label>
<Button size="sm" variant="outline" onClick={addAction} className="h-7 text-xs">
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{settings.actions.length === 0 ? (
<div className="rounded-lg border border-dashed p-3 text-center text-xs text-gray-500">
.
</div>
) : (
<div className="space-y-3">
{settings.actions.map((action, actionIndex) => (
<div key={action.id} className="rounded border bg-white p-3">
<div className="mb-3 flex items-center justify-between">
<Input
value={action.name}
onChange={(e) => updateAction(actionIndex, "name", e.target.value)}
className="h-7 flex-1 text-xs font-medium"
placeholder="액션 이름"
/>
<Button size="sm" variant="ghost" onClick={() => removeAction(actionIndex)} className="h-7 w-7 p-0">
<Trash2 className="h-3 w-3" />
</Button>
</div>
<div className="grid grid-cols-1 gap-3">
{/* 액션 타입 */}
<div>
<Label className="text-xs"> </Label>
<Select
value={action.actionType}
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
updateAction(actionIndex, "actionType", value)
}
>
<SelectTrigger className="h-7 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="insert">INSERT</SelectItem>
<SelectItem value="update">UPDATE</SelectItem>
<SelectItem value="delete">DELETE</SelectItem>
<SelectItem value="upsert">UPSERT</SelectItem>
</SelectContent>
</Select>
</div>
</div>
{/* 액션별 개별 실행 조건 */}
<ActionConditionsSection
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
/>
{/* 데이터 분할 설정 */}
<ActionSplitConfig
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
toTableColumns={toTableColumns}
/>
{/* 필드 매핑 */}
<ActionFieldMappings
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
availableTables={availableTables}
tableColumnsCache={tableColumnsCache}
/>
</div>
))}
</div>
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,115 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Globe } from "lucide-react";
import { ExternalCallSettings as ExternalCallSettingsType } from "@/types/connectionTypes";
interface ExternalCallSettingsProps {
settings: ExternalCallSettingsType;
onSettingsChange: (settings: ExternalCallSettingsType) => void;
}
export const ExternalCallSettings: React.FC<ExternalCallSettingsProps> = ({ settings, onSettingsChange }) => {
return (
<div className="rounded-lg border border-l-4 border-l-orange-500 bg-orange-50/30 p-4">
<div className="mb-3 flex items-center gap-2">
<Globe className="h-4 w-4 text-orange-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="callType" className="text-sm">
</Label>
<Select
value={settings.callType}
onValueChange={(value: "rest-api" | "email" | "webhook" | "ftp" | "queue") =>
onSettingsChange({ ...settings, callType: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="rest-api">REST API </SelectItem>
<SelectItem value="email"> </SelectItem>
<SelectItem value="webhook"></SelectItem>
<SelectItem value="ftp">FTP </SelectItem>
<SelectItem value="queue"> </SelectItem>
</SelectContent>
</Select>
</div>
{settings.callType === "rest-api" && (
<>
<div>
<Label htmlFor="apiUrl" className="text-sm">
API URL
</Label>
<Input
id="apiUrl"
value={settings.apiUrl}
onChange={(e) => onSettingsChange({ ...settings, apiUrl: e.target.value })}
placeholder="https://api.example.com/webhook"
className="text-sm"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="httpMethod" className="text-sm">
HTTP Method
</Label>
<Select
value={settings.httpMethod}
onValueChange={(value: "GET" | "POST" | "PUT" | "DELETE") =>
onSettingsChange({ ...settings, httpMethod: value })
}
>
<SelectTrigger className="text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="GET">GET</SelectItem>
<SelectItem value="POST">POST</SelectItem>
<SelectItem value="PUT">PUT</SelectItem>
<SelectItem value="DELETE">DELETE</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="headers" className="text-sm">
Headers
</Label>
<Textarea
id="headers"
value={settings.headers}
onChange={(e) => onSettingsChange({ ...settings, headers: e.target.value })}
placeholder="{}"
rows={1}
className="text-sm"
/>
</div>
</div>
<div>
<Label htmlFor="bodyTemplate" className="text-sm">
Body Template
</Label>
<Textarea
id="bodyTemplate"
value={settings.bodyTemplate}
onChange={(e) => onSettingsChange({ ...settings, bodyTemplate: e.target.value })}
placeholder="{}"
rows={2}
className="text-sm"
/>
</div>
</>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,188 @@
"use client";
import React from "react";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Badge } from "@/components/ui/badge";
import { Key } from "lucide-react";
import { TableInfo, ColumnInfo } from "@/lib/api/dataflow";
import { SimpleKeySettings as SimpleKeySettingsType } from "@/types/connectionTypes";
interface SimpleKeySettingsProps {
settings: SimpleKeySettingsType;
onSettingsChange: (settings: SimpleKeySettingsType) => void;
availableTables: TableInfo[];
selectedFromTable: string;
selectedToTable: string;
fromTableColumns: ColumnInfo[];
toTableColumns: ColumnInfo[];
selectedFromColumns: string[];
selectedToColumns: string[];
onFromColumnsChange: (columns: string[]) => void;
onToColumnsChange: (columns: string[]) => void;
}
export const SimpleKeySettings: React.FC<SimpleKeySettingsProps> = ({
settings,
onSettingsChange,
availableTables,
selectedFromTable,
selectedToTable,
fromTableColumns,
toTableColumns,
selectedFromColumns,
selectedToColumns,
onFromColumnsChange,
onToColumnsChange,
}) => {
return (
<div className="space-y-4">
{/* 테이블 및 컬럼 선택 */}
<div className="rounded-lg border bg-gray-50 p-4">
<div className="mb-4 text-sm font-medium"> </div>
{/* 현재 선택된 테이블 표시 */}
<div className="mb-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedFromTable)?.displayName || selectedFromTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedFromTable})</span>
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-1">
<span className="text-sm font-medium text-gray-800">
{availableTables.find((t) => t.tableName === selectedToTable)?.displayName || selectedToTable}
</span>
<span className="ml-2 text-xs text-gray-500">({selectedToTable})</span>
</div>
</div>
</div>
{/* 컬럼 선택 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600">From </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{fromTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedFromColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
onFromColumnsChange([...selectedFromColumns, column.columnName]);
} else {
onFromColumnsChange(selectedFromColumns.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{fromTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedFromTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600">To </Label>
<div className="mt-2 max-h-32 overflow-y-auto rounded border bg-white p-2">
{toTableColumns.map((column) => (
<label key={column.columnName} className="flex items-center gap-2 py-1 text-sm">
<input
type="checkbox"
checked={selectedToColumns.includes(column.columnName)}
onChange={(e) => {
if (e.target.checked) {
onToColumnsChange([...selectedToColumns, column.columnName]);
} else {
onToColumnsChange(selectedToColumns.filter((col) => col !== column.columnName));
}
}}
className="rounded"
/>
<span>{column.columnName}</span>
<span className="text-xs text-gray-500">({column.dataType})</span>
</label>
))}
{toTableColumns.length === 0 && (
<div className="py-2 text-xs text-gray-500">
{selectedToTable ? "컬럼을 불러오는 중..." : "테이블을 먼저 선택해주세요"}
</div>
)}
</div>
</div>
</div>
{/* 선택된 컬럼 미리보기 */}
{(selectedFromColumns.length > 0 || selectedToColumns.length > 0) && (
<div className="mt-4 grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-medium text-gray-600"> From </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedFromColumns.length > 0 ? (
selectedFromColumns.map((column) => (
<Badge key={column} variant="outline" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
<div>
<Label className="text-xs font-medium text-gray-600"> To </Label>
<div className="mt-1 flex flex-wrap gap-1">
{selectedToColumns.length > 0 ? (
selectedToColumns.map((column) => (
<Badge key={column} variant="secondary" className="text-xs">
{column}
</Badge>
))
) : (
<span className="text-xs text-gray-400"> </span>
)}
</div>
</div>
</div>
)}
</div>
{/* 단순 키값 연결 설정 */}
<div className="rounded-lg border border-l-4 border-l-blue-500 bg-blue-50/30 p-4">
<div className="mb-3 flex items-center gap-2">
<Key className="h-4 w-4 text-blue-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="space-y-3">
<div>
<Label htmlFor="notes" className="text-sm">
</Label>
<Textarea
id="notes"
value={settings.notes}
onChange={(e) => onSettingsChange({ ...settings, notes: e.target.value })}
placeholder="데이터 연결에 대한 설명을 입력하세요"
rows={2}
className="text-sm"
/>
</div>
</div>
</div>
</div>
);
};