ERP-node/backend-node/src/controllers/tableHistoryController.ts

407 lines
11 KiB
TypeScript

/**
* 테이블 이력 조회 컨트롤러
* 테이블 타입 관리의 {테이블명}_log 테이블과 연동
*/
import { Request, Response } from "express";
import { query } from "../database/db";
import { logger } from "../utils/logger";
export class TableHistoryController {
/**
* 특정 레코드의 변경 이력 조회
*/
static async getRecordHistory(req: Request, res: Response): Promise<void> {
try {
const { tableName, recordId } = req.params;
const { limit = 50, offset = 0, operationType, changedBy, startDate, endDate } = req.query;
logger.info(`📜 테이블 이력 조회 요청:`, {
tableName,
recordId,
limit,
offset,
});
// 로그 테이블명 생성
const logTableName = `${tableName}_log`;
// 동적 WHERE 조건 생성
const whereConditions: string[] = [`original_id = $1`];
const queryParams: any[] = [recordId];
let paramIndex = 2;
// 작업 유형 필터
if (operationType) {
whereConditions.push(`operation_type = $${paramIndex}`);
queryParams.push(operationType);
paramIndex++;
}
// 변경자 필터
if (changedBy) {
whereConditions.push(`changed_by ILIKE $${paramIndex}`);
queryParams.push(`%${changedBy}%`);
paramIndex++;
}
// 날짜 범위 필터
if (startDate) {
whereConditions.push(`changed_at >= $${paramIndex}`);
queryParams.push(startDate);
paramIndex++;
}
if (endDate) {
whereConditions.push(`changed_at <= $${paramIndex}`);
queryParams.push(endDate);
paramIndex++;
}
// LIMIT과 OFFSET 파라미터 추가
queryParams.push(limit);
const limitParam = `$${paramIndex}`;
paramIndex++;
queryParams.push(offset);
const offsetParam = `$${paramIndex}`;
const whereClause = whereConditions.join(" AND ");
// 이력 조회 쿼리 (log_id로 정렬 - 시간 데이터 불일치 문제 해결)
const historyQuery = `
SELECT
log_id,
operation_type,
original_id,
changed_column,
old_value,
new_value,
changed_by,
changed_at,
ip_address,
user_agent,
full_row_before,
full_row_after
FROM ${logTableName}
WHERE ${whereClause}
ORDER BY log_id DESC
LIMIT ${limitParam} OFFSET ${offsetParam}
`;
// 전체 카운트 쿼리
const countQuery = `
SELECT COUNT(*) as total
FROM ${logTableName}
WHERE ${whereClause}
`;
const [historyRecords, countResult] = await Promise.all([
query<any>(historyQuery, queryParams),
query<any>(countQuery, queryParams.slice(0, -2)), // LIMIT, OFFSET 제외
]);
const total = parseInt(countResult[0]?.total || "0", 10);
logger.info(`✅ 이력 조회 완료: ${historyRecords.length}건 / 전체 ${total}`);
res.json({
success: true,
data: {
records: historyRecords,
pagination: {
total,
limit: parseInt(limit as string, 10),
offset: parseInt(offset as string, 10),
hasMore: parseInt(offset as string, 10) + historyRecords.length < total,
},
},
message: "이력 조회 성공",
});
} catch (error: any) {
logger.error(`❌ 테이블 이력 조회 실패:`, error);
// 테이블이 존재하지 않는 경우
if (error.code === "42P01") {
res.status(404).json({
success: false,
message: "이력 테이블이 존재하지 않습니다. 테이블 타입 관리에서 이력 관리를 활성화해주세요.",
errorCode: "TABLE_NOT_FOUND",
});
return;
}
res.status(500).json({
success: false,
message: "이력 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
}
/**
* 전체 테이블 이력 조회 (레코드 ID 없이)
*/
static async getAllTableHistory(req: Request, res: Response): Promise<void> {
try {
const { tableName } = req.params;
const { limit = 50, offset = 0, operationType, changedBy, startDate, endDate } = req.query;
logger.info(`📜 전체 테이블 이력 조회 요청:`, {
tableName,
limit,
offset,
});
// 로그 테이블명 생성
const logTableName = `${tableName}_log`;
// 동적 WHERE 조건 생성
const whereConditions: string[] = [];
const queryParams: any[] = [];
let paramIndex = 1;
// 작업 유형 필터
if (operationType) {
whereConditions.push(`operation_type = $${paramIndex}`);
queryParams.push(operationType);
paramIndex++;
}
// 변경자 필터
if (changedBy) {
whereConditions.push(`changed_by ILIKE $${paramIndex}`);
queryParams.push(`%${changedBy}%`);
paramIndex++;
}
// 날짜 범위 필터
if (startDate) {
whereConditions.push(`changed_at >= $${paramIndex}`);
queryParams.push(startDate);
paramIndex++;
}
if (endDate) {
whereConditions.push(`changed_at <= $${paramIndex}`);
queryParams.push(endDate);
paramIndex++;
}
// LIMIT과 OFFSET 파라미터 추가
queryParams.push(limit);
const limitParam = `$${paramIndex}`;
paramIndex++;
queryParams.push(offset);
const offsetParam = `$${paramIndex}`;
const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
// 이력 조회 쿼리 (log_id로 정렬 - 시간 데이터 불일치 문제 해결)
const historyQuery = `
SELECT
log_id,
operation_type,
original_id,
changed_column,
old_value,
new_value,
changed_by,
changed_at,
ip_address,
user_agent,
full_row_before,
full_row_after
FROM ${logTableName}
${whereClause}
ORDER BY log_id DESC
LIMIT ${limitParam} OFFSET ${offsetParam}
`;
// 전체 카운트 쿼리
const countQuery = `
SELECT COUNT(*) as total
FROM ${logTableName}
${whereClause}
`;
const [historyRecords, countResult] = await Promise.all([
query<any>(historyQuery, queryParams),
query<any>(countQuery, queryParams.slice(0, -2)), // LIMIT, OFFSET 제외
]);
const total = parseInt(countResult[0]?.total || "0", 10);
res.json({
success: true,
data: {
records: historyRecords,
pagination: {
total,
limit: Number(limit),
offset: Number(offset),
hasMore: Number(offset) + Number(limit) < total,
},
},
message: "전체 테이블 이력 조회 성공",
});
} catch (error: any) {
logger.error(`❌ 전체 테이블 이력 조회 실패:`, error);
if (error.code === "42P01") {
res.status(404).json({
success: false,
message: "이력 테이블이 존재하지 않습니다.",
errorCode: "TABLE_NOT_FOUND",
});
return;
}
res.status(500).json({
success: false,
message: "이력 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
}
/**
* 테이블 전체 이력 요약 조회
*/
static async getTableHistorySummary(req: Request, res: Response): Promise<void> {
try {
const { tableName } = req.params;
const logTableName = `${tableName}_log`;
const summaryQuery = `
SELECT
operation_type,
COUNT(*) as count,
COUNT(DISTINCT original_id) as affected_records,
COUNT(DISTINCT changed_by) as unique_users,
MIN(changed_at) as first_change,
MAX(changed_at) as last_change
FROM ${logTableName}
GROUP BY operation_type
`;
const summary = await query<any>(summaryQuery);
res.json({
success: true,
data: summary,
message: "이력 요약 조회 성공",
});
} catch (error: any) {
logger.error(`❌ 테이블 이력 요약 조회 실패:`, error);
if (error.code === "42P01") {
res.status(404).json({
success: false,
message: "이력 테이블이 존재하지 않습니다.",
errorCode: "TABLE_NOT_FOUND",
});
return;
}
res.status(500).json({
success: false,
message: "이력 요약 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
}
/**
* 특정 레코드의 변경 타임라인 조회 (그룹화)
*/
static async getRecordTimeline(req: Request, res: Response): Promise<void> {
try {
const { tableName, recordId } = req.params;
const logTableName = `${tableName}_log`;
// 변경 이벤트별로 그룹화 (동일 시간대 변경을 하나의 이벤트로)
const timelineQuery = `
WITH grouped_changes AS (
SELECT
changed_at,
changed_by,
operation_type,
ip_address,
json_agg(
json_build_object(
'column', changed_column,
'oldValue', old_value,
'newValue', new_value
) ORDER BY changed_column
) as changes,
full_row_before,
full_row_after
FROM ${logTableName}
WHERE original_id = $1
GROUP BY changed_at, changed_by, operation_type, ip_address, full_row_before, full_row_after
ORDER BY changed_at DESC
LIMIT 100
)
SELECT * FROM grouped_changes
`;
const timeline = await query<any>(timelineQuery, [recordId]);
res.json({
success: true,
data: timeline,
message: "타임라인 조회 성공",
});
} catch (error: any) {
logger.error(`❌ 레코드 타임라인 조회 실패:`, error);
res.status(500).json({
success: false,
message: "타임라인 조회 중 오류가 발생했습니다.",
error: error.message,
});
}
}
/**
* 이력 테이블 존재 여부 확인
*/
static async checkHistoryTableExists(req: Request, res: Response): Promise<void> {
try {
const { tableName } = req.params;
const logTableName = `${tableName}_log`;
const checkQuery = `
SELECT EXISTS (
SELECT FROM information_schema.tables
WHERE table_schema = 'public'
AND table_name = $1
) as exists
`;
const result = await query<any>(checkQuery, [logTableName]);
const exists = result[0]?.exists || false;
res.json({
success: true,
data: {
tableName,
logTableName,
exists,
historyEnabled: exists,
},
message: exists ? "이력 테이블이 존재합니다." : "이력 테이블이 존재하지 않습니다.",
});
} catch (error: any) {
logger.error(`❌ 이력 테이블 존재 여부 확인 실패:`, error);
res.status(500).json({
success: false,
message: "이력 테이블 확인 중 오류가 발생했습니다.",
error: error.message,
});
}
}
}