- Integrated client IP address retrieval in the audit logging functionality across multiple controllers, including admin, common code, department, flow, screen, and table management. - Updated the `auditLogService` to include a new method for obtaining the client's IP address, ensuring accurate logging of user actions. - This enhancement improves traceability and accountability by capturing the source of requests, thereby strengthening the overall logging mechanism within the application.
508 lines
13 KiB
TypeScript
508 lines
13 KiB
TypeScript
/**
|
|
* DDL 실행 컨트롤러
|
|
* 테이블/컬럼 생성 API 엔드포인트
|
|
*/
|
|
|
|
import { Response } from "express";
|
|
import { AuthenticatedRequest } from "../middleware/superAdminMiddleware";
|
|
import { DDLExecutionService } from "../services/ddlExecutionService";
|
|
import { DDLAuditLogger } from "../services/ddlAuditLogger";
|
|
import { CreateTableRequest, AddColumnRequest } from "../types/ddl";
|
|
import { logger } from "../utils/logger";
|
|
import { auditLogService, getClientIp } from "../services/auditLogService";
|
|
|
|
export class DDLController {
|
|
/**
|
|
* POST /api/ddl/tables - 새 테이블 생성
|
|
*/
|
|
static async createTable(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName, columns, description }: CreateTableRequest = req.body;
|
|
const userId = req.user!.userId;
|
|
const userCompanyCode = req.user!.companyCode;
|
|
|
|
// 입력값 기본 검증
|
|
if (!tableName || !columns || columns.length === 0) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "테이블명과 최소 1개의 컬럼이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("테이블 생성 요청", {
|
|
tableName,
|
|
userId,
|
|
columnCount: columns.length,
|
|
ip: req.ip,
|
|
});
|
|
|
|
// inputType을 webType으로 변환 (레거시 호환성)
|
|
const processedColumns = columns.map((col) => ({
|
|
...col,
|
|
webType: (col.inputType || col.webType || "text") as any,
|
|
}));
|
|
|
|
// DDL 실행 서비스 호출
|
|
const ddlService = new DDLExecutionService();
|
|
const result = await ddlService.createTable(
|
|
tableName,
|
|
processedColumns,
|
|
userCompanyCode,
|
|
userId,
|
|
description
|
|
);
|
|
|
|
if (result.success) {
|
|
auditLogService.log({
|
|
companyCode: userCompanyCode || "",
|
|
userId,
|
|
action: "CREATE",
|
|
resourceType: "TABLE",
|
|
resourceId: tableName,
|
|
resourceName: tableName,
|
|
tableName,
|
|
summary: `테이블 "${tableName}" 생성 (${columns.length}개 컬럼)`,
|
|
changes: { after: { tableName, columnCount: columns.length, description } },
|
|
ipAddress: getClientIp(req),
|
|
requestPath: req.originalUrl,
|
|
});
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: result.message,
|
|
data: {
|
|
tableName,
|
|
columnCount: columns.length,
|
|
executedQuery: result.executedQuery,
|
|
},
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: result.message,
|
|
error: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error("테이블 생성 컨트롤러 오류:", {
|
|
error: (error as Error).message,
|
|
stack: (error as Error).stack,
|
|
userId: req.user?.userId,
|
|
body: req.body,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
details: "테이블 생성 중 서버 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/ddl/tables/:tableName/columns - 컬럼 추가
|
|
*/
|
|
static async addColumn(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
const { column }: AddColumnRequest = req.body;
|
|
const userId = req.user!.userId;
|
|
const userCompanyCode = req.user!.companyCode;
|
|
|
|
// 입력값 기본 검증
|
|
if (!tableName) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "테이블명이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!column || !column.name || (!column.inputType && !column.webType)) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "컬럼명과 입력타입이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("컬럼 추가 요청", {
|
|
tableName,
|
|
columnName: column.name,
|
|
webType: column.webType,
|
|
userId,
|
|
ip: req.ip,
|
|
});
|
|
|
|
// inputType을 webType으로 변환 (레거시 호환성)
|
|
const processedColumn = {
|
|
...column,
|
|
webType: (column.inputType || column.webType || "text") as any,
|
|
};
|
|
|
|
// DDL 실행 서비스 호출
|
|
const ddlService = new DDLExecutionService();
|
|
const result = await ddlService.addColumn(
|
|
tableName,
|
|
processedColumn,
|
|
userCompanyCode,
|
|
userId
|
|
);
|
|
|
|
if (result.success) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: result.message,
|
|
data: {
|
|
tableName,
|
|
columnName: column.name,
|
|
webType: column.webType,
|
|
executedQuery: result.executedQuery,
|
|
},
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: result.message,
|
|
error: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error("컬럼 추가 컨트롤러 오류:", {
|
|
error: (error as Error).message,
|
|
stack: (error as Error).stack,
|
|
userId: req.user?.userId,
|
|
tableName: req.params.tableName,
|
|
body: req.body,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
details: "컬럼 추가 중 서버 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/ddl/logs - DDL 실행 로그 조회
|
|
*/
|
|
static async getDDLLogs(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { limit, userId, ddlType } = req.query;
|
|
|
|
const logs = await DDLAuditLogger.getRecentDDLLogs(
|
|
limit ? parseInt(limit as string) : 50,
|
|
userId as string,
|
|
ddlType as string
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
logs,
|
|
total: logs.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("DDL 로그 조회 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "LOG_RETRIEVAL_FAILED",
|
|
details: "DDL 로그 조회 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/ddl/statistics - DDL 실행 통계 조회
|
|
*/
|
|
static async getDDLStatistics(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { fromDate, toDate } = req.query;
|
|
|
|
const statistics = await DDLAuditLogger.getDDLStatistics(
|
|
fromDate ? new Date(fromDate as string) : undefined,
|
|
toDate ? new Date(toDate as string) : undefined
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: statistics,
|
|
});
|
|
} catch (error) {
|
|
logger.error("DDL 통계 조회 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "STATISTICS_RETRIEVAL_FAILED",
|
|
details: "DDL 통계 조회 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/ddl/tables/:tableName/info - 생성된 테이블 정보 조회
|
|
*/
|
|
static async getTableInfo(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
const ddlService = new DDLExecutionService();
|
|
const tableInfo = await ddlService.getCreatedTableInfo(tableName);
|
|
|
|
if (!tableInfo) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: {
|
|
code: "TABLE_NOT_FOUND",
|
|
details: `테이블 '${tableName}'을 찾을 수 없습니다.`,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: tableInfo,
|
|
});
|
|
} catch (error) {
|
|
logger.error("테이블 정보 조회 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "TABLE_INFO_RETRIEVAL_FAILED",
|
|
details: "테이블 정보 조회 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/ddl/tables/:tableName/history - 테이블 DDL 히스토리 조회
|
|
*/
|
|
static async getTableDDLHistory(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
|
|
const history = await DDLAuditLogger.getTableDDLHistory(tableName);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
tableName,
|
|
history,
|
|
total: history.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("테이블 DDL 히스토리 조회 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "HISTORY_RETRIEVAL_FAILED",
|
|
details: "테이블 DDL 히스토리 조회 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/ddl/validate/table - 테이블 생성 사전 검증
|
|
*/
|
|
static async validateTableCreation(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName, columns }: CreateTableRequest = req.body;
|
|
|
|
if (!tableName || !columns) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "테이블명과 컬럼 정보가 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 검증만 수행 (실제 생성하지 않음)
|
|
const { DDLSafetyValidator } = await import(
|
|
"../services/ddlSafetyValidator"
|
|
);
|
|
const validationReport = DDLSafetyValidator.generateValidationReport(
|
|
tableName,
|
|
columns
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
isValid: validationReport.validationResult.isValid,
|
|
errors: validationReport.validationResult.errors,
|
|
warnings: validationReport.validationResult.warnings,
|
|
summary: validationReport.summary,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("테이블 생성 검증 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "VALIDATION_ERROR",
|
|
details: "테이블 생성 검증 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/ddl/tables/:tableName - 테이블 삭제 (최고 관리자 전용)
|
|
*/
|
|
static async dropTable(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { tableName } = req.params;
|
|
const userId = req.user!.userId;
|
|
const userCompanyCode = req.user!.companyCode;
|
|
|
|
// 입력값 기본 검증
|
|
if (!tableName) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
code: "INVALID_INPUT",
|
|
details: "테이블명이 필요합니다.",
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
logger.info("테이블 삭제 요청", {
|
|
tableName,
|
|
userId,
|
|
userCompanyCode,
|
|
ip: req.ip,
|
|
});
|
|
|
|
// DDL 실행 서비스 호출
|
|
const ddlService = new DDLExecutionService();
|
|
const result = await ddlService.dropTable(
|
|
tableName,
|
|
userCompanyCode,
|
|
userId
|
|
);
|
|
|
|
if (result.success) {
|
|
res.status(200).json({
|
|
success: true,
|
|
message: result.message,
|
|
data: {
|
|
tableName,
|
|
executedQuery: result.executedQuery,
|
|
},
|
|
});
|
|
} else {
|
|
res.status(400).json({
|
|
success: false,
|
|
message: result.message,
|
|
error: result.error,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
logger.error("테이블 삭제 컨트롤러 오류:", {
|
|
error: (error as Error).message,
|
|
stack: (error as Error).stack,
|
|
userId: req.user?.userId,
|
|
tableName: req.params.tableName,
|
|
});
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "INTERNAL_SERVER_ERROR",
|
|
details: "테이블 삭제 중 서버 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/ddl/logs/cleanup - 오래된 DDL 로그 정리
|
|
*/
|
|
static async cleanupOldLogs(
|
|
req: AuthenticatedRequest,
|
|
res: Response
|
|
): Promise<void> {
|
|
try {
|
|
const { retentionDays } = req.query;
|
|
const days = retentionDays ? parseInt(retentionDays as string) : 90;
|
|
|
|
const deletedCount = await DDLAuditLogger.cleanupOldLogs(days);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `${deletedCount}개의 오래된 DDL 로그가 삭제되었습니다.`,
|
|
data: {
|
|
deletedCount,
|
|
retentionDays: days,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error("DDL 로그 정리 오류:", error);
|
|
|
|
res.status(500).json({
|
|
success: false,
|
|
error: {
|
|
code: "LOG_CLEANUP_FAILED",
|
|
details: "DDL 로그 정리 중 오류가 발생했습니다.",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|