feat: 배치 관리 시스템 테스트 및 업데이트 기능 개선

- 배치 스케줄러 서비스 안정성 향상
- 외부 DB 연결 서비스 개선
- 배치 컨트롤러 및 관리 컨트롤러 업데이트
- 프론트엔드 배치 관리 페이지 개선
- Prisma 스키마 업데이트
This commit is contained in:
hjjeong 2025-09-29 13:48:59 +09:00
parent 2448f26bc3
commit 9680991962
10 changed files with 315 additions and 29 deletions

View File

@ -111,6 +111,10 @@ model batch_mappings {
from_api_url String? @db.VarChar(500) from_api_url String? @db.VarChar(500)
from_api_key String? @db.VarChar(200) from_api_key String? @db.VarChar(200)
from_api_method String? @db.VarChar(10) from_api_method String? @db.VarChar(10)
from_api_param_type String? @db.VarChar(10) // 'url' 또는 'query'
from_api_param_name String? @db.VarChar(100) // 파라미터명
from_api_param_value String? @db.VarChar(500) // 파라미터 값 또는 템플릿
from_api_param_source String? @db.VarChar(10) // 'static' 또는 'dynamic'
to_connection_type String @db.VarChar(20) to_connection_type String @db.VarChar(20)
to_connection_id Int? to_connection_id Int?
to_table_name String @db.VarChar(100) to_table_name String @db.VarChar(100)

View File

@ -3,6 +3,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { BatchService } from "../services/batchService"; import { BatchService } from "../services/batchService";
import { BatchSchedulerService } from "../services/batchSchedulerService";
import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes"; import { BatchConfigFilter, CreateBatchConfigRequest, UpdateBatchConfigRequest } from "../types/batchTypes";
export interface AuthenticatedRequest extends Request { export interface AuthenticatedRequest extends Request {
@ -190,6 +191,11 @@ export class BatchController {
cronSchedule, cronSchedule,
mappings mappings
} as CreateBatchConfigRequest); } as CreateBatchConfigRequest);
// 생성된 배치가 활성화 상태라면 스케줄러에 등록
if (batchConfig.data && batchConfig.data.is_active === 'Y' && batchConfig.data.id) {
await BatchSchedulerService.updateBatchSchedule(batchConfig.data.id);
}
return res.status(201).json({ return res.status(201).json({
success: true, success: true,
@ -235,6 +241,9 @@ export class BatchController {
message: "배치 설정을 찾을 수 없습니다." message: "배치 설정을 찾을 수 없습니다."
}); });
} }
// 스케줄러에서 배치 스케줄 업데이트 (활성화 시 즉시 스케줄 등록)
await BatchSchedulerService.updateBatchSchedule(Number(id));
return res.json({ return res.json({
success: true, success: true,

View File

@ -282,7 +282,13 @@ export class BatchManagementController {
firstMapping.from_api_key!, firstMapping.from_api_key!,
firstMapping.from_table_name, firstMapping.from_table_name,
firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET',
mappings.map(m => m.from_column_name) mappings.map(m => m.from_column_name),
100, // limit
// 파라미터 정보 전달
firstMapping.from_api_param_type,
firstMapping.from_api_param_name,
firstMapping.from_api_param_value,
firstMapping.from_api_param_source
); );
console.log(`API 조회 결과:`, { console.log(`API 조회 결과:`, {
@ -482,7 +488,16 @@ export class BatchManagementController {
*/ */
static async previewRestApiData(req: AuthenticatedRequest, res: Response) { static async previewRestApiData(req: AuthenticatedRequest, res: Response) {
try { try {
const { apiUrl, apiKey, endpoint, method = 'GET' } = req.body; const {
apiUrl,
apiKey,
endpoint,
method = 'GET',
paramType,
paramName,
paramValue,
paramSource
} = req.body;
if (!apiUrl || !apiKey || !endpoint) { if (!apiUrl || !apiKey || !endpoint) {
return res.status(400).json({ return res.status(400).json({
@ -491,6 +506,15 @@ export class BatchManagementController {
}); });
} }
console.log("🔍 REST API 미리보기 요청:", {
apiUrl,
endpoint,
paramType,
paramName,
paramValue,
paramSource
});
// RestApiConnector 사용하여 데이터 조회 // RestApiConnector 사용하여 데이터 조회
const { RestApiConnector } = await import('../database/RestApiConnector'); const { RestApiConnector } = await import('../database/RestApiConnector');
@ -503,8 +527,28 @@ export class BatchManagementController {
// 연결 테스트 // 연결 테스트
await connector.connect(); await connector.connect();
// 파라미터가 있는 경우 엔드포인트 수정
let finalEndpoint = endpoint;
if (paramType && paramName && paramValue) {
if (paramType === 'url') {
// URL 파라미터: /api/users/{userId} → /api/users/123
if (endpoint.includes(`{${paramName}}`)) {
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
} else {
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
finalEndpoint = `${endpoint}/${paramValue}`;
}
} else if (paramType === 'query') {
// 쿼리 파라미터: /api/users?userId=123
const separator = endpoint.includes('?') ? '&' : '?';
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
}
}
console.log("🔗 최종 엔드포인트:", finalEndpoint);
// 데이터 조회 (최대 5개만) - GET 메서드만 지원 // 데이터 조회 (최대 5개만) - GET 메서드만 지원
const result = await connector.executeQuery(endpoint, method); const result = await connector.executeQuery(finalEndpoint, method);
console.log(`[previewRestApiData] executeQuery 결과:`, { console.log(`[previewRestApiData] executeQuery 결과:`, {
rowCount: result.rowCount, rowCount: result.rowCount,
rowsLength: result.rows ? result.rows.length : 'undefined', rowsLength: result.rows ? result.rows.length : 'undefined',

View File

@ -697,7 +697,12 @@ export class BatchExternalDbService {
endpoint: string, endpoint: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET', method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
columns?: string[], columns?: string[],
limit: number = 100 limit: number = 100,
// 파라미터 정보 추가
paramType?: 'url' | 'query',
paramName?: string,
paramValue?: string,
paramSource?: 'static' | 'dynamic'
): Promise<ApiResponse<any[]>> { ): Promise<ApiResponse<any[]>> {
try { try {
console.log(`[BatchExternalDbService] REST API 데이터 조회: ${apiUrl}${endpoint}`); console.log(`[BatchExternalDbService] REST API 데이터 조회: ${apiUrl}${endpoint}`);
@ -712,8 +717,33 @@ export class BatchExternalDbService {
// 연결 테스트 // 연결 테스트
await connector.connect(); await connector.connect();
// 파라미터가 있는 경우 엔드포인트 수정
const { logger } = await import('../utils/logger');
logger.info(`[BatchExternalDbService] 파라미터 정보`, {
paramType, paramName, paramValue, paramSource
});
let finalEndpoint = endpoint;
if (paramType && paramName && paramValue) {
if (paramType === 'url') {
// URL 파라미터: /api/users/{userId} → /api/users/123
if (endpoint.includes(`{${paramName}}`)) {
finalEndpoint = endpoint.replace(`{${paramName}}`, paramValue);
} else {
// 엔드포인트에 {paramName}이 없으면 뒤에 추가
finalEndpoint = `${endpoint}/${paramValue}`;
}
} else if (paramType === 'query') {
// 쿼리 파라미터: /api/users?userId=123
const separator = endpoint.includes('?') ? '&' : '?';
finalEndpoint = `${endpoint}${separator}${paramName}=${paramValue}`;
}
logger.info(`[BatchExternalDbService] 파라미터 적용된 엔드포인트: ${finalEndpoint}`);
}
// 데이터 조회 // 데이터 조회
const result = await connector.executeQuery(endpoint, method); const result = await connector.executeQuery(finalEndpoint, method);
let data = result.rows; let data = result.rows;
// 컬럼 필터링 (지정된 컬럼만 추출) // 컬럼 필터링 (지정된 컬럼만 추출)
@ -734,7 +764,8 @@ export class BatchExternalDbService {
data = data.slice(0, limit); data = data.slice(0, limit);
} }
console.log(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`); logger.info(`[BatchExternalDbService] REST API 데이터 조회 완료: ${data.length}개 레코드`);
logger.info(`[BatchExternalDbService] 조회된 데이터`, { data });
return { return {
success: true, success: true,

View File

@ -112,7 +112,7 @@ export class BatchSchedulerService {
/** /**
* *
*/ */
static async updateBatchSchedule(configId: number) { static async updateBatchSchedule(configId: number, executeImmediately: boolean = true) {
try { try {
// 기존 스케줄 제거 // 기존 스케줄 제거
await this.unscheduleBatchConfig(configId); await this.unscheduleBatchConfig(configId);
@ -132,6 +132,12 @@ export class BatchSchedulerService {
if (config.is_active === 'Y') { if (config.is_active === 'Y') {
await this.scheduleBatchConfig(config); await this.scheduleBatchConfig(config);
logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`); logger.info(`배치 스케줄 업데이트 완료: ${config.batch_name} (ID: ${configId})`);
// 활성화 시 즉시 실행 (옵션)
if (executeImmediately) {
logger.info(`🚀 배치 활성화 즉시 실행: ${config.batch_name} (ID: ${configId})`);
await this.executeBatchConfig(config);
}
} else { } else {
logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`); logger.info(`비활성화된 배치 스케줄 제거: ${config.batch_name} (ID: ${configId})`);
} }
@ -239,7 +245,13 @@ export class BatchSchedulerService {
firstMapping.from_api_key!, firstMapping.from_api_key!,
firstMapping.from_table_name, firstMapping.from_table_name,
firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET', firstMapping.from_api_method as 'GET' | 'POST' | 'PUT' | 'DELETE' || 'GET',
mappings.map((m: any) => m.from_column_name) mappings.map((m: any) => m.from_column_name),
100, // limit
// 파라미터 정보 전달
firstMapping.from_api_param_type,
firstMapping.from_api_param_name,
firstMapping.from_api_param_value,
firstMapping.from_api_param_source
); );
if (apiResult.success && apiResult.data) { if (apiResult.success && apiResult.data) {

View File

@ -168,6 +168,10 @@ export class BatchService {
from_api_url: mapping.from_api_url, from_api_url: mapping.from_api_url,
from_api_key: mapping.from_api_key, from_api_key: mapping.from_api_key,
from_api_method: mapping.from_api_method, from_api_method: mapping.from_api_method,
from_api_param_type: mapping.from_api_param_type,
from_api_param_name: mapping.from_api_param_name,
from_api_param_value: mapping.from_api_param_value,
from_api_param_source: mapping.from_api_param_source,
to_connection_type: mapping.to_connection_type, to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id, to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name, to_table_name: mapping.to_table_name,
@ -176,7 +180,7 @@ export class BatchService {
to_api_url: mapping.to_api_url, to_api_url: mapping.to_api_url,
to_api_key: mapping.to_api_key, to_api_key: mapping.to_api_key,
to_api_method: mapping.to_api_method, to_api_method: mapping.to_api_method,
// to_api_body: mapping.to_api_body, // Request Body 템플릿 추가 - 임시 주석 처리 to_api_body: mapping.to_api_body,
mapping_order: mapping.mapping_order || index + 1, mapping_order: mapping.mapping_order || index + 1,
created_by: userId, created_by: userId,
}, },
@ -260,11 +264,22 @@ export class BatchService {
from_table_name: mapping.from_table_name, from_table_name: mapping.from_table_name,
from_column_name: mapping.from_column_name, from_column_name: mapping.from_column_name,
from_column_type: mapping.from_column_type, from_column_type: mapping.from_column_type,
from_api_url: mapping.from_api_url,
from_api_key: mapping.from_api_key,
from_api_method: mapping.from_api_method,
from_api_param_type: mapping.from_api_param_type,
from_api_param_name: mapping.from_api_param_name,
from_api_param_value: mapping.from_api_param_value,
from_api_param_source: mapping.from_api_param_source,
to_connection_type: mapping.to_connection_type, to_connection_type: mapping.to_connection_type,
to_connection_id: mapping.to_connection_id, to_connection_id: mapping.to_connection_id,
to_table_name: mapping.to_table_name, to_table_name: mapping.to_table_name,
to_column_name: mapping.to_column_name, to_column_name: mapping.to_column_name,
to_column_type: mapping.to_column_type, to_column_type: mapping.to_column_type,
to_api_url: mapping.to_api_url,
to_api_key: mapping.to_api_key,
to_api_method: mapping.to_api_method,
to_api_body: mapping.to_api_body,
mapping_order: mapping.mapping_order || index + 1, mapping_order: mapping.mapping_order || index + 1,
created_by: userId, created_by: userId,
}, },
@ -707,18 +722,39 @@ export class BatchService {
const updateColumns = columns.filter(col => col !== primaryKeyColumn); const updateColumns = columns.filter(col => col !== primaryKeyColumn);
const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', '); const updateSet = updateColumns.map(col => `${col} = EXCLUDED.${col}`).join(', ');
// 먼저 해당 레코드가 존재하는지 확인
const checkQuery = `SELECT COUNT(*) as count FROM ${tableName} WHERE ${primaryKeyColumn} = $1`;
const existsResult = await prisma.$queryRawUnsafe(checkQuery, record[primaryKeyColumn]);
const exists = (existsResult as any)[0]?.count > 0;
let query: string; let query: string;
if (updateSet) { if (exists && updateSet) {
// UPSERT: 중복 시 업데이트 // 기존 레코드가 있으면 UPDATE (값이 다른 경우에만)
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders}) const whereConditions = updateColumns.map((col, index) =>
ON CONFLICT (${primaryKeyColumn}) DO UPDATE SET ${updateSet}`; `${col} IS DISTINCT FROM $${index + 2}`
).join(' OR ');
query = `UPDATE ${tableName} SET ${updateSet.replace(/EXCLUDED\./g, '')}
WHERE ${primaryKeyColumn} = $1 AND (${whereConditions})`;
// 파라미터: [primaryKeyValue, ...updateValues]
const updateValues = [record[primaryKeyColumn], ...updateColumns.map(col => record[col])];
const updateResult = await prisma.$executeRawUnsafe(query, ...updateValues);
if (updateResult > 0) {
console.log(`[BatchService] 레코드 업데이트: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
} else {
console.log(`[BatchService] 레코드 변경사항 없음: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
}
} else if (!exists) {
// 새 레코드 삽입
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})`;
await prisma.$executeRawUnsafe(query, ...values);
console.log(`[BatchService] 새 레코드 삽입: ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
} else { } else {
// Primary Key만 있는 경우 중복 시 무시 console.log(`[BatchService] 레코드 이미 존재 (변경사항 없음): ${primaryKeyColumn}=${record[primaryKeyColumn]}`);
query = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders})
ON CONFLICT (${primaryKeyColumn}) DO NOTHING`;
} }
await prisma.$executeRawUnsafe(query, ...values);
successCount++; successCount++;
} catch (error) { } catch (error) {
console.error(`레코드 UPSERT 실패:`, error); console.error(`레코드 UPSERT 실패:`, error);

View File

@ -37,6 +37,10 @@ export interface BatchMapping {
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용 from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // REST API 전용
from_api_url?: string; // REST API 서버 URL from_api_url?: string; // REST API 서버 URL
from_api_key?: string; // REST API 키 from_api_key?: string; // REST API 키
from_api_param_type?: 'url' | 'query'; // API 파라미터 타입
from_api_param_name?: string; // API 파라미터명
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입
// TO 정보 // TO 정보
to_connection_type: 'internal' | 'external' | 'restapi'; to_connection_type: 'internal' | 'external' | 'restapi';
@ -92,6 +96,10 @@ export interface BatchMappingRequest {
from_api_url?: string; from_api_url?: string;
from_api_key?: string; from_api_key?: string;
from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; from_api_method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
from_api_param_type?: 'url' | 'query'; // API 파라미터 타입
from_api_param_name?: string; // API 파라미터명
from_api_param_value?: string; // API 파라미터 값 또는 템플릿
from_api_param_source?: 'static' | 'dynamic'; // 파라미터 소스 타입
to_connection_type: 'internal' | 'external' | 'restapi'; to_connection_type: 'internal' | 'external' | 'restapi';
to_connection_id?: number; to_connection_id?: number;
to_table_name: string; to_table_name: string;

View File

@ -53,6 +53,12 @@ export default function BatchManagementNewPage() {
const [fromApiKey, setFromApiKey] = useState(""); const [fromApiKey, setFromApiKey] = useState("");
const [fromEndpoint, setFromEndpoint] = useState(""); const [fromEndpoint, setFromEndpoint] = useState("");
const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원 const [fromApiMethod, setFromApiMethod] = useState<'GET'>('GET'); // GET만 지원
// REST API 파라미터 설정
const [apiParamType, setApiParamType] = useState<'none' | 'url' | 'query'>('none');
const [apiParamName, setApiParamName] = useState(""); // 파라미터명 (예: userId, id)
const [apiParamValue, setApiParamValue] = useState(""); // 파라미터 값 또는 템플릿
const [apiParamSource, setApiParamSource] = useState<'static' | 'dynamic'>('static'); // 정적 값 또는 동적 값
// DB → REST API용 상태 // DB → REST API용 상태
const [fromConnection, setFromConnection] = useState<BatchConnectionInfo | null>(null); const [fromConnection, setFromConnection] = useState<BatchConnectionInfo | null>(null);
@ -309,7 +315,14 @@ export default function BatchManagementNewPage() {
fromApiUrl, fromApiUrl,
fromApiKey, fromApiKey,
fromEndpoint, fromEndpoint,
fromApiMethod fromApiMethod,
// 파라미터 정보 추가
apiParamType !== 'none' ? {
paramType: apiParamType,
paramName: apiParamName,
paramValue: apiParamValue,
paramSource: apiParamSource
} : undefined
); );
console.log("API 미리보기 결과:", result); console.log("API 미리보기 결과:", result);
@ -371,6 +384,11 @@ export default function BatchManagementNewPage() {
from_api_url: fromApiUrl, from_api_url: fromApiUrl,
from_api_key: fromApiKey, from_api_key: fromApiKey,
from_api_method: fromApiMethod, from_api_method: fromApiMethod,
// API 파라미터 정보 추가
from_api_param_type: apiParamType !== 'none' ? apiParamType : undefined,
from_api_param_name: apiParamType !== 'none' ? apiParamName : undefined,
from_api_param_value: apiParamType !== 'none' ? apiParamValue : undefined,
from_api_param_source: apiParamType !== 'none' ? apiParamSource : undefined,
to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external', to_connection_type: toConnection?.type === 'internal' ? 'internal' : 'external',
to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id, to_connection_id: toConnection?.type === 'internal' ? undefined : toConnection?.id,
to_table_name: toTable, to_table_name: toTable,
@ -661,14 +679,119 @@ export default function BatchManagementNewPage() {
</div> </div>
{/* API 파라미터 설정 */}
<div className="space-y-4">
<div className="border-t pt-4">
<Label className="text-base font-medium">API </Label>
<p className="text-sm text-gray-600 mt-1"> .</p>
</div>
<div>
<Label> </Label>
<Select value={apiParamType} onValueChange={(value: any) => setApiParamType(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="none"> </SelectItem>
<SelectItem value="url">URL (/api/users/{`{userId}`})</SelectItem>
<SelectItem value="query"> (/api/users?userId=123)</SelectItem>
</SelectContent>
</Select>
</div>
{apiParamType !== 'none' && (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="apiParamName"> *</Label>
<Input
id="apiParamName"
value={apiParamName}
onChange={(e) => setApiParamName(e.target.value)}
placeholder="userId, id, email 등"
/>
</div>
<div>
<Label> </Label>
<Select value={apiParamSource} onValueChange={(value: any) => setApiParamSource(value)}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="static"></SelectItem>
<SelectItem value="dynamic"> ( )</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div>
<Label htmlFor="apiParamValue">
{apiParamSource === 'static' ? '파라미터 값' : '파라미터 템플릿'} *
</Label>
<Input
id="apiParamValue"
value={apiParamValue}
onChange={(e) => setApiParamValue(e.target.value)}
placeholder={
apiParamSource === 'static'
? "123, john@example.com 등"
: "{{user_id}}, {{email}} 등 (실행 시 치환됨)"
}
/>
{apiParamSource === 'dynamic' && (
<p className="text-xs text-gray-500 mt-1">
. : {`{{user_id}}`} ID
</p>
)}
</div>
{apiParamType === 'url' && (
<div className="p-3 bg-blue-50 rounded-lg">
<div className="text-sm font-medium text-blue-800">URL </div>
<div className="text-sm text-blue-700 mt-1">
: /api/users/{`{${apiParamName || 'userId'}}`}
</div>
<div className="text-sm text-blue-700">
: /api/users/{apiParamValue || '123'}
</div>
</div>
)}
{apiParamType === 'query' && (
<div className="p-3 bg-green-50 rounded-lg">
<div className="text-sm font-medium text-green-800"> </div>
<div className="text-sm text-green-700 mt-1">
: {fromEndpoint || '/api/users'}?{apiParamName || 'userId'}={apiParamValue || '123'}
</div>
</div>
)}
</>
)}
</div>
{fromApiUrl && fromApiKey && fromEndpoint && ( {fromApiUrl && fromApiKey && fromEndpoint && (
<div className="space-y-3"> <div className="space-y-3">
<div className="p-3 bg-gray-50 rounded-lg"> <div className="p-3 bg-gray-50 rounded-lg">
<div className="text-sm font-medium text-gray-700">API </div> <div className="text-sm font-medium text-gray-700">API </div>
<div className="text-sm text-gray-600 mt-1"> <div className="text-sm text-gray-600 mt-1">
{fromApiMethod} {fromApiUrl}{fromEndpoint} {fromApiMethod} {fromApiUrl}
{apiParamType === 'url' && apiParamName && apiParamValue
? fromEndpoint.replace(`{${apiParamName}}`, apiParamValue) || fromEndpoint + `/${apiParamValue}`
: fromEndpoint
}
{apiParamType === 'query' && apiParamName && apiParamValue
? `?${apiParamName}=${apiParamValue}`
: ''
}
</div> </div>
<div className="text-xs text-gray-500 mt-1">Headers: X-API-Key: {fromApiKey.substring(0, 10)}...</div> <div className="text-xs text-gray-500 mt-1">Headers: X-API-Key: {fromApiKey.substring(0, 10)}...</div>
{apiParamType !== 'none' && apiParamName && apiParamValue && (
<div className="text-xs text-blue-600 mt-1">
: {apiParamName} = {apiParamValue} ({apiParamSource === 'static' ? '고정값' : '동적값'})
</div>
)}
</div> </div>
<Button onClick={previewRestApiData} variant="outline" className="w-full"> <Button onClick={previewRestApiData} variant="outline" className="w-full">
<RefreshCw className="w-4 h-4 mr-2" /> <RefreshCw className="w-4 h-4 mr-2" />

View File

@ -66,6 +66,7 @@ export default function BatchEditPage() {
// 배치 타입 감지 // 배치 타입 감지
const [batchType, setBatchType] = useState<'db-to-db' | 'restapi-to-db' | 'db-to-restapi' | null>(null); const [batchType, setBatchType] = useState<'db-to-db' | 'restapi-to-db' | 'db-to-restapi' | null>(null);
// 페이지 로드 시 배치 정보 조회 // 페이지 로드 시 배치 정보 조회
useEffect(() => { useEffect(() => {
if (batchId) { if (batchId) {
@ -342,9 +343,11 @@ export default function BatchEditPage() {
// 매핑 업데이트 // 매핑 업데이트
const updateMapping = (index: number, field: keyof BatchMapping, value: any) => { const updateMapping = (index: number, field: keyof BatchMapping, value: any) => {
const updatedMappings = [...mappings]; setMappings(prevMappings => {
updatedMappings[index] = { ...updatedMappings[index], [field]: value }; const updatedMappings = [...prevMappings];
setMappings(updatedMappings); updatedMappings[index] = { ...updatedMappings[index], [field]: value };
return updatedMappings;
});
}; };
// 배치 설정 저장 // 배치 설정 저장

View File

@ -120,23 +120,39 @@ class BatchManagementAPIClass {
apiUrl: string, apiUrl: string,
apiKey: string, apiKey: string,
endpoint: string, endpoint: string,
method: 'GET' = 'GET' method: 'GET' = 'GET',
paramInfo?: {
paramType: 'url' | 'query';
paramName: string;
paramValue: string;
paramSource: 'static' | 'dynamic';
}
): Promise<{ ): Promise<{
fields: string[]; fields: string[];
samples: any[]; samples: any[];
totalCount: number; totalCount: number;
}> { }> {
try { try {
const response = await apiClient.post<BatchApiResponse<{ const requestData: any = {
fields: string[];
samples: any[];
totalCount: number;
}>>(`${this.BASE_PATH}/rest-api/preview`, {
apiUrl, apiUrl,
apiKey, apiKey,
endpoint, endpoint,
method method
}); };
// 파라미터 정보가 있으면 추가
if (paramInfo) {
requestData.paramType = paramInfo.paramType;
requestData.paramName = paramInfo.paramName;
requestData.paramValue = paramInfo.paramValue;
requestData.paramSource = paramInfo.paramSource;
}
const response = await apiClient.post<BatchApiResponse<{
fields: string[];
samples: any[];
totalCount: number;
}>>(`${this.BASE_PATH}/rest-api/preview`, requestData);
if (!response.data.success) { if (!response.data.success) {
throw new Error(response.data.message || "REST API 미리보기에 실패했습니다."); throw new Error(response.data.message || "REST API 미리보기에 실패했습니다.");