diff --git a/frontend/app/(main)/admin/dataflow/page.tsx b/frontend/app/(main)/admin/dataflow/page.tsx index 8abc7da4..a5f25a97 100644 --- a/frontend/app/(main)/admin/dataflow/page.tsx +++ b/frontend/app/(main)/admin/dataflow/page.tsx @@ -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 (
-
+
{/* 페이지 제목 */}
-

관계 관리

-

테이블 간 데이터 관계를 시각적으로 설계하고 관리합니다

+

제어 관리

+

테이블 간 데이터 제어를 시각적으로 설계하고 관리합니다

{/* 단계별 내용 */}
- {/* 관계도 목록 단계 */} + {/* 제어 목록 단계 */} {currentStep === "list" && } - {/* 관계도 설계 단계 - 🎨 새로운 UI 사용 */} + {/* 제어 설계 단계 - 🎨 새로운 UI 사용 */} {currentStep === "design" && ( { diff --git a/frontend/components/dataflow/DataFlowList.tsx b/frontend/components/dataflow/DataFlowList.tsx index 349b72ec..5f7906c1 100644 --- a/frontend/components/dataflow/DataFlowList.tsx +++ b/frontend/components/dataflow/DataFlowList.tsx @@ -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) {
setSearchTerm(e.target.value)} className="w-80 pl-10" @@ -189,17 +189,17 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) {
- {/* 관계 목록 테이블 */} + {/* 제어 목록 테이블 */} - 데이터 흐름 관계 ({total}) + 데이터 흐름 제어 ({total}) @@ -207,10 +207,10 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - 관계명 + 제어명 회사 코드 테이블 수 - 관계 수 + 액션 수 최근 수정 작업 @@ -284,8 +284,8 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { {diagrams.length === 0 && (
-
관계가 없습니다
-
새 관계를 생성하여 테이블 간 데이터 관계를 설정해보세요.
+
제어가 없습니다
+
새 제어를 생성하여 테이블 간 데이터 제어를 설정해보세요.
)} @@ -320,11 +320,11 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - 관계 복사 + 제어 복사 - “{selectedDiagramForAction?.diagramName}” 관계를 복사하시겠습니까? + “{selectedDiagramForAction?.diagramName}” 제어를 복사하시겠습니까?
- 새로운 관계는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다. + 새로운 제어는 원본 이름 뒤에 (1), (2), (3)... 형태로 생성됩니다.
@@ -342,12 +342,12 @@ export default function DataFlowList({ onDesignDiagram }: DataFlowListProps) { - 관계 삭제 + 제어 삭제 - “{selectedDiagramForAction?.diagramName}” 관계를 완전히 삭제하시겠습니까? + “{selectedDiagramForAction?.diagramName}” 제어를 완전히 삭제하시겠습니까?
- 이 작업은 되돌릴 수 없으며, 모든 관계 정보가 영구적으로 삭제됩니다. + 이 작업은 되돌릴 수 없으며, 모든 제어 정보가 영구적으로 삭제됩니다.
diff --git a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx index db25934f..8c5514c3 100644 --- a/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx +++ b/frontend/components/dataflow/connection/redesigned/DataConnectionDesigner.tsx @@ -629,17 +629,17 @@ const DataConnectionDesigner: React.FC = ({ 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 = ({ }; 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 = ({ {/* 메인 컨텐츠 - 좌우 분할 레이아웃 */}
{/* 좌측 패널 (30%) - 항상 표시 */} -
+
- {/* 우측 패널 (70%) */} -
+ {/* 우측 패널 (80%) */} +
diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfigStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfigStep.tsx index 31075933..a381b1f9 100644 --- a/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfigStep.tsx +++ b/frontend/components/dataflow/connection/redesigned/RightPanel/ActionConfigStep.tsx @@ -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 = ({ 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 = ({ - - {/* 액션 타입 선택 */} -
-

액션 타입

- + + + + + {actionTypes.map((type) => ( + +
+
+ {type.label} +

{type.description}

+
+
+
+ ))} +
+ + + {actionType && ( +
+
+ + {actionTypes.find((t) => t.value === actionType)?.label} + + {actionTypes.find((t) => t.value === actionType)?.description}
- - ))} - - - - {actionType && ( -
-
- - {actionTypes.find((t) => t.value === actionType)?.label} - - {actionTypes.find((t) => t.value === actionType)?.description} + )}
-
- )} -
- {/* 상세 조건 설정 */} - {actionType && !isLoading && fromColumns.length > 0 && toColumns.length > 0 && ( - { - // 액션 조건 배열 전체 업데이트 - actions.setActionConditions(conditions); - }} - onFieldMappingsChange={setFieldMappings} - /> - )} + {/* 상세 조건 설정 */} + {actionType && !isLoading && fromColumns.length > 0 && toColumns.length > 0 && ( + { + // 액션 조건 배열 전체 업데이트 + actions.setActionConditions(conditions); + }} + onFieldMappingsChange={setFieldMappings} + /> + )} - {/* 로딩 상태 */} - {isLoading && ( -
-
컬럼 정보를 불러오는 중...
-
- )} + {/* 로딩 상태 */} + {isLoading && ( +
+
컬럼 정보를 불러오는 중...
+
+ )} - {/* INSERT 액션 안내 */} - {actionType === "insert" && ( -
-

INSERT 액션

-

- INSERT 액션은 별도의 실행 조건이 필요하지 않습니다. 매핑된 모든 데이터가 새로운 레코드로 삽입됩니다. -

-
- )} + {/* INSERT 액션 안내 */} + {actionType === "insert" && ( +
+

INSERT 액션

+

+ INSERT 액션은 별도의 실행 조건이 필요하지 않습니다. 매핑된 모든 데이터가 새로운 레코드로 삽입됩니다. +

+
+ )} - {/* 액션 요약 */} - {actionType && ( -
-

설정 요약

-
-
- 액션 타입: - {actionType.toUpperCase()} -
- {actionType !== "insert" && ( - <> -
- 실행 조건: - - {actionConditions.length > 0 ? `${actionConditions.length}개 조건` : "조건 없음"} - -
- {actionType !== "delete" && ( + {/* 액션 요약 */} + {actionType && ( +
+

설정 요약

+
- 필드 매핑: - - {fieldMappings.length > 0 ? `${fieldMappings.length}개 필드` : "필드 없음"} - + 액션 타입: + {actionType.toUpperCase()}
- )} - + {actionType !== "insert" && ( + <> +
+ 실행 조건: + + {actionConditions.length > 0 ? `${actionConditions.length}개 조건` : "조건 없음"} + +
+ {actionType !== "delete" && ( +
+ 필드 매핑: + + {fieldMappings.length > 0 ? `${fieldMappings.length}개 필드` : "필드 없음"} + +
+ )} + + )} +
+
)}
-
- )} + + + {/* 흐름 미리보기 탭 */} + + { + // 편집 버튼 클릭 시 해당 단계로 이동하는 로직 추가 가능 + console.log(`편집 요청: ${step}`); + }} + /> + + {/* 하단 네비게이션 */} -
+
+
+ + +
+ + {/* 2. 조건 노드 (중앙) */} +
+ onEdit("conditions")} + > + +
+
+
+ + 조건 검증 +
+ {hasConditions ? ( +
+ {/* 실제 조건들 표시 */} +
+ {controlConditions.slice(0, 3).map((condition, index) => ( +
+

+ {index > 0 && ( + + {condition.logicalOperator || "AND"} + + )} + {getFieldLabel(condition.field)}{" "} + {condition.operator}{" "} + {condition.value} +

+
+ ))} + {controlConditions.length > 3 && ( +

외 {controlConditions.length - 3}개...

+ )} +
+
+
+ + 충족 → 실행 +
+
+ + 불만족 → 중단 +
+
+
+ ) : ( +

조건 없음 (항상 실행)

+ )} +
+ +
+
+
+
+ + {/* 3. 액션 노드들 (우측) */} +
+ + + {hasActions ? ( +
+ {Object.entries(actionGroups).map(([groupKey, actions]) => ( + + ))} +
+ ) : ( + + + +

액션 미설정

+
+
+ )} +
+
+ + {/* 조건 불만족 시 중단 표시 (하단) */} + {hasConditions && ( +
+ + 조건 불만족 → 실행 중단 +
+ )} +
+ + {/* 통계 요약 */} + + +
+
+

소스

+

{hasSource ? 1 : 0}

+
+
+
+

조건

+

{controlConditions.length}

+
+
+
+

액션

+

{dataflowActions.length}

+
+
+
+
+
+ ); +}; + +// 액션 플로우 카드 컴포넌트 +interface ActionFlowCardProps { + type: string; + actions: Array<{ + actionType: string; + targetTable?: string; + name?: string; + fieldMappings?: any[]; + }>; + getTableLabel: (tableName: string) => string; +} + +const ActionFlowCard: React.FC = ({ 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 ( + + +
+ + {type.toUpperCase()} +
+
+
+ + {displayName} +
+ {isTableLabel && action.targetTable && ( + ({action.targetTable}) + )} +
+
+
+ ); +}; diff --git a/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx b/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx index 5adb54cb..6d6fb6a0 100644 --- a/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx +++ b/frontend/components/dataflow/connection/redesigned/RightPanel/MultiActionConfigStep.tsx @@ -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 = ({ onLoadColumns, }) => { const [expandedGroups, setExpandedGroups] = useState>(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 = ({ 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 = ({ : "text-muted-foreground hover:text-foreground" }`} > - {tab.icon} {tab.label} {tab.id === "actions" && ( {actionGroups.filter((g) => g.isEnabled).length} )} - {tab.id === "mapping" && hasInsertActions && ( - - {fieldMappings.length} - - )} ))} - {/* 탭 설명 */} -
-

{tabs.find((tab) => tab.id === activeTab)?.description}

-
- {/* 탭별 컨텐츠 */}
{activeTab === "control" && ( @@ -671,6 +665,36 @@ const MultiActionConfigStep: React.FC = ({
)} + + {activeTab === "visualization" && ( + + 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"); + } + }} + /> + )} {/* 하단 네비게이션 */}