제어관리 데이터 저장기능
This commit is contained in:
@@ -24,8 +24,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// API import
|
||||
import { getColumnsFromConnection } from "@/lib/api/multiConnection";
|
||||
// API import (컬럼 로드는 중앙에서 관리)
|
||||
|
||||
// 타입 import
|
||||
import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection";
|
||||
@@ -40,6 +39,9 @@ interface MultiActionConfigStepProps {
|
||||
toTable?: TableInfo;
|
||||
fromConnection?: Connection;
|
||||
toConnection?: Connection;
|
||||
// 컬럼 정보 (중앙에서 관리) 🔧 추가
|
||||
fromColumns?: ColumnInfo[];
|
||||
toColumns?: ColumnInfo[];
|
||||
// 제어 조건 관련
|
||||
controlConditions: any[];
|
||||
onUpdateControlCondition: (index: number, condition: any) => void;
|
||||
@@ -47,12 +49,14 @@ interface MultiActionConfigStepProps {
|
||||
onAddControlCondition: () => void;
|
||||
// 액션 그룹 관련
|
||||
actionGroups: ActionGroup[];
|
||||
groupsLogicalOperator?: "AND" | "OR";
|
||||
onUpdateActionGroup: (groupId: string, updates: Partial<ActionGroup>) => void;
|
||||
onDeleteActionGroup: (groupId: string) => void;
|
||||
onAddActionGroup: () => void;
|
||||
onAddActionToGroup: (groupId: string) => void;
|
||||
onUpdateActionInGroup: (groupId: string, actionId: string, updates: Partial<SingleAction>) => void;
|
||||
onDeleteActionFromGroup: (groupId: string, actionId: string) => void;
|
||||
onSetGroupsLogicalOperator?: (operator: "AND" | "OR") => void;
|
||||
// 필드 매핑 관련
|
||||
fieldMappings: FieldMapping[];
|
||||
onCreateMapping: (fromField: ColumnInfo, toField: ColumnInfo) => void;
|
||||
@@ -60,6 +64,8 @@ interface MultiActionConfigStepProps {
|
||||
// 네비게이션
|
||||
onNext: () => void;
|
||||
onBack: () => void;
|
||||
// 컬럼 로드 액션
|
||||
onLoadColumns?: () => Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,55 +81,41 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
||||
toTable,
|
||||
fromConnection,
|
||||
toConnection,
|
||||
fromColumns = [], // 🔧 중앙에서 관리되는 컬럼 정보
|
||||
toColumns = [], // 🔧 중앙에서 관리되는 컬럼 정보
|
||||
controlConditions,
|
||||
onUpdateControlCondition,
|
||||
onDeleteControlCondition,
|
||||
onAddControlCondition,
|
||||
actionGroups,
|
||||
groupsLogicalOperator = "AND",
|
||||
onUpdateActionGroup,
|
||||
onDeleteActionGroup,
|
||||
onAddActionGroup,
|
||||
onAddActionToGroup,
|
||||
onUpdateActionInGroup,
|
||||
onDeleteActionFromGroup,
|
||||
onSetGroupsLogicalOperator,
|
||||
fieldMappings,
|
||||
onCreateMapping,
|
||||
onDeleteMapping,
|
||||
onNext,
|
||||
onBack,
|
||||
onLoadColumns,
|
||||
}) => {
|
||||
const [fromColumns, setFromColumns] = useState<ColumnInfo[]>([]);
|
||||
const [toColumns, setToColumns] = useState<ColumnInfo[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(["group_1"])); // 첫 번째 그룹은 기본 열림
|
||||
const [activeTab, setActiveTab] = useState<"control" | "actions" | "mapping">("control"); // 현재 활성 탭
|
||||
|
||||
// 컬럼 정보 로드
|
||||
// 컬럼 로딩 상태 확인
|
||||
const isColumnsLoaded = fromColumns.length > 0 && toColumns.length > 0;
|
||||
|
||||
// 컴포넌트 마운트 시 컬럼 로드
|
||||
useEffect(() => {
|
||||
const loadColumns = async () => {
|
||||
if (!fromConnection || !toConnection || !fromTable || !toTable) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [fromCols, toCols] = await Promise.all([
|
||||
getColumnsFromConnection(fromConnection.id, fromTable.tableName),
|
||||
getColumnsFromConnection(toConnection.id, toTable.tableName),
|
||||
]);
|
||||
|
||||
setFromColumns(Array.isArray(fromCols) ? fromCols : []);
|
||||
setToColumns(Array.isArray(toCols) ? toCols : []);
|
||||
} catch (error) {
|
||||
console.error("❌ 컬럼 정보 로드 실패:", error);
|
||||
toast.error("필드 정보를 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadColumns();
|
||||
}, [fromConnection, toConnection, fromTable, toTable]);
|
||||
if (!isColumnsLoaded && fromConnection && toConnection && fromTable && toTable && onLoadColumns) {
|
||||
console.log("🔄 MultiActionConfigStep: 컬럼 로드 시작");
|
||||
onLoadColumns();
|
||||
}
|
||||
}, [isColumnsLoaded, fromConnection?.id, toConnection?.id, fromTable?.tableName, toTable?.tableName]);
|
||||
|
||||
// 그룹 확장/축소 토글
|
||||
const toggleGroupExpansion = (groupId: string) => {
|
||||
@@ -171,13 +163,10 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
||||
group.actions.some((action) => action.actionType === "insert" && action.isEnabled),
|
||||
);
|
||||
|
||||
// 탭 정보
|
||||
// 탭 정보 (컬럼 매핑 탭 제거)
|
||||
const tabs = [
|
||||
{ id: "control" as const, label: "제어 조건", icon: "🎯", description: "전체 제어 실행 조건" },
|
||||
{ id: "actions" as const, label: "액션 설정", icon: "⚙️", description: "액션 그룹 및 실행 조건" },
|
||||
...(hasInsertActions
|
||||
? [{ id: "mapping" as const, label: "컬럼 매핑", icon: "🔗", description: "INSERT 액션 필드 매핑" }]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -280,265 +269,405 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 액션 그룹 목록 */}
|
||||
<div className="space-y-4">
|
||||
{actionGroups.map((group, groupIndex) => (
|
||||
<div key={group.id} className="bg-card rounded-lg border">
|
||||
{/* 그룹 헤더 */}
|
||||
<Collapsible
|
||||
open={expandedGroups.has(group.id)}
|
||||
onOpenChange={() => toggleGroupExpansion(group.id)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="hover:bg-muted/50 flex cursor-pointer items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedGroups.has(group.id) ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={group.name}
|
||||
onChange={(e) => onUpdateActionGroup(group.id, { name: e.target.value })}
|
||||
className="h-8 w-40"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Badge className={getLogicalOperatorColor(group.logicalOperator)}>
|
||||
{group.logicalOperator}
|
||||
</Badge>
|
||||
<Badge variant={group.isEnabled ? "default" : "secondary"}>
|
||||
{group.actions.length}개 액션
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 그룹 논리 연산자 선택 */}
|
||||
<Select
|
||||
value={group.logicalOperator}
|
||||
onValueChange={(value: "AND" | "OR") =>
|
||||
onUpdateActionGroup(group.id, { logicalOperator: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20" onClick={(e) => e.stopPropagation()}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AND">AND</SelectItem>
|
||||
<SelectItem value="OR">OR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 그룹 활성화/비활성화 */}
|
||||
<Switch
|
||||
checked={group.isEnabled}
|
||||
onCheckedChange={(checked) => onUpdateActionGroup(group.id, { isEnabled: checked })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
{/* 그룹 삭제 */}
|
||||
{actionGroups.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteActionGroup(group.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{/* 그룹 내용 */}
|
||||
<CollapsibleContent>
|
||||
<div className="bg-muted/20 border-t p-4">
|
||||
{/* 액션 추가 버튼 */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onAddActionToGroup(group.id)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
액션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 액션 목록 */}
|
||||
<div className="space-y-3">
|
||||
{group.actions.map((action, actionIndex) => (
|
||||
<div key={action.id} className="rounded-md border bg-white p-3">
|
||||
{/* 액션 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">{getActionTypeIcon(action.actionType)}</span>
|
||||
<Input
|
||||
value={action.name}
|
||||
onChange={(e) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { name: e.target.value })
|
||||
}
|
||||
className="h-8 w-32"
|
||||
/>
|
||||
<Select
|
||||
value={action.actionType}
|
||||
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
|
||||
onUpdateActionInGroup(group.id, action.id, { actionType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24">
|
||||
<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 className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={action.isEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { isEnabled: checked })
|
||||
}
|
||||
/>
|
||||
{group.actions.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDeleteActionFromGroup(group.id, action.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 조건 설정 */}
|
||||
<ActionConditionBuilder
|
||||
actionType={action.actionType}
|
||||
fromColumns={fromColumns}
|
||||
toColumns={toColumns}
|
||||
conditions={action.conditions}
|
||||
fieldMappings={action.fieldMappings}
|
||||
onConditionsChange={(conditions) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { conditions })
|
||||
}
|
||||
onFieldMappingsChange={(fieldMappings) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { fieldMappings })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 그룹 로직 설명 */}
|
||||
<div className="mt-4 rounded-md bg-blue-50 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-900">{group.logicalOperator} 조건 그룹</div>
|
||||
<div className="text-blue-700">
|
||||
{group.logicalOperator === "AND"
|
||||
? "이 그룹의 모든 액션이 실행 가능한 조건일 때만 실행됩니다."
|
||||
: "이 그룹의 액션 중 하나라도 실행 가능한 조건이면 해당 액션만 실행됩니다."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
{/* 그룹 간 연결선 (마지막 그룹이 아닌 경우) */}
|
||||
{groupIndex < actionGroups.length - 1 && (
|
||||
<div className="flex justify-center py-2">
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-xs">
|
||||
<div className="bg-border h-px w-8"></div>
|
||||
<span>다음 그룹</span>
|
||||
<div className="bg-border h-px w-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 그룹 간 논리 연산자 선택 */}
|
||||
{actionGroups.length > 1 && (
|
||||
<div className="rounded-md border bg-blue-50 p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<h4 className="text-sm font-medium text-blue-900">그룹 간 실행 조건</h4>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "mapping" && hasInsertActions && (
|
||||
<div className="space-y-4">
|
||||
{/* 컬럼 매핑 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-medium">컬럼 매핑</h3>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{fieldMappings.length}개 매핑
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-sm">INSERT 액션에 필요한 필드들을 매핑하세요</div>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 매핑 캔버스 */}
|
||||
{isLoading ? (
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<div className="text-muted-foreground">컬럼 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
) : fromColumns.length > 0 && toColumns.length > 0 ? (
|
||||
<div className="rounded-lg border bg-white p-4">
|
||||
<FieldMappingCanvas
|
||||
fromFields={fromColumns}
|
||||
toFields={toColumns}
|
||||
mappings={fieldMappings}
|
||||
onCreateMapping={onCreateMapping}
|
||||
onDeleteMapping={onDeleteMapping}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-64 flex-col items-center justify-center space-y-3 rounded-lg border border-dashed">
|
||||
<AlertTriangle className="text-muted-foreground h-8 w-8" />
|
||||
<div className="text-muted-foreground">컬럼 정보를 찾을 수 없습니다.</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
FROM 컬럼: {fromColumns.length}개, TO 컬럼: {toColumns.length}개
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="groups-and"
|
||||
name="groups-operator"
|
||||
checked={groupsLogicalOperator === "AND"}
|
||||
onChange={() => onSetGroupsLogicalOperator?.("AND")}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label htmlFor="groups-and" className="text-sm text-blue-800">
|
||||
<span className="font-medium">AND</span> - 모든 그룹의 조건이 참일 때 실행
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id="groups-or"
|
||||
name="groups-operator"
|
||||
checked={groupsLogicalOperator === "OR"}
|
||||
onChange={() => onSetGroupsLogicalOperator?.("OR")}
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<label htmlFor="groups-or" className="text-sm text-blue-800">
|
||||
<span className="font-medium">OR</span> - 하나 이상의 그룹 조건이 참일 때 실행
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 매핑되지 않은 필드 처리 옵션 */}
|
||||
<div className="rounded-md border bg-yellow-50 p-4">
|
||||
<h4 className="mb-3 flex items-center gap-2 font-medium text-yellow-800">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
매핑되지 않은 필드 처리
|
||||
</h4>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="radio" id="empty" name="unmapped-strategy" defaultChecked className="h-4 w-4" />
|
||||
<label htmlFor="empty" className="text-yellow-700">
|
||||
비워두기 (NULL 또는 빈 값)
|
||||
</label>
|
||||
{/* 액션 그룹 목록 */}
|
||||
<div className="space-y-4">
|
||||
{actionGroups.map((group, groupIndex) => (
|
||||
<div key={group.id}>
|
||||
{/* 그룹 간 논리 연산자 표시 (첫 번째 그룹 제외) */}
|
||||
{groupIndex > 0 && (
|
||||
<div className="my-2 flex items-center justify-center">
|
||||
<div
|
||||
className={`rounded px-2 py-1 text-xs font-medium ${
|
||||
groupsLogicalOperator === "AND"
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-orange-100 text-orange-800"
|
||||
}`}
|
||||
>
|
||||
{groupsLogicalOperator}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="bg-card rounded-lg border">
|
||||
{/* 그룹 헤더 */}
|
||||
<Collapsible
|
||||
open={expandedGroups.has(group.id)}
|
||||
onOpenChange={() => toggleGroupExpansion(group.id)}
|
||||
>
|
||||
<CollapsibleTrigger asChild>
|
||||
<div className="hover:bg-muted/50 flex cursor-pointer items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{expandedGroups.has(group.id) ? (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
value={group.name}
|
||||
onChange={(e) => onUpdateActionGroup(group.id, { name: e.target.value })}
|
||||
className="h-8 w-40"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<Badge className={getLogicalOperatorColor(group.logicalOperator)}>
|
||||
{group.logicalOperator}
|
||||
</Badge>
|
||||
<Badge variant={group.isEnabled ? "default" : "secondary"}>
|
||||
{group.actions.length}개 액션
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* 그룹 논리 연산자 선택 */}
|
||||
<Select
|
||||
value={group.logicalOperator}
|
||||
onValueChange={(value: "AND" | "OR") =>
|
||||
onUpdateActionGroup(group.id, { logicalOperator: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-20" onClick={(e) => e.stopPropagation()}>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="AND">AND</SelectItem>
|
||||
<SelectItem value="OR">OR</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* 그룹 활성화/비활성화 */}
|
||||
<Switch
|
||||
checked={group.isEnabled}
|
||||
onCheckedChange={(checked) => onUpdateActionGroup(group.id, { isEnabled: checked })}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
|
||||
{/* 그룹 삭제 */}
|
||||
{actionGroups.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteActionGroup(group.id);
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleTrigger>
|
||||
|
||||
{/* 그룹 내용 */}
|
||||
<CollapsibleContent>
|
||||
<div className="bg-muted/20 border-t p-4">
|
||||
{/* 액션 추가 버튼 */}
|
||||
<div className="mb-4 flex justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onAddActionToGroup(group.id)}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
액션 추가
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 액션 목록 */}
|
||||
<div className="space-y-3">
|
||||
{group.actions.map((action, actionIndex) => (
|
||||
<div key={action.id} className="rounded-md border bg-white p-3">
|
||||
{/* 액션 헤더 */}
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-lg">{getActionTypeIcon(action.actionType)}</span>
|
||||
<Input
|
||||
value={action.name}
|
||||
onChange={(e) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { name: e.target.value })
|
||||
}
|
||||
className="h-8 w-32"
|
||||
/>
|
||||
<Select
|
||||
value={action.actionType}
|
||||
onValueChange={(value: "insert" | "update" | "delete" | "upsert") =>
|
||||
onUpdateActionInGroup(group.id, action.id, { actionType: value })
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-24">
|
||||
<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 className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={action.isEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { isEnabled: checked })
|
||||
}
|
||||
/>
|
||||
{group.actions.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDeleteActionFromGroup(group.id, action.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 액션 조건 설정 */}
|
||||
{isColumnsLoaded ? (
|
||||
<ActionConditionBuilder
|
||||
actionType={action.actionType}
|
||||
fromColumns={fromColumns}
|
||||
toColumns={toColumns}
|
||||
conditions={action.conditions}
|
||||
fieldMappings={(() => {
|
||||
// 필드값 설정용: FieldValueMapping 타입만 필터링
|
||||
const fieldValueMappings = (action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.valueType && // valueType이 있고
|
||||
!mapping.fromField && // fromField가 없고
|
||||
!mapping.toField, // toField가 없으면 FieldValueMapping
|
||||
);
|
||||
|
||||
console.log("📋 ActionConditionBuilder에 전달되는 필드값 설정:", {
|
||||
allMappings: action.fieldMappings,
|
||||
filteredFieldValueMappings: fieldValueMappings,
|
||||
});
|
||||
|
||||
return fieldValueMappings;
|
||||
})()}
|
||||
columnMappings={
|
||||
// 컬럼 매핑용: FieldMapping 타입만 필터링
|
||||
(action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.fromField &&
|
||||
mapping.toField &&
|
||||
mapping.fromField.columnName &&
|
||||
mapping.toField.columnName,
|
||||
)
|
||||
}
|
||||
onConditionsChange={(conditions) =>
|
||||
onUpdateActionInGroup(group.id, action.id, { conditions })
|
||||
}
|
||||
onFieldMappingsChange={(newFieldMappings) => {
|
||||
// 필드값 설정만 업데이트, 컬럼 매핑은 유지
|
||||
const existingColumnMappings = (action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.fromField &&
|
||||
mapping.toField &&
|
||||
mapping.fromField.columnName &&
|
||||
mapping.toField.columnName,
|
||||
);
|
||||
|
||||
console.log("🔄 필드값 설정 업데이트:", {
|
||||
existingColumnMappings,
|
||||
newFieldMappings,
|
||||
combined: [...existingColumnMappings, ...newFieldMappings],
|
||||
});
|
||||
|
||||
onUpdateActionInGroup(group.id, action.id, {
|
||||
fieldMappings: [...existingColumnMappings, ...newFieldMappings],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-muted-foreground flex items-center justify-center py-4">
|
||||
컬럼 정보를 불러오는 중...
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* INSERT 액션일 때만 필드 매핑 UI 표시 */}
|
||||
{action.actionType === "insert" && isColumnsLoaded && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<h5 className="text-sm font-medium">필드 매핑</h5>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{action.fieldMappings?.length || 0}개 매핑
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* 컬럼 매핑 캔버스 */}
|
||||
<div className="rounded-lg border bg-white p-3">
|
||||
<FieldMappingCanvas
|
||||
fromFields={fromColumns}
|
||||
toFields={toColumns}
|
||||
mappings={
|
||||
// 컬럼 매핑만 FieldMappingCanvas에 전달
|
||||
(action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.fromField &&
|
||||
mapping.toField &&
|
||||
mapping.fromField.columnName &&
|
||||
mapping.toField.columnName,
|
||||
)
|
||||
}
|
||||
onCreateMapping={(fromField, toField) => {
|
||||
const newMapping = {
|
||||
id: `${fromField.columnName}_to_${toField.columnName}_${Date.now()}`,
|
||||
fromField,
|
||||
toField,
|
||||
isValid: true,
|
||||
validationMessage: undefined,
|
||||
};
|
||||
|
||||
// 기존 필드값 설정은 유지하고 새 컬럼 매핑만 추가
|
||||
const existingFieldValueMappings = (action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.valueType && // valueType이 있고
|
||||
!mapping.fromField && // fromField가 없고
|
||||
!mapping.toField, // toField가 없으면 FieldValueMapping
|
||||
);
|
||||
|
||||
const existingColumnMappings = (action.fieldMappings || []).filter(
|
||||
(mapping) =>
|
||||
mapping.fromField &&
|
||||
mapping.toField &&
|
||||
mapping.fromField.columnName &&
|
||||
mapping.toField.columnName,
|
||||
);
|
||||
|
||||
onUpdateActionInGroup(group.id, action.id, {
|
||||
fieldMappings: [
|
||||
...existingFieldValueMappings,
|
||||
...existingColumnMappings,
|
||||
newMapping,
|
||||
],
|
||||
});
|
||||
}}
|
||||
onDeleteMapping={(mappingId) => {
|
||||
// 컬럼 매핑만 삭제하고 필드값 설정은 유지
|
||||
const remainingMappings = (action.fieldMappings || []).filter(
|
||||
(mapping) => mapping.id !== mappingId,
|
||||
);
|
||||
|
||||
onUpdateActionInGroup(group.id, action.id, {
|
||||
fieldMappings: remainingMappings,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 매핑되지 않은 필드 처리 옵션 */}
|
||||
<div className="rounded-md border bg-yellow-50 p-3">
|
||||
<h6 className="mb-2 flex items-center gap-1 text-xs font-medium text-yellow-800">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
매핑되지 않은 필드 처리
|
||||
</h6>
|
||||
<div className="space-y-2 text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`empty-${action.id}`}
|
||||
name={`unmapped-${action.id}`}
|
||||
defaultChecked
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<label htmlFor={`empty-${action.id}`} className="text-yellow-700">
|
||||
비워두기 (NULL 값)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`default-${action.id}`}
|
||||
name={`unmapped-${action.id}`}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<label htmlFor={`default-${action.id}`} className="text-yellow-700">
|
||||
기본값 사용
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
id={`skip-${action.id}`}
|
||||
name={`unmapped-${action.id}`}
|
||||
className="h-3 w-3"
|
||||
/>
|
||||
<label htmlFor={`skip-${action.id}`} className="text-yellow-700">
|
||||
필드 제외
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 그룹 로직 설명 */}
|
||||
<div className="mt-4 rounded-md bg-blue-50 p-3">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 h-4 w-4 text-blue-600" />
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-blue-900">{group.logicalOperator} 조건 그룹</div>
|
||||
<div className="text-blue-700">
|
||||
{group.logicalOperator === "AND"
|
||||
? "이 그룹의 모든 액션이 실행 가능한 조건일 때만 실행됩니다."
|
||||
: "이 그룹의 액션 중 하나라도 실행 가능한 조건이면 해당 액션만 실행됩니다."}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="radio" id="default" name="unmapped-strategy" className="h-4 w-4" />
|
||||
<label htmlFor="default" className="text-yellow-700">
|
||||
기본값 사용 (데이터베이스 DEFAULT 값)
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<input type="radio" id="skip" name="unmapped-strategy" className="h-4 w-4" />
|
||||
<label htmlFor="skip" className="text-yellow-700">
|
||||
해당 필드 제외 (INSERT 구문에 포함하지 않음)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user