배치 UPSERT 기능 및 고정값 매핑 버그 수정

This commit is contained in:
dohyeons
2025-12-04 17:26:29 +09:00
parent 7a2f80b646
commit ef3b85f343
9 changed files with 1176 additions and 576 deletions

View File

@@ -176,8 +176,8 @@ export class BatchService {
// 배치 설정 생성
const batchConfigResult = await client.query(
`INSERT INTO batch_configs
(batch_name, description, cron_schedule, is_active, company_code, created_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, NOW(), NOW())
(batch_name, description, cron_schedule, is_active, company_code, save_mode, conflict_key, auth_service_name, data_array_path, created_by, created_date, updated_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW())
RETURNING *`,
[
data.batchName,
@@ -185,6 +185,10 @@ export class BatchService {
data.cronSchedule,
data.isActive || "Y",
data.companyCode,
data.saveMode || "INSERT",
data.conflictKey || null,
data.authServiceName || null,
data.dataArrayPath || null,
userId,
]
);
@@ -201,37 +205,38 @@ export class BatchService {
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, created_by, created_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, NOW())
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, mapping_type, created_by, created_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, NOW())
RETURNING *`,
[
batchConfig.id,
data.companyCode, // 멀티테넌시: 배치 설정과 동일한 company_code 사용
mapping.from_connection_type,
mapping.from_connection_id,
mapping.from_table_name,
mapping.from_column_name,
mapping.from_column_type,
mapping.from_api_url,
mapping.from_api_key,
mapping.from_api_method,
mapping.from_api_param_type,
mapping.from_api_param_name,
mapping.from_api_param_value,
mapping.from_api_param_source,
mapping.from_api_body, // FROM REST API Body
mapping.to_connection_type,
mapping.to_connection_id,
mapping.to_table_name,
mapping.to_column_name,
mapping.to_column_type,
mapping.to_api_url,
mapping.to_api_key,
mapping.to_api_method,
mapping.to_api_body,
mapping.mapping_order || index + 1,
userId,
]
batchConfig.id,
data.companyCode, // 멀티테넌시: 배치 설정과 동일한 company_code 사용
mapping.from_connection_type,
mapping.from_connection_id,
mapping.from_table_name,
mapping.from_column_name,
mapping.from_column_type,
mapping.from_api_url,
mapping.from_api_key,
mapping.from_api_method,
mapping.from_api_param_type,
mapping.from_api_param_name,
mapping.from_api_param_value,
mapping.from_api_param_source,
mapping.from_api_body, // FROM REST API Body
mapping.to_connection_type,
mapping.to_connection_id,
mapping.to_table_name,
mapping.to_column_name,
mapping.to_column_type,
mapping.to_api_url,
mapping.to_api_key,
mapping.to_api_method,
mapping.to_api_body,
mapping.mapping_order || index + 1,
mapping.mapping_type || "direct", // 매핑 타입: direct 또는 fixed
userId,
]
);
mappings.push(mappingResult.rows[0]);
}
@@ -311,6 +316,18 @@ export class BatchService {
updateFields.push(`is_active = $${paramIndex++}`);
updateValues.push(data.isActive);
}
if (data.saveMode !== undefined) {
updateFields.push(`save_mode = $${paramIndex++}`);
updateValues.push(data.saveMode);
}
if (data.conflictKey !== undefined) {
updateFields.push(`conflict_key = $${paramIndex++}`);
updateValues.push(data.conflictKey || null);
}
if (data.authServiceName !== undefined) {
updateFields.push(`auth_service_name = $${paramIndex++}`);
updateValues.push(data.authServiceName || null);
}
// 배치 설정 업데이트
const batchConfigResult = await client.query(
@@ -339,8 +356,8 @@ export class BatchService {
from_column_type, from_api_url, from_api_key, from_api_method, from_api_param_type,
from_api_param_name, from_api_param_value, from_api_param_source, from_api_body,
to_connection_type, to_connection_id, to_table_name, to_column_name, to_column_type,
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, created_by, created_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, NOW())
to_api_url, to_api_key, to_api_method, to_api_body, mapping_order, mapping_type, created_by, created_date)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, NOW())
RETURNING *`,
[
id,
@@ -368,6 +385,7 @@ export class BatchService {
mapping.to_api_method,
mapping.to_api_body,
mapping.mapping_order || index + 1,
mapping.mapping_type || "direct", // 매핑 타입: direct 또는 fixed
userId,
]
);
@@ -554,9 +572,7 @@ export class BatchService {
try {
if (connectionType === "internal") {
// 내부 DB 데이터 조회
const data = await query<any>(
`SELECT * FROM ${tableName} LIMIT 10`
);
const data = await query<any>(`SELECT * FROM ${tableName} LIMIT 10`);
return {
success: true,
data,
@@ -729,19 +745,27 @@ export class BatchService {
/**
* 테이블에 데이터 삽입 (연결 타입에 따라 내부/외부 DB 구분)
* @param tableName 테이블명
* @param data 삽입할 데이터 배열
* @param connectionType 연결 타입 (internal/external)
* @param connectionId 외부 연결 ID
* @param saveMode 저장 모드 (INSERT/UPSERT)
* @param conflictKey UPSERT 시 충돌 기준 컬럼명
*/
static async insertDataToTable(
tableName: string,
data: any[],
connectionType: "internal" | "external" = "internal",
connectionId?: number
connectionId?: number,
saveMode: "INSERT" | "UPSERT" = "INSERT",
conflictKey?: string
): Promise<{
successCount: number;
failedCount: number;
}> {
try {
console.log(
`[BatchService] 테이블에 데이터 삽입: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ""}), ${data.length}개 레코드`
`[BatchService] 테이블에 데이터 ${saveMode}: ${tableName} (${connectionType}${connectionId ? `:${connectionId}` : ""}), ${data.length}개 레코드${conflictKey ? `, 충돌키: ${conflictKey}` : ""}`
);
if (!data || data.length === 0) {
@@ -753,24 +777,45 @@ export class BatchService {
let successCount = 0;
let failedCount = 0;
// 각 레코드를 개별적으로 삽입 (UPSERT 방식으로 중복 처리)
// 각 레코드를 개별적으로 삽입
for (const record of data) {
try {
const columns = Object.keys(record);
const values = Object.values(record);
const placeholders = values
.map((_, i) => `$${i + 1}`)
.join(", ");
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
const queryStr = `INSERT INTO ${tableName} (${columns.join(
", "
)}) VALUES (${placeholders})`;
let queryStr: string;
if (saveMode === "UPSERT" && conflictKey) {
// UPSERT 모드: ON CONFLICT DO UPDATE
// 충돌 키를 제외한 컬럼들만 UPDATE
const updateColumns = columns.filter(
(col) => col !== conflictKey
);
const updateSet = updateColumns
.map((col) => `${col} = EXCLUDED.${col}`)
.join(", ");
// updated_date 컬럼이 있으면 현재 시간으로 업데이트
const hasUpdatedDate = columns.includes("updated_date");
const finalUpdateSet = hasUpdatedDate
? `${updateSet}, updated_date = NOW()`
: updateSet;
queryStr = `INSERT INTO ${tableName} (${columns.join(", ")})
VALUES (${placeholders})
ON CONFLICT (${conflictKey})
DO UPDATE SET ${finalUpdateSet}`;
} else {
// INSERT 모드: 기존 방식
queryStr = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders})`;
}
await query(queryStr, values);
successCount++;
} catch (insertError) {
console.error(
`내부 DB 데이터 삽입 실패 (${tableName}):`,
`내부 DB 데이터 ${saveMode} 실패 (${tableName}):`,
insertError
);
failedCount++;
@@ -779,7 +824,13 @@ export class BatchService {
return { successCount, failedCount };
} else if (connectionType === "external" && connectionId) {
// 외부 DB에 데이터 삽입
// 외부 DB에 데이터 삽입 (UPSERT는 내부 DB만 지원)
if (saveMode === "UPSERT") {
console.warn(
`[BatchService] 외부 DB는 UPSERT를 지원하지 않습니다. INSERT로 실행합니다.`
);
}
const result = await BatchExternalDbService.insertDataToTable(
connectionId,
tableName,
@@ -799,7 +850,7 @@ export class BatchService {
);
}
} catch (error) {
console.error(`데이터 삽입 오류 (${tableName}):`, error);
console.error(`데이터 ${saveMode} 오류 (${tableName}):`, error);
return { successCount: 0, failedCount: data ? data.length : 0 };
}
}