ERP-node/제어관리_외부커넥션_통합_개선_계획서.md

1492 lines
43 KiB
Markdown

# 🔧 제어관리 외부 커넥션 통합 개선 계획서
## 📋 프로젝트 개요
### 목적
현재 외부 커넥션 관리에서 관리되고 있는 데이터베이스 커넥션 정보를 제어관리의 데이터 저장 액션에서 활용할 수 있도록 통합하여, 사용자가 다양한 외부 데이터베이스에 데이터를 저장할 수 있는 기능을 구현합니다.
### 현재 상황 분석
#### 기존 외부 커넥션 관리
- **테이블**: `external_db_connections`
- **지원 DB**: MySQL, PostgreSQL, Oracle, SQL Server, SQLite, MariaDB
- **관리 기능**: 연결 정보 CRUD, 연결 테스트, 암호화 저장
- **API**: `/api/external-db-connections/*` 엔드포인트
#### 기존 제어관리 시스템
- **연결 종류**: 현재 "데이터 저장" 타입 지원
- **액션 타입**: INSERT, UPDATE, DELETE
- **매핑**: FROM 테이블 → TO 테이블 컬럼 매핑
- **제약**: 현재는 메인 데이터베이스 내에서만 동작
### 변경 요구사항
1. **커넥션 선택 기능 추가**
- INSERT 액션 타입 선택 시 커넥션 선택 단계 추가
- FROM/TO 테이블 각각에 대해 독립적인 커넥션 설정
2. **테이블 선택 기능 개선**
- 선택한 커넥션에 있는 테이블 목록 동적 로딩
- FROM 커넥션의 테이블과 TO 커넥션의 테이블 독립 선택
3. **컬럼 매핑 규칙 유지**
- FROM 테이블의 1개 컬럼 → TO 테이블의 2개 이상 컬럼 매핑 가능
- FROM 테이블의 2개 이상 컬럼 → TO 테이블의 1개 컬럼 매핑 **불가**
- 기존 UI 구조 최대한 유지
## 🏗️ 시스템 아키텍처 설계
### 1. 데이터 구조 확장
#### 기존 DataSaveSettings 구조
```typescript
interface DataSaveSettings {
connectionType: "data-save";
actions: Array<{
actionType: "insert" | "update" | "delete";
targetTable: string;
fieldMappings: FieldMapping[];
}>;
}
```
#### 개선된 DataSaveSettings 구조
```typescript
interface EnhancedDataSaveSettings {
connectionType: "data-save";
actions: Array<{
actionType: "insert" | "update" | "delete";
// 🆕 커넥션 정보 추가
fromConnection?: {
connectionId?: number;
connectionName?: string;
dbType?: string;
};
toConnection?: {
connectionId?: number;
connectionName?: string;
dbType?: string;
};
// 기존 필드들
targetTable: string;
fromTable?: string; // 🆕 명시적으로 추가
fieldMappings: EnhancedFieldMapping[];
}>;
}
interface EnhancedFieldMapping {
sourceTable: string;
sourceField: string;
targetTable: string;
targetField: string;
defaultValue?: string;
transformFunction?: string;
// 🆕 커넥션 정보 추가
sourceConnectionId?: number;
targetConnectionId?: number;
}
```
### 2. UI 컴포넌트 구조 개선
#### 단계별 설정 플로우
```
1. 액션 타입 선택 (INSERT/UPDATE/DELETE)
2. [모든 액션 타입] 커넥션 설정 단계
├─ FROM 커넥션 선택 (데이터 소스)
└─ TO 커넥션 선택 (데이터 대상)
3. 테이블 선택 단계
├─ FROM 테이블 선택 (선택한 FROM 커넥션의 테이블들)
└─ TO 테이블 선택 (선택한 TO 커넥션의 테이블들)
4. 컬럼 매핑 단계 (액션 타입별 UI)
├─ INSERT: InsertFieldMappingPanel
├─ UPDATE: UpdateFieldMappingPanel
└─ DELETE: DeleteConditionPanel
```
#### 새로운 컴포넌트 구조
```typescript
// 1. 커넥션 선택 컴포넌트 (신규)
interface ConnectionSelectionPanelProps {
fromConnectionId?: number;
toConnectionId?: number;
onFromConnectionChange: (connectionId: number) => void;
onToConnectionChange: (connectionId: number) => void;
availableConnections: ExternalDbConnection[];
actionType: "insert" | "update" | "delete";
// 🆕 자기 자신 테이블 작업 지원
allowSameConnection?: boolean;
currentConnectionId?: number; // 현재 메인 DB 커넥션
}
// 2. 테이블 선택 컴포넌트 (확장)
interface TableSelectionPanelProps {
fromConnectionId?: number;
toConnectionId?: number;
selectedFromTable?: string;
selectedToTable?: string;
onFromTableChange: (tableName: string) => void;
onToTableChange: (tableName: string) => void;
actionType: "insert" | "update" | "delete";
// 🆕 자기 자신 테이블 작업 지원
allowSameTable?: boolean;
showSameTableWarning?: boolean;
}
// 3. 액션 타입별 매핑 컴포넌트 (확장)
interface InsertFieldMappingPanelProps {
// INSERT: FROM → TO 매핑
}
interface UpdateFieldMappingPanelProps {
// UPDATE: FROM 조건 + TO 업데이트 필드
fromTableColumns: ColumnInfo[];
toTableColumns: ColumnInfo[];
updateConditions: UpdateCondition[];
updateFields: UpdateFieldMapping[];
onConditionsChange: (conditions: UpdateCondition[]) => void;
onFieldsChange: (fields: UpdateFieldMapping[]) => void;
}
interface DeleteConditionPanelProps {
// DELETE: FROM 조건 + TO 삭제 조건
fromTableColumns: ColumnInfo[];
toTableColumns: ColumnInfo[];
deleteConditions: DeleteCondition[];
onConditionsChange: (conditions: DeleteCondition[]) => void;
}
```
## 🔧 구현 세부 계획
### Phase 1: 백엔드 인프라 구축 (2주)
#### 1.1 외부 커넥션 조회 API 확장
```typescript
// 기존 API 확장
GET / api / external - db - connections / active;
// 응답: 활성화된 모든 커넥션 목록
GET / api / external - db - connections / { connectionId } / tables;
// 응답: 특정 커넥션의 테이블 목록
GET / api / external -
db -
connections / { connectionId } / tables / { tableName } / columns;
// 응답: 특정 테이블의 컬럼 정보
```
#### 1.2 다중 커넥션 쿼리 실행 서비스
```typescript
export class MultiConnectionQueryService {
// 소스 커넥션에서 데이터 조회
async fetchDataFromConnection(
connectionId: number,
tableName: string,
conditions?: Record<string, any>
): Promise<Record<string, any>[]>;
// 대상 커넥션에 데이터 삽입
async insertDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>
): Promise<any>;
// 🆕 대상 커넥션에 데이터 업데이트
async updateDataToConnection(
connectionId: number,
tableName: string,
data: Record<string, any>,
conditions: Record<string, any>
): Promise<any>;
// 🆕 대상 커넥션에서 데이터 삭제
async deleteDataFromConnection(
connectionId: number,
tableName: string,
conditions: Record<string, any>
): Promise<any>;
// 커넥션별 테이블 목록 조회
async getTablesFromConnection(connectionId: number): Promise<TableInfo[]>;
// 커넥션별 컬럼 정보 조회
async getColumnsFromConnection(
connectionId: number,
tableName: string
): Promise<ColumnInfo[]>;
// 🆕 자기 자신 테이블 작업 전용 메서드들
async validateSelfTableOperation(
tableName: string,
operation: "update" | "delete",
conditions: any[]
): Promise<ValidationResult>;
// 🆕 메인 DB 작업 (connectionId = 0인 경우)
async executeOnMainDatabase(
operation: "select" | "insert" | "update" | "delete",
tableName: string,
data?: Record<string, any>,
conditions?: Record<string, any>
): Promise<any>;
}
```
#### 1.3 제어관리 서비스 확장
```typescript
export class EnhancedDataflowControlService {
// 기존 메서드 확장
async executeDataflowControl(
diagramId: number,
relationshipId: string,
triggerType: "insert" | "update" | "delete",
sourceData: Record<string, any>,
tableName: string,
// 🆕 추가 매개변수
sourceConnectionId?: number,
targetConnectionId?: number
): Promise<{
success: boolean;
message: string;
executedActions?: any[];
errors?: string[];
}>;
// 🆕 다중 커넥션 INSERT 실행
private async executeMultiConnectionInsert(
action: ControlAction,
sourceData: Record<string, any>,
sourceConnectionId?: number,
targetConnectionId?: number
): Promise<any>;
// 🆕 다중 커넥션 UPDATE 실행
private async executeMultiConnectionUpdate(
action: ControlAction,
sourceData: Record<string, any>,
sourceConnectionId?: number,
targetConnectionId?: number
): Promise<any>;
// 🆕 다중 커넥션 DELETE 실행
private async executeMultiConnectionDelete(
action: ControlAction,
sourceData: Record<string, any>,
sourceConnectionId?: number,
targetConnectionId?: number
): Promise<any>;
}
```
### Phase 2: 프론트엔드 UI 개선 (3주)
#### 2.1 ConnectionSelectionPanel 컴포넌트 개발
```typescript
export const ConnectionSelectionPanel: React.FC<
ConnectionSelectionPanelProps
> = ({
fromConnectionId,
toConnectionId,
onFromConnectionChange,
onToConnectionChange,
availableConnections,
actionType,
}) => {
const getConnectionLabels = () => {
switch (actionType) {
case "insert":
return {
from: {
title: "소스 데이터베이스 연결",
desc: "데이터를 가져올 데이터베이스 연결을 선택하세요",
},
to: {
title: "대상 데이터베이스 연결",
desc: "데이터를 저장할 데이터베이스 연결을 선택하세요",
},
};
case "update":
return {
from: {
title: "조건 확인 데이터베이스",
desc: "업데이트 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
},
to: {
title: "업데이트 대상 데이터베이스",
desc: "데이터를 업데이트할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
},
};
case "delete":
return {
from: {
title: "조건 확인 데이터베이스",
desc: "삭제 조건을 확인할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
},
to: {
title: "삭제 대상 데이터베이스",
desc: "데이터를 삭제할 데이터베이스 연결을 선택하세요 (자기 자신 가능)",
},
};
}
};
// 🆕 자기 자신 테이블 작업 시 경고 메시지
const getSameConnectionWarning = () => {
if (fromConnectionId === toConnectionId && fromConnectionId) {
switch (actionType) {
case "update":
return "⚠️ 같은 데이터베이스에서 UPDATE 작업을 수행합니다. 조건을 신중히 설정하세요.";
case "delete":
return "🚨 같은 데이터베이스에서 DELETE 작업을 수행합니다. 데이터 손실에 주의하세요.";
}
}
return null;
};
const labels = getConnectionLabels();
const warningMessage = getSameConnectionWarning();
return (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-6">
{/* FROM 커넥션 선택 */}
<Card>
<CardHeader>
<CardTitle>{labels.from.title}</CardTitle>
<CardDescription>{labels.from.desc}</CardDescription>
</CardHeader>
<CardContent>
<Select
value={fromConnectionId?.toString() || ""}
onValueChange={(value) => onFromConnectionChange(parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="커넥션을 선택하세요" />
</SelectTrigger>
<SelectContent>
{/* 🆕 현재 메인 DB도 선택 가능 */}
<SelectItem value="0">
<div className="flex items-center gap-2">
<Badge variant="default">현재 DB</Badge>
<span>메인 데이터베이스 (현재 시스템)</span>
</div>
</SelectItem>
{availableConnections.map((conn) => (
<SelectItem key={conn.id} value={conn.id!.toString()}>
<div className="flex items-center gap-2">
<Badge variant="outline">{conn.db_type}</Badge>
<span>{conn.connection_name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</CardContent>
</Card>
{/* TO 커넥션 선택 */}
<Card>
<CardHeader>
<CardTitle>{labels.to.title}</CardTitle>
<CardDescription>{labels.to.desc}</CardDescription>
</CardHeader>
<CardContent>
<Select
value={toConnectionId?.toString() || ""}
onValueChange={(value) => onToConnectionChange(parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="커넥션을 선택하세요" />
</SelectTrigger>
<SelectContent>
{/* 🆕 현재 메인 DB도 선택 가능 */}
<SelectItem value="0">
<div className="flex items-center gap-2">
<Badge variant="default">현재 DB</Badge>
<span>메인 데이터베이스 (현재 시스템)</span>
</div>
</SelectItem>
{availableConnections.map((conn) => (
<SelectItem key={conn.id} value={conn.id!.toString()}>
<div className="flex items-center gap-2">
<Badge variant="outline">{conn.db_type}</Badge>
<span>{conn.connection_name}</span>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
</CardContent>
</Card>
</div>
{/* 🆕 자기 자신 테이블 작업 시 경고 */}
{warningMessage && (
<Alert variant={actionType === "delete" ? "destructive" : "default"}>
<AlertCircle className="h-4 w-4" />
<AlertTitle>주의사항</AlertTitle>
<AlertDescription>{warningMessage}</AlertDescription>
</Alert>
)}
</div>
);
};
```
#### 2.2 TableSelectionPanel 컴포넌트 확장
```typescript
export const TableSelectionPanel: React.FC<TableSelectionPanelProps> = ({
fromConnectionId,
toConnectionId,
selectedFromTable,
selectedToTable,
onFromTableChange,
onToTableChange,
}) => {
const [fromTables, setFromTables] = useState<TableInfo[]>([]);
const [toTables, setToTables] = useState<TableInfo[]>([]);
const [loading, setLoading] = useState(false);
// 커넥션 변경 시 테이블 목록 로딩
useEffect(() => {
if (fromConnectionId) {
loadTablesFromConnection(fromConnectionId, setFromTables);
}
}, [fromConnectionId]);
useEffect(() => {
if (toConnectionId) {
loadTablesFromConnection(toConnectionId, setToTables);
}
}, [toConnectionId]);
return (
<div className="grid grid-cols-2 gap-6">
{/* FROM 테이블 선택 */}
<TableSelector
title="소스 테이블"
tables={fromTables}
selectedTable={selectedFromTable}
onTableChange={onFromTableChange}
connectionId={fromConnectionId}
disabled={!fromConnectionId}
/>
{/* TO 테이블 선택 */}
<TableSelector
title="대상 테이블"
tables={toTables}
selectedTable={selectedToTable}
onTableChange={onToTableChange}
connectionId={toConnectionId}
disabled={!toConnectionId}
/>
</div>
);
};
```
#### 2.3 InsertFieldMappingPanel 확장
```typescript
// 기존 컴포넌트에 커넥션 정보 추가
interface EnhancedInsertFieldMappingPanelProps
extends InsertFieldMappingPanelProps {
fromConnectionId?: number;
toConnectionId?: number;
fromConnectionName?: string;
toConnectionName?: string;
}
// 컬럼 로딩 로직 수정
useEffect(() => {
if (fromConnectionId && fromTableName) {
loadColumnsFromConnection(fromConnectionId, fromTableName).then(
setFromTableColumns
);
}
}, [fromConnectionId, fromTableName]);
useEffect(() => {
if (toConnectionId && toTableName) {
loadColumnsFromConnection(toConnectionId, toTableName).then(
setToTableColumns
);
}
}, [toConnectionId, toTableName]);
```
### Phase 3: 통합 및 테스트 (1주)
#### 3.1 ActionFieldMappings 컴포넌트 통합
```typescript
export const ActionFieldMappings: React.FC<ActionFieldMappingsProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
// ... 기존 props
}) => {
const renderActionSpecificUI = () => {
// 공통 단계: 커넥션 선택과 테이블 선택
const commonSteps = (
<>
{/* 1단계: 커넥션 선택 */}
<ConnectionSelectionPanel
fromConnectionId={action.fromConnection?.connectionId}
toConnectionId={action.toConnection?.connectionId}
onFromConnectionChange={handleFromConnectionChange}
onToConnectionChange={handleToConnectionChange}
availableConnections={availableConnections}
actionType={action.actionType}
/>
{/* 2단계: 테이블 선택 */}
{hasConnectionsSelected && (
<TableSelectionPanel
fromConnectionId={action.fromConnection?.connectionId}
toConnectionId={action.toConnection?.connectionId}
selectedFromTable={action.fromTable}
selectedToTable={action.targetTable}
onFromTableChange={handleFromTableChange}
onToTableChange={handleToTableChange}
actionType={action.actionType}
/>
)}
</>
);
// 3단계: 액션 타입별 매핑/조건 설정
let specificPanel = null;
if (hasTablesSelected) {
switch (action.actionType) {
case "insert":
specificPanel = (
<InsertFieldMappingPanel
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
toTableColumns={toTableColumns}
fromTableName={action.fromTable}
toTableName={action.targetTable}
fromConnectionId={action.fromConnection?.connectionId}
toConnectionId={action.toConnection?.connectionId}
fromConnectionName={action.fromConnection?.connectionName}
toConnectionName={action.toConnection?.connectionName}
/>
);
break;
case "update":
specificPanel = (
<UpdateFieldMappingPanel
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
toTableColumns={toTableColumns}
fromConnectionId={action.fromConnection?.connectionId}
toConnectionId={action.toConnection?.connectionId}
/>
);
break;
case "delete":
specificPanel = (
<DeleteConditionPanel
action={action}
actionIndex={actionIndex}
settings={settings}
onSettingsChange={onSettingsChange}
fromTableColumns={fromTableColumns}
toTableColumns={toTableColumns}
fromConnectionId={action.fromConnection?.connectionId}
toConnectionId={action.toConnection?.connectionId}
/>
);
break;
}
}
return (
<div className="space-y-6">
{commonSteps}
{specificPanel}
</div>
);
};
return renderActionSpecificUI();
};
```
## 🔄 액션 타입별 상세 구현
### 1. UPDATE 액션 구현
#### UpdateFieldMappingPanel 컴포넌트
```typescript
export const UpdateFieldMappingPanel: React.FC<
UpdateFieldMappingPanelProps
> = ({
action,
actionIndex,
settings,
onSettingsChange,
fromTableColumns,
toTableColumns,
fromConnectionId,
toConnectionId,
}) => {
const [updateConditions, setUpdateConditions] = useState<UpdateCondition[]>(
[]
);
const [updateFields, setUpdateFields] = useState<UpdateFieldMapping[]>([]);
return (
<div className="space-y-6">
{/* UPDATE 조건 설정 */}
<Card>
<CardHeader>
<CardTitle>🔍 업데이트 조건 설정</CardTitle>
<CardDescription>
FROM 테이블에서 어떤 조건을 만족하는 데이터가 있을 TO 테이블을
업데이트할지 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<UpdateConditionBuilder
fromTableColumns={fromTableColumns}
conditions={updateConditions}
onConditionsChange={setUpdateConditions}
/>
</CardContent>
</Card>
{/* UPDATE 필드 매핑 */}
<Card>
<CardHeader>
<CardTitle>📝 업데이트 필드 매핑</CardTitle>
<CardDescription>
FROM 테이블의 값을 TO 테이블의 어떤 필드에 업데이트할지 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<UpdateFieldMapper
fromTableColumns={fromTableColumns}
toTableColumns={toTableColumns}
fieldMappings={updateFields}
onFieldMappingsChange={setUpdateFields}
/>
</CardContent>
</Card>
{/* WHERE 조건 설정 */}
<Card>
<CardHeader>
<CardTitle>🎯 업데이트 대상 조건</CardTitle>
<CardDescription>
TO 테이블에서 어떤 레코드를 업데이트할지 WHERE 조건을 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<WhereConditionBuilder
toTableColumns={toTableColumns}
fromTableColumns={fromTableColumns}
onConditionsChange={handleWhereConditionsChange}
/>
</CardContent>
</Card>
</div>
);
};
```
#### UPDATE 데이터 타입 정의
```typescript
interface UpdateCondition {
id: string;
fromColumn: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
value: string | string[];
logicalOperator?: "AND" | "OR";
}
interface UpdateFieldMapping {
id: string;
fromColumn: string;
toColumn: string;
transformFunction?: string;
defaultValue?: string;
}
interface WhereCondition {
id: string;
toColumn: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
valueSource: "from_column" | "static" | "current_timestamp";
fromColumn?: string; // valueSource가 "from_column"인 경우
staticValue?: string; // valueSource가 "static"인 경우
logicalOperator?: "AND" | "OR";
}
```
### 2. DELETE 액션 구현
#### DeleteConditionPanel 컴포넌트
```typescript
export const DeleteConditionPanel: React.FC<DeleteConditionPanelProps> = ({
action,
actionIndex,
settings,
onSettingsChange,
fromTableColumns,
toTableColumns,
fromConnectionId,
toConnectionId,
}) => {
const [deleteConditions, setDeleteConditions] = useState<DeleteCondition[]>(
[]
);
const [whereConditions, setWhereConditions] = useState<WhereCondition[]>([]);
return (
<div className="space-y-6">
{/* DELETE 트리거 조건 설정 */}
<Card>
<CardHeader>
<CardTitle>🔥 삭제 트리거 조건</CardTitle>
<CardDescription>
FROM 테이블에서 어떤 조건을 만족하는 데이터가 있을 TO 테이블에서
삭제를 실행할지 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<DeleteTriggerConditionBuilder
fromTableColumns={fromTableColumns}
conditions={deleteConditions}
onConditionsChange={setDeleteConditions}
/>
</CardContent>
</Card>
{/* DELETE WHERE 조건 설정 */}
<Card>
<CardHeader>
<CardTitle>🎯 삭제 대상 조건</CardTitle>
<CardDescription>
TO 테이블에서 어떤 레코드를 삭제할지 WHERE 조건을 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<DeleteWhereConditionBuilder
toTableColumns={toTableColumns}
fromTableColumns={fromTableColumns}
conditions={whereConditions}
onConditionsChange={setWhereConditions}
/>
</CardContent>
</Card>
{/* 안전장치 설정 */}
<Card>
<CardHeader>
<CardTitle>🛡️ 삭제 안전장치</CardTitle>
<CardDescription>
예상치 못한 대량 삭제를 방지하기 위한 안전장치를 설정하세요
</CardDescription>
</CardHeader>
<CardContent>
<DeleteSafetySettings
maxDeleteCount={action.maxDeleteCount || 100}
requireConfirmation={action.requireConfirmation || true}
onSettingsChange={handleSafetySettingsChange}
/>
</CardContent>
</Card>
</div>
);
};
```
#### DELETE 데이터 타입 정의
```typescript
interface DeleteCondition {
id: string;
fromColumn: string;
operator:
| "="
| "!="
| ">"
| "<"
| ">="
| "<="
| "LIKE"
| "IN"
| "NOT IN"
| "EXISTS"
| "NOT EXISTS";
value: string | string[];
logicalOperator?: "AND" | "OR";
}
interface DeleteWhereCondition {
id: string;
toColumn: string;
operator: "=" | "!=" | ">" | "<" | ">=" | "<=" | "LIKE" | "IN" | "NOT IN";
valueSource: "from_column" | "static" | "condition_result";
fromColumn?: string;
staticValue?: string;
logicalOperator?: "AND" | "OR";
}
interface DeleteSafetySettings {
maxDeleteCount: number;
requireConfirmation: boolean;
dryRunFirst: boolean;
logAllDeletes: boolean;
}
```
## 🔒 매핑 규칙 구현
### 1. INSERT: FROM → TO 컬럼 매핑 제약사항
#### 허용되는 매핑 (기존과 동일)
```typescript
// ✅ 1:1 매핑
FROM.column1 TO.column1
// ✅ 1:N 매핑 (하나의 FROM 컬럼이 여러 TO 컬럼에 매핑)
FROM.column1 TO.column1
FROM.column1 TO.column2
FROM.column1 TO.column3
```
#### 금지되는 매핑 (신규 검증 로직)
```typescript
// ❌ N:1 매핑 (여러 FROM 컬럼이 하나의 TO 컬럼에 매핑)
FROM.column1 TO.column1
FROM.column2 TO.column1 // 이미 매핑된 TO.column1에 추가 매핑 시도
```
### 2. UPDATE: 조건 및 필드 매핑 제약사항
#### 허용되는 UPDATE 패턴
```typescript
// ✅ 조건부 업데이트
IF (FROM.status = 'completed')
THEN UPDATE TO.table SET status = FROM.new_status WHERE TO.id = FROM.ref_id
// ✅ 다중 필드 업데이트
UPDATE TO.table SET
column1 = FROM.value1,
column2 = FROM.value2,
updated_at = CURRENT_TIMESTAMP
WHERE TO.id = FROM.ref_id
// ✅ 조건부 필드 매핑
IF (FROM.priority > 5) THEN TO.urgent_flag = 'Y'
ELSE TO.urgent_flag = 'N'
```
#### UPDATE 제약사항
```typescript
// ❌ WHERE 조건 없는 전체 테이블 업데이트 (안전장치)
// ❌ PRIMARY KEY 컬럼 업데이트
// ⚠️ 자기 자신 테이블 업데이트 (허용하되 특별한 주의사항)
// 🆕 자기 자신 테이블 UPDATE 시 안전장치
const validateSelfTableUpdate = (
fromTable: string,
toTable: string,
updateConditions: UpdateCondition[],
whereConditions: WhereCondition[]
): ValidationResult => {
if (fromTable === toTable) {
// 1. WHERE 조건 필수
if (!whereConditions.length) {
return {
isValid: false,
error: "자기 자신 테이블 업데이트 시 WHERE 조건이 필수입니다.",
};
}
// 2. 업데이트 조건과 WHERE 조건이 겹치지 않도록 체크
const conditionColumns = updateConditions.map((c) => c.fromColumn);
const whereColumns = whereConditions.map((c) => c.toColumn);
const overlap = conditionColumns.filter((col) =>
whereColumns.includes(col)
);
if (overlap.length > 0) {
return {
isValid: false,
error: `업데이트 조건과 WHERE 조건에서 같은 컬럼(${overlap.join(
", "
)})을 사용하면 예상치 못한 결과가 발생할 수 있습니다.`,
};
}
// 3. 무한 루프 방지 체크
const hasInfiniteLoopRisk = updateConditions.some((condition) =>
whereConditions.some(
(where) =>
where.fromColumn === condition.toColumn &&
where.toColumn === condition.fromColumn
)
);
if (hasInfiniteLoopRisk) {
return {
isValid: false,
error: "자기 참조 업데이트로 인한 무한 루프 위험이 있습니다.",
};
}
}
return { isValid: true };
};
```
### 3. DELETE: 조건 및 안전장치 제약사항
#### 허용되는 DELETE 패턴
```typescript
// ✅ 조건부 삭제
IF (FROM.is_expired = 'Y')
THEN DELETE FROM TO.table WHERE TO.ref_id = FROM.id
// ✅ 관련 데이터 정리
IF (FROM.status = 'cancelled')
THEN DELETE FROM TO.order_items WHERE TO.order_id = FROM.order_id
// ✅ 카스케이드 삭제 시뮬레이션
DELETE FROM TO.child_table WHERE TO.parent_id = FROM.deleted_id
```
#### DELETE 제약사항 및 안전장치
```typescript
// ❌ WHERE 조건 없는 전체 테이블 삭제 (강력한 안전장치)
// ❌ 일정 개수 이상의 대량 삭제 (maxDeleteCount 제한)
// ⚠️ 외래키 제약조건 위반 가능성 체크
// ⚠️ 자기 자신 테이블 삭제 (허용하되 특별한 주의사항)
const validateDeleteSafety = (
fromTable: string,
toTable: string,
deleteConditions: DeleteCondition[],
whereConditions: WhereCondition[],
safetySettings: DeleteSafetySettings
): ValidationResult => {
// 1. WHERE 조건 필수 체크
if (!whereConditions.length) {
return {
isValid: false,
error: "DELETE 작업에는 반드시 WHERE 조건이 필요합니다.",
};
}
// 2. 대량 삭제 제한 체크
if (safetySettings.maxDeleteCount < 1) {
return {
isValid: false,
error: "최대 삭제 개수는 1 이상이어야 합니다.",
};
}
// 🆕 3. 자기 자신 테이블 삭제 시 추가 안전장치
if (fromTable === toTable) {
// 강화된 안전장치: 더 엄격한 제한
const selfDeleteMaxCount = Math.min(safetySettings.maxDeleteCount, 10);
if (safetySettings.maxDeleteCount > selfDeleteMaxCount) {
return {
isValid: false,
error: `자기 자신 테이블 삭제 시 최대 ${selfDeleteMaxCount}개까지만 허용됩니다.`,
};
}
// 삭제 조건이 너무 광범위한지 체크
const hasBroadCondition = deleteConditions.some(
(condition) =>
condition.operator === "!=" ||
condition.operator === "NOT IN" ||
condition.operator === "NOT EXISTS"
);
if (hasBroadCondition) {
return {
isValid: false,
error:
"자기 자신 테이블 삭제 시 부정 조건(!=, NOT IN, NOT EXISTS)은 위험합니다.",
};
}
// WHERE 조건이 충분히 구체적인지 체크
if (whereConditions.length < 2) {
return {
isValid: false,
error:
"자기 자신 테이블 삭제 시 WHERE 조건을 2개 이상 설정하는 것을 권장합니다.",
};
}
}
return { isValid: true };
};
// 🆕 자기 자신 테이블 작업 시 실제 사용 예시
const exampleSelfTableOperations = {
// ✅ 안전한 자기 자신 테이블 UPDATE
safeUpdate: `
UPDATE user_info
SET last_login = NOW(), login_count = login_count + 1
WHERE user_id = 'specific_user' AND status = 'active'
`,
// ✅ 안전한 자기 자신 테이블 DELETE
safeDelete: `
DELETE FROM temp_data
WHERE created_at < NOW() - INTERVAL '7 days'
AND status = 'processed'
AND batch_id = 'specific_batch'
LIMIT 10
`,
// ❌ 위험한 작업들
dangerousOperations: [
"UPDATE table SET column = value (WHERE 조건 없음)",
"DELETE FROM table WHERE status != 'active' (부정 조건으로 예상보다 많이 삭제될 수 있음)",
"UPDATE table SET id = new_id WHERE id = old_id (키 값 변경으로 참조 무결성 위험)",
],
};
```
### 4. 공통 검증 로직
#### 매핑 제약사항 통합 검증
```typescript
const validateMappingConstraints = (
actionType: "insert" | "update" | "delete",
newMapping: ColumnMapping,
existingMappings: ColumnMapping[]
): ValidationResult => {
switch (actionType) {
case "insert":
return validateInsertMapping(newMapping, existingMappings);
case "update":
return validateUpdateMapping(newMapping, existingMappings);
case "delete":
return validateDeleteConditions(newMapping, existingMappings);
}
};
const validateInsertMapping = (
newMapping: ColumnMapping,
existingMappings: ColumnMapping[]
): ValidationResult => {
// TO 컬럼이 이미 다른 FROM 컬럼과 매핑되어 있는지 확인
const existingToMapping = existingMappings.find(
(mapping) => mapping.toColumnName === newMapping.toColumnName
);
if (
existingToMapping &&
existingToMapping.fromColumnName &&
existingToMapping.fromColumnName !== newMapping.fromColumnName
) {
return {
isValid: false,
error: `대상 컬럼 '${newMapping.toColumnName}'은 이미 '${existingToMapping.fromColumnName}'과 매핑되어 있습니다.`,
};
}
return { isValid: true };
};
```
### 2. UI에서의 제약사항 표시
#### 컬럼 선택 시 비활성화 로직
```typescript
const isToColumnClickable = (toColumn: ColumnInfo) => {
const currentMapping = columnMappings.find(
(m) => m.toColumnName === toColumn.columnName
);
// 이미 다른 FROM 컬럼과 매핑된 경우 클릭 불가
if (currentMapping?.fromColumnName) {
return false;
}
// 기본값이 설정된 경우 클릭 불가
if (currentMapping?.defaultValue && currentMapping.defaultValue.trim()) {
return false;
}
// 데이터 타입 호환성 체크
if (!selectedFromColumn) return true;
const fromColumn = fromTableColumns.find(
(col) => col.columnName === selectedFromColumn
);
if (!fromColumn) return true;
return fromColumn.dataType === toColumn.dataType;
};
```
#### 시각적 피드백
```typescript
// TO 컬럼 렌더링 시 상태 표시
const getToColumnStatus = (toColumn: ColumnInfo) => {
const mapping = columnMappings.find(
(m) => m.toColumnName === toColumn.columnName
);
if (mapping?.fromColumnName) {
return {
status: "mapped",
color: "bg-green-100 border-green-300",
icon: "🔗",
label: `← ${mapping.fromColumnName}`,
};
}
if (mapping?.defaultValue) {
return {
status: "default",
color: "bg-blue-100 border-blue-300",
icon: "📝",
label: `기본값: ${mapping.defaultValue}`,
};
}
return {
status: "unmapped",
color: "bg-gray-100 border-gray-300",
icon: "⚪",
label: "미설정",
};
};
```
## 📊 데이터 플로우
### 1. 설정 저장 플로우
```
사용자 설정 입력
ConnectionSelectionPanel → 커넥션 ID 저장
TableSelectionPanel → 테이블명 저장
InsertFieldMappingPanel → 필드 매핑 저장
DataSaveSettings 업데이트
dataflow_diagrams.plan 필드에 JSON 저장
```
### 2. 실행 플로우
#### INSERT 실행 플로우
```
제어관리 트리거 발생 (INSERT)
EnhancedDataflowControlService.executeDataflowControl()
소스 커넥션에서 데이터 조회 (MultiConnectionQueryService.fetchDataFromConnection)
필드 매핑 규칙 적용 (1:N 매핑 지원)
대상 커넥션에 데이터 삽입 (MultiConnectionQueryService.insertDataToConnection)
결과 반환
```
#### UPDATE 실행 플로우
```
제어관리 트리거 발생 (UPDATE)
EnhancedDataflowControlService.executeDataflowControl()
소스 커넥션에서 조건 데이터 조회 (UPDATE 조건 확인)
조건 만족 시 FROM 데이터 추출
필드 매핑 규칙 적용 (FROM → TO 필드 매핑)
WHERE 조건 생성 (TO 테이블 대상 레코드 식별)
대상 커넥션에서 데이터 업데이트 (MultiConnectionQueryService.updateDataToConnection)
결과 반환
```
#### DELETE 실행 플로우
```
제어관리 트리거 발생 (DELETE)
EnhancedDataflowControlService.executeDataflowControl()
소스 커넥션에서 삭제 트리거 조건 확인
조건 만족 시 삭제 대상 식별
안전장치 검증 (maxDeleteCount, WHERE 조건 필수)
WHERE 조건 생성 (TO 테이블 삭제 대상 레코드)
[dryRunFirst=true인 경우] 삭제 예상 개수 확인
대상 커넥션에서 데이터 삭제 (MultiConnectionQueryService.deleteDataFromConnection)
삭제 로그 기록 (logAllDeletes=true인 경우)
결과 반환
```
## 🛠️ 기술적 고려사항
### 1. 성능 최적화
- **커넥션 풀링**: 외부 DB별 커넥션 풀 관리
- **캐싱**: 테이블/컬럼 정보 캐싱 (Redis 활용)
- **비동기 처리**: 대용량 데이터 처리 시 큐잉 시스템 활용
### 2. 보안 강화
- **커넥션 정보 암호화**: 기존 시스템과 동일한 수준 유지
- **접근 권한 관리**: 회사별 커넥션 접근 제어
- **감사 로깅**: 모든 외부 DB 접근 기록
### 3. 오류 처리
```typescript
export class ConnectionError extends Error {
constructor(
message: string,
public connectionId: number,
public originalError?: Error
) {
super(message);
this.name = "ConnectionError";
}
}
export class MappingValidationError extends Error {
constructor(message: string, public mappingErrors: ValidationError[]) {
super(message);
this.name = "MappingValidationError";
}
}
```
### 4. 호환성 유지
- **기존 설정 마이그레이션**: 기존 제어관리 설정을 새 구조로 자동 변환
- **점진적 전환**: 기존 기능 유지하면서 새 기능 추가
- **롤백 계획**: 문제 발생 시 이전 버전으로 복원 가능
## 📅 일정 계획
### Week 1-2: 백엔드 인프라
- [ ] MultiConnectionQueryService 개발
- [ ] 외부 커넥션 API 확장
- [ ] EnhancedDataflowControlService 개발
### Week 3-4: 프론트엔드 UI
- [ ] ConnectionSelectionPanel 개발 (액션 타입별 라벨링)
- [ ] TableSelectionPanel 개발 (액션 타입 지원)
- [ ] InsertFieldMappingPanel 확장
- [ ] UpdateFieldMappingPanel 개발
- [ ] DeleteConditionPanel 개발
### Week 5: 통합 및 테스트
- [ ] 컴포넌트 통합
- [ ] 매핑 제약사항 검증 로직
- [ ] 종합 테스트
### Week 6: 문서화 및 배포
- [ ] 사용자 가이드 작성
- [ ] 개발자 문서 업데이트
- [ ] 배포 및 사용자 교육
## 🎯 성공 지표
### 기능적 지표
- ✅ 다양한 외부 DB에 INSERT/UPDATE/DELETE 성공률 > 95%
- ✅ 매핑 제약사항 검증 정확도 100%
- ✅ DELETE 안전장치 동작률 100%
- ✅ 기존 제어관리 기능 호환성 100%
### 성능 지표
- ✅ 커넥션 설정 UI 응답 시간 < 2초
- 테이블/컬럼 로딩 시간 < 3초
- INSERT/UPDATE/DELETE 처리 시간 < 5초
- 대량 DELETE 검증 시간 < 3초
### 사용성 지표
- 설정 완료까지 필요한 클릭 < 10회
- 매핑 오류 발생 명확한 안내 메시지 제공
- 기존 사용자의 학습 비용 최소화
## 💡 자기 자신 테이블 작업 실제 사용 케이스
### 1. UPDATE 사용 케이스
#### 케이스 1: 사용자 로그인 정보 업데이트
```sql
-- 트리거: 사용자가 로그인할 때
-- FROM: login_logs 테이블에서 최근 로그인 기록 확인
-- TO: user_info 테이블의 last_login, login_count 업데이트
IF (login_logs.status = 'success' AND login_logs.created_at > NOW() - INTERVAL '1 minute')
THEN UPDATE user_info
SET last_login = login_logs.created_at,
login_count = login_count + 1,
updated_at = NOW()
WHERE user_info.user_id = login_logs.user_id
```
#### 케이스 2: 재고 수량 실시간 업데이트
```sql
-- 트리거: 주문이 완료될 때
-- FROM: order_items 테이블에서 주문 수량 확인
-- TO: product_inventory 테이블의 재고 수량 차감
IF (order_items.status = 'confirmed')
THEN UPDATE product_inventory
SET current_stock = current_stock - order_items.quantity,
last_updated = NOW()
WHERE product_inventory.product_id = order_items.product_id
```
### 2. DELETE 사용 케이스
#### 케이스 1: 임시 데이터 자동 정리
```sql
-- 트리거: 배치 작업 완료 시
-- FROM: batch_jobs 테이블에서 완료된 작업 확인
-- TO: temp_processing_data 테이블의 임시 데이터 삭제
IF (batch_jobs.status = 'completed' AND batch_jobs.completed_at < NOW() - INTERVAL '1 hour')
THEN DELETE FROM temp_processing_data
WHERE temp_processing_data.batch_id = batch_jobs.batch_id
AND temp_processing_data.status = 'processed'
LIMIT 100
```
#### 케이스 2: 만료된 세션 정리
```sql
-- 트리거: 시스템 정리 작업 시
-- FROM: user_sessions 테이블에서 만료된 세션 확인
-- TO: user_sessions 테이블에서 만료된 세션 삭제
IF (user_sessions.last_activity < NOW() - INTERVAL '24 hours')
THEN DELETE FROM user_sessions
WHERE user_sessions.last_activity < NOW() - INTERVAL '24 hours'
AND user_sessions.status = 'inactive'
LIMIT 50
```
### 3. 복합 시나리오
#### 케이스 3: 주문 상태 변경에 따른 연쇄 업데이트
```sql
-- 1단계: 주문 상태 업데이트
UPDATE orders SET status = 'shipped', shipped_at = NOW()
WHERE order_id = 'ORD001' AND status = 'processing'
-- 2단계: 배송 정보 생성 (INSERT)
INSERT INTO shipping_info (order_id, tracking_number, created_at)
VALUES ('ORD001', 'TRACK001', NOW())
-- 3단계: 고객 주문 이력 업데이트
UPDATE customer_stats
SET total_orders = total_orders + 1, last_order_date = NOW()
WHERE customer_id = (SELECT customer_id FROM orders WHERE order_id = 'ORD001')
```
## 🚀 향후 확장 계획
### Phase 4: 고급 기능
- **데이터 변환 함수**: 필드 매핑 커스텀 변환 로직 지원
- **배치 처리**: 대용량 데이터 일괄 처리
- **스케줄링**: 정기적 데이터 동기화
- **🆕 자기 자신 테이블 트랜잭션**: 복잡한 자기 참조 작업의 원자성 보장
### Phase 5: 모니터링
- **실시간 모니터링**: 외부 DB 연결 상태 실시간 추적
- **성능 분석**: 쿼리 실행 시간 리소스 사용량 분석
- **알림 시스템**: 오류 발생 자동 알림
- **🆕 자기 자신 테이블 작업 감시**: 위험한 자기 참조 작업 모니터링
### Phase 6: 안전성 강화
- **🆕 Dry Run 모드**: 실제 실행 결과 예측
- **🆕 롤백 시스템**: 자기 자신 테이블 작업 자동 백업 복원
- **🆕 단계별 승인**: 위험한 자기 참조 작업에 대한 관리자 승인 프로세스
계획서를 바탕으로 체계적이고 안전한 제어관리 기능 개선을 진행할 있습니다.