ERP-node/PHASE_EXTERNAL_CONNECTION_R...

760 lines
20 KiB
Markdown
Raw Normal View History

2025-10-20 17:24:41 +09:00
# 외부 커넥션 관리 REST API 지원 확장 계획서
## 📋 프로젝트 개요
### 목적
현재 외부 데이터베이스 연결만 관리하는 `/admin/external-connections` 페이지에 REST API 연결 관리 기능을 추가하여, DB와 REST API 커넥션을 통합 관리할 수 있도록 확장합니다.
### 현재 상황
- **기존 기능**: 외부 데이터베이스 연결 정보만 관리 (MySQL, PostgreSQL, Oracle, SQL Server, SQLite)
- **기존 테이블**: `external_db_connections` - DB 연결 정보 저장
- **기존 UI**: 단일 화면에서 DB 연결 목록 표시 및 CRUD 작업
### 요구사항
1. **탭 전환**: DB 연결 관리 ↔ REST API 연결 관리 간 탭 전환 UI
2. **REST API 관리**: 요청 주소별 헤더(키-값 쌍) 관리
3. **연결 테스트**: REST API 호출이 정상 작동하는지 테스트 기능
---
## 🗄️ 데이터베이스 설계
### 신규 테이블: `external_rest_api_connections`
```sql
CREATE TABLE external_rest_api_connections (
id SERIAL PRIMARY KEY,
-- 기본 정보
connection_name VARCHAR(100) NOT NULL UNIQUE,
description TEXT,
-- REST API 연결 정보
base_url VARCHAR(500) NOT NULL, -- 기본 URL (예: https://api.example.com)
default_headers JSONB DEFAULT '{}', -- 기본 헤더 정보 (키-값 쌍)
-- 인증 설정
auth_type VARCHAR(20) DEFAULT 'none', -- none, api-key, bearer, basic, oauth2
auth_config JSONB, -- 인증 관련 설정
-- 고급 설정
timeout INTEGER DEFAULT 30000, -- 요청 타임아웃 (ms)
retry_count INTEGER DEFAULT 0, -- 재시도 횟수
retry_delay INTEGER DEFAULT 1000, -- 재시도 간격 (ms)
-- 관리 정보
company_code VARCHAR(20) DEFAULT '*',
is_active CHAR(1) DEFAULT 'Y',
created_date TIMESTAMP DEFAULT NOW(),
created_by VARCHAR(50),
updated_date TIMESTAMP DEFAULT NOW(),
updated_by VARCHAR(50),
-- 테스트 정보
last_test_date TIMESTAMP,
last_test_result CHAR(1), -- Y: 성공, N: 실패
last_test_message TEXT
);
-- 인덱스
CREATE INDEX idx_rest_api_connections_company ON external_rest_api_connections(company_code);
CREATE INDEX idx_rest_api_connections_active ON external_rest_api_connections(is_active);
CREATE INDEX idx_rest_api_connections_name ON external_rest_api_connections(connection_name);
```
### 샘플 데이터
```sql
INSERT INTO external_rest_api_connections (
connection_name, description, base_url, default_headers, auth_type, auth_config
) VALUES
(
'기상청 API',
'기상청 공공데이터 API',
'https://apis.data.go.kr/1360000/VilageFcstInfoService_2.0',
'{"Content-Type": "application/json", "Accept": "application/json"}',
'api-key',
'{"keyLocation": "query", "keyName": "serviceKey", "keyValue": "your-api-key-here"}'
),
(
'사내 인사 시스템 API',
'인사정보 조회용 내부 API',
'https://hr.company.com/api/v1',
'{"Content-Type": "application/json"}',
'bearer',
'{"token": "your-bearer-token-here"}'
);
```
---
## 🔧 백엔드 구현
### 1. 타입 정의
```typescript
// backend-node/src/types/externalRestApiTypes.ts
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";
export interface ExternalRestApiConnection {
id?: number;
connection_name: string;
description?: string;
base_url: string;
default_headers: Record<string, string>;
auth_type: AuthType;
auth_config?: {
// API Key
keyLocation?: "header" | "query";
keyName?: string;
keyValue?: string;
// Bearer Token
token?: string;
// Basic Auth
username?: string;
password?: string;
// OAuth2
clientId?: string;
clientSecret?: string;
tokenUrl?: string;
accessToken?: string;
};
timeout?: number;
retry_count?: number;
retry_delay?: number;
company_code: string;
is_active: string;
created_date?: Date;
created_by?: string;
updated_date?: Date;
updated_by?: string;
last_test_date?: Date;
last_test_result?: string;
last_test_message?: string;
}
export interface ExternalRestApiConnectionFilter {
auth_type?: string;
is_active?: string;
company_code?: string;
search?: string;
}
export interface RestApiTestRequest {
id?: number;
base_url: string;
endpoint?: string; // 테스트할 엔드포인트 (선택)
method?: "GET" | "POST" | "PUT" | "DELETE";
headers?: Record<string, string>;
auth_type?: AuthType;
auth_config?: any;
timeout?: number;
}
export interface RestApiTestResult {
success: boolean;
message: string;
response_time?: number;
status_code?: number;
response_data?: any;
error_details?: string;
}
```
### 2. 서비스 계층
```typescript
// backend-node/src/services/externalRestApiConnectionService.ts
export class ExternalRestApiConnectionService {
// CRUD 메서드
static async getConnections(filter: ExternalRestApiConnectionFilter);
static async getConnectionById(id: number);
static async createConnection(data: ExternalRestApiConnection);
static async updateConnection(
id: number,
data: Partial<ExternalRestApiConnection>
);
static async deleteConnection(id: number);
// 테스트 메서드
static async testConnection(
testRequest: RestApiTestRequest
): Promise<RestApiTestResult>;
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult>;
// 헬퍼 메서드
private static buildHeaders(
connection: ExternalRestApiConnection
): Record<string, string>;
private static validateConnectionData(data: ExternalRestApiConnection): void;
private static encryptSensitiveData(authConfig: any): any;
private static decryptSensitiveData(authConfig: any): any;
}
```
### 3. API 라우트
```typescript
// backend-node/src/routes/externalRestApiConnectionRoutes.ts
// GET /api/external-rest-api-connections - 목록 조회
// GET /api/external-rest-api-connections/:id - 상세 조회
// POST /api/external-rest-api-connections - 새 연결 생성
// PUT /api/external-rest-api-connections/:id - 연결 수정
// DELETE /api/external-rest-api-connections/:id - 연결 삭제
// POST /api/external-rest-api-connections/test - 연결 테스트 (신규)
// POST /api/external-rest-api-connections/:id/test - ID로 테스트 (기존 연결)
```
### 4. 연결 테스트 구현
```typescript
// REST API 연결 테스트 로직
static async testConnection(testRequest: RestApiTestRequest): Promise<RestApiTestResult> {
const startTime = Date.now();
try {
// 헤더 구성
const headers = { ...testRequest.headers };
// 인증 헤더 추가
if (testRequest.auth_type === 'bearer' && testRequest.auth_config?.token) {
headers['Authorization'] = `Bearer ${testRequest.auth_config.token}`;
} else if (testRequest.auth_type === 'basic') {
const credentials = Buffer.from(
`${testRequest.auth_config.username}:${testRequest.auth_config.password}`
).toString('base64');
headers['Authorization'] = `Basic ${credentials}`;
} else if (testRequest.auth_type === 'api-key') {
if (testRequest.auth_config.keyLocation === 'header') {
headers[testRequest.auth_config.keyName] = testRequest.auth_config.keyValue;
}
}
// URL 구성
let url = testRequest.base_url;
if (testRequest.endpoint) {
url = `${testRequest.base_url}${testRequest.endpoint}`;
}
// API Key가 쿼리에 있는 경우
if (testRequest.auth_type === 'api-key' &&
testRequest.auth_config.keyLocation === 'query') {
const separator = url.includes('?') ? '&' : '?';
url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`;
}
// HTTP 요청 실행
const response = await fetch(url, {
method: testRequest.method || 'GET',
headers,
signal: AbortSignal.timeout(testRequest.timeout || 30000),
});
const responseTime = Date.now() - startTime;
const responseData = await response.json().catch(() => null);
return {
success: response.ok,
message: response.ok ? '연결 성공' : `연결 실패 (${response.status})`,
response_time: responseTime,
status_code: response.status,
response_data: responseData,
};
} catch (error) {
return {
success: false,
message: '연결 실패',
error_details: error instanceof Error ? error.message : '알 수 없는 오류',
};
}
}
```
---
## 🎨 프론트엔드 구현
### 1. 탭 구조 설계
```typescript
// frontend/app/(main)/admin/external-connections/page.tsx
type ConnectionTabType = "database" | "rest-api";
const [activeTab, setActiveTab] = useState<ConnectionTabType>("database");
```
### 2. 메인 페이지 구조 개선
```tsx
// 탭 헤더
<Tabs
value={activeTab}
onValueChange={(value) => setActiveTab(value as ConnectionTabType)}
>
<TabsList className="grid w-[400px] grid-cols-2">
<TabsTrigger value="database" className="flex items-center gap-2">
<Database className="h-4 w-4" />
데이터베이스 연결
</TabsTrigger>
<TabsTrigger value="rest-api" className="flex items-center gap-2">
<Globe className="h-4 w-4" />
REST API 연결
</TabsTrigger>
</TabsList>
{/* 데이터베이스 연결 탭 */}
<TabsContent value="database">
<DatabaseConnectionList />
</TabsContent>
{/* REST API 연결 탭 */}
<TabsContent value="rest-api">
<RestApiConnectionList />
</TabsContent>
</Tabs>
```
### 3. REST API 연결 목록 컴포넌트
```typescript
// frontend/components/admin/RestApiConnectionList.tsx
export function RestApiConnectionList() {
const [connections, setConnections] = useState<ExternalRestApiConnection[]>(
[]
);
const [searchTerm, setSearchTerm] = useState("");
const [authTypeFilter, setAuthTypeFilter] = useState("ALL");
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingConnection, setEditingConnection] = useState<
ExternalRestApiConnection | undefined
>();
// 테이블 컬럼:
// - 연결명
// - 기본 URL
// - 인증 타입
// - 헤더 수 (default_headers 개수)
// - 상태 (활성/비활성)
// - 마지막 테스트 (날짜 + 결과)
// - 작업 (테스트/편집/삭제)
}
```
### 4. REST API 연결 설정 모달
```typescript
// frontend/components/admin/RestApiConnectionModal.tsx
export function RestApiConnectionModal({
isOpen,
onClose,
onSave,
connection,
}: RestApiConnectionModalProps) {
// 섹션 구성:
// 1. 기본 정보
// - 연결명 (필수)
// - 설명
// - 기본 URL (필수)
// 2. 헤더 관리 (키-값 추가/삭제)
// - 동적 입력 필드
// - + 버튼으로 추가
// - 각 행에 삭제 버튼
// 3. 인증 설정
// - 인증 타입 선택 (none/api-key/bearer/basic/oauth2)
// - 선택된 타입별 설정 필드 표시
// 4. 고급 설정 (접기/펼치기)
// - 타임아웃
// - 재시도 설정
// 5. 테스트 섹션
// - 테스트 엔드포인트 입력 (선택)
// - 테스트 실행 버튼
// - 테스트 결과 표시
}
```
### 5. 헤더 관리 컴포넌트
```typescript
// frontend/components/admin/HeadersManager.tsx
interface HeadersManagerProps {
headers: Record<string, string>;
onChange: (headers: Record<string, string>) => void;
}
export function HeadersManager({ headers, onChange }: HeadersManagerProps) {
const [headersList, setHeadersList] = useState<
Array<{ key: string; value: string }>
>(Object.entries(headers).map(([key, value]) => ({ key, value })));
const addHeader = () => {
setHeadersList([...headersList, { key: "", value: "" }]);
};
const removeHeader = (index: number) => {
const newList = headersList.filter((_, i) => i !== index);
setHeadersList(newList);
updateParent(newList);
};
const updateHeader = (
index: number,
field: "key" | "value",
value: string
) => {
const newList = [...headersList];
newList[index][field] = value;
setHeadersList(newList);
updateParent(newList);
};
const updateParent = (list: Array<{ key: string; value: string }>) => {
const headersObject = list.reduce((acc, { key, value }) => {
if (key.trim()) acc[key] = value;
return acc;
}, {} as Record<string, string>);
onChange(headersObject);
};
// UI: 테이블 형태로 키-값 입력 필드 표시
// 각 행: [키 입력] [값 입력] [삭제 버튼]
// 하단: [+ 헤더 추가] 버튼
}
```
### 6. 인증 설정 컴포넌트
```typescript
// frontend/components/admin/AuthenticationConfig.tsx
export function AuthenticationConfig({
authType,
authConfig,
onChange,
}: AuthenticationConfigProps) {
// authType에 따라 다른 입력 필드 표시
// none: 추가 필드 없음
// api-key:
// - 키 위치 (header/query)
// - 키 이름
// - 키 값
// bearer:
// - 토큰 값
// basic:
// - 사용자명
// - 비밀번호
// oauth2:
// - Client ID
// - Client Secret
// - Token URL
// - Access Token (읽기전용, 자동 갱신)
}
```
### 7. API 클라이언트
```typescript
// frontend/lib/api/externalRestApiConnection.ts
export class ExternalRestApiConnectionAPI {
private static readonly BASE_URL = "/api/external-rest-api-connections";
static async getConnections(filter?: ExternalRestApiConnectionFilter) {
const params = new URLSearchParams();
if (filter?.search) params.append("search", filter.search);
if (filter?.auth_type && filter.auth_type !== "ALL") {
params.append("auth_type", filter.auth_type);
}
if (filter?.is_active && filter.is_active !== "ALL") {
params.append("is_active", filter.is_active);
}
const response = await fetch(`${this.BASE_URL}?${params}`);
return this.handleResponse(response);
}
static async getConnectionById(id: number) {
const response = await fetch(`${this.BASE_URL}/${id}`);
return this.handleResponse(response);
}
static async createConnection(data: ExternalRestApiConnection) {
const response = await fetch(this.BASE_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return this.handleResponse(response);
}
static async updateConnection(
id: number,
data: Partial<ExternalRestApiConnection>
) {
const response = await fetch(`${this.BASE_URL}/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return this.handleResponse(response);
}
static async deleteConnection(id: number) {
const response = await fetch(`${this.BASE_URL}/${id}`, {
method: "DELETE",
});
return this.handleResponse(response);
}
static async testConnection(
testRequest: RestApiTestRequest
): Promise<RestApiTestResult> {
const response = await fetch(`${this.BASE_URL}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(testRequest),
});
return this.handleResponse(response);
}
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult> {
const response = await fetch(`${this.BASE_URL}/${id}/test`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ endpoint }),
});
return this.handleResponse(response);
}
private static async handleResponse(response: Response) {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || "요청 실패");
}
return response.json();
}
}
```
---
## 📋 구현 순서
### Phase 1: 데이터베이스 및 백엔드 기본 구조 (1일)
- [x] 데이터베이스 테이블 생성 (`external_rest_api_connections`)
- [ ] 타입 정의 작성 (`externalRestApiTypes.ts`)
- [ ] 서비스 계층 기본 CRUD 구현
- [ ] API 라우트 기본 구현
### Phase 2: 연결 테스트 기능 (1일)
- [ ] 연결 테스트 로직 구현
- [ ] 인증 타입별 헤더 구성 로직
- [ ] 에러 처리 및 타임아웃 관리
- [ ] 테스트 결과 저장 (last_test_date, last_test_result)
### Phase 3: 프론트엔드 기본 UI (1-2일)
- [ ] 탭 구조 추가 (Database / REST API)
- [ ] REST API 연결 목록 컴포넌트
- [ ] API 클라이언트 작성
- [ ] 기본 CRUD UI 구현
### Phase 4: 모달 및 상세 기능 (1-2일)
- [ ] REST API 연결 설정 모달
- [ ] 헤더 관리 컴포넌트 (키-값 동적 추가/삭제)
- [ ] 인증 설정 컴포넌트 (타입별 입력 필드)
- [ ] 고급 설정 섹션
### Phase 5: 테스트 및 통합 (1일)
- [ ] 연결 테스트 UI
- [ ] 테스트 결과 표시
- [ ] 에러 처리 및 사용자 피드백
- [ ] 전체 기능 통합 테스트
### Phase 6: 최적화 및 마무리 (0.5일)
- [ ] 민감 정보 암호화 (API 키, 토큰, 비밀번호)
- [ ] UI/UX 개선
- [ ] 문서화
---
## 🧪 테스트 시나리오
### 1. REST API 연결 등록 테스트
- [ ] 기본 정보 입력 (연결명, URL)
- [ ] 헤더 추가/삭제
- [ ] 각 인증 타입별 설정
- [ ] 유효성 검증 (필수 필드, URL 형식)
### 2. 연결 테스트
- [ ] 인증 없는 API 테스트
- [ ] API Key (header/query) 테스트
- [ ] Bearer Token 테스트
- [ ] Basic Auth 테스트
- [ ] 타임아웃 시나리오
- [ ] 네트워크 오류 시나리오
### 3. 데이터 관리
- [ ] 목록 조회 및 필터링
- [ ] 연결 수정
- [ ] 연결 삭제
- [ ] 활성/비활성 전환
### 4. 통합 시나리오
- [ ] DB 연결 탭 ↔ REST API 탭 전환
- [ ] 여러 연결 등록 및 관리
- [ ] 동시 테스트 실행
---
## 🔒 보안 고려사항
### 1. 민감 정보 암호화
```typescript
// API 키, 토큰, 비밀번호 암호화
private static encryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const encrypted = { ...authConfig };
// 암호화 대상 필드
if (encrypted.keyValue) {
encrypted.keyValue = encrypt(encrypted.keyValue);
}
if (encrypted.token) {
encrypted.token = encrypt(encrypted.token);
}
if (encrypted.password) {
encrypted.password = encrypt(encrypted.password);
}
if (encrypted.clientSecret) {
encrypted.clientSecret = encrypt(encrypted.clientSecret);
}
return encrypted;
}
```
### 2. 접근 권한 제어
- 관리자 권한만 접근
- 회사별 데이터 분리
- API 호출 시 인증 토큰 검증
### 3. 테스트 요청 제한
- Rate Limiting (1분에 최대 10회)
- 타임아웃 설정 (최대 30초)
- 동시 테스트 제한
---
## 📊 성능 최적화
### 1. 헤더 데이터 구조
```typescript
// JSONB 필드 인덱싱 (PostgreSQL)
CREATE INDEX idx_rest_api_headers ON external_rest_api_connections
USING GIN (default_headers);
CREATE INDEX idx_rest_api_auth_config ON external_rest_api_connections
USING GIN (auth_config);
```
### 2. 캐싱 전략
- 자주 사용되는 연결 정보 캐싱
- 테스트 결과 임시 캐싱 (5분)
---
## 📚 향후 확장 가능성
### 1. 엔드포인트 관리
각 REST API 연결에 대해 자주 사용하는 엔드포인트를 사전 등록하여 빠른 호출 가능
### 2. 요청 템플릿
HTTP 메서드별 요청 바디 템플릿 관리
### 3. 응답 매핑
REST API 응답을 내부 데이터 구조로 변환하는 매핑 룰 설정
### 4. 로그 및 모니터링
- API 호출 이력 기록
- 응답 시간 모니터링
- 오류율 추적
---
## ✅ 완료 체크리스트
### 백엔드
- [ ] 데이터베이스 테이블 생성
- [ ] 타입 정의
- [ ] 서비스 계층 CRUD
- [ ] 연결 테스트 로직
- [ ] API 라우트
- [ ] 민감 정보 암호화
### 프론트엔드
- [ ] 탭 구조
- [ ] REST API 연결 목록
- [ ] 연결 설정 모달
- [ ] 헤더 관리 컴포넌트
- [ ] 인증 설정 컴포넌트
- [ ] API 클라이언트
- [ ] 연결 테스트 UI
### 테스트
- [ ] 단위 테스트
- [ ] 통합 테스트
- [ ] 사용자 시나리오 테스트
### 문서
- [ ] API 문서
- [ ] 사용자 가이드
- [ ] 배포 가이드
---
**작성일**: 2025-10-20
**버전**: 1.0
**담당**: AI Assistant