333 lines
9.3 KiB
Markdown
333 lines
9.3 KiB
Markdown
# 액션 노드 타겟 선택 시스템 개선 계획
|
|
|
|
## 📋 현재 문제점
|
|
|
|
### 1. 타겟 타입 구분 부재
|
|
- INSERT/UPDATE/DELETE/UPSERT 액션 노드가 타겟 테이블만 선택 가능
|
|
- 내부 DB인지, 외부 DB인지, REST API인지 구분 없음
|
|
- 실행 시 항상 내부 DB로만 동작
|
|
|
|
### 2. 외부 시스템 연동 불가
|
|
- 외부 DB에 데이터 저장 불가
|
|
- 외부 REST API 호출 불가
|
|
- 하이브리드 플로우 구성 불가 (내부 → 외부 데이터 전송)
|
|
|
|
---
|
|
|
|
## 🎯 개선 목표
|
|
|
|
액션 노드에서 다음 3가지 타겟 타입을 선택할 수 있도록 개선:
|
|
|
|
### 1. 내부 데이터베이스 (Internal DB)
|
|
- 현재 시스템의 PostgreSQL
|
|
- 기존 동작 유지
|
|
|
|
### 2. 외부 데이터베이스 (External DB)
|
|
- 외부 커넥션 관리에서 설정한 DB
|
|
- PostgreSQL, MySQL, Oracle, MSSQL, MariaDB 지원
|
|
|
|
### 3. REST API
|
|
- 외부 REST API 호출
|
|
- HTTP 메서드: POST, PUT, PATCH, DELETE
|
|
- 인증: None, Basic, Bearer Token, API Key
|
|
|
|
---
|
|
|
|
## 📐 타입 정의 확장
|
|
|
|
### TargetType 추가
|
|
```typescript
|
|
export type TargetType = "internal" | "external" | "api";
|
|
|
|
export interface BaseActionNodeData {
|
|
displayName: string;
|
|
targetType: TargetType; // 🔥 새로 추가
|
|
|
|
// targetType === "internal"
|
|
targetTable?: string;
|
|
targetTableLabel?: string;
|
|
|
|
// targetType === "external"
|
|
externalConnectionId?: number;
|
|
externalConnectionName?: string;
|
|
externalDbType?: string;
|
|
externalTargetTable?: string;
|
|
externalTargetSchema?: string;
|
|
|
|
// targetType === "api"
|
|
apiEndpoint?: string;
|
|
apiMethod?: "POST" | "PUT" | "PATCH" | "DELETE";
|
|
apiAuthType?: "none" | "basic" | "bearer" | "apikey";
|
|
apiAuthConfig?: {
|
|
username?: string;
|
|
password?: string;
|
|
token?: string;
|
|
apiKey?: string;
|
|
apiKeyHeader?: string;
|
|
};
|
|
apiHeaders?: Record<string, string>;
|
|
apiBodyTemplate?: string; // JSON 템플릿
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 🎨 UI 설계
|
|
|
|
### 1. 타겟 타입 선택 (공통)
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 타겟 타입 │
|
|
│ ○ 내부 데이터베이스 (기본) │
|
|
│ ○ 외부 데이터베이스 │
|
|
│ ○ REST API │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### 2-A. 내부 DB 선택 시 (기존 UI 유지)
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 테이블 선택: [검색 가능 Combobox] │
|
|
│ 필드 매핑: │
|
|
│ • source_field → target_field │
|
|
│ • ... │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### 2-B. 외부 DB 선택 시
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ 외부 커넥션: [🐘 PostgreSQL - 운영DB]│
|
|
│ 스키마: [public ▼] │
|
|
│ 테이블: [users ▼] │
|
|
│ 필드 매핑: │
|
|
│ • source_field → target_field │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### 2-C. REST API 선택 시
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ API 엔드포인트: │
|
|
│ [https://api.example.com/users] │
|
|
│ │
|
|
│ HTTP 메서드: [POST ▼] │
|
|
│ │
|
|
│ 인증 타입: [Bearer Token ▼] │
|
|
│ Token: [••••••••••••••] │
|
|
│ │
|
|
│ 헤더 (선택): │
|
|
│ Content-Type: application/json │
|
|
│ + 헤더 추가 │
|
|
│ │
|
|
│ 바디 템플릿: │
|
|
│ { │
|
|
│ "name": "{{source.name}}", │
|
|
│ "email": "{{source.email}}" │
|
|
│ } │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 🔧 구현 단계
|
|
|
|
### Phase 1: 타입 정의 및 기본 UI (1-2시간)
|
|
- [ ] `types/node-editor.ts`에 `TargetType` 추가
|
|
- [ ] `InsertActionNodeData` 등 인터페이스 확장
|
|
- [ ] 속성 패널에 타겟 타입 선택 라디오 버튼 추가
|
|
|
|
### Phase 2: 내부 DB 타입 (기존 유지)
|
|
- [ ] `targetType === "internal"` 처리
|
|
- [ ] 기존 로직 그대로 유지
|
|
- [ ] 기본값으로 설정
|
|
|
|
### Phase 3: 외부 DB 타입 (2-3시간)
|
|
- [ ] 외부 커넥션 선택 UI
|
|
- [ ] 외부 테이블/컬럼 로드 (기존 API 재사용)
|
|
- [ ] 백엔드: `nodeFlowExecutionService.ts`에 외부 DB 실행 로직 추가
|
|
- [ ] `DatabaseConnectorFactory` 활용
|
|
|
|
### Phase 4: REST API 타입 (3-4시간)
|
|
- [ ] API 엔드포인트 설정 UI
|
|
- [ ] HTTP 메서드 선택
|
|
- [ ] 인증 타입별 설정 UI
|
|
- [ ] 바디 템플릿 에디터 (변수 치환 지원)
|
|
- [ ] 백엔드: Axios를 사용한 API 호출 로직
|
|
- [ ] 응답 처리 및 에러 핸들링
|
|
|
|
### Phase 5: 노드 시각화 개선 (1시간)
|
|
- [ ] 노드에 타겟 타입 아이콘 표시
|
|
- 내부 DB: 💾
|
|
- 외부 DB: 🔌 + DB 타입 아이콘
|
|
- REST API: 🌐
|
|
- [ ] 노드 색상 구분
|
|
|
|
### Phase 6: 검증 및 테스트 (2시간)
|
|
- [ ] 타겟 타입별 필수 값 검증
|
|
- [ ] 연결 규칙 업데이트
|
|
- [ ] 통합 테스트
|
|
|
|
---
|
|
|
|
## 🔍 백엔드 실행 로직
|
|
|
|
### nodeFlowExecutionService.ts
|
|
```typescript
|
|
private static async executeInsertAction(
|
|
node: FlowNode,
|
|
inputData: any[],
|
|
context: ExecutionContext
|
|
): Promise<any[]> {
|
|
const { targetType } = node.data;
|
|
|
|
switch (targetType) {
|
|
case "internal":
|
|
return this.executeInternalInsert(node, inputData);
|
|
|
|
case "external":
|
|
return this.executeExternalInsert(node, inputData);
|
|
|
|
case "api":
|
|
return this.executeApiInsert(node, inputData);
|
|
|
|
default:
|
|
throw new Error(`지원하지 않는 타겟 타입: ${targetType}`);
|
|
}
|
|
}
|
|
|
|
// 🔥 외부 DB INSERT
|
|
private static async executeExternalInsert(
|
|
node: FlowNode,
|
|
inputData: any[]
|
|
): Promise<any[]> {
|
|
const { externalConnectionId, externalTargetTable, fieldMappings } = node.data;
|
|
|
|
const connector = await DatabaseConnectorFactory.getConnector(
|
|
externalConnectionId!,
|
|
node.data.externalDbType!
|
|
);
|
|
|
|
const results = [];
|
|
for (const row of inputData) {
|
|
const values = fieldMappings.map(m => row[m.sourceField]);
|
|
const columns = fieldMappings.map(m => m.targetField);
|
|
|
|
const result = await connector.executeQuery(
|
|
`INSERT INTO ${externalTargetTable} (${columns.join(", ")}) VALUES (${...})`,
|
|
values
|
|
);
|
|
results.push(result);
|
|
}
|
|
|
|
await connector.disconnect();
|
|
return results;
|
|
}
|
|
|
|
// 🔥 REST API INSERT (POST)
|
|
private static async executeApiInsert(
|
|
node: FlowNode,
|
|
inputData: any[]
|
|
): Promise<any[]> {
|
|
const {
|
|
apiEndpoint,
|
|
apiMethod,
|
|
apiAuthType,
|
|
apiAuthConfig,
|
|
apiHeaders,
|
|
apiBodyTemplate
|
|
} = node.data;
|
|
|
|
const axios = require("axios");
|
|
const headers = { ...apiHeaders };
|
|
|
|
// 인증 헤더 추가
|
|
if (apiAuthType === "bearer" && apiAuthConfig?.token) {
|
|
headers["Authorization"] = `Bearer ${apiAuthConfig.token}`;
|
|
} else if (apiAuthType === "apikey" && apiAuthConfig?.apiKey) {
|
|
headers[apiAuthConfig.apiKeyHeader || "X-API-Key"] = apiAuthConfig.apiKey;
|
|
}
|
|
|
|
const results = [];
|
|
for (const row of inputData) {
|
|
// 템플릿 변수 치환
|
|
const body = this.replaceTemplateVariables(apiBodyTemplate, row);
|
|
|
|
const response = await axios({
|
|
method: apiMethod || "POST",
|
|
url: apiEndpoint,
|
|
headers,
|
|
data: JSON.parse(body),
|
|
});
|
|
|
|
results.push(response.data);
|
|
}
|
|
|
|
return results;
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 📊 우선순위
|
|
|
|
### High Priority
|
|
1. **Phase 1**: 타입 정의 및 기본 UI
|
|
2. **Phase 2**: 내부 DB 타입 (기존 유지)
|
|
3. **Phase 3**: 외부 DB 타입
|
|
|
|
### Medium Priority
|
|
4. **Phase 4**: REST API 타입
|
|
5. **Phase 5**: 노드 시각화
|
|
|
|
### Low Priority
|
|
6. **Phase 6**: 고급 기능 (재시도, 배치 처리 등)
|
|
|
|
---
|
|
|
|
## 🎯 예상 효과
|
|
|
|
### 1. 유연성 증가
|
|
- 다양한 시스템 간 데이터 연동 가능
|
|
- 하이브리드 플로우 구성 (내부 → 외부 → API)
|
|
|
|
### 2. 사용 사례 확장
|
|
```
|
|
[사례 1] 내부 DB → 외부 DB 동기화
|
|
TableSource(내부)
|
|
→ DataTransform
|
|
→ INSERT(외부 DB)
|
|
|
|
[사례 2] 내부 DB → REST API 전송
|
|
TableSource(내부)
|
|
→ DataTransform
|
|
→ INSERT(REST API)
|
|
|
|
[사례 3] 복합 플로우
|
|
TableSource(내부)
|
|
→ INSERT(외부 DB)
|
|
→ INSERT(REST API - 알림)
|
|
```
|
|
|
|
### 3. 기존 기능과의 호환
|
|
- 기본값: `targetType = "internal"`
|
|
- 기존 플로우 마이그레이션 불필요
|
|
|
|
---
|
|
|
|
## ⚠️ 주의사항
|
|
|
|
### 1. 보안
|
|
- API 인증 정보 암호화 저장
|
|
- 민감 데이터 로깅 방지
|
|
|
|
### 2. 에러 핸들링
|
|
- 외부 시스템 타임아웃 처리
|
|
- 재시도 로직 (선택적)
|
|
- 부분 실패 처리 (이미 구현됨)
|
|
|
|
### 3. 성능
|
|
- 외부 DB 연결 풀 관리 (이미 구현됨)
|
|
- REST API Rate Limiting 고려
|
|
|