UI 개선 - 저장 방식별 차별화 및 중복 제거

This commit is contained in:
hyeonsu 2025-09-12 16:15:36 +09:00
parent 3344a5785c
commit 8e6f8d2a27
1 changed files with 572 additions and 54 deletions

View File

@ -58,9 +58,30 @@ interface SimpleKeySettings {
// 데이터 저장 설정
interface DataSaveSettings {
sourceField: string;
targetField: string;
saveConditions: string;
saveMode: "simple" | "conditional" | "split"; // 저장 방식
actions: Array<{
id: string;
name: string;
actionType: "insert" | "update" | "delete" | "upsert";
conditions?: Array<{
field: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE";
value: string;
}>;
fieldMappings: Array<{
sourceTable?: string;
sourceField: string;
targetTable?: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
}>;
splitConfig?: {
sourceField: string; // 분할할 소스 필드
delimiter: string; // 구분자 (예: ",")
targetField: string; // 분할된 값이 들어갈 필드
};
}>;
}
// 외부 호출 설정
@ -103,9 +124,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
});
const [dataSaveSettings, setDataSaveSettings] = useState<DataSaveSettings>({
sourceField: "",
targetField: "",
saveConditions: "",
saveMode: "simple",
actions: [],
});
const [externalCallSettings, setExternalCallSettings] = useState<ExternalCallSettings>({
@ -124,6 +144,8 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
const [toTableColumns, setToTableColumns] = useState<ColumnInfo[]>([]);
const [selectedFromColumns, setSelectedFromColumns] = useState<string[]>([]);
const [selectedToColumns, setSelectedToColumns] = useState<string[]>([]);
// 필요시 로드하는 테이블 컬럼 캐시
const [tableColumnsCache, setTableColumnsCache] = useState<{ [tableName: string]: ColumnInfo[] }>({});
// 조건부 연결을 위한 새로운 상태들
const [conditions, setConditions] = useState<ConditionNode[]>([]);
@ -179,9 +201,15 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
// 데이터 저장 기본값 설정
setDataSaveSettings({
sourceField: "",
targetField: "",
saveConditions: "데이터 저장 조건을 입력하세요",
saveMode: "simple",
actions: [
{
id: "action_1",
name: `${fromDisplayName}에서 ${toDisplayName}로 데이터 저장`,
actionType: "insert",
fieldMappings: [],
},
],
});
// 외부 호출 기본값 설정
@ -253,6 +281,51 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
}));
}, [selectedFromColumns, selectedToColumns]);
// 테이블 컬럼 로드 함수 (캐시 활용)
const loadTableColumns = async (tableName: string): Promise<ColumnInfo[]> => {
if (tableColumnsCache[tableName]) {
return tableColumnsCache[tableName];
}
try {
const columns = await DataFlowAPI.getTableColumns(tableName);
setTableColumnsCache((prev) => ({
...prev,
[tableName]: columns,
}));
return columns;
} catch (error) {
console.error(`${tableName} 컬럼 로드 실패:`, error);
return [];
}
};
// 테이블 선택 시 컬럼 로드
useEffect(() => {
const loadColumns = async () => {
const tablesToLoad = new Set<string>();
// 필드 매핑에서 사용되는 모든 테이블 수집
dataSaveSettings.actions.forEach((action) => {
action.fieldMappings.forEach((mapping) => {
if (mapping.sourceTable && !tableColumnsCache[mapping.sourceTable]) {
tablesToLoad.add(mapping.sourceTable);
}
if (mapping.targetTable && !tableColumnsCache[mapping.targetTable]) {
tablesToLoad.add(mapping.targetTable);
}
});
});
// 필요한 테이블들의 컬럼만 로드
for (const tableName of tablesToLoad) {
await loadTableColumns(tableName);
}
};
loadColumns();
}, [dataSaveSettings.actions, tableColumnsCache]); // eslint-disable-line react-hooks/exhaustive-deps
const handleConfirm = () => {
if (!config.relationshipName || !connection) {
toast.error("필수 정보를 모두 입력해주세요.");
@ -303,7 +376,24 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
},
plan: {
sourceTable: fromTableName,
targetActions: [], // 나중에 액션 설정 UI에서 채울 예정
targetActions:
config.connectionType === "data-save"
? dataSaveSettings.actions.map((action) => ({
id: action.id,
actionType: action.actionType,
enabled: true,
conditions: action.conditions,
fieldMappings: action.fieldMappings.map((mapping) => ({
sourceTable: mapping.sourceTable,
sourceField: mapping.sourceField,
targetTable: mapping.targetTable,
targetField: mapping.targetField,
defaultValue: mapping.defaultValue,
transformFunction: mapping.transformFunction,
})),
splitConfig: action.splitConfig,
}))
: [],
},
}
: {};
@ -395,7 +485,7 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<div className="rounded-lg border border-l-4 border-l-purple-500 bg-purple-50/30 p-4">
<div className="mb-4 flex items-center gap-2">
<Zap className="h-4 w-4 text-purple-500" />
<span className="text-sm font-medium"> </span>
<span className="text-sm font-medium"> ( )</span>
</div>
{/* 실행 조건 설정 */}
@ -528,50 +618,478 @@ export const ConnectionSetupModal: React.FC<ConnectionSetupModalProps> = ({
<Save className="h-4 w-4 text-green-500" />
<span className="text-sm font-medium"> </span>
</div>
<div className="space-y-3">
<div className="grid grid-cols-2 gap-3">
<div>
<Label htmlFor="sourceField" className="text-sm">
</Label>
<Input
id="sourceField"
value={dataSaveSettings.sourceField}
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, sourceField: e.target.value })}
placeholder="소스 필드"
className="text-sm"
/>
</div>
<div>
<Label htmlFor="targetField" className="text-sm">
</Label>
<div className="flex items-center gap-2">
<Input
id="targetField"
value={dataSaveSettings.targetField}
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, targetField: e.target.value })}
placeholder="대상 필드"
className="text-sm"
/>
<Button size="sm" variant="outline">
<Plus className="h-3 w-3" />
</Button>
</div>
</div>
</div>
<div className="space-y-4">
{/* 저장 방식 선택 */}
<div>
<Label htmlFor="saveConditions" className="text-sm">
</Label>
<Textarea
id="saveConditions"
value={dataSaveSettings.saveConditions}
onChange={(e) => setDataSaveSettings({ ...dataSaveSettings, saveConditions: e.target.value })}
placeholder="데이터 저장 조건을 입력하세요"
rows={2}
className="text-sm"
/>
<Label className="text-sm font-medium"> </Label>
<Select
value={dataSaveSettings.saveMode}
onValueChange={(value: "simple" | "conditional" | "split") =>
setDataSaveSettings({ ...dataSaveSettings, saveMode: value })
}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="simple"> - </SelectItem>
<SelectItem value="conditional"> - </SelectItem>
<SelectItem value="split"> - </SelectItem>
</SelectContent>
</Select>
</div>
{/* 저장 방식별 설명 */}
<div className="rounded bg-blue-50 p-3 text-xs">
{dataSaveSettings.saveMode === "simple" && (
<div>
<strong> :</strong> From To (
{selectedToTable || "선택된 테이블"}) .
</div>
)}
{dataSaveSettings.saveMode === "conditional" && (
<div>
<strong> :</strong> .
<br />
: 평일 ,
</div>
)}
{dataSaveSettings.saveMode === "split" && (
<div>
<strong> :</strong> .
<br />
: &quot;,,&quot; 3
</div>
)}
</div>
{/* 액션 목록 */}
<div>
<div className="mb-2 flex items-center justify-between">
<Label className="text-sm font-medium"> </Label>
<Button
size="sm"
variant="outline"
onClick={() => {
const newAction = {
id: `action_${dataSaveSettings.actions.length + 1}`,
name: `액션 ${dataSaveSettings.actions.length + 1}`,
actionType: "insert" as const,
fieldMappings: [],
...(dataSaveSettings.saveMode === "conditional" ? { conditions: [] } : {}),
...(dataSaveSettings.saveMode === "split"
? {
splitConfig: { sourceField: "", delimiter: ",", targetField: "" },
}
: {}),
};
setDataSaveSettings({
...dataSaveSettings,
actions: [...dataSaveSettings.actions, newAction],
});
}}
className="h-7 text-xs"
>
<Plus className="mr-1 h-3 w-3" />
</Button>
</div>
{dataSaveSettings.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">
{dataSaveSettings.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) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].name = e.target.value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-7 flex-1 text-xs font-medium"
placeholder="액션 이름"
/>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newActions = dataSaveSettings.actions.filter((_, i) => i !== actionIndex);
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
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") => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].actionType = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<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>
{/* 액션별 개별 실행 조건 (조건부 저장일 때만) */}
{dataSaveSettings.saveMode !== "simple" && (
<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={() => {
const newActions = [...dataSaveSettings.actions];
if (!newActions[actionIndex].conditions) {
newActions[actionIndex].conditions = [];
}
newActions[actionIndex].conditions = [
...(newActions[actionIndex].conditions || []),
{ field: "", operator: "=", value: "" },
];
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 text-xs"
>
<Plus className="mr-1 h-2 w-2" />
</Button>
</div>
{action.conditions && action.conditions.length > 0 && (
<div className="space-y-2">
{action.conditions.map((condition, condIndex) => (
<div key={condIndex} className="grid grid-cols-4 items-center gap-2">
<Select
value={condition.field}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].conditions![condIndex].field = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<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>
<Select
value={condition.operator}
onValueChange={(value: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE") => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].conditions![condIndex].operator = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<SelectTrigger className="h-6 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>
<Input
value={condition.value}
onChange={(e) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].conditions![condIndex].value = e.target.value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 text-xs"
placeholder="값"
/>
<Button
size="sm"
variant="ghost"
onClick={() => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].conditions = newActions[actionIndex].conditions!.filter(
(_, i) => i !== condIndex,
);
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 w-6 p-0"
>
<Trash2 className="h-2 w-2" />
</Button>
</div>
))}
</div>
)}
</div>
)}
{/* 분할 저장일 때 분할 설정 */}
{dataSaveSettings.saveMode === "split" && action.splitConfig && (
<div className="mt-3">
<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) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].splitConfig!.sourceField = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<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) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].splitConfig!.delimiter = e.target.value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 text-xs"
placeholder=","
/>
</div>
<div>
<Label className="text-xs text-gray-500"> </Label>
<Input
value={action.splitConfig.targetField}
onChange={(e) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].splitConfig!.targetField = e.target.value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-6 text-xs"
placeholder="product_name"
/>
</div>
</div>
</div>
)}
{/* 필드 매핑 */}
<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={() => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings.push({
sourceTable: "",
sourceField: "",
targetTable: "",
targetField: "",
defaultValue: "",
transformFunction: "",
});
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
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={mappingIndex} className="rounded border bg-gray-50 p-4">
{/* 필드 매핑 영역 */}
<div className="mb-3">
<div className="grid grid-cols-5 items-end gap-3">
{/* 소스 테이블 */}
<div>
<Label className="mb-1 block text-xs text-gray-600"> </Label>
<Select
value={mapping.sourceTable || ""}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].sourceTable = value;
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = ""; // 컬럼 초기화
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 소스 컬럼 */}
<div>
<Label className="mb-1 block text-xs text-gray-600"> </Label>
<Select
value={mapping.sourceField}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].sourceField = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
disabled={!mapping.sourceTable}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{mapping.sourceTable &&
tableColumnsCache[mapping.sourceTable]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 화살표 */}
<div className="flex items-end justify-center pb-2">
<div className="text-center text-lg font-bold text-gray-400"></div>
</div>
{/* 타겟 테이블 */}
<div>
<Label className="mb-1 block text-xs text-gray-600"> </Label>
<Select
value={mapping.targetTable || ""}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].targetTable = value;
newActions[actionIndex].fieldMappings[mappingIndex].targetField = ""; // 컬럼 초기화
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="테이블 선택" />
</SelectTrigger>
<SelectContent>
{availableTables.map((table) => (
<SelectItem key={table.tableName} value={table.tableName}>
{table.tableName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 타겟 컬럼 */}
<div>
<Label className="mb-1 block text-xs text-gray-600"> </Label>
<Select
value={mapping.targetField}
onValueChange={(value) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].targetField = value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
disabled={!mapping.targetTable}
>
<SelectTrigger className="h-8 text-xs">
<SelectValue placeholder="컬럼 선택" />
</SelectTrigger>
<SelectContent>
{mapping.targetTable &&
tableColumnsCache[mapping.targetTable]?.map((column) => (
<SelectItem key={column.columnName} value={column.columnName}>
{column.columnName}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
{/* 기본값 및 삭제 버튼 */}
<div className="flex items-center gap-3">
<div className="flex-1">
<Label className="mb-1 block text-xs text-gray-600"> ()</Label>
<Input
value={mapping.defaultValue || ""}
onChange={(e) => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings[mappingIndex].defaultValue =
e.target.value;
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-8 text-xs"
placeholder="고정값 또는 기본값 입력"
/>
</div>
<div className="flex items-end">
<Button
size="sm"
variant="ghost"
onClick={() => {
const newActions = [...dataSaveSettings.actions];
newActions[actionIndex].fieldMappings = newActions[
actionIndex
].fieldMappings.filter((_, i) => i !== mappingIndex);
setDataSaveSettings({ ...dataSaveSettings, actions: newActions });
}}
className="h-8 w-8 p-0 text-red-500 hover:text-red-700"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>