1492 lines
43 KiB
Markdown
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 모드**: 실제 실행 전 결과 예측
|
|
- **🆕 롤백 시스템**: 자기 자신 테이블 작업 시 자동 백업 및 복원
|
|
- **🆕 단계별 승인**: 위험한 자기 참조 작업에 대한 관리자 승인 프로세스
|
|
|
|
이 계획서를 바탕으로 체계적이고 안전한 제어관리 기능 개선을 진행할 수 있습니다.
|