ERP-node/backend-node/src/services/externalRestApiConnectionSe...

893 lines
26 KiB
TypeScript
Raw Normal View History

2025-10-21 10:59:15 +09:00
import { Pool, QueryResult } from "pg";
import axios, { AxiosResponse } from "axios";
import https from "https";
2025-10-21 10:59:15 +09:00
import { getPool } from "../database/db";
import logger from "../utils/logger";
import {
ExternalRestApiConnection,
ExternalRestApiConnectionFilter,
RestApiTestRequest,
RestApiTestResult,
AuthType,
} from "../types/externalRestApiTypes";
import { ApiResponse } from "../types/common";
import crypto from "crypto";
const pool = getPool();
// 암호화 설정
const ENCRYPTION_KEY =
process.env.DB_PASSWORD_SECRET || "default-secret-key-change-in-production";
const ALGORITHM = "aes-256-gcm";
export class ExternalRestApiConnectionService {
/**
* REST API
*/
static async getConnections(
2025-10-28 11:54:44 +09:00
filter: ExternalRestApiConnectionFilter = {},
userCompanyCode?: string
2025-10-21 10:59:15 +09:00
): Promise<ApiResponse<ExternalRestApiConnection[]>> {
try {
let query = `
SELECT
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,
2025-10-21 10:59:15 +09:00
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_date, created_by,
updated_date, updated_by, last_test_date, last_test_result, last_test_message
FROM external_rest_api_connections
WHERE 1=1
`;
const params: any[] = [];
let paramIndex = 1;
2025-10-28 11:54:44 +09:00
// 회사별 필터링 (최고 관리자가 아닌 경우 필수)
if (userCompanyCode && userCompanyCode !== "*") {
2025-10-21 10:59:15 +09:00
query += ` AND company_code = $${paramIndex}`;
2025-10-28 11:54:44 +09:00
params.push(userCompanyCode);
2025-10-21 10:59:15 +09:00
paramIndex++;
2025-10-28 11:54:44 +09:00
logger.info(`회사별 REST API 연결 필터링: ${userCompanyCode}`);
} else if (userCompanyCode === "*") {
logger.info(`최고 관리자: 모든 REST API 연결 조회`);
// 필터가 있으면 적용
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
} else {
// userCompanyCode가 없는 경우 (하위 호환성)
if (filter.company_code) {
query += ` AND company_code = $${paramIndex}`;
params.push(filter.company_code);
paramIndex++;
}
2025-10-21 10:59:15 +09:00
}
// 활성 상태 필터
if (filter.is_active) {
query += ` AND is_active = $${paramIndex}`;
params.push(filter.is_active);
paramIndex++;
}
// 인증 타입 필터
if (filter.auth_type) {
query += ` AND auth_type = $${paramIndex}`;
params.push(filter.auth_type);
paramIndex++;
}
// 검색어 필터 (연결명, 설명, URL)
if (filter.search) {
query += ` AND (
connection_name ILIKE $${paramIndex} OR
description ILIKE $${paramIndex} OR
base_url ILIKE $${paramIndex}
)`;
params.push(`%${filter.search}%`);
paramIndex++;
}
query += ` ORDER BY created_date DESC`;
const result: QueryResult<any> = await pool.query(query, params);
// 민감 정보 복호화
const connections = result.rows.map((row: any) => ({
...row,
auth_config: row.auth_config
? this.decryptSensitiveData(row.auth_config)
: null,
}));
return {
success: true,
data: connections,
message: `${connections.length}개의 연결을 조회했습니다.`,
};
} catch (error) {
logger.error("REST API 연결 목록 조회 오류:", error);
return {
success: false,
message: "연결 목록 조회에 실패했습니다.",
error: {
code: "FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API
*/
static async getConnectionById(
2025-10-28 11:54:44 +09:00
id: number,
userCompanyCode?: string
2025-10-21 10:59:15 +09:00
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
2025-10-28 11:54:44 +09:00
let query = `
2025-10-21 10:59:15 +09:00
SELECT
id, connection_name, description, base_url, endpoint_path, default_headers,
default_method,
default_request_body AS default_body,
2025-10-21 10:59:15 +09:00
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_date, created_by,
updated_date, updated_by, last_test_date, last_test_result, last_test_message
FROM external_rest_api_connections
WHERE id = $1
`;
2025-10-28 11:54:44 +09:00
const params: any[] = [id];
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
const result: QueryResult<any> = await pool.query(query, params);
2025-10-21 10:59:15 +09:00
if (result.rows.length === 0) {
return {
success: false,
2025-10-28 11:54:44 +09:00
message: "연결을 찾을 수 없거나 권한이 없습니다.",
2025-10-21 10:59:15 +09:00
};
}
const connection = result.rows[0];
connection.auth_config = connection.auth_config
? this.decryptSensitiveData(connection.auth_config)
: null;
return {
success: true,
data: connection,
message: "연결을 조회했습니다.",
};
} catch (error) {
logger.error("REST API 연결 상세 조회 오류:", error);
return {
success: false,
message: "연결 조회에 실패했습니다.",
error: {
code: "FETCH_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API
*/
static async createConnection(
data: ExternalRestApiConnection
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
// 유효성 검증
this.validateConnectionData(data);
// 민감 정보 암호화
const encryptedAuthConfig = data.auth_config
? this.encryptSensitiveData(data.auth_config)
: null;
const query = `
INSERT INTO external_rest_api_connections (
connection_name, description, base_url, endpoint_path, default_headers,
default_method, default_request_body,
2025-10-21 10:59:15 +09:00
auth_type, auth_config, timeout, retry_count, retry_delay,
company_code, is_active, created_by
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
2025-10-21 10:59:15 +09:00
RETURNING *
`;
const params = [
data.connection_name,
data.description || null,
data.base_url,
data.endpoint_path || null,
2025-10-21 10:59:15 +09:00
JSON.stringify(data.default_headers || {}),
data.default_method || "GET",
data.default_body || null,
2025-10-21 10:59:15 +09:00
data.auth_type,
encryptedAuthConfig ? JSON.stringify(encryptedAuthConfig) : null,
data.timeout || 30000,
data.retry_count || 0,
data.retry_delay || 1000,
data.company_code || "*",
data.is_active || "Y",
data.created_by || "system",
];
const result: QueryResult<any> = await pool.query(query, params);
logger.info(`REST API 연결 생성 성공: ${data.connection_name}`);
return {
success: true,
data: result.rows[0],
message: "연결이 생성되었습니다.",
};
} catch (error: any) {
logger.error("REST API 연결 생성 오류:", error);
// 중복 키 오류 처리
if (error.code === "23505") {
return {
success: false,
message: "이미 존재하는 연결명입니다.",
};
}
return {
success: false,
message: "연결 생성에 실패했습니다.",
error: {
code: "CREATE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API
*/
static async updateConnection(
id: number,
2025-10-28 11:54:44 +09:00
data: Partial<ExternalRestApiConnection>,
userCompanyCode?: string
2025-10-21 10:59:15 +09:00
): Promise<ApiResponse<ExternalRestApiConnection>> {
try {
2025-10-28 11:54:44 +09:00
// 기존 연결 확인 (회사 코드로 권한 체크)
const existing = await this.getConnectionById(id, userCompanyCode);
2025-10-21 10:59:15 +09:00
if (!existing.success) {
return existing;
}
// 민감 정보 암호화
const encryptedAuthConfig = data.auth_config
? this.encryptSensitiveData(data.auth_config)
: undefined;
const updateFields: string[] = [];
const params: any[] = [];
let paramIndex = 1;
if (data.connection_name !== undefined) {
updateFields.push(`connection_name = $${paramIndex}`);
params.push(data.connection_name);
paramIndex++;
}
if (data.description !== undefined) {
updateFields.push(`description = $${paramIndex}`);
params.push(data.description);
paramIndex++;
}
if (data.base_url !== undefined) {
updateFields.push(`base_url = $${paramIndex}`);
params.push(data.base_url);
paramIndex++;
}
if (data.endpoint_path !== undefined) {
updateFields.push(`endpoint_path = $${paramIndex}`);
params.push(data.endpoint_path);
paramIndex++;
}
2025-10-21 10:59:15 +09:00
if (data.default_headers !== undefined) {
updateFields.push(`default_headers = $${paramIndex}`);
params.push(JSON.stringify(data.default_headers));
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++;
}
2025-10-21 10:59:15 +09:00
if (data.auth_type !== undefined) {
updateFields.push(`auth_type = $${paramIndex}`);
params.push(data.auth_type);
paramIndex++;
}
if (encryptedAuthConfig !== undefined) {
updateFields.push(`auth_config = $${paramIndex}`);
params.push(JSON.stringify(encryptedAuthConfig));
paramIndex++;
}
if (data.timeout !== undefined) {
updateFields.push(`timeout = $${paramIndex}`);
params.push(data.timeout);
paramIndex++;
}
if (data.retry_count !== undefined) {
updateFields.push(`retry_count = $${paramIndex}`);
params.push(data.retry_count);
paramIndex++;
}
if (data.retry_delay !== undefined) {
updateFields.push(`retry_delay = $${paramIndex}`);
params.push(data.retry_delay);
paramIndex++;
}
if (data.is_active !== undefined) {
updateFields.push(`is_active = $${paramIndex}`);
params.push(data.is_active);
paramIndex++;
}
if (data.updated_by !== undefined) {
updateFields.push(`updated_by = $${paramIndex}`);
params.push(data.updated_by);
paramIndex++;
}
updateFields.push(`updated_date = NOW()`);
params.push(id);
const query = `
UPDATE external_rest_api_connections
SET ${updateFields.join(", ")}
WHERE id = $${paramIndex}
RETURNING *
`;
const result: QueryResult<any> = await pool.query(query, params);
logger.info(`REST API 연결 수정 성공: ID ${id}`);
return {
success: true,
data: result.rows[0],
message: "연결이 수정되었습니다.",
};
} catch (error: any) {
logger.error("REST API 연결 수정 오류:", error);
if (error.code === "23505") {
return {
success: false,
message: "이미 존재하는 연결명입니다.",
};
}
return {
success: false,
message: "연결 수정에 실패했습니다.",
error: {
code: "UPDATE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API
*/
2025-10-28 11:54:44 +09:00
static async deleteConnection(
id: number,
userCompanyCode?: string
): Promise<ApiResponse<void>> {
2025-10-21 10:59:15 +09:00
try {
2025-10-28 11:54:44 +09:00
let query = `
2025-10-21 10:59:15 +09:00
DELETE FROM external_rest_api_connections
WHERE id = $1
`;
2025-10-28 11:54:44 +09:00
const params: any[] = [id];
// 회사별 필터링 (최고 관리자가 아닌 경우)
if (userCompanyCode && userCompanyCode !== "*") {
query += ` AND company_code = $2`;
params.push(userCompanyCode);
}
query += ` RETURNING connection_name`;
const result: QueryResult<any> = await pool.query(query, params);
2025-10-21 10:59:15 +09:00
if (result.rows.length === 0) {
return {
success: false,
2025-10-28 11:54:44 +09:00
message: "연결을 찾을 수 없거나 권한이 없습니다.",
2025-10-21 10:59:15 +09:00
};
}
2025-10-28 11:54:44 +09:00
logger.info(
`REST API 연결 삭제 성공: ${result.rows[0].connection_name} (회사: ${userCompanyCode || "전체"})`
);
2025-10-21 10:59:15 +09:00
return {
success: true,
message: "연결이 삭제되었습니다.",
};
} catch (error) {
logger.error("REST API 연결 삭제 오류:", error);
return {
success: false,
message: "연결 삭제에 실패했습니다.",
error: {
code: "DELETE_ERROR",
details: error instanceof Error ? error.message : "알 수 없는 오류",
},
};
}
}
/**
* REST API ( )
*/
static async testConnection(
testRequest: RestApiTestRequest,
userCompanyCode?: string
2025-10-21 10:59:15 +09:00
): Promise<RestApiTestResult> {
const startTime = Date.now();
try {
// 헤더 구성
const headers = { ...testRequest.headers };
// 인증 헤더 추가
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 (
2025-10-21 10:59:15 +09:00
testRequest.auth_type === "bearer" &&
testRequest.auth_config?.token
) {
headers["Authorization"] = `Bearer ${testRequest.auth_config.token}`;
} else if (testRequest.auth_type === "basic" && testRequest.auth_config) {
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" &&
testRequest.auth_config
) {
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.endpoint.startsWith("/")
? `${testRequest.base_url}${testRequest.endpoint}`
: `${testRequest.base_url}/${testRequest.endpoint}`;
}
// API Key가 쿼리에 있는 경우
if (
testRequest.auth_type === "api-key" &&
testRequest.auth_config?.keyLocation === "query" &&
testRequest.auth_config?.keyName &&
testRequest.auth_config?.keyValue
) {
const separator = url.includes("?") ? "&" : "?";
url = `${url}${separator}${testRequest.auth_config.keyName}=${testRequest.auth_config.keyValue}`;
}
logger.info(
`REST API 연결 테스트: ${testRequest.method || "GET"} ${url}`
);
// Body 처리
let body: any = undefined;
if (testRequest.body) {
// 이미 문자열이면 그대로, 객체면 JSON 문자열로 변환
if (typeof testRequest.body === "string") {
body = testRequest.body;
} else {
body = JSON.stringify(testRequest.body);
}
// Content-Type 헤더가 없으면 기본적으로 application/json 추가
const hasContentType = Object.keys(headers).some(
(k) => k.toLowerCase() === "content-type"
);
if (!hasContentType) {
headers["Content-Type"] = "application/json";
}
}
// HTTP 요청 실행 (배치관리 RestApiConnector와 동일하게 TLS 검증 우회 옵션 적용)
const httpsAgent = new https.Agent({
// 배치관리와 동일하게, 일부 내부망/자체 서명 인증서를 사용하는 API를 위해
// 인증서 검증을 비활성화한다.
// 공개 인터넷용 API에는 신중히 사용해야 함.
rejectUnauthorized: false,
2025-10-21 10:59:15 +09:00
});
const requestConfig = {
url,
method: (testRequest.method || "GET") as any,
headers,
data: body,
httpsAgent,
timeout: testRequest.timeout || 30000,
// 4xx/5xx 도 예외가 아니라 응답 객체로 처리
validateStatus: () => true,
};
2025-10-21 10:59:15 +09:00
// 요청 상세 로그 (민감 정보는 최소화)
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;
2025-10-21 10:59:15 +09:00
return {
success: response.status >= 200 && response.status < 300,
message:
response.status >= 200 && response.status < 300
2025-10-21 10:59:15 +09:00
? "연결 성공"
: `연결 실패 (${response.status} ${response.statusText})`,
response_time: responseTime,
status_code: response.status,
response_data: responseData,
};
} catch (error) {
const responseTime = Date.now() - startTime;
logger.error("REST API 연결 테스트 오류:", error);
return {
success: false,
message: "연결 실패",
response_time: responseTime,
error_details:
error instanceof Error ? error.message : "알 수 없는 오류",
};
}
}
/**
* REST API (ID )
*/
static async testConnectionById(
id: number,
endpoint?: string
): Promise<RestApiTestResult> {
try {
const connectionResult = await this.getConnectionById(id);
if (!connectionResult.success || !connectionResult.data) {
return {
success: false,
message: "연결을 찾을 수 없습니다.",
};
}
const connection = connectionResult.data;
// 리스트에서 endpoint를 넘기지 않으면,
// 저장된 endpoint_path를 기본 엔드포인트로 사용
const effectiveEndpoint =
endpoint || connection.endpoint_path || undefined;
2025-10-21 10:59:15 +09:00
const testRequest: RestApiTestRequest = {
id: connection.id,
base_url: connection.base_url,
endpoint: effectiveEndpoint,
method: (connection.default_method as any) || "GET", // 기본 메서드 적용
2025-10-21 10:59:15 +09:00
headers: connection.default_headers,
body: connection.default_body, // 기본 바디 적용
2025-10-21 10:59:15 +09:00
auth_type: connection.auth_type,
auth_config: connection.auth_config,
timeout: connection.timeout,
};
const result = await this.testConnection(
testRequest,
connection.company_code
);
2025-10-21 10:59:15 +09:00
// 테스트 결과 저장
await pool.query(
`
UPDATE external_rest_api_connections
SET
last_test_date = NOW(),
last_test_result = $1,
last_test_message = $2
WHERE id = $3
`,
[result.success ? "Y" : "N", result.message, id]
);
return result;
} catch (error) {
logger.error("REST API 연결 테스트 (ID) 오류:", error);
const errorMessage =
error instanceof Error ? error.message : "알 수 없는 오류";
// 예외가 발생한 경우에도 마지막 테스트 결과를 실패로 기록
try {
await pool.query(
`
UPDATE external_rest_api_connections
SET
last_test_date = NOW(),
last_test_result = $1,
last_test_message = $2
WHERE id = $3
`,
["N", errorMessage, id]
);
} catch (updateError) {
logger.error(
"REST API 연결 테스트 (ID) 오류 기록 실패:",
updateError
);
}
2025-10-21 10:59:15 +09:00
return {
success: false,
message: "연결 테스트에 실패했습니다.",
error_details: errorMessage,
2025-10-21 10:59:15 +09:00
};
}
}
/**
*
*/
private static encryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const encrypted = { ...authConfig };
// 암호화 대상 필드
if (encrypted.keyValue) {
encrypted.keyValue = this.encrypt(encrypted.keyValue);
}
if (encrypted.token) {
encrypted.token = this.encrypt(encrypted.token);
}
if (encrypted.password) {
encrypted.password = this.encrypt(encrypted.password);
}
if (encrypted.clientSecret) {
encrypted.clientSecret = this.encrypt(encrypted.clientSecret);
}
return encrypted;
}
/**
*
*/
private static decryptSensitiveData(authConfig: any): any {
if (!authConfig) return null;
const decrypted = { ...authConfig };
// 복호화 대상 필드
try {
if (decrypted.keyValue) {
decrypted.keyValue = this.decrypt(decrypted.keyValue);
}
if (decrypted.token) {
decrypted.token = this.decrypt(decrypted.token);
}
if (decrypted.password) {
decrypted.password = this.decrypt(decrypted.password);
}
if (decrypted.clientSecret) {
decrypted.clientSecret = this.decrypt(decrypted.clientSecret);
}
} catch (error) {
logger.warn("민감 정보 복호화 실패 (암호화되지 않은 데이터일 수 있음)");
}
return decrypted;
}
/**
*
*/
private static encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
}
/**
*
*/
private static decrypt(text: string): string {
const parts = text.split(":");
if (parts.length !== 3) {
// 암호화되지 않은 데이터
return text;
}
const iv = Buffer.from(parts[0], "hex");
const authTag = Buffer.from(parts[1], "hex");
const encryptedText = parts[2];
const key = crypto.scryptSync(ENCRYPTION_KEY, "salt", 32);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encryptedText, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
/**
*
*/
private static validateConnectionData(data: ExternalRestApiConnection): void {
if (!data.connection_name || data.connection_name.trim() === "") {
throw new Error("연결명은 필수입니다.");
}
if (!data.base_url || data.base_url.trim() === "") {
throw new Error("기본 URL은 필수입니다.");
}
// URL 형식 검증
try {
new URL(data.base_url);
} catch {
throw new Error("올바른 URL 형식이 아닙니다.");
}
// 인증 타입 검증
const validAuthTypes: AuthType[] = [
"none",
"api-key",
"bearer",
"basic",
"oauth2",
"db-token",
2025-10-21 10:59:15 +09:00
];
if (!validAuthTypes.includes(data.auth_type)) {
throw new Error("올바르지 않은 인증 타입입니다.");
}
}
}