Files
wace_rps/제어관리_외부커넥션_통합_개선_계획서.md
T

1492 lines
43 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🔧 제어관리 외부 커넥션 통합 개선 계획서
## 📋 프로젝트 개요
### 목적
현재 외부 커넥션 관리에서 관리되고 있는 데이터베이스 커넥션 정보를 제어관리의 데이터 저장 액션에서 활용할 수 있도록 통합하여, 사용자가 다양한 외부 데이터베이스에 데이터를 저장할 수 있는 기능을 구현합니다.
### 현재 상황 분석
#### 기존 외부 커넥션 관리
- **테이블**: `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 모드**: 실제 실행 전 결과 예측
- **🆕 롤백 시스템**: 자기 자신 테이블 작업 시 자동 백업 및 복원
- **🆕 단계별 승인**: 위험한 자기 참조 작업에 대한 관리자 승인 프로세스
이 계획서를 바탕으로 체계적이고 안전한 제어관리 기능 개선을 진행할 수 있습니다.