54a8f97f78
배치 생성 흐름 검증 중 발견된 4가지 이슈 일괄 정정.
1) BatchManagementService.previewRestApiData — camelCase 키 명시 remap
직전 커밋(b752de23)에서 convertCamelToSnake() 호출 추가했지만 그 함수의 실제 구현이
batch_configs 전용 snake→snake remap 이라 사실상 no-op. 프론트의 apiUrl 등 camelCase
가 변환되지 않아 isBlank(api_url)=true → 400.
→ previewRestApiData 진입부에 직접 remap (apiUrl/apiKey/requestBody/dataArrayPath/
paramType/paramName/paramValue/paramSource/authServiceName 9개 키).
2) batchManagement.ts.previewRestApiData — 응답 totalCount 정규화
백엔드는 total_count (snake_case) 로 응답하는데 프론트는 result.totalCount 로 읽음.
토스트가 "2개 필드, undefined개 레코드" 로 표시됨.
→ 응답 normalize: total_count ?? totalCount ?? 0.
3) batch-management-new/page.tsx — root h-full overflow-y-auto
페이지 root 가 overflow 처리가 없어 FROM/TO 카드 아래의 매핑 카드가 탭 컨테이너
밖으로 잘려 사용자가 못 봄.
→ root div 에 h-full overflow-y-auto 추가.
4) RestApiToDbMappingCard — v5 컨벤션에 맞춘 컴팩트화
다른 메뉴들과 톤 통일. CardHeader 패딩 축소, 폰트 size 일괄 다운,
행 padding p-3 → p-2, Select/Input h-9 → h-7 text-xs, 순서 원형 h-6 → h-5,
카드 내부 height 360 → 300px, 매핑 추가 버튼/삭제 버튼 컴팩트.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
237 lines
6.6 KiB
TypeScript
237 lines
6.6 KiB
TypeScript
// 배치관리 전용 API 클라이언트 (기존 소스와 완전 분리)
|
|
// 작성일: 2024-12-24
|
|
|
|
import { apiClient } from "./client";
|
|
|
|
// 배치관리 전용 타입 정의
|
|
export interface BatchConnectionInfo {
|
|
type: "internal" | "external";
|
|
id?: number;
|
|
name: string;
|
|
db_type?: string;
|
|
}
|
|
|
|
export interface BatchColumnInfo {
|
|
column_name: string;
|
|
data_type: string;
|
|
is_nullable?: string;
|
|
column_default?: string | null;
|
|
}
|
|
|
|
export interface BatchTableInfo {
|
|
table_name: string;
|
|
columns: BatchColumnInfo[];
|
|
description?: string | null;
|
|
}
|
|
|
|
export interface BatchApiResponse<T = unknown> {
|
|
success: boolean;
|
|
data?: T;
|
|
message?: string;
|
|
error?: string;
|
|
}
|
|
|
|
class BatchManagementAPIClass {
|
|
private static readonly BASE_PATH = "/batch-management";
|
|
|
|
/**
|
|
* 사용 가능한 커넥션 목록 조회
|
|
*/
|
|
static async getAvailableConnections(): Promise<BatchConnectionInfo[]> {
|
|
try {
|
|
const response = await apiClient.get<BatchApiResponse<BatchConnectionInfo[]>>(`${this.BASE_PATH}/connections`);
|
|
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.message || "커넥션 목록 조회에 실패했습니다.");
|
|
}
|
|
|
|
return response.data.data || [];
|
|
} catch (error) {
|
|
console.error("커넥션 목록 조회 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 커넥션의 테이블 목록 조회
|
|
*/
|
|
static async getTablesFromConnection(
|
|
connectionType: "internal" | "external",
|
|
connectionId?: number,
|
|
): Promise<string[]> {
|
|
try {
|
|
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
|
if (connectionType === "external" && connectionId) {
|
|
url += `/${connectionId}`;
|
|
}
|
|
url += "/tables";
|
|
|
|
const response = await apiClient.get<BatchApiResponse<string[]>>(url);
|
|
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.message || "테이블 목록 조회에 실패했습니다.");
|
|
}
|
|
|
|
return response.data.data || [];
|
|
} catch (error) {
|
|
console.error("테이블 목록 조회 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 특정 테이블의 컬럼 정보 조회
|
|
*/
|
|
static async getTableColumns(
|
|
connectionType: "internal" | "external",
|
|
tableName: string,
|
|
connectionId?: number,
|
|
): Promise<BatchColumnInfo[]> {
|
|
try {
|
|
let url = `${this.BASE_PATH}/connections/${connectionType}`;
|
|
if (connectionType === "external" && connectionId) {
|
|
url += `/${connectionId}`;
|
|
}
|
|
url += `/tables/${encodeURIComponent(tableName)}/columns`;
|
|
|
|
console.log("🔍 컬럼 조회 API 호출:", { url, connectionType, connectionId, tableName });
|
|
|
|
const response = await apiClient.get<BatchApiResponse<BatchColumnInfo[]>>(url);
|
|
|
|
console.log("🔍 컬럼 조회 API 응답:", response.data);
|
|
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.message || "컬럼 정보 조회에 실패했습니다.");
|
|
}
|
|
|
|
return response.data.data || [];
|
|
} catch (error) {
|
|
console.error("❌ 컬럼 정보 조회 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* REST API 데이터 미리보기
|
|
*/
|
|
static async previewRestApiData(
|
|
apiUrl: string,
|
|
apiKey: string,
|
|
endpoint: string,
|
|
method: "GET" | "POST" | "PUT" | "DELETE" = "GET",
|
|
paramInfo?: {
|
|
paramType: "url" | "query";
|
|
paramName: string;
|
|
paramValue: string;
|
|
paramSource: "static" | "dynamic";
|
|
},
|
|
requestBody?: string,
|
|
authServiceName?: string, // DB에서 토큰 가져올 서비스명
|
|
dataArrayPath?: string, // 데이터 배열 경로 (예: response, data.items)
|
|
): Promise<{
|
|
fields: string[];
|
|
samples: any[];
|
|
totalCount: number;
|
|
}> {
|
|
try {
|
|
const requestData: any = {
|
|
apiUrl,
|
|
apiKey,
|
|
endpoint,
|
|
method,
|
|
requestBody,
|
|
};
|
|
|
|
// 파라미터 정보가 있으면 추가
|
|
if (paramInfo) {
|
|
requestData.paramType = paramInfo.paramType;
|
|
requestData.paramName = paramInfo.paramName;
|
|
requestData.paramValue = paramInfo.paramValue;
|
|
requestData.paramSource = paramInfo.paramSource;
|
|
}
|
|
|
|
// DB에서 토큰 가져올 서비스명 추가
|
|
if (authServiceName) {
|
|
requestData.authServiceName = authServiceName;
|
|
}
|
|
|
|
// 데이터 배열 경로 추가
|
|
if (dataArrayPath) {
|
|
requestData.dataArrayPath = dataArrayPath;
|
|
}
|
|
|
|
const response = await apiClient.post<
|
|
BatchApiResponse<{
|
|
fields: string[];
|
|
samples: any[];
|
|
// 백엔드는 snake_case (total_count) 로 응답하므로 두 키 모두 옵션으로 받음
|
|
total_count?: number;
|
|
totalCount?: number;
|
|
}>
|
|
>(`${this.BASE_PATH}/rest-api/preview`, requestData);
|
|
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");
|
|
}
|
|
|
|
const raw = response.data.data;
|
|
return {
|
|
fields: raw?.fields ?? [],
|
|
samples: raw?.samples ?? [],
|
|
// 백엔드는 total_count 로 응답 → camelCase totalCount 로 normalize
|
|
totalCount: raw?.total_count ?? raw?.totalCount ?? 0,
|
|
};
|
|
} catch (error) {
|
|
console.error("REST API 미리보기 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 인증 서비스명 목록 조회
|
|
*/
|
|
static async getAuthServiceNames(): Promise<string[]> {
|
|
try {
|
|
const response = await apiClient.get<BatchApiResponse<string[]>>(`${this.BASE_PATH}/auth-services`);
|
|
|
|
if (!response.data.success) {
|
|
throw new Error(response.data.message || "인증 서비스 목록 조회에 실패했습니다.");
|
|
}
|
|
|
|
return response.data.data || [];
|
|
} catch (error) {
|
|
console.error("인증 서비스 목록 조회 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* REST API 배치 저장
|
|
*/
|
|
static async saveRestApiBatch(batchData: {
|
|
batchName: string;
|
|
batchType: string;
|
|
cronSchedule: string;
|
|
description?: string;
|
|
apiMappings: any[];
|
|
authServiceName?: string;
|
|
dataArrayPath?: string; // REST API 응답에서 데이터 배열 경로
|
|
saveMode?: "INSERT" | "UPSERT";
|
|
conflictKey?: string;
|
|
}): Promise<{ success: boolean; message: string; data?: any }> {
|
|
try {
|
|
const response = await apiClient.post<BatchApiResponse<any>>(`${this.BASE_PATH}/rest-api/save`, batchData);
|
|
return {
|
|
success: response.data.success,
|
|
message: response.data.message || "",
|
|
data: response.data.data,
|
|
};
|
|
} catch (error) {
|
|
console.error("REST API 배치 저장 오류:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const BatchManagementAPI = BatchManagementAPIClass;
|