제어관리 데이터 저장기능

This commit is contained in:
kjs
2025-09-26 13:52:32 +09:00
parent 2a4e379dc4
commit 9454e3a81f
17 changed files with 1417 additions and 781 deletions

View File

@@ -7,7 +7,8 @@ import { toast } from "sonner";
import { X, ArrowLeft } from "lucide-react";
// API import
import { saveDataflowRelationship } from "@/lib/api/dataflowSave";
import { saveDataflowRelationship, checkRelationshipNameDuplicate } from "@/lib/api/dataflowSave";
import { getColumnsFromConnection } from "@/lib/api/multiConnection";
// 타입 import
import {
@@ -26,7 +27,6 @@ import { ColumnInfo, Connection, TableInfo } from "@/lib/types/multiConnection";
// 컴포넌트 import
import LeftPanel from "./LeftPanel/LeftPanel";
import RightPanel from "./RightPanel/RightPanel";
import SaveRelationshipDialog from "./SaveRelationshipDialog";
/**
* 🎨 데이터 연결 설정 메인 디자이너
@@ -74,6 +74,7 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
isEnabled: true,
},
],
groupsLogicalOperator: "AND" as "AND" | "OR",
// 기존 호환성 필드들 (deprecated)
actionType: "insert",
@@ -81,11 +82,15 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
actionFieldMappings: [],
isLoading: false,
validationErrors: [],
// 컬럼 정보 초기화
fromColumns: [],
toColumns: [],
...initialData,
}));
// 💾 저장 다이얼로그 상태
const [showSaveDialog, setShowSaveDialog] = useState(false);
// 🔧 수정 모드 감지 (initialData에 diagramId가 있으면 수정 모드)
const diagramId = initialData?.diagramId;
// 🔄 초기 데이터 로드
useEffect(() => {
@@ -96,15 +101,50 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
setState((prev) => ({
...prev,
connectionType: initialData.connectionType || prev.connectionType,
// 🔧 관계 정보 로드
relationshipName: initialData.relationshipName || prev.relationshipName,
description: initialData.description || prev.description,
groupsLogicalOperator: initialData.groupsLogicalOperator || prev.groupsLogicalOperator,
fromConnection: initialData.fromConnection || prev.fromConnection,
toConnection: initialData.toConnection || prev.toConnection,
fromTable: initialData.fromTable || prev.fromTable,
toTable: initialData.toTable || prev.toTable,
actionType: initialData.actionType || prev.actionType,
controlConditions: initialData.controlConditions || prev.controlConditions,
actionConditions: initialData.actionConditions || prev.actionConditions,
fieldMappings: initialData.fieldMappings || prev.fieldMappings,
currentStep: initialData.fromConnection && initialData.toConnection ? 2 : 1, // 연결 정보가 있으면 2단계부터 시작
// 🔧 액션 그룹 데이터 로드 (기존 호환성 포함)
actionGroups:
initialData.actionGroups ||
// 기존 단일 액션 데이터를 그룹으로 변환
(initialData.actionType || initialData.actionConditions
? [
{
id: "group_1",
name: "기본 액션 그룹",
logicalOperator: "AND" as const,
actions: [
{
id: "action_1",
name: "액션 1",
actionType: initialData.actionType || ("insert" as const),
conditions: initialData.actionConditions || [],
fieldMappings: initialData.actionFieldMappings || [],
isEnabled: true,
},
],
isEnabled: true,
},
]
: prev.actionGroups),
// 기존 호환성 필드들
actionType: initialData.actionType || prev.actionType,
actionConditions: initialData.actionConditions || prev.actionConditions,
actionFieldMappings: initialData.actionFieldMappings || prev.actionFieldMappings,
currentStep: initialData.fromConnection && initialData.toConnection ? 4 : 1, // 연결 정보가 있으면 4단계부터 시작
}));
console.log("✅ 초기 데이터 로드 완료");
@@ -130,6 +170,26 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
toast.success(`연결 타입이 ${type === "data_save" ? "데이터 저장" : "외부 호출"}로 변경되었습니다.`);
}, []),
// 🔧 관계 정보 설정
setRelationshipName: useCallback((name: string) => {
setState((prev) => ({
...prev,
relationshipName: name,
}));
}, []),
setDescription: useCallback((description: string) => {
setState((prev) => ({
...prev,
description: description,
}));
}, []),
setGroupsLogicalOperator: useCallback((operator: "AND" | "OR") => {
setState((prev) => ({ ...prev, groupsLogicalOperator: operator }));
console.log("🔄 그룹 간 논리 연산자 변경:", operator);
}, []),
// 단계 이동
goToStep: useCallback((step: 1 | 2 | 3 | 4) => {
setState((prev) => ({ ...prev, currentStep: step }));
@@ -152,21 +212,75 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
setState((prev) => ({
...prev,
[type === "from" ? "fromTable" : "toTable"]: table,
// 테이블 변경 시 매핑 초기화
// 테이블 변경 시 매핑과 컬럼 정보 초기화
fieldMappings: [],
fromColumns: type === "from" ? [] : prev.fromColumns,
toColumns: type === "to" ? [] : prev.toColumns,
}));
toast.success(
`${type === "from" ? "소스" : "대상"} 테이블이 선택되었습니다: ${table.displayName || table.tableName}`,
);
}, []),
// 필드 매핑 생성
// 컬럼 정보 로드 (중앙 관리)
loadColumns: useCallback(async () => {
if (!state.fromConnection || !state.toConnection || !state.fromTable || !state.toTable) {
console.log("❌ 컬럼 로드: 필수 정보 누락");
return;
}
// 이미 로드된 경우 스킵 (배열 길이로 확인)
if (state.fromColumns && state.toColumns && state.fromColumns.length > 0 && state.toColumns.length > 0) {
console.log("✅ 컬럼 정보 이미 로드됨, 스킵", {
fromColumns: state.fromColumns.length,
toColumns: state.toColumns.length,
});
return;
}
console.log("🔄 중앙 컬럼 로드 시작:", {
from: `${state.fromConnection.id}/${state.fromTable.tableName}`,
to: `${state.toConnection.id}/${state.toTable.tableName}`,
});
setState((prev) => ({
...prev,
isLoading: true,
fromColumns: [],
toColumns: [],
}));
try {
const [fromCols, toCols] = await Promise.all([
getColumnsFromConnection(state.fromConnection.id, state.fromTable.tableName),
getColumnsFromConnection(state.toConnection.id, state.toTable.tableName),
]);
console.log("✅ 중앙 컬럼 로드 완료:", {
fromColumns: fromCols.length,
toColumns: toCols.length,
});
setState((prev) => ({
...prev,
fromColumns: Array.isArray(fromCols) ? fromCols : [],
toColumns: Array.isArray(toCols) ? toCols : [],
isLoading: false,
}));
} catch (error) {
console.error("❌ 중앙 컬럼 로드 실패:", error);
setState((prev) => ({ ...prev, isLoading: false }));
toast.error("컬럼 정보를 불러오는데 실패했습니다.");
}
}, [state.fromConnection, state.toConnection, state.fromTable, state.toTable, state.fromColumns, state.toColumns]),
// 필드 매핑 생성 (호환성용 - 실제로는 각 액션에서 직접 관리)
createMapping: useCallback((fromField: ColumnInfo, toField: ColumnInfo) => {
const newMapping: FieldMapping = {
id: `${fromField.columnName}_to_${toField.columnName}_${Date.now()}`,
fromField,
toField,
isValid: true, // 기본적으로 유효하다고 가정, 나중에 검증
isValid: true,
validationMessage: undefined,
};
@@ -175,6 +289,11 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
fieldMappings: [...prev.fieldMappings, newMapping],
}));
console.log("🔗 전역 매핑 생성 (호환성):", {
newMapping,
fieldName: `${fromField.columnName}${toField.columnName}`,
});
toast.success(`매핑이 생성되었습니다: ${fromField.columnName}${toField.columnName}`);
}, []),
@@ -188,12 +307,14 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
}));
}, []),
// 필드 매핑 삭제
// 필드 매핑 삭제 (호환성용 - 실제로는 각 액션에서 직접 관리)
deleteMapping: useCallback((mappingId: string) => {
setState((prev) => ({
...prev,
fieldMappings: prev.fieldMappings.filter((mapping) => mapping.id !== mappingId),
}));
console.log("🗑️ 전역 매핑 삭제 (호환성):", { mappingId });
toast.success("매핑이 삭제되었습니다.");
}, []),
@@ -404,10 +525,73 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
toast.success("액션이 삭제되었습니다.");
}, []),
// 매핑 저장 (다이얼로그 표시)
// 매핑 저장 (직접 저장)
saveMappings: useCallback(async () => {
setShowSaveDialog(true);
}, []),
// 관계명과 설명이 없으면 저장할 수 없음
if (!state.relationshipName?.trim()) {
toast.error("관계 이름을 입력해주세요.");
actions.goToStep(1); // 첫 번째 단계로 이동
return;
}
// 중복 체크 (수정 모드가 아닌 경우에만)
if (!diagramId) {
try {
const duplicateCheck = await checkRelationshipNameDuplicate(state.relationshipName, diagramId);
if (duplicateCheck.isDuplicate) {
toast.error(`"${state.relationshipName}" 이름이 이미 사용 중입니다. 다른 이름을 사용해주세요.`);
actions.goToStep(1); // 첫 번째 단계로 이동
return;
}
} catch (error) {
console.error("중복 체크 실패:", error);
toast.error("관계명 중복 체크 중 오류가 발생했습니다.");
return;
}
}
setState((prev) => ({ ...prev, isLoading: true }));
try {
// 실제 저장 로직 구현
const saveData = {
relationshipName: state.relationshipName,
description: state.description,
connectionType: state.connectionType,
fromConnection: state.fromConnection,
toConnection: state.toConnection,
fromTable: state.fromTable,
toTable: state.toTable,
// 🔧 멀티 액션 그룹 데이터 포함
actionGroups: state.actionGroups,
groupsLogicalOperator: state.groupsLogicalOperator,
// 기존 호환성을 위한 필드들 (첫 번째 액션 그룹의 첫 번째 액션에서 추출)
actionType: state.actionGroups[0]?.actions[0]?.actionType || state.actionType || "insert",
controlConditions: state.controlConditions,
actionConditions: state.actionGroups[0]?.actions[0]?.conditions || state.actionConditions || [],
fieldMappings: state.actionGroups[0]?.actions[0]?.fieldMappings || state.fieldMappings || [],
};
console.log("💾 직접 저장 시작:", { saveData, diagramId, isEdit: !!diagramId });
// 백엔드 API 호출 (수정 모드인 경우 diagramId 전달)
const result = await saveDataflowRelationship(saveData, diagramId);
console.log("✅ 저장 완료:", result);
setState((prev) => ({ ...prev, isLoading: false }));
toast.success(`"${state.relationshipName}" 관계가 성공적으로 저장되었습니다.`);
// 저장 후 닫기
if (onClose) {
onClose();
}
} catch (error: any) {
console.error("❌ 저장 실패:", error);
setState((prev) => ({ ...prev, isLoading: false }));
toast.error(error.message || "저장 중 오류가 발생했습니다.");
}
}, [state, diagramId, onClose]),
// 테스트 실행
testExecution: useCallback(async (): Promise<TestResult> => {
@@ -434,52 +618,6 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
}, []),
};
// 💾 실제 저장 함수
const handleSaveWithName = useCallback(
async (relationshipName: string, description?: string) => {
setState((prev) => ({ ...prev, isLoading: true }));
try {
// 실제 저장 로직 구현
const saveData = {
relationshipName,
description,
connectionType: state.connectionType,
fromConnection: state.fromConnection,
toConnection: state.toConnection,
fromTable: state.fromTable,
toTable: state.toTable,
actionType: state.actionType,
controlConditions: state.controlConditions,
actionConditions: state.actionConditions,
fieldMappings: state.fieldMappings,
};
console.log("💾 저장 시작:", saveData);
// 백엔드 API 호출
const result = await saveDataflowRelationship(saveData);
console.log("✅ 저장 완료:", result);
setState((prev) => ({ ...prev, isLoading: false }));
toast.success(`"${relationshipName}" 관계가 성공적으로 저장되었습니다.`);
// 저장 후 상위 컴포넌트에 알림 (필요한 경우)
if (onClose) {
onClose();
}
} catch (error: any) {
setState((prev) => ({ ...prev, isLoading: false }));
const errorMessage = error.message || "저장 중 오류가 발생했습니다.";
toast.error(errorMessage);
console.error("❌ 저장 오류:", error);
}
},
[state, onClose],
);
return (
<div className="overflow-hidden rounded-lg border bg-white shadow-sm">
{/* 상단 네비게이션 */}
@@ -514,16 +652,6 @@ const DataConnectionDesigner: React.FC<DataConnectionDesignerProps> = ({
<RightPanel state={state} actions={actions} />
</div>
</div>
{/* 💾 저장 다이얼로그 */}
<SaveRelationshipDialog
open={showSaveDialog}
onOpenChange={setShowSaveDialog}
onSave={handleSaveWithName}
actionType={state.actionType}
fromTable={state.fromTable?.tableName}
toTable={state.toTable?.tableName}
/>
</div>
);
};