제어관리 수정
This commit is contained in:
parent
3f76d16afe
commit
db25b0435f
|
|
@ -24,13 +24,13 @@ export default function DataFlowPage() {
|
|||
// 단계별 제목과 설명
|
||||
const stepConfig = {
|
||||
list: {
|
||||
title: "데이터 흐름 관계도 관리",
|
||||
description: "생성된 관계도들을 확인하고 관리하세요",
|
||||
title: "데이터 흐름 제어 관리",
|
||||
description: "생성된 제어들을 확인하고 관리하세요",
|
||||
icon: "📊",
|
||||
},
|
||||
design: {
|
||||
title: "새 관계도 설계",
|
||||
description: "테이블 간 데이터 관계를 시각적으로 설계하세요",
|
||||
title: "새 제어 설계",
|
||||
description: "테이블 간 데이터 제어를 시각적으로 설계하세요",
|
||||
icon: "🎨",
|
||||
},
|
||||
};
|
||||
|
|
@ -62,7 +62,7 @@ export default function DataFlowPage() {
|
|||
};
|
||||
|
||||
const handleSave = (relationships: TableRelationship[]) => {
|
||||
console.log("저장된 관계:", relationships);
|
||||
console.log("저장된 제어:", relationships);
|
||||
// 저장 후 목록으로 돌아가기 - 다음 렌더링 사이클로 지연
|
||||
setTimeout(() => {
|
||||
goToStep("list");
|
||||
|
|
@ -71,28 +71,28 @@ export default function DataFlowPage() {
|
|||
}, 0);
|
||||
};
|
||||
|
||||
// 관계도 수정 핸들러
|
||||
// 제어 수정 핸들러
|
||||
const handleDesignDiagram = async (diagram: DataFlowDiagram | null) => {
|
||||
if (diagram) {
|
||||
// 기존 관계도 수정 - 저장된 관계 정보 로드
|
||||
// 기존 제어 수정 - 저장된 제어 정보 로드
|
||||
try {
|
||||
console.log("📖 관계도 수정 모드:", diagram);
|
||||
console.log("📖 제어 수정 모드:", diagram);
|
||||
|
||||
// 저장된 관계 정보 로드
|
||||
// 저장된 제어 정보 로드
|
||||
const relationshipData = await loadDataflowRelationship(diagram.diagramId);
|
||||
console.log("✅ 관계 정보 로드 완료:", relationshipData);
|
||||
console.log("✅ 제어 정보 로드 완료:", relationshipData);
|
||||
|
||||
setEditingDiagram(diagram);
|
||||
setLoadedRelationshipData(relationshipData);
|
||||
goToNextStep("design");
|
||||
|
||||
toast.success(`"${diagram.diagramName}" 관계를 불러왔습니다.`);
|
||||
toast.success(`"${diagram.diagramName}" 제어를 불러왔습니다.`);
|
||||
} catch (error: any) {
|
||||
console.error("❌ 관계 정보 로드 실패:", error);
|
||||
toast.error(error.message || "관계 정보를 불러오는데 실패했습니다.");
|
||||
console.error("❌ 제어 정보 로드 실패:", error);
|
||||
toast.error(error.message || "제어 정보를 불러오는데 실패했습니다.");
|
||||
}
|
||||
} else {
|
||||
// 새 관계도 생성 - 현재 페이지에서 처리
|
||||
// 새 제어 생성 - 현재 페이지에서 처리
|
||||
setEditingDiagram(null);
|
||||
setLoadedRelationshipData(null);
|
||||
goToNextStep("design");
|
||||
|
|
@ -101,21 +101,21 @@ export default function DataFlowPage() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="container mx-auto space-y-4 p-4">
|
||||
<div className="mx-auto space-y-4 px-5 py-4">
|
||||
{/* 페이지 제목 */}
|
||||
<div className="flex items-center justify-between rounded-lg border bg-white p-4 shadow-sm">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">관계 관리</h1>
|
||||
<p className="mt-2 text-gray-600">테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다</p>
|
||||
<h1 className="text-3xl font-bold text-gray-900">제어 관리</h1>
|
||||
<p className="mt-2 text-gray-600">테이블 간 데이터 제어를 시각적으로 설계하고 관리합니다</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 단계별 내용 */}
|
||||
<div className="space-y-6">
|
||||
{/* 관계도 목록 단계 */}
|
||||
{/* 제어 목록 단계 */}
|
||||
{currentStep === "list" && <DataFlowList onDesignDiagram={handleDesignDiagram} />}
|
||||
|
||||
{/* 관계도 설계 단계 - 🎨 새로운 UI 사용 */}
|
||||
{/* 제어 설계 단계 - 🎨 새로운 UI 사용 */}
|
||||
{currentStep === "design" && (
|
||||
<DataConnectionDesigner
|
||||
onClose={() => {
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
tables.push(relationships.toTable.tableName);
|
||||
}
|
||||
|
||||
// 관계 수 계산 (actionGroups 기준)
|
||||
// 제어 수 계산 (actionGroups 기준)
|
||||
const actionGroups = relationships.actionGroups || [];
|
||||
const relationshipCount = actionGroups.reduce((count: number, group: any) => {
|
||||
return count + (group.actions?.length || 0);
|
||||
|
|
@ -79,7 +79,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
relationshipId: diagram.diagram_id, // 호환성을 위해 추가
|
||||
diagramName: diagram.diagram_name,
|
||||
connectionType: relationships.connectionType || "data_save", // 실제 연결 타입 사용
|
||||
relationshipType: "multi-relationship", // 다중 관계 타입
|
||||
relationshipType: "multi-relationship", // 다중 제어 타입
|
||||
relationshipCount: relationshipCount || 1, // 최소 1개는 있다고 가정
|
||||
tableCount: tables.length,
|
||||
tables: tables,
|
||||
|
|
@ -96,14 +96,14 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
setTotal(response.pagination.total || 0);
|
||||
setTotalPages(Math.max(1, Math.ceil((response.pagination.total || 0) / 20)));
|
||||
} catch (error) {
|
||||
console.error("관계 목록 조회 실패", error);
|
||||
toast.error("관계 목록을 불러오는데 실패했습니다.");
|
||||
console.error("제어 목록 조회 실패", error);
|
||||
toast.error("제어 목록을 불러오는데 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [currentPage, searchTerm, companyCode]);
|
||||
|
||||
// 관계 목록 로드
|
||||
// 제어 목록 로드
|
||||
useEffect(() => {
|
||||
loadDiagrams();
|
||||
}, [loadDiagrams]);
|
||||
|
|
@ -130,13 +130,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
undefined,
|
||||
user?.userId || "SYSTEM",
|
||||
);
|
||||
toast.success(`관계가 성공적으로 복사되었습니다: ${copiedDiagram.diagram_name}`);
|
||||
toast.success(`제어가 성공적으로 복사되었습니다: ${copiedDiagram.diagram_name}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계 복사 실패:", error);
|
||||
toast.error("관계 복사에 실패했습니다.");
|
||||
console.error("제어 복사 실패:", error);
|
||||
toast.error("제어 복사에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowCopyModal(false);
|
||||
|
|
@ -151,13 +151,13 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
try {
|
||||
setLoading(true);
|
||||
await DataFlowAPI.deleteJsonDataFlowDiagram(selectedDiagramForAction.diagramId, companyCode);
|
||||
toast.success(`관계가 삭제되었습니다: ${selectedDiagramForAction.diagramName}`);
|
||||
toast.success(`제어가 삭제되었습니다: ${selectedDiagramForAction.diagramName}`);
|
||||
|
||||
// 목록 새로고침
|
||||
await loadDiagrams();
|
||||
} catch (error) {
|
||||
console.error("관계 삭제 실패:", error);
|
||||
toast.error("관계 삭제에 실패했습니다.");
|
||||
console.error("제어 삭제 실패:", error);
|
||||
toast.error("제어 삭제에 실패했습니다.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setShowDeleteModal(false);
|
||||
|
|
@ -181,7 +181,7 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<div className="relative">
|
||||
<Search className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 transform text-gray-400" />
|
||||
<Input
|
||||
placeholder="관계명, 테이블명으로 검색..."
|
||||
placeholder="제어명, 테이블명으로 검색..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-80 pl-10"
|
||||
|
|
@ -189,17 +189,17 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
</div>
|
||||
</div>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => onDesignDiagram(null)}>
|
||||
<Plus className="mr-2 h-4 w-4" />새 관계 생성
|
||||
<Plus className="mr-2 h-4 w-4" />새 제어 생성
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 관계 목록 테이블 */}
|
||||
{/* 제어 목록 테이블 */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center">
|
||||
<Network className="mr-2 h-5 w-5" />
|
||||
데이터 흐름 관계 ({total})
|
||||
데이터 흐름 제어 ({total})
|
||||
</span>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
|
@ -207,10 +207,10 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>관계명</TableHead>
|
||||
<TableHead>제어명</TableHead>
|
||||
<TableHead>회사 코드</TableHead>
|
||||
<TableHead>테이블 수</TableHead>
|
||||
<TableHead>관계 수</TableHead>
|
||||
<TableHead>액션 수</TableHead>
|
||||
<TableHead>최근 수정</TableHead>
|
||||
<TableHead>작업</TableHead>
|
||||
</TableRow>
|
||||
|
|
@ -284,8 +284,8 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
{diagrams.length === 0 && (
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
<Network className="mx-auto mb-4 h-12 w-12 text-gray-300" />
|
||||
<div className="mb-2 text-lg font-medium">관계가 없습니다</div>
|
||||
<div className="text-sm">새 관계를 생성하여 테이블 간 데이터 관계를 설정해보세요.</div>
|
||||
<div className="mb-2 text-lg font-medium">제어가 없습니다</div>
|
||||
<div className="text-sm">새 제어를 생성하여 테이블 간 데이터 제어를 설정해보세요.</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
@ -320,11 +320,11 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Dialog open={showCopyModal} onOpenChange={setShowCopyModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>관계 복사</DialogTitle>
|
||||
<DialogTitle>제어 복사</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계를 복사하시겠습니까?
|
||||
“{selectedDiagramForAction?.diagramName}” 제어를 복사하시겠습니까?
|
||||
<br />
|
||||
새로운 관계는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
|
||||
새로운 제어는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
|
|
@ -342,12 +342,12 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
|
|||
<Dialog open={showDeleteModal} onOpenChange={setShowDeleteModal}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-red-600">관계 삭제</DialogTitle>
|
||||
<DialogTitle className="text-red-600">제어 삭제</DialogTitle>
|
||||
<DialogDescription>
|
||||
“{selectedDiagramForAction?.diagramName}” 관계를 완전히 삭제하시겠습니까?
|
||||
“{selectedDiagramForAction?.diagramName}” 제어를 완전히 삭제하시겠습니까?
|
||||
<br />
|
||||
<span className="font-medium text-red-600">
|
||||
이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다.
|
||||
이 작업은 되돌릴 수 없으며, 모든 제어 정보가 영구적으로 삭제됩니다.
|
||||
</span>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
|
|
|||
|
|
@ -629,17 +629,17 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
|
|||
console.log("🧹 데이터 저장 타입으로 변경 - 기존 외부호출 설정 정리");
|
||||
try {
|
||||
const { ExternalCallConfigAPI } = await import("@/lib/api/externalCallConfig");
|
||||
|
||||
|
||||
// 기존 외부호출 설정이 있는지 확인하고 삭제 또는 비활성화
|
||||
const existingConfigs = await ExternalCallConfigAPI.getConfigs({
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
|
||||
const existingConfig = existingConfigs.data?.find(
|
||||
(config: any) => config.config_name === (state.relationshipName || "외부호출 설정")
|
||||
(config: any) => config.config_name === (state.relationshipName || "외부호출 설정"),
|
||||
);
|
||||
|
||||
|
||||
if (existingConfig) {
|
||||
console.log("🗑️ 기존 외부호출 설정 비활성화:", existingConfig.id);
|
||||
// 설정을 비활성화 (삭제하지 않고 is_active를 'N'으로 변경)
|
||||
|
|
@ -669,22 +669,22 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
|
|||
};
|
||||
|
||||
let configResult;
|
||||
|
||||
|
||||
if (diagramId) {
|
||||
// 수정 모드: 기존 설정이 있는지 확인하고 업데이트 또는 생성
|
||||
console.log("🔄 수정 모드 - 외부호출 설정 처리");
|
||||
|
||||
|
||||
try {
|
||||
// 먼저 기존 설정 조회 시도
|
||||
const existingConfigs = await ExternalCallConfigAPI.getConfigs({
|
||||
company_code: "*",
|
||||
is_active: "Y",
|
||||
});
|
||||
|
||||
|
||||
const existingConfig = existingConfigs.data?.find(
|
||||
(config: any) => config.config_name === (state.relationshipName || "외부호출 설정")
|
||||
(config: any) => config.config_name === (state.relationshipName || "외부호출 설정"),
|
||||
);
|
||||
|
||||
|
||||
if (existingConfig) {
|
||||
// 기존 설정 업데이트
|
||||
console.log("📝 기존 외부호출 설정 업데이트:", existingConfig.id);
|
||||
|
|
@ -801,12 +801,12 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
|
|||
{/* 메인 컨텐츠 - 좌우 분할 레이아웃 */}
|
||||
<div className="flex h-[calc(100vh-200px)] min-h-[700px] overflow-hidden">
|
||||
{/* 좌측 패널 (30%) - 항상 표시 */}
|
||||
<div className="flex w-[30%] flex-col border-r bg-white">
|
||||
<div className="flex w-[20%] flex-col border-r bg-white">
|
||||
<LeftPanel state={state} actions={actions} />
|
||||
</div>
|
||||
|
||||
{/* 우측 패널 (70%) */}
|
||||
<div className="flex w-[70%] flex-col bg-gray-50">
|
||||
{/* 우측 패널 (80%) */}
|
||||
<div className="flex w-[80%] flex-col bg-gray-50">
|
||||
<RightPanel key={state.connectionType} state={state} actions={actions} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { ArrowLeft, Settings, CheckCircle } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { ArrowLeft, Settings, CheckCircle, Eye } from "lucide-react";
|
||||
|
||||
// 타입 import
|
||||
import { DataConnectionState, DataConnectionActions } from "../types/redesigned";
|
||||
|
|
@ -14,6 +15,7 @@ import { getColumnsFromConnection } from "@/lib/api/multiConnection";
|
|||
|
||||
// 컴포넌트 import
|
||||
import ActionConditionBuilder from "./ActionConfig/ActionConditionBuilder";
|
||||
import { DataflowVisualization } from "./DataflowVisualization";
|
||||
|
||||
interface ActionConfigStepProps {
|
||||
state: DataConnectionState;
|
||||
|
|
@ -78,7 +80,8 @@ const ActionConfigStep: React.FC<ActionConfigStepProps> = ({
|
|||
|
||||
const canComplete =
|
||||
actionType &&
|
||||
(actionType === "insert" || (actionConditions.length > 0 && (actionType === "delete" || fieldMappings.length > 0)));
|
||||
(actionType === "insert" ||
|
||||
((actionConditions || []).length > 0 && (actionType === "delete" || fieldMappings.length > 0)));
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -89,106 +92,137 @@ const ActionConfigStep: React.FC<ActionConfigStepProps> = ({
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="max-h-[calc(100vh-400px)] min-h-[400px] space-y-6 overflow-y-auto">
|
||||
{/* 액션 타입 선택 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold">액션 타입</h3>
|
||||
<Select value={actionType} onValueChange={actions.setActionType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="액션 타입을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{actionTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<span className="font-medium">{type.label}</span>
|
||||
<p className="text-muted-foreground text-xs">{type.description}</p>
|
||||
<CardContent className="flex h-full flex-col overflow-hidden p-0">
|
||||
<Tabs defaultValue="config" className="flex h-full flex-col">
|
||||
<div className="flex-shrink-0 border-b px-4 pt-4">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="config" className="flex items-center gap-2">
|
||||
<Settings className="h-4 w-4" />
|
||||
액션 설정
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="visualization" className="flex items-center gap-2">
|
||||
<Eye className="h-4 w-4" />
|
||||
흐름 미리보기
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
|
||||
{/* 액션 설정 탭 */}
|
||||
<TabsContent value="config" className="mt-0 flex-1 overflow-y-auto p-4">
|
||||
<div className="space-y-6">
|
||||
{/* 액션 타입 선택 */}
|
||||
<div className="space-y-3">
|
||||
<h3 className="text-lg font-semibold">액션 타입</h3>
|
||||
<Select value={actionType} onValueChange={actions.setActionType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="액션 타입을 선택하세요" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{actionTypes.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div>
|
||||
<span className="font-medium">{type.label}</span>
|
||||
<p className="text-muted-foreground text-xs">{type.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{actionType && (
|
||||
<div className="bg-primary/5 border-primary/20 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-primary">
|
||||
{actionTypes.find((t) => t.value === actionType)?.label}
|
||||
</Badge>
|
||||
<span className="text-sm">{actionTypes.find((t) => t.value === actionType)?.description}</span>
|
||||
</div>
|
||||
</div>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{actionType && (
|
||||
<div className="bg-primary/5 border-primary/20 rounded-lg border p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-primary">
|
||||
{actionTypes.find((t) => t.value === actionType)?.label}
|
||||
</Badge>
|
||||
<span className="text-sm">{actionTypes.find((t) => t.value === actionType)?.description}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 상세 조건 설정 */}
|
||||
{actionType && !isLoading && fromColumns.length > 0 && toColumns.length > 0 && (
|
||||
<ActionConditionBuilder
|
||||
actionType={actionType}
|
||||
fromColumns={fromColumns}
|
||||
toColumns={toColumns}
|
||||
conditions={actionConditions}
|
||||
fieldMappings={fieldMappings}
|
||||
onConditionsChange={(conditions) => {
|
||||
// 액션 조건 배열 전체 업데이트
|
||||
actions.setActionConditions(conditions);
|
||||
}}
|
||||
onFieldMappingsChange={setFieldMappings}
|
||||
/>
|
||||
)}
|
||||
{/* 상세 조건 설정 */}
|
||||
{actionType && !isLoading && fromColumns.length > 0 && toColumns.length > 0 && (
|
||||
<ActionConditionBuilder
|
||||
actionType={actionType}
|
||||
fromColumns={fromColumns}
|
||||
toColumns={toColumns}
|
||||
conditions={actionConditions || []}
|
||||
fieldMappings={fieldMappings}
|
||||
onConditionsChange={(conditions) => {
|
||||
// 액션 조건 배열 전체 업데이트
|
||||
actions.setActionConditions(conditions);
|
||||
}}
|
||||
onFieldMappingsChange={setFieldMappings}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 로딩 상태 */}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">컬럼 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
)}
|
||||
{/* 로딩 상태 */}
|
||||
{isLoading && (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="text-muted-foreground">컬럼 정보를 불러오는 중...</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* INSERT 액션 안내 */}
|
||||
{actionType === "insert" && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-green-800">INSERT 액션</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
INSERT 액션은 별도의 실행 조건이 필요하지 않습니다. 매핑된 모든 데이터가 새로운 레코드로 삽입됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{/* INSERT 액션 안내 */}
|
||||
{actionType === "insert" && (
|
||||
<div className="rounded-lg border border-green-200 bg-green-50 p-4">
|
||||
<h4 className="mb-2 text-sm font-medium text-green-800">INSERT 액션</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
INSERT 액션은 별도의 실행 조건이 필요하지 않습니다. 매핑된 모든 데이터가 새로운 레코드로 삽입됩니다.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 액션 요약 */}
|
||||
{actionType && (
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<h4 className="mb-3 text-sm font-medium">설정 요약</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>액션 타입:</span>
|
||||
<Badge variant="outline">{actionType.toUpperCase()}</Badge>
|
||||
</div>
|
||||
{actionType !== "insert" && (
|
||||
<>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>실행 조건:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{actionConditions.length > 0 ? `${actionConditions.length}개 조건` : "조건 없음"}
|
||||
</span>
|
||||
</div>
|
||||
{actionType !== "delete" && (
|
||||
{/* 액션 요약 */}
|
||||
{actionType && (
|
||||
<div className="bg-muted/50 rounded-lg p-4">
|
||||
<h4 className="mb-3 text-sm font-medium">설정 요약</h4>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>필드 매핑:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{fieldMappings.length > 0 ? `${fieldMappings.length}개 필드` : "필드 없음"}
|
||||
</span>
|
||||
<span>액션 타입:</span>
|
||||
<Badge variant="outline">{actionType.toUpperCase()}</Badge>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
{actionType !== "insert" && (
|
||||
<>
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>실행 조건:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{actionConditions.length > 0 ? `${actionConditions.length}개 조건` : "조건 없음"}
|
||||
</span>
|
||||
</div>
|
||||
{actionType !== "delete" && (
|
||||
<div className="flex justify-between text-sm">
|
||||
<span>필드 매핑:</span>
|
||||
<span className="text-muted-foreground">
|
||||
{fieldMappings.length > 0 ? `${fieldMappings.length}개 필드` : "필드 없음"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
{/* 흐름 미리보기 탭 */}
|
||||
<TabsContent value="visualization" className="mt-0 flex-1 overflow-y-auto">
|
||||
<DataflowVisualization
|
||||
state={state}
|
||||
onEdit={(step) => {
|
||||
// 편집 버튼 클릭 시 해당 단계로 이동하는 로직 추가 가능
|
||||
console.log(`편집 요청: ${step}`);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 하단 네비게이션 */}
|
||||
<div className="border-t pt-4">
|
||||
<div className="flex-shrink-0 border-t bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Button variant="outline" onClick={onBack} className="flex items-center gap-2">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,321 @@
|
|||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Database, Filter, Zap, CheckCircle, XCircle, Edit } from "lucide-react";
|
||||
import { DataConnectionState } from "../types/redesigned";
|
||||
|
||||
interface DataflowVisualizationProps {
|
||||
state: Partial<DataConnectionState> & {
|
||||
dataflowActions?: Array<{
|
||||
actionType: string;
|
||||
targetTable?: string;
|
||||
name?: string;
|
||||
fieldMappings?: any[];
|
||||
}>;
|
||||
};
|
||||
onEdit: (step: "source" | "conditions" | "actions") => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* 🎯 Sankey 다이어그램 스타일 데이터 흐름 시각화
|
||||
*/
|
||||
export const DataflowVisualization: React.FC<DataflowVisualizationProps> = ({ state, onEdit }) => {
|
||||
const { fromTable, toTable, controlConditions = [], dataflowActions = [], fromColumns = [], toColumns = [] } = state;
|
||||
|
||||
// 상태 계산
|
||||
const hasSource = !!fromTable;
|
||||
const hasConditions = controlConditions.length > 0;
|
||||
const hasActions = dataflowActions.length > 0;
|
||||
const isComplete = hasSource && hasActions;
|
||||
|
||||
// 필드명을 라벨명으로 변환하는 함수
|
||||
const getFieldLabel = (fieldName: string) => {
|
||||
// fromColumns와 toColumns에서 해당 필드 찾기
|
||||
const allColumns = [...fromColumns, ...toColumns];
|
||||
const column = allColumns.find((col) => col.columnName === fieldName);
|
||||
return column?.displayName || column?.labelKo || fieldName;
|
||||
};
|
||||
|
||||
// 테이블명을 라벨명으로 변환하는 함수
|
||||
const getTableLabel = (tableName: string) => {
|
||||
// fromTable 또는 toTable의 라벨 반환
|
||||
if (fromTable?.tableName === tableName) {
|
||||
return fromTable?.tableLabel || fromTable?.displayName || tableName;
|
||||
}
|
||||
if (toTable?.tableName === tableName) {
|
||||
return toTable?.tableLabel || toTable?.displayName || tableName;
|
||||
}
|
||||
return tableName;
|
||||
};
|
||||
|
||||
// 액션 그룹별로 대표 액션 1개씩만 표시
|
||||
const actionGroups = dataflowActions.reduce(
|
||||
(acc, action, index) => {
|
||||
// 각 액션을 개별 그룹으로 처리 (실제로는 actionGroups에서 온 것)
|
||||
const groupKey = `group_${index}`;
|
||||
acc[groupKey] = [action];
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, typeof dataflowActions>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 p-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-600">전체 데이터 흐름을 한눈에 확인</p>
|
||||
<Badge variant={isComplete ? "default" : "secondary"} className="text-sm">
|
||||
{isComplete ? "✅ 설정 완료" : "⚠️ 설정 필요"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Sankey 다이어그램 */}
|
||||
<div className="relative flex items-center justify-center py-12">
|
||||
{/* 연결선 레이어 */}
|
||||
<svg className="absolute inset-0 h-full w-full" style={{ zIndex: 0 }}>
|
||||
{/* 소스 → 조건 선 */}
|
||||
{hasSource && <line x1="25%" y1="50%" x2="50%" y2="50%" stroke="#60a5fa" strokeWidth="3" />}
|
||||
|
||||
{/* 조건 → 액션들 선 (여러 개) */}
|
||||
{hasConditions &&
|
||||
hasActions &&
|
||||
Object.keys(actionGroups).map((groupKey, index) => {
|
||||
const totalActions = Object.keys(actionGroups).length;
|
||||
const startY = 50; // 조건 노드 중앙
|
||||
// 액션이 여러 개면 위에서 아래로 분산
|
||||
const endY =
|
||||
totalActions > 1
|
||||
? 30 + (index * 40) / (totalActions - 1) // 30%~70% 사이에 분산
|
||||
: 50; // 액션이 1개면 중앙
|
||||
|
||||
return (
|
||||
<line
|
||||
key={groupKey}
|
||||
x1="50%"
|
||||
y1={`${startY}%`}
|
||||
x2="75%"
|
||||
y2={`${endY}%`}
|
||||
stroke="#34d399"
|
||||
strokeWidth="2"
|
||||
opacity="0.7"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
<div className="relative flex w-full items-center justify-around" style={{ zIndex: 1 }}>
|
||||
{/* 1. 소스 노드 */}
|
||||
<div className="flex flex-col items-center" style={{ width: "28%" }}>
|
||||
<Card
|
||||
className={`w-full cursor-pointer border-2 transition-all hover:shadow-lg ${
|
||||
hasSource ? "border-blue-400 bg-blue-50" : "border-gray-300 bg-gray-50"
|
||||
}`}
|
||||
onClick={() => onEdit("source")}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-blue-600" />
|
||||
<span className="text-sm font-semibold text-gray-900">데이터 소스</span>
|
||||
</div>
|
||||
{hasSource ? (
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{fromTable.tableLabel || fromTable.displayName || fromTable.tableName}
|
||||
</p>
|
||||
{(fromTable.tableLabel || fromTable.displayName) && (
|
||||
<p className="text-xs text-gray-500">({fromTable.tableName})</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">미설정</p>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 2. 조건 노드 (중앙) */}
|
||||
<div className="flex flex-col items-center" style={{ width: "28%" }}>
|
||||
<Card
|
||||
className={`w-full cursor-pointer border-2 transition-all hover:shadow-lg ${
|
||||
hasConditions ? "border-yellow-400 bg-yellow-50" : "border-gray-300 bg-gray-50"
|
||||
}`}
|
||||
onClick={() => onEdit("conditions")}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Filter className="h-5 w-5 text-yellow-600" />
|
||||
<span className="text-sm font-semibold text-gray-900">조건 검증</span>
|
||||
</div>
|
||||
{hasConditions ? (
|
||||
<div className="space-y-2">
|
||||
{/* 실제 조건들 표시 */}
|
||||
<div className="max-h-32 space-y-1 overflow-y-auto">
|
||||
{controlConditions.slice(0, 3).map((condition, index) => (
|
||||
<div key={index} className="rounded bg-white/50 px-2 py-1">
|
||||
<p className="text-xs font-medium text-gray-800">
|
||||
{index > 0 && (
|
||||
<span className="mr-1 font-bold text-blue-600">
|
||||
{condition.logicalOperator || "AND"}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-blue-800">{getFieldLabel(condition.field)}</span>{" "}
|
||||
<span className="text-gray-600">{condition.operator}</span>{" "}
|
||||
<span className="text-green-700">{condition.value}</span>
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
{controlConditions.length > 3 && (
|
||||
<p className="px-2 text-xs text-gray-500">외 {controlConditions.length - 3}개...</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center justify-between border-t pt-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<CheckCircle className="h-3 w-3 text-green-600" />
|
||||
<span className="text-xs text-gray-600">충족 → 실행</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<XCircle className="h-3 w-3 text-red-600" />
|
||||
<span className="text-xs text-gray-600">불만족 → 중단</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">조건 없음 (항상 실행)</p>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
|
||||
<Edit className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 3. 액션 노드들 (우측) */}
|
||||
<div className="flex flex-col items-center gap-3" style={{ width: "28%" }}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit("actions")}
|
||||
className="mb-2 flex items-center gap-2 self-end"
|
||||
>
|
||||
<Edit className="h-3 w-3" />
|
||||
<span className="text-xs">전체 편집</span>
|
||||
</Button>
|
||||
|
||||
{hasActions ? (
|
||||
<div className="w-full space-y-3">
|
||||
{Object.entries(actionGroups).map(([groupKey, actions]) => (
|
||||
<ActionFlowCard
|
||||
key={groupKey}
|
||||
type={actions[0].actionType}
|
||||
actions={actions}
|
||||
getTableLabel={getTableLabel}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Card className="w-full border-2 border-dashed border-gray-300 bg-gray-50">
|
||||
<CardContent className="p-4 text-center">
|
||||
<Zap className="mx-auto mb-2 h-6 w-6 text-gray-400" />
|
||||
<p className="text-xs text-gray-500">액션 미설정</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 조건 불만족 시 중단 표시 (하단) */}
|
||||
{hasConditions && (
|
||||
<div
|
||||
className="absolute bottom-0 flex items-center gap-2 rounded-lg border-2 border-red-300 bg-red-50 px-3 py-2"
|
||||
style={{ left: "50%", transform: "translateX(-50%)" }}
|
||||
>
|
||||
<XCircle className="h-4 w-4 text-red-600" />
|
||||
<span className="text-xs font-medium text-red-900">조건 불만족 → 실행 중단</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 통계 요약 */}
|
||||
<Card className="border-gray-200 bg-gradient-to-r from-gray-50 to-slate-50">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-around text-center">
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">소스</p>
|
||||
<p className="text-lg font-bold text-blue-600">{hasSource ? 1 : 0}</p>
|
||||
</div>
|
||||
<div className="h-8 w-px bg-gray-300"></div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">조건</p>
|
||||
<p className="text-lg font-bold text-yellow-600">{controlConditions.length}</p>
|
||||
</div>
|
||||
<div className="h-8 w-px bg-gray-300"></div>
|
||||
<div>
|
||||
<p className="text-xs text-gray-600">액션</p>
|
||||
<p className="text-lg font-bold text-green-600">{dataflowActions.length}</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 액션 플로우 카드 컴포넌트
|
||||
interface ActionFlowCardProps {
|
||||
type: string;
|
||||
actions: Array<{
|
||||
actionType: string;
|
||||
targetTable?: string;
|
||||
name?: string;
|
||||
fieldMappings?: any[];
|
||||
}>;
|
||||
getTableLabel: (tableName: string) => string;
|
||||
}
|
||||
|
||||
const ActionFlowCard: React.FC<ActionFlowCardProps> = ({ type, actions, getTableLabel }) => {
|
||||
const actionColors = {
|
||||
insert: { bg: "bg-blue-50", border: "border-blue-300", text: "text-blue-900", icon: "text-blue-600" },
|
||||
update: { bg: "bg-green-50", border: "border-green-300", text: "text-green-900", icon: "text-green-600" },
|
||||
delete: { bg: "bg-red-50", border: "border-red-300", text: "text-red-900", icon: "text-red-600" },
|
||||
upsert: { bg: "bg-purple-50", border: "border-purple-300", text: "text-purple-900", icon: "text-purple-600" },
|
||||
};
|
||||
|
||||
const colors = actionColors[type as keyof typeof actionColors] || actionColors.insert;
|
||||
const action = actions[0]; // 그룹당 1개만 표시
|
||||
const displayName = action.targetTable ? getTableLabel(action.targetTable) : action.name || "액션";
|
||||
const isTableLabel = action.targetTable && getTableLabel(action.targetTable) !== action.targetTable;
|
||||
|
||||
return (
|
||||
<Card className={`border-2 ${colors.border} ${colors.bg}`}>
|
||||
<CardContent className="p-3">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<Zap className={`h-4 w-4 ${colors.icon}`} />
|
||||
<span className={`text-sm font-semibold ${colors.text}`}>{type.toUpperCase()}</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Database className="h-3 w-3 text-gray-500" />
|
||||
<span className="truncate font-medium text-gray-900">{displayName}</span>
|
||||
</div>
|
||||
{isTableLabel && action.targetTable && (
|
||||
<span className="ml-5 truncate text-xs text-gray-500">({action.targetTable})</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
|
@ -21,6 +21,7 @@ import {
|
|||
Save,
|
||||
Play,
|
||||
AlertTriangle,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
|
|
@ -28,6 +29,9 @@ import { toast } from "sonner";
|
|||
|
||||
// 타입 import
|
||||
import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection";
|
||||
|
||||
// 컴포넌트 import
|
||||
import { DataflowVisualization } from "./DataflowVisualization";
|
||||
import { ActionGroup, SingleAction, FieldMapping } from "../types/redesigned";
|
||||
|
||||
// 컴포넌트 import
|
||||
|
|
@ -104,7 +108,7 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
|||
onLoadColumns,
|
||||
}) => {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set(["group_1"])); // 첫 번째 그룹은 기본 열림
|
||||
const [activeTab, setActiveTab] = useState<"control" | "actions" | "mapping">("control"); // 현재 활성 탭
|
||||
const [activeTab, setActiveTab] = useState<"control" | "actions" | "visualization">("control"); // 현재 활성 탭
|
||||
|
||||
// 컬럼 로딩 상태 확인
|
||||
const isColumnsLoaded = fromColumns.length > 0 && toColumns.length > 0;
|
||||
|
|
@ -163,10 +167,11 @@ 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: "액션 그룹 및 실행 조건" },
|
||||
{ id: "control" as const, label: "제어 조건", description: "전체 제어 실행 조건" },
|
||||
{ id: "actions" as const, label: "액션 설정", description: "액션 그룹 및 실행 조건" },
|
||||
{ id: "visualization" as const, label: "흐름 미리보기", description: "전체 데이터 흐름을 한눈에 확인" },
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
@ -192,27 +197,16 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
|||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
<span>{tab.icon}</span>
|
||||
<span>{tab.label}</span>
|
||||
{tab.id === "actions" && (
|
||||
<Badge variant="outline" className="ml-1 text-xs">
|
||||
{actionGroups.filter((g) => g.isEnabled).length}
|
||||
</Badge>
|
||||
)}
|
||||
{tab.id === "mapping" && hasInsertActions && (
|
||||
<Badge variant="outline" className="ml-1 text-xs">
|
||||
{fieldMappings.length}
|
||||
</Badge>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 탭 설명 */}
|
||||
<div className="bg-muted/30 mb-4 rounded-md p-3">
|
||||
<p className="text-muted-foreground text-sm">{tabs.find((tab) => tab.id === activeTab)?.description}</p>
|
||||
</div>
|
||||
|
||||
{/* 탭별 컨텐츠 */}
|
||||
<div className="min-h-0 flex-1 overflow-y-auto">
|
||||
{activeTab === "control" && (
|
||||
|
|
@ -671,6 +665,36 @@ const MultiActionConfigStep: React.FC<MultiActionConfigStepProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "visualization" && (
|
||||
<DataflowVisualization
|
||||
state={{
|
||||
fromTable,
|
||||
toTable,
|
||||
fromConnection,
|
||||
toConnection,
|
||||
fromColumns,
|
||||
toColumns,
|
||||
controlConditions,
|
||||
dataflowActions: actionGroups.flatMap((group) =>
|
||||
group.actions
|
||||
.filter((action) => action.isEnabled)
|
||||
.map((action) => ({
|
||||
...action,
|
||||
targetTable: toTable?.tableName || "",
|
||||
})),
|
||||
),
|
||||
}}
|
||||
onEdit={(step) => {
|
||||
// 편집 버튼 클릭 시 해당 탭으로 이동
|
||||
if (step === "conditions") {
|
||||
setActiveTab("control");
|
||||
} else if (step === "actions") {
|
||||
setActiveTab("actions");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 하단 네비게이션 */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue