2025-09-22 17:00:59 +09:00
|
|
|
/**
|
|
|
|
|
* 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";
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
// inputType을 webType으로 변환 (레거시 호환성)
|
|
|
|
|
const processedColumns = columns.map((col) => ({
|
|
|
|
|
...col,
|
|
|
|
|
webType: (col.inputType || col.webType || "text") as any,
|
|
|
|
|
}));
|
|
|
|
|
|
2025-09-22 17:00:59 +09:00
|
|
|
// DDL 실행 서비스 호출
|
|
|
|
|
const ddlService = new DDLExecutionService();
|
|
|
|
|
const result = await ddlService.createTable(
|
|
|
|
|
tableName,
|
2025-09-23 10:40:21 +09:00
|
|
|
processedColumns,
|
2025-09-22 17:00:59 +09:00
|
|
|
userCompanyCode,
|
|
|
|
|
userId,
|
|
|
|
|
description
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
if (!column || !column.name || (!column.inputType && !column.webType)) {
|
2025-09-22 17:00:59 +09:00
|
|
|
res.status(400).json({
|
|
|
|
|
success: false,
|
|
|
|
|
error: {
|
|
|
|
|
code: "INVALID_INPUT",
|
2025-09-23 10:40:21 +09:00
|
|
|
details: "컬럼명과 입력타입이 필요합니다.",
|
2025-09-22 17:00:59 +09:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("컬럼 추가 요청", {
|
|
|
|
|
tableName,
|
|
|
|
|
columnName: column.name,
|
|
|
|
|
webType: column.webType,
|
|
|
|
|
userId,
|
|
|
|
|
ip: req.ip,
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-23 10:40:21 +09:00
|
|
|
// inputType을 webType으로 변환 (레거시 호환성)
|
|
|
|
|
const processedColumn = {
|
|
|
|
|
...column,
|
|
|
|
|
webType: (column.inputType || column.webType || "text") as any,
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-22 17:00:59 +09:00
|
|
|
// DDL 실행 서비스 호출
|
|
|
|
|
const ddlService = new DDLExecutionService();
|
|
|
|
|
const result = await ddlService.addColumn(
|
|
|
|
|
tableName,
|
2025-09-23 10:40:21 +09:00
|
|
|
processedColumn,
|
2025-09-22 17:00:59 +09:00
|
|
|
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: "테이블 생성 검증 중 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 10:07:07 +09:00
|
|
|
/**
|
|
|
|
|
* 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: "테이블 삭제 중 서버 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-22 17:00:59 +09:00
|
|
|
/**
|
|
|
|
|
* 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 로그 정리 중 오류가 발생했습니다.",
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|