Files
vexplor/backend-node/src/controllers/dataflowExecutionController.ts
kjs 3982aabc24 refactor: Enhance unique constraint validation across data operations
- Integrated `TableManagementService` to validate unique constraints before insert, update, and upsert actions in various controllers, including `dataflowExecutionController`, `dynamicFormController`, and `tableManagementController`.
- Improved error handling in `errorHandler` to provide detailed messages indicating which field has a unique constraint violation.
- Updated the `formatPgError` utility to extract and display specific column labels for unique constraint violations, enhancing user feedback.
- Adjusted the table schema retrieval to include company-specific nullable and unique constraints, ensuring accurate representation of database rules.

These changes improve data integrity by preventing duplicate entries and enhance user experience through clearer error messages related to unique constraints.
2026-03-10 16:15:20 +09:00

343 lines
9.3 KiB
TypeScript

/**
* 🔥 데이터플로우 실행 컨트롤러
*
* 버튼 제어에서 관계 실행 시 사용되는 컨트롤러
*/
import { Request, Response } from "express";
import { AuthenticatedRequest } from "../types/auth";
import { query } from "../database/db";
import logger from "../utils/logger";
import { TableManagementService } from "../services/tableManagementService";
/**
* 데이터 액션 실행
*/
export async function executeDataAction(
req: AuthenticatedRequest,
res: Response
): Promise<void> {
try {
const { tableName, data, actionType, connection } = req.body;
const companyCode = req.user?.companyCode || "*";
logger.info(`데이터 액션 실행 시작: ${actionType} on ${tableName}`, {
tableName,
actionType,
dataKeys: Object.keys(data),
connection: connection?.name,
});
// 연결 정보에 따라 다른 데이터베이스에 저장
let result;
if (connection && connection.id !== 0) {
// 외부 데이터베이스 연결
result = await executeExternalDatabaseAction(
tableName,
data,
actionType,
connection
);
} else {
// 메인 데이터베이스 (현재 시스템)
result = await executeMainDatabaseAction(
tableName,
data,
actionType,
companyCode
);
}
logger.info(`데이터 액션 실행 완료: ${actionType} on ${tableName}`, result);
res.json({
success: true,
message: `데이터 액션 실행 완료: ${actionType}`,
data: result,
});
} catch (error: any) {
logger.error("데이터 액션 실행 실패:", error);
res.status(500).json({
success: false,
message: `데이터 액션 실행 실패: ${error.message}`,
errorCode: "DATA_ACTION_EXECUTION_ERROR",
});
}
}
/**
* 메인 데이터베이스에서 데이터 액션 실행
*/
async function executeMainDatabaseAction(
tableName: string,
data: Record<string, any>,
actionType: string,
companyCode: string
): Promise<any> {
try {
// 회사 코드 추가
const dataWithCompany = {
...data,
company_code: companyCode,
};
// UNIQUE 제약조건 검증 (INSERT/UPDATE/UPSERT 전)
if (["insert", "update", "upsert"].includes(actionType.toLowerCase())) {
const tms = new TableManagementService();
const uniqueViolations = await tms.validateUniqueConstraints(
tableName,
dataWithCompany,
companyCode
);
if (uniqueViolations.length > 0) {
throw new Error(`중복된 값이 존재합니다: ${uniqueViolations.join(", ")}`);
}
}
switch (actionType.toLowerCase()) {
case "insert":
return await executeInsert(tableName, dataWithCompany);
case "update":
return await executeUpdate(tableName, dataWithCompany);
case "upsert":
return await executeUpsert(tableName, dataWithCompany);
case "delete":
return await executeDelete(tableName, dataWithCompany);
default:
throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
}
} catch (error) {
logger.error(`메인 DB 액션 실행 오류 (${actionType}):`, error);
throw error;
}
}
/**
* 외부 데이터베이스에서 데이터 액션 실행
*/
async function executeExternalDatabaseAction(
tableName: string,
data: Record<string, any>,
actionType: string,
connection: any
): Promise<any> {
try {
logger.info(
`외부 DB 액션 실행: ${connection.name} (${connection.host}:${connection.port})`
);
logger.info(`테이블: ${tableName}, 액션: ${actionType}`, data);
// 🔥 실제 외부 DB 연결 및 실행 로직 구현
const { MultiConnectionQueryService } = await import(
"../services/multiConnectionQueryService"
);
const queryService = new MultiConnectionQueryService();
let result;
switch (actionType.toLowerCase()) {
case "insert":
result = await queryService.insertDataToConnection(
connection.id,
tableName,
data
);
logger.info(`외부 DB INSERT 성공:`, result);
break;
case "update":
// TODO: UPDATE 로직 구현 (조건 필요)
throw new Error(
"UPDATE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
);
case "delete":
// TODO: DELETE 로직 구현 (조건 필요)
throw new Error(
"DELETE 액션은 아직 지원되지 않습니다. 조건 설정이 필요합니다."
);
default:
throw new Error(`지원하지 않는 액션 타입: ${actionType}`);
}
return {
success: true,
message: `외부 DB 액션 실행 완료: ${actionType} on ${tableName}`,
connection: connection.name,
data: result,
affectedRows: 1,
};
} catch (error) {
logger.error(`외부 DB 액션 실행 오류 (${actionType}):`, error);
throw error;
}
}
/**
* INSERT 실행
*/
async function executeInsert(
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
// 동적 테이블 접근을 위한 raw query 사용
const columns = Object.keys(data).join(", ");
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(", ");
const insertQuery = `INSERT INTO ${tableName} (${columns}) VALUES (${placeholders}) RETURNING *`;
logger.info(`INSERT 쿼리 실행:`, { query: insertQuery, values });
const result = await query<any>(insertQuery, values);
return {
success: true,
action: "insert",
tableName,
data: result,
affectedRows: result.length,
};
} catch (error) {
logger.error(`INSERT 실행 오류:`, error);
throw error;
}
}
/**
* UPDATE 실행
*/
async function executeUpdate(
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
logger.info(`UPDATE 액션 시작:`, { tableName, receivedData: data });
// 1. 테이블의 실제 기본키 조회
const primaryKeyQuery = `
SELECT a.attname as column_name
FROM pg_index i
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
WHERE i.indrelid = $1::regclass AND i.indisprimary
`;
const pkResult = await query<{ column_name: string }>(primaryKeyQuery, [
tableName,
]);
if (!pkResult || pkResult.length === 0) {
throw new Error(`테이블 ${tableName}의 기본키를 찾을 수 없습니다`);
}
const primaryKeyColumn = pkResult[0].column_name;
logger.info(`테이블 ${tableName}의 기본키:`, primaryKeyColumn);
// 2. 기본키 값 추출
const primaryKeyValue = data[primaryKeyColumn];
if (!primaryKeyValue && primaryKeyValue !== 0) {
logger.error(`UPDATE 실패: 기본키 값이 없음`, {
primaryKeyColumn,
receivedData: data,
availableKeys: Object.keys(data),
});
throw new Error(
`UPDATE를 위한 기본키 값이 필요합니다 (${primaryKeyColumn})`
);
}
// 3. 업데이트할 데이터에서 기본키 제외
const updateData = { ...data };
delete updateData[primaryKeyColumn];
logger.info(`UPDATE 데이터 준비:`, {
primaryKeyColumn,
primaryKeyValue,
updateFields: Object.keys(updateData),
});
// 4. 동적 UPDATE 쿼리 생성
const setClause = Object.keys(updateData)
.map((key, index) => `${key} = $${index + 1}`)
.join(", ");
const values = Object.values(updateData);
const updateQuery = `UPDATE ${tableName} SET ${setClause} WHERE ${primaryKeyColumn} = $${values.length + 1} RETURNING *`;
logger.info(`UPDATE 쿼리 실행:`, {
query: updateQuery,
values: [...values, primaryKeyValue],
});
const result = await query<any>(updateQuery, [...values, primaryKeyValue]);
logger.info(`UPDATE 성공:`, { affectedRows: result.length });
return {
success: true,
action: "update",
tableName,
data: result,
affectedRows: result.length,
};
} catch (error) {
logger.error(`UPDATE 실행 오류:`, error);
throw error;
}
}
/**
* UPSERT 실행
*/
async function executeUpsert(
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
// 먼저 INSERT를 시도하고, 실패하면 UPDATE
try {
return await executeInsert(tableName, data);
} catch (insertError) {
// INSERT 실패 시 UPDATE 시도
logger.info(`INSERT 실패, UPDATE 시도:`, insertError);
return await executeUpdate(tableName, data);
}
} catch (error) {
logger.error(`UPSERT 실행 오류:`, error);
throw error;
}
}
/**
* DELETE 실행
*/
async function executeDelete(
tableName: string,
data: Record<string, any>
): Promise<any> {
try {
const { id } = data;
if (!id) {
throw new Error("DELETE를 위한 ID가 필요합니다");
}
const deleteQuery = `DELETE FROM ${tableName} WHERE id = $1 RETURNING *`;
logger.info(`DELETE 쿼리 실행:`, { query: deleteQuery, values: [id] });
const result = await query<any>(deleteQuery, [id]);
return {
success: true,
action: "delete",
tableName,
data: result,
affectedRows: result.length,
};
} catch (error) {
logger.error(`DELETE 실행 오류:`, error);
throw error;
}
}