외부 REST API 커넥션 POST/Body + DB 토큰 테스트 지원
This commit is contained in:
parent
5b98819191
commit
f3c5c90d7b
42
PLAN.MD
42
PLAN.MD
|
|
@ -1,28 +1,36 @@
|
||||||
# 프로젝트: Digital Twin 에디터 안정화
|
# 프로젝트: 외부 REST API 커넥션 관리 확장 (POST/Body 지원)
|
||||||
|
|
||||||
## 개요
|
## 개요
|
||||||
|
현재 GET 방식 위주로 구현된 외부 REST API 커넥션 관리 기능을 확장하여, POST, PUT, DELETE 등 다양한 HTTP 메서드와 JSON Request Body를 설정하고 테스트할 수 있도록 개선합니다. 이를 통해 토큰 발급 API나 데이터 전송 API 등 다양한 외부 시스템과의 연동을 지원합니다.
|
||||||
Digital Twin 에디터(`DigitalTwinEditor.tsx`)에서 발생한 런타임 에러(`TypeError: Cannot read properties of undefined`)를 수정하고, 전반적인 안정성을 확보합니다.
|
|
||||||
|
|
||||||
## 핵심 기능
|
## 핵심 기능
|
||||||
|
1. **DB 스키마 확장**: `external_rest_api_connections` 테이블에 `default_method`, `default_body` 컬럼 추가
|
||||||
1. `DigitalTwinEditor` 버그 수정
|
2. **백엔드 로직 개선**:
|
||||||
2. 비동기 함수 입력값 유효성 검증 강화
|
- 커넥션 생성/수정 시 메서드와 바디 정보 저장
|
||||||
3. 외부 DB 연결 상태에 따른 방어 코드 추가
|
- 연결 테스트 시 설정된 메서드와 바디를 사용하여 요청 수행
|
||||||
|
- SSL 인증서 검증 우회 옵션 적용 (내부망/테스트망 지원)
|
||||||
|
3. **프론트엔드 UI 개선**:
|
||||||
|
- 커넥션 설정 모달에 HTTP 메서드 선택(Select) 및 Body 입력(Textarea/JSON Editor) 필드 추가
|
||||||
|
- 테스트 기능에서 Body 데이터 포함하여 요청 전송
|
||||||
|
|
||||||
## 테스트 계획
|
## 테스트 계획
|
||||||
|
### 1단계: 기본 기능 및 DB 마이그레이션
|
||||||
|
- [x] DB 마이그레이션 스크립트 작성 및 실행
|
||||||
|
- [x] 백엔드 타입 정의 수정 (`default_method`, `default_body` 추가)
|
||||||
|
|
||||||
### 1단계: 긴급 버그 수정
|
### 2단계: 백엔드 로직 구현
|
||||||
|
- [x] 커넥션 생성/수정 API 수정 (필드 추가)
|
||||||
|
- [x] 커넥션 상세 조회 API 확인
|
||||||
|
- [x] 연결 테스트 API 수정 (Method, Body 반영하여 요청 전송)
|
||||||
|
|
||||||
- [x] `loadMaterialCountsForLocations` 함수에서 `locaKeys` undefined 체크 추가 (완료)
|
### 3단계: 프론트엔드 구현
|
||||||
- [ ] 에디터 로드 및 객체 조작 시 에러 발생 여부 확인
|
- [x] 커넥션 관리 리스트/모달 UI 수정
|
||||||
|
- [x] 연결 테스트 UI 수정 및 기능 확인
|
||||||
|
|
||||||
### 2단계: 잠재적 문제 점검
|
## 에러 처리 계획
|
||||||
|
- **JSON 파싱 에러**: Body 입력값이 유효한 JSON이 아닐 경우 에러 처리
|
||||||
- [ ] `loadLayout` 등 주요 로딩 함수의 데이터 유효성 검사
|
- **API 호출 에러**: 외부 API 호출 실패 시 상세 로그 기록 및 클라이언트에 에러 메시지 전달
|
||||||
- [ ] `handleToolDragStart`, `handleCanvasDrop` 등 인터랙션 함수의 예외 처리
|
- **SSL 인증 에러**: `rejectUnauthorized: false` 옵션으로 처리 (기존 `RestApiConnector` 활용)
|
||||||
|
|
||||||
## 진행 상태
|
## 진행 상태
|
||||||
|
- [완료] 모든 단계 구현 완료
|
||||||
- [진행중] 1단계 긴급 버그 수정 완료 후 사용자 피드백 대기 중
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,10 @@ router.post(
|
||||||
}
|
}
|
||||||
|
|
||||||
const result =
|
const result =
|
||||||
await ExternalRestApiConnectionService.testConnection(testRequest);
|
await ExternalRestApiConnectionService.testConnection(
|
||||||
|
testRequest,
|
||||||
|
req.user?.companyCode
|
||||||
|
);
|
||||||
|
|
||||||
return res.status(200).json(result);
|
return res.status(200).json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { Pool, QueryResult } from "pg";
|
import { Pool, QueryResult } from "pg";
|
||||||
|
import axios, { AxiosResponse } from "axios";
|
||||||
|
import https from "https";
|
||||||
import { getPool } from "../database/db";
|
import { getPool } from "../database/db";
|
||||||
import logger from "../utils/logger";
|
import logger from "../utils/logger";
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,6 +32,10 @@ export class ExternalRestApiConnectionService {
|
||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
id, connection_name, description, base_url, endpoint_path, default_headers,
|
id, connection_name, description, base_url, endpoint_path, default_headers,
|
||||||
|
default_method,
|
||||||
|
-- DB 스키마의 컬럼명은 default_request_body 기준이고
|
||||||
|
-- 코드에서는 default_body 필드로 사용하기 위해 alias 처리
|
||||||
|
default_request_body AS default_body,
|
||||||
auth_type, auth_config, timeout, retry_count, retry_delay,
|
auth_type, auth_config, timeout, retry_count, retry_delay,
|
||||||
company_code, is_active, created_date, created_by,
|
company_code, is_active, created_date, created_by,
|
||||||
updated_date, updated_by, last_test_date, last_test_result, last_test_message
|
updated_date, updated_by, last_test_date, last_test_result, last_test_message
|
||||||
|
|
@ -129,6 +135,8 @@ export class ExternalRestApiConnectionService {
|
||||||
let query = `
|
let query = `
|
||||||
SELECT
|
SELECT
|
||||||
id, connection_name, description, base_url, endpoint_path, default_headers,
|
id, connection_name, description, base_url, endpoint_path, default_headers,
|
||||||
|
default_method,
|
||||||
|
default_request_body AS default_body,
|
||||||
auth_type, auth_config, timeout, retry_count, retry_delay,
|
auth_type, auth_config, timeout, retry_count, retry_delay,
|
||||||
company_code, is_active, created_date, created_by,
|
company_code, is_active, created_date, created_by,
|
||||||
updated_date, updated_by, last_test_date, last_test_result, last_test_message
|
updated_date, updated_by, last_test_date, last_test_result, last_test_message
|
||||||
|
|
@ -194,9 +202,10 @@ export class ExternalRestApiConnectionService {
|
||||||
const query = `
|
const query = `
|
||||||
INSERT INTO external_rest_api_connections (
|
INSERT INTO external_rest_api_connections (
|
||||||
connection_name, description, base_url, endpoint_path, default_headers,
|
connection_name, description, base_url, endpoint_path, default_headers,
|
||||||
|
default_method, default_request_body,
|
||||||
auth_type, auth_config, timeout, retry_count, retry_delay,
|
auth_type, auth_config, timeout, retry_count, retry_delay,
|
||||||
company_code, is_active, created_by
|
company_code, is_active, created_by
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -206,6 +215,8 @@ export class ExternalRestApiConnectionService {
|
||||||
data.base_url,
|
data.base_url,
|
||||||
data.endpoint_path || null,
|
data.endpoint_path || null,
|
||||||
JSON.stringify(data.default_headers || {}),
|
JSON.stringify(data.default_headers || {}),
|
||||||
|
data.default_method || "GET",
|
||||||
|
data.default_body || null,
|
||||||
data.auth_type,
|
data.auth_type,
|
||||||
encryptedAuthConfig ? JSON.stringify(encryptedAuthConfig) : null,
|
encryptedAuthConfig ? JSON.stringify(encryptedAuthConfig) : null,
|
||||||
data.timeout || 30000,
|
data.timeout || 30000,
|
||||||
|
|
@ -301,6 +312,18 @@ export class ExternalRestApiConnectionService {
|
||||||
paramIndex++;
|
paramIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.default_method !== undefined) {
|
||||||
|
updateFields.push(`default_method = $${paramIndex}`);
|
||||||
|
params.push(data.default_method);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.default_body !== undefined) {
|
||||||
|
updateFields.push(`default_request_body = $${paramIndex}`);
|
||||||
|
params.push(data.default_body);
|
||||||
|
paramIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.auth_type !== undefined) {
|
if (data.auth_type !== undefined) {
|
||||||
updateFields.push(`auth_type = $${paramIndex}`);
|
updateFields.push(`auth_type = $${paramIndex}`);
|
||||||
params.push(data.auth_type);
|
params.push(data.auth_type);
|
||||||
|
|
@ -441,7 +464,8 @@ export class ExternalRestApiConnectionService {
|
||||||
* REST API 연결 테스트 (테스트 요청 데이터 기반)
|
* REST API 연결 테스트 (테스트 요청 데이터 기반)
|
||||||
*/
|
*/
|
||||||
static async testConnection(
|
static async testConnection(
|
||||||
testRequest: RestApiTestRequest
|
testRequest: RestApiTestRequest,
|
||||||
|
userCompanyCode?: string
|
||||||
): Promise<RestApiTestResult> {
|
): Promise<RestApiTestResult> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
|
@ -450,7 +474,78 @@ export class ExternalRestApiConnectionService {
|
||||||
const headers = { ...testRequest.headers };
|
const headers = { ...testRequest.headers };
|
||||||
|
|
||||||
// 인증 헤더 추가
|
// 인증 헤더 추가
|
||||||
if (
|
if (testRequest.auth_type === "db-token") {
|
||||||
|
const cfg = testRequest.auth_config || {};
|
||||||
|
const {
|
||||||
|
dbTableName,
|
||||||
|
dbValueColumn,
|
||||||
|
dbWhereColumn,
|
||||||
|
dbWhereValue,
|
||||||
|
dbHeaderName,
|
||||||
|
dbHeaderTemplate,
|
||||||
|
} = cfg;
|
||||||
|
|
||||||
|
if (!dbTableName || !dbValueColumn) {
|
||||||
|
throw new Error("DB 토큰 설정이 올바르지 않습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userCompanyCode) {
|
||||||
|
throw new Error("DB 토큰 모드에서는 회사 코드가 필요합니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasWhereColumn = !!dbWhereColumn;
|
||||||
|
const hasWhereValue =
|
||||||
|
dbWhereValue !== undefined && dbWhereValue !== null && dbWhereValue !== "";
|
||||||
|
|
||||||
|
// where 컬럼/값은 둘 다 비우거나 둘 다 채워야 함
|
||||||
|
if (hasWhereColumn !== hasWhereValue) {
|
||||||
|
throw new Error(
|
||||||
|
"DB 토큰 설정에서 조건 컬럼과 조건 값은 둘 다 비우거나 둘 다 입력해야 합니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 식별자 검증 (간단한 화이트리스트)
|
||||||
|
const identifierRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
||||||
|
if (
|
||||||
|
!identifierRegex.test(dbTableName) ||
|
||||||
|
!identifierRegex.test(dbValueColumn) ||
|
||||||
|
(hasWhereColumn && !identifierRegex.test(dbWhereColumn as string))
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"DB 토큰 설정에 유효하지 않은 테이블 또는 컬럼명이 포함되어 있습니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = `
|
||||||
|
SELECT ${dbValueColumn} AS token_value
|
||||||
|
FROM ${dbTableName}
|
||||||
|
WHERE company_code = $1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const params: any[] = [userCompanyCode];
|
||||||
|
|
||||||
|
if (hasWhereColumn && hasWhereValue) {
|
||||||
|
sql += ` AND ${dbWhereColumn} = $2`;
|
||||||
|
params.push(dbWhereValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += `
|
||||||
|
ORDER BY updated_date DESC
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tokenResult: QueryResult<any> = await pool.query(sql, params);
|
||||||
|
|
||||||
|
if (tokenResult.rowCount === 0) {
|
||||||
|
throw new Error("DB에서 토큰을 찾을 수 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenValue = tokenResult.rows[0]["token_value"];
|
||||||
|
const headerName = dbHeaderName || "Authorization";
|
||||||
|
const template = dbHeaderTemplate || "Bearer {{value}}";
|
||||||
|
|
||||||
|
headers[headerName] = template.replace("{{value}}", tokenValue);
|
||||||
|
} else if (
|
||||||
testRequest.auth_type === "bearer" &&
|
testRequest.auth_type === "bearer" &&
|
||||||
testRequest.auth_config?.token
|
testRequest.auth_config?.token
|
||||||
) {
|
) {
|
||||||
|
|
@ -493,25 +588,71 @@ export class ExternalRestApiConnectionService {
|
||||||
`REST API 연결 테스트: ${testRequest.method || "GET"} ${url}`
|
`REST API 연결 테스트: ${testRequest.method || "GET"} ${url}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// HTTP 요청 실행
|
// Body 처리
|
||||||
const response = await fetch(url, {
|
let body: any = undefined;
|
||||||
method: testRequest.method || "GET",
|
if (testRequest.body) {
|
||||||
headers,
|
// 이미 문자열이면 그대로, 객체면 JSON 문자열로 변환
|
||||||
signal: AbortSignal.timeout(testRequest.timeout || 30000),
|
if (typeof testRequest.body === "string") {
|
||||||
});
|
body = testRequest.body;
|
||||||
|
} else {
|
||||||
|
body = JSON.stringify(testRequest.body);
|
||||||
|
}
|
||||||
|
|
||||||
const responseTime = Date.now() - startTime;
|
// Content-Type 헤더가 없으면 기본적으로 application/json 추가
|
||||||
let responseData = null;
|
const hasContentType = Object.keys(headers).some(
|
||||||
|
(k) => k.toLowerCase() === "content-type"
|
||||||
try {
|
);
|
||||||
responseData = await response.json();
|
if (!hasContentType) {
|
||||||
} catch {
|
headers["Content-Type"] = "application/json";
|
||||||
// JSON 파싱 실패는 무시 (텍스트 응답일 수 있음)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTP 요청 실행 (배치관리 RestApiConnector와 동일하게 TLS 검증 우회 옵션 적용)
|
||||||
|
const httpsAgent = new https.Agent({
|
||||||
|
// 배치관리와 동일하게, 일부 내부망/자체 서명 인증서를 사용하는 API를 위해
|
||||||
|
// 인증서 검증을 비활성화한다.
|
||||||
|
// 공개 인터넷용 API에는 신중히 사용해야 함.
|
||||||
|
rejectUnauthorized: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestConfig = {
|
||||||
|
url,
|
||||||
|
method: (testRequest.method || "GET") as any,
|
||||||
|
headers,
|
||||||
|
data: body,
|
||||||
|
httpsAgent,
|
||||||
|
timeout: testRequest.timeout || 30000,
|
||||||
|
// 4xx/5xx 도 예외가 아니라 응답 객체로 처리
|
||||||
|
validateStatus: () => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 요청 상세 로그 (민감 정보는 최소화)
|
||||||
|
logger.info(
|
||||||
|
`REST API 연결 테스트 요청 상세: ${JSON.stringify({
|
||||||
|
method: requestConfig.method,
|
||||||
|
url: requestConfig.url,
|
||||||
|
headers: {
|
||||||
|
...requestConfig.headers,
|
||||||
|
// Authorization 헤더는 마스킹
|
||||||
|
Authorization: requestConfig.headers?.Authorization
|
||||||
|
? "***masked***"
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
hasBody: !!body,
|
||||||
|
})}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response: AxiosResponse = await axios.request(requestConfig);
|
||||||
|
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
// axios는 response.data에 이미 파싱된 응답 본문을 담아준다.
|
||||||
|
// JSON이 아니어도 그대로 내려보내서 프론트에서 확인할 수 있게 한다.
|
||||||
|
const responseData = response.data ?? null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: response.ok,
|
success: response.status >= 200 && response.status < 300,
|
||||||
message: response.ok
|
message:
|
||||||
|
response.status >= 200 && response.status < 300
|
||||||
? "연결 성공"
|
? "연결 성공"
|
||||||
: `연결 실패 (${response.status} ${response.statusText})`,
|
: `연결 실패 (${response.status} ${response.statusText})`,
|
||||||
response_time: responseTime,
|
response_time: responseTime,
|
||||||
|
|
@ -552,17 +693,27 @@ export class ExternalRestApiConnectionService {
|
||||||
|
|
||||||
const connection = connectionResult.data;
|
const connection = connectionResult.data;
|
||||||
|
|
||||||
|
// 리스트에서 endpoint를 넘기지 않으면,
|
||||||
|
// 저장된 endpoint_path를 기본 엔드포인트로 사용
|
||||||
|
const effectiveEndpoint =
|
||||||
|
endpoint || connection.endpoint_path || undefined;
|
||||||
|
|
||||||
const testRequest: RestApiTestRequest = {
|
const testRequest: RestApiTestRequest = {
|
||||||
id: connection.id,
|
id: connection.id,
|
||||||
base_url: connection.base_url,
|
base_url: connection.base_url,
|
||||||
endpoint,
|
endpoint: effectiveEndpoint,
|
||||||
|
method: (connection.default_method as any) || "GET", // 기본 메서드 적용
|
||||||
headers: connection.default_headers,
|
headers: connection.default_headers,
|
||||||
|
body: connection.default_body, // 기본 바디 적용
|
||||||
auth_type: connection.auth_type,
|
auth_type: connection.auth_type,
|
||||||
auth_config: connection.auth_config,
|
auth_config: connection.auth_config,
|
||||||
timeout: connection.timeout,
|
timeout: connection.timeout,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.testConnection(testRequest);
|
const result = await this.testConnection(
|
||||||
|
testRequest,
|
||||||
|
connection.company_code
|
||||||
|
);
|
||||||
|
|
||||||
// 테스트 결과 저장
|
// 테스트 결과 저장
|
||||||
await pool.query(
|
await pool.query(
|
||||||
|
|
@ -709,6 +860,7 @@ export class ExternalRestApiConnectionService {
|
||||||
"bearer",
|
"bearer",
|
||||||
"basic",
|
"basic",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
|
"db-token",
|
||||||
];
|
];
|
||||||
if (!validAuthTypes.includes(data.auth_type)) {
|
if (!validAuthTypes.includes(data.auth_type)) {
|
||||||
throw new Error("올바르지 않은 인증 타입입니다.");
|
throw new Error("올바르지 않은 인증 타입입니다.");
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
// 외부 REST API 연결 관리 타입 정의
|
// 외부 REST API 연결 관리 타입 정의
|
||||||
|
|
||||||
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";
|
export type AuthType =
|
||||||
|
| "none"
|
||||||
|
| "api-key"
|
||||||
|
| "bearer"
|
||||||
|
| "basic"
|
||||||
|
| "oauth2"
|
||||||
|
| "db-token";
|
||||||
|
|
||||||
export interface ExternalRestApiConnection {
|
export interface ExternalRestApiConnection {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -9,6 +15,11 @@ export interface ExternalRestApiConnection {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
endpoint_path?: string;
|
endpoint_path?: string;
|
||||||
default_headers: Record<string, string>;
|
default_headers: Record<string, string>;
|
||||||
|
|
||||||
|
// 기본 메서드 및 바디 추가
|
||||||
|
default_method?: string;
|
||||||
|
default_body?: string;
|
||||||
|
|
||||||
auth_type: AuthType;
|
auth_type: AuthType;
|
||||||
auth_config?: {
|
auth_config?: {
|
||||||
// API Key
|
// API Key
|
||||||
|
|
@ -28,6 +39,14 @@ export interface ExternalRestApiConnection {
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
tokenUrl?: string;
|
tokenUrl?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
||||||
|
// DB 기반 토큰 모드
|
||||||
|
dbTableName?: string;
|
||||||
|
dbValueColumn?: string;
|
||||||
|
dbWhereColumn?: string;
|
||||||
|
dbWhereValue?: string;
|
||||||
|
dbHeaderName?: string;
|
||||||
|
dbHeaderTemplate?: string;
|
||||||
};
|
};
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
retry_count?: number;
|
retry_count?: number;
|
||||||
|
|
@ -54,8 +73,9 @@ export interface RestApiTestRequest {
|
||||||
id?: number;
|
id?: number;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
body?: any; // 테스트 요청 바디 추가
|
||||||
auth_type?: AuthType;
|
auth_type?: AuthType;
|
||||||
auth_config?: any;
|
auth_config?: any;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
|
|
@ -76,4 +96,5 @@ export const AUTH_TYPE_OPTIONS = [
|
||||||
{ value: "bearer", label: "Bearer Token" },
|
{ value: "bearer", label: "Bearer Token" },
|
||||||
{ value: "basic", label: "Basic Auth" },
|
{ value: "basic", label: "Basic Auth" },
|
||||||
{ value: "oauth2", label: "OAuth 2.0" },
|
{ value: "oauth2", label: "OAuth 2.0" },
|
||||||
|
{ value: "db-token", label: "DB 토큰" },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ export function AuthenticationConfig({
|
||||||
<SelectItem value="bearer">Bearer Token</SelectItem>
|
<SelectItem value="bearer">Bearer Token</SelectItem>
|
||||||
<SelectItem value="basic">Basic Auth</SelectItem>
|
<SelectItem value="basic">Basic Auth</SelectItem>
|
||||||
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
<SelectItem value="oauth2">OAuth 2.0</SelectItem>
|
||||||
|
<SelectItem value="db-token">DB 토큰</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -192,6 +193,94 @@ export function AuthenticationConfig({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{authType === "db-token" && (
|
||||||
|
<div className="space-y-4 rounded-md border bg-gray-50 p-4">
|
||||||
|
<h4 className="text-sm font-medium">DB 기반 토큰 설정</h4>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-table-name">테이블명</Label>
|
||||||
|
<Input
|
||||||
|
id="db-table-name"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbTableName || ""}
|
||||||
|
onChange={(e) => updateAuthConfig("dbTableName", e.target.value)}
|
||||||
|
placeholder="예: auth_tokens"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-value-column">값 컬럼명</Label>
|
||||||
|
<Input
|
||||||
|
id="db-value-column"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbValueColumn || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAuthConfig("dbValueColumn", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="예: access_token"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-where-column">조건 컬럼명</Label>
|
||||||
|
<Input
|
||||||
|
id="db-where-column"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbWhereColumn || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAuthConfig("dbWhereColumn", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="예: service_name"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-where-value">조건 값</Label>
|
||||||
|
<Input
|
||||||
|
id="db-where-value"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbWhereValue || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAuthConfig("dbWhereValue", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="예: kakao"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-header-name">헤더 이름 (선택)</Label>
|
||||||
|
<Input
|
||||||
|
id="db-header-name"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbHeaderName || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAuthConfig("dbHeaderName", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder="기본값: Authorization"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="db-header-template">
|
||||||
|
헤더 템플릿 (선택, {{value}} 치환)
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="db-header-template"
|
||||||
|
type="text"
|
||||||
|
value={authConfig.dbHeaderTemplate || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateAuthConfig("dbHeaderTemplate", e.target.value)
|
||||||
|
}
|
||||||
|
placeholder='기본값: "Bearer {{value}}"'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
company_code는 현재 로그인한 사용자의 회사 코드로 자동 필터링됩니다.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{authType === "none" && (
|
{authType === "none" && (
|
||||||
<div className="rounded-md border border-dashed p-4 text-center text-sm text-gray-500">
|
<div className="rounded-md border border-dashed p-4 text-center text-sm text-gray-500">
|
||||||
인증이 필요하지 않은 공개 API입니다.
|
인증이 필요하지 않은 공개 API입니다.
|
||||||
|
|
|
||||||
|
|
@ -21,10 +21,13 @@ import {
|
||||||
ExternalRestApiConnection,
|
ExternalRestApiConnection,
|
||||||
AuthType,
|
AuthType,
|
||||||
RestApiTestResult,
|
RestApiTestResult,
|
||||||
|
RestApiTestRequest,
|
||||||
} from "@/lib/api/externalRestApiConnection";
|
} from "@/lib/api/externalRestApiConnection";
|
||||||
import { HeadersManager } from "./HeadersManager";
|
import { HeadersManager } from "./HeadersManager";
|
||||||
import { AuthenticationConfig } from "./AuthenticationConfig";
|
import { AuthenticationConfig } from "./AuthenticationConfig";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
|
||||||
interface RestApiConnectionModalProps {
|
interface RestApiConnectionModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
@ -42,6 +45,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
const [baseUrl, setBaseUrl] = useState("");
|
const [baseUrl, setBaseUrl] = useState("");
|
||||||
const [endpointPath, setEndpointPath] = useState("");
|
const [endpointPath, setEndpointPath] = useState("");
|
||||||
const [defaultHeaders, setDefaultHeaders] = useState<Record<string, string>>({});
|
const [defaultHeaders, setDefaultHeaders] = useState<Record<string, string>>({});
|
||||||
|
const [defaultMethod, setDefaultMethod] = useState("GET");
|
||||||
|
const [defaultBody, setDefaultBody] = useState("");
|
||||||
const [authType, setAuthType] = useState<AuthType>("none");
|
const [authType, setAuthType] = useState<AuthType>("none");
|
||||||
const [authConfig, setAuthConfig] = useState<any>({});
|
const [authConfig, setAuthConfig] = useState<any>({});
|
||||||
const [timeout, setTimeout] = useState(30000);
|
const [timeout, setTimeout] = useState(30000);
|
||||||
|
|
@ -52,6 +57,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
// UI 상태
|
// UI 상태
|
||||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||||
const [testEndpoint, setTestEndpoint] = useState("");
|
const [testEndpoint, setTestEndpoint] = useState("");
|
||||||
|
const [testMethod, setTestMethod] = useState("GET");
|
||||||
|
const [testBody, setTestBody] = useState("");
|
||||||
const [testing, setTesting] = useState(false);
|
const [testing, setTesting] = useState(false);
|
||||||
const [testResult, setTestResult] = useState<RestApiTestResult | null>(null);
|
const [testResult, setTestResult] = useState<RestApiTestResult | null>(null);
|
||||||
const [testRequestUrl, setTestRequestUrl] = useState<string>("");
|
const [testRequestUrl, setTestRequestUrl] = useState<string>("");
|
||||||
|
|
@ -65,12 +72,19 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
setBaseUrl(connection.base_url);
|
setBaseUrl(connection.base_url);
|
||||||
setEndpointPath(connection.endpoint_path || "");
|
setEndpointPath(connection.endpoint_path || "");
|
||||||
setDefaultHeaders(connection.default_headers || {});
|
setDefaultHeaders(connection.default_headers || {});
|
||||||
|
setDefaultMethod(connection.default_method || "GET");
|
||||||
|
setDefaultBody(connection.default_body || "");
|
||||||
setAuthType(connection.auth_type);
|
setAuthType(connection.auth_type);
|
||||||
setAuthConfig(connection.auth_config || {});
|
setAuthConfig(connection.auth_config || {});
|
||||||
setTimeout(connection.timeout || 30000);
|
setTimeout(connection.timeout || 30000);
|
||||||
setRetryCount(connection.retry_count || 0);
|
setRetryCount(connection.retry_count || 0);
|
||||||
setRetryDelay(connection.retry_delay || 1000);
|
setRetryDelay(connection.retry_delay || 1000);
|
||||||
setIsActive(connection.is_active === "Y");
|
setIsActive(connection.is_active === "Y");
|
||||||
|
|
||||||
|
// 테스트 초기값 설정
|
||||||
|
setTestEndpoint("");
|
||||||
|
setTestMethod(connection.default_method || "GET");
|
||||||
|
setTestBody(connection.default_body || "");
|
||||||
} else {
|
} else {
|
||||||
// 초기화
|
// 초기화
|
||||||
setConnectionName("");
|
setConnectionName("");
|
||||||
|
|
@ -78,16 +92,22 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
setBaseUrl("");
|
setBaseUrl("");
|
||||||
setEndpointPath("");
|
setEndpointPath("");
|
||||||
setDefaultHeaders({ "Content-Type": "application/json" });
|
setDefaultHeaders({ "Content-Type": "application/json" });
|
||||||
|
setDefaultMethod("GET");
|
||||||
|
setDefaultBody("");
|
||||||
setAuthType("none");
|
setAuthType("none");
|
||||||
setAuthConfig({});
|
setAuthConfig({});
|
||||||
setTimeout(30000);
|
setTimeout(30000);
|
||||||
setRetryCount(0);
|
setRetryCount(0);
|
||||||
setRetryDelay(1000);
|
setRetryDelay(1000);
|
||||||
setIsActive(true);
|
setIsActive(true);
|
||||||
|
|
||||||
|
// 테스트 초기값 설정
|
||||||
|
setTestEndpoint("");
|
||||||
|
setTestMethod("GET");
|
||||||
|
setTestBody("");
|
||||||
}
|
}
|
||||||
|
|
||||||
setTestResult(null);
|
setTestResult(null);
|
||||||
setTestEndpoint("");
|
|
||||||
setTestRequestUrl("");
|
setTestRequestUrl("");
|
||||||
}, [connection, isOpen]);
|
}, [connection, isOpen]);
|
||||||
|
|
||||||
|
|
@ -111,14 +131,18 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
setTestRequestUrl(fullUrl);
|
setTestRequestUrl(fullUrl);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await ExternalRestApiConnectionAPI.testConnection({
|
const testRequest: RestApiTestRequest = {
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
endpoint: testEndpoint || undefined,
|
endpoint: testEndpoint || undefined,
|
||||||
|
method: testMethod as any,
|
||||||
headers: defaultHeaders,
|
headers: defaultHeaders,
|
||||||
|
body: testBody ? JSON.parse(testBody) : undefined,
|
||||||
auth_type: authType,
|
auth_type: authType,
|
||||||
auth_config: authConfig,
|
auth_config: authConfig,
|
||||||
timeout,
|
timeout,
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const result = await ExternalRestApiConnectionAPI.testConnection(testRequest);
|
||||||
|
|
||||||
setTestResult(result);
|
setTestResult(result);
|
||||||
|
|
||||||
|
|
@ -178,6 +202,20 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// JSON 유효성 검증
|
||||||
|
if (defaultBody && defaultMethod !== "GET" && defaultMethod !== "DELETE") {
|
||||||
|
try {
|
||||||
|
JSON.parse(defaultBody);
|
||||||
|
} catch {
|
||||||
|
toast({
|
||||||
|
title: "입력 오류",
|
||||||
|
description: "기본 Body가 올바른 JSON 형식이 아닙니다.",
|
||||||
|
variant: "destructive",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -187,6 +225,8 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
endpoint_path: endpointPath || undefined,
|
endpoint_path: endpointPath || undefined,
|
||||||
default_headers: defaultHeaders,
|
default_headers: defaultHeaders,
|
||||||
|
default_method: defaultMethod,
|
||||||
|
default_body: defaultBody || undefined,
|
||||||
auth_type: authType,
|
auth_type: authType,
|
||||||
auth_config: authType === "none" ? undefined : authConfig,
|
auth_config: authType === "none" ? undefined : authConfig,
|
||||||
timeout,
|
timeout,
|
||||||
|
|
@ -262,12 +302,28 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
<Label htmlFor="base-url">
|
<Label htmlFor="base-url">
|
||||||
기본 URL <span className="text-destructive">*</span>
|
기본 URL <span className="text-destructive">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<div className="flex gap-2">
|
||||||
id="base-url"
|
<Select value={defaultMethod} onValueChange={setDefaultMethod}>
|
||||||
value={baseUrl}
|
<SelectTrigger className="w-[100px]">
|
||||||
onChange={(e) => setBaseUrl(e.target.value)}
|
<SelectValue placeholder="Method" />
|
||||||
placeholder="https://api.example.com"
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">GET</SelectItem>
|
||||||
|
<SelectItem value="POST">POST</SelectItem>
|
||||||
|
<SelectItem value="PUT">PUT</SelectItem>
|
||||||
|
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||||
|
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
id="base-url"
|
||||||
|
value={baseUrl}
|
||||||
|
onChange={(e) => setBaseUrl(e.target.value)}
|
||||||
|
placeholder="https://api.example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p className="text-muted-foreground text-xs">
|
<p className="text-muted-foreground text-xs">
|
||||||
도메인 부분만 입력하세요 (예: https://apihub.kma.go.kr)
|
도메인 부분만 입력하세요 (예: https://apihub.kma.go.kr)
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -286,6 +342,21 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 기본 Body (POST, PUT, PATCH일 때만 표시) */}
|
||||||
|
{(defaultMethod === "POST" || defaultMethod === "PUT" || defaultMethod === "PATCH") && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="default-body">기본 Request Body (JSON)</Label>
|
||||||
|
<Textarea
|
||||||
|
id="default-body"
|
||||||
|
value={defaultBody}
|
||||||
|
onChange={(e) => setDefaultBody(e.target.value)}
|
||||||
|
placeholder='{"key": "value"}'
|
||||||
|
className="font-mono text-xs"
|
||||||
|
rows={5}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch id="is-active" checked={isActive} onCheckedChange={setIsActive} />
|
<Switch id="is-active" checked={isActive} onCheckedChange={setIsActive} />
|
||||||
<Label htmlFor="is-active" className="cursor-pointer">
|
<Label htmlFor="is-active" className="cursor-pointer">
|
||||||
|
|
@ -370,13 +441,45 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
<h3 className="text-sm font-semibold">연결 테스트</h3>
|
<h3 className="text-sm font-semibold">연결 테스트</h3>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="test-endpoint">테스트 엔드포인트 (선택)</Label>
|
<Label htmlFor="test-endpoint">테스트 설정</Label>
|
||||||
<Input
|
<div className="flex gap-2 mb-2">
|
||||||
id="test-endpoint"
|
<Select value={testMethod} onValueChange={setTestMethod}>
|
||||||
value={testEndpoint}
|
<SelectTrigger className="w-[100px]">
|
||||||
onChange={(e) => setTestEndpoint(e.target.value)}
|
<SelectValue placeholder="Method" />
|
||||||
placeholder="엔드포인트 또는 빈칸(기본 URL만 테스트)"
|
</SelectTrigger>
|
||||||
/>
|
<SelectContent>
|
||||||
|
<SelectItem value="GET">GET</SelectItem>
|
||||||
|
<SelectItem value="POST">POST</SelectItem>
|
||||||
|
<SelectItem value="PUT">PUT</SelectItem>
|
||||||
|
<SelectItem value="DELETE">DELETE</SelectItem>
|
||||||
|
<SelectItem value="PATCH">PATCH</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<div className="flex-1">
|
||||||
|
<Input
|
||||||
|
id="test-endpoint"
|
||||||
|
value={testEndpoint}
|
||||||
|
onChange={(e) => setTestEndpoint(e.target.value)}
|
||||||
|
placeholder="엔드포인트 (예: /users/1)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(testMethod === "POST" || testMethod === "PUT" || testMethod === "PATCH") && (
|
||||||
|
<div className="mt-2">
|
||||||
|
<Label htmlFor="test-body" className="text-xs text-muted-foreground mb-1 block">
|
||||||
|
Test Request Body (JSON)
|
||||||
|
</Label>
|
||||||
|
<Textarea
|
||||||
|
id="test-body"
|
||||||
|
value={testBody}
|
||||||
|
onChange={(e) => setTestBody(e.target.value)}
|
||||||
|
placeholder='{"test": "data"}'
|
||||||
|
className="font-mono text-xs"
|
||||||
|
rows={3}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button type="button" variant="outline" onClick={handleTest} disabled={testing}>
|
<Button type="button" variant="outline" onClick={handleTest} disabled={testing}>
|
||||||
|
|
@ -388,10 +491,22 @@ export function RestApiConnectionModal({ isOpen, onClose, onSave, connection }:
|
||||||
{testRequestUrl && (
|
{testRequestUrl && (
|
||||||
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
|
<div className="bg-muted/30 space-y-3 rounded-md border p-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground mb-1 text-xs font-medium">테스트 요청 URL</div>
|
<div className="text-muted-foreground mb-1 text-xs font-medium">테스트 요청</div>
|
||||||
<code className="text-foreground block text-xs break-all">GET {testRequestUrl}</code>
|
<div className="flex items-center gap-2 mb-1">
|
||||||
|
<Badge variant="outline">{testMethod}</Badge>
|
||||||
|
<code className="text-foreground text-xs break-all">{testRequestUrl}</code>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{testBody && (testMethod === "POST" || testMethod === "PUT" || testMethod === "PATCH") && (
|
||||||
|
<div>
|
||||||
|
<div className="text-muted-foreground mb-1 text-xs font-medium">Request Body</div>
|
||||||
|
<pre className="bg-muted p-2 rounded text-xs overflow-auto max-h-[100px]">
|
||||||
|
{testBody}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{Object.keys(defaultHeaders).length > 0 && (
|
{Object.keys(defaultHeaders).length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground mb-1 text-xs font-medium">요청 헤더</div>
|
<div className="text-muted-foreground mb-1 text-xs font-medium">요청 헤더</div>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { apiClient } from "./client";
|
import { apiClient } from "./client";
|
||||||
|
|
||||||
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2";
|
export type AuthType = "none" | "api-key" | "bearer" | "basic" | "oauth2" | "db-token";
|
||||||
|
|
||||||
export interface ExternalRestApiConnection {
|
export interface ExternalRestApiConnection {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
@ -11,18 +11,34 @@ export interface ExternalRestApiConnection {
|
||||||
base_url: string;
|
base_url: string;
|
||||||
endpoint_path?: string;
|
endpoint_path?: string;
|
||||||
default_headers: Record<string, string>;
|
default_headers: Record<string, string>;
|
||||||
|
// 기본 메서드 및 바디 추가
|
||||||
|
default_method?: string;
|
||||||
|
default_body?: string;
|
||||||
|
|
||||||
auth_type: AuthType;
|
auth_type: AuthType;
|
||||||
auth_config?: {
|
auth_config?: {
|
||||||
|
// API Key
|
||||||
keyLocation?: "header" | "query";
|
keyLocation?: "header" | "query";
|
||||||
keyName?: string;
|
keyName?: string;
|
||||||
keyValue?: string;
|
keyValue?: string;
|
||||||
|
// Bearer Token
|
||||||
token?: string;
|
token?: string;
|
||||||
|
// Basic Auth
|
||||||
username?: string;
|
username?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
|
// OAuth2
|
||||||
clientId?: string;
|
clientId?: string;
|
||||||
clientSecret?: string;
|
clientSecret?: string;
|
||||||
tokenUrl?: string;
|
tokenUrl?: string;
|
||||||
accessToken?: string;
|
accessToken?: string;
|
||||||
|
|
||||||
|
// DB 기반 토큰 모드
|
||||||
|
dbTableName?: string;
|
||||||
|
dbValueColumn?: string;
|
||||||
|
dbWhereColumn?: string;
|
||||||
|
dbWhereValue?: string;
|
||||||
|
dbHeaderName?: string;
|
||||||
|
dbHeaderTemplate?: string;
|
||||||
};
|
};
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
retry_count?: number;
|
retry_count?: number;
|
||||||
|
|
@ -49,9 +65,11 @@ export interface RestApiTestRequest {
|
||||||
id?: number;
|
id?: number;
|
||||||
base_url: string;
|
base_url: string;
|
||||||
endpoint?: string;
|
endpoint?: string;
|
||||||
method?: "GET" | "POST" | "PUT" | "DELETE";
|
method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
||||||
headers?: Record<string, string>;
|
headers?: Record<string, string>;
|
||||||
|
body?: unknown; // 테스트 요청 바디 추가
|
||||||
auth_type?: AuthType;
|
auth_type?: AuthType;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
auth_config?: any;
|
auth_config?: any;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +79,7 @@ export interface RestApiTestResult {
|
||||||
message: string;
|
message: string;
|
||||||
response_time?: number;
|
response_time?: number;
|
||||||
status_code?: number;
|
status_code?: number;
|
||||||
response_data?: any;
|
response_data?: unknown;
|
||||||
error_details?: string;
|
error_details?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +89,7 @@ export interface ApiResponse<T> {
|
||||||
message?: string;
|
message?: string;
|
||||||
error?: {
|
error?: {
|
||||||
code: string;
|
code: string;
|
||||||
details?: any;
|
details?: unknown;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +202,7 @@ export class ExternalRestApiConnectionAPI {
|
||||||
{ value: "bearer", label: "Bearer Token" },
|
{ value: "bearer", label: "Bearer Token" },
|
||||||
{ value: "basic", label: "Basic Auth" },
|
{ value: "basic", label: "Basic Auth" },
|
||||||
{ value: "oauth2", label: "OAuth 2.0" },
|
{ value: "oauth2", label: "OAuth 2.0" },
|
||||||
|
{ value: "db-token", label: "DB 토큰" },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue