ERP-node/backend-node/src/services/ddlSafetyValidator.ts

391 lines
10 KiB
TypeScript

/**
* DDL 안전성 검증 서비스
* 테이블/컬럼 생성 전 모든 보안 검증을 수행
*/
import {
CreateColumnDefinition,
ValidationResult,
SYSTEM_TABLES,
RESERVED_WORDS,
RESERVED_COLUMNS,
} from "../types/ddl";
import { logger } from "../utils/logger";
export class DDLSafetyValidator {
/**
* 테이블 생성 전 전체 검증
*/
static validateTableCreation(
tableName: string,
columns: CreateColumnDefinition[]
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
try {
// 1. 테이블명 기본 검증
const tableNameValidation = this.validateTableName(tableName);
if (!tableNameValidation.isValid) {
errors.push(...tableNameValidation.errors);
}
// 2. 컬럼 기본 검증
if (columns.length === 0) {
errors.push("최소 1개의 컬럼이 필요합니다.");
}
// 3. 컬럼 목록 검증
const columnsValidation = this.validateColumnList(columns);
if (!columnsValidation.isValid) {
errors.push(...columnsValidation.errors);
}
if (columnsValidation.warnings) {
warnings.push(...columnsValidation.warnings);
}
// 4. 컬럼명 중복 검증
const duplicateValidation = this.validateColumnDuplication(columns);
if (!duplicateValidation.isValid) {
errors.push(...duplicateValidation.errors);
}
logger.info("테이블 생성 검증 완료", {
tableName,
columnCount: columns.length,
errorCount: errors.length,
warningCount: warnings.length,
});
return {
isValid: errors.length === 0,
errors,
warnings,
};
} catch (error) {
logger.error("테이블 생성 검증 중 오류 발생:", error);
return {
isValid: false,
errors: ["테이블 생성 검증 중 내부 오류가 발생했습니다."],
};
}
}
/**
* 컬럼 추가 전 검증
*/
static validateColumnAddition(
tableName: string,
column: CreateColumnDefinition
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
try {
// 1. 테이블명 검증 (시스템 테이블 확인)
if (this.isSystemTable(tableName)) {
errors.push(
`'${tableName}'은 시스템 테이블이므로 컬럼을 추가할 수 없습니다.`
);
}
// 2. 컬럼 정의 검증
const columnValidation = this.validateSingleColumn(column);
if (!columnValidation.isValid) {
errors.push(...columnValidation.errors);
}
if (columnValidation.warnings) {
warnings.push(...columnValidation.warnings);
}
logger.info("컬럼 추가 검증 완료", {
tableName,
columnName: column.name,
webType: column.webType,
errorCount: errors.length,
});
return {
isValid: errors.length === 0,
errors,
warnings,
};
} catch (error) {
logger.error("컬럼 추가 검증 중 오류 발생:", error);
return {
isValid: false,
errors: ["컬럼 추가 검증 중 내부 오류가 발생했습니다."],
};
}
}
/**
* 테이블명 검증
*/
private static validateTableName(tableName: string): ValidationResult {
const errors: string[] = [];
// 1. 기본 형식 검증
if (!this.isValidTableName(tableName)) {
errors.push(
"유효하지 않은 테이블명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다."
);
}
// 2. 길이 검증
if (tableName.length > 63) {
errors.push("테이블명은 63자를 초과할 수 없습니다.");
}
if (tableName.length < 2) {
errors.push("테이블명은 최소 2자 이상이어야 합니다.");
}
// 3. 시스템 테이블 보호
if (this.isSystemTable(tableName)) {
errors.push(
`'${tableName}'은 시스템 테이블명으로 사용할 수 없습니다. 다른 이름을 선택해주세요.`
);
}
// 4. 예약어 검증
if (this.isReservedWord(tableName)) {
errors.push(
`'${tableName}'은 SQL 예약어이므로 테이블명으로 사용할 수 없습니다.`
);
}
// 5. 일반적인 네이밍 컨벤션 검증
if (tableName.startsWith("_") || tableName.endsWith("_")) {
errors.push("테이블명은 언더스코어로 시작하거나 끝날 수 없습니다.");
}
if (tableName.includes("__")) {
errors.push("테이블명에 연속된 언더스코어는 사용할 수 없습니다.");
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* 컬럼 목록 검증
*/
private static validateColumnList(
columns: CreateColumnDefinition[]
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
for (let i = 0; i < columns.length; i++) {
const column = columns[i];
const columnValidation = this.validateSingleColumn(column, i + 1);
if (!columnValidation.isValid) {
errors.push(...columnValidation.errors);
}
if (columnValidation.warnings) {
warnings.push(...columnValidation.warnings);
}
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* 개별 컬럼 검증
*/
private static validateSingleColumn(
column: CreateColumnDefinition,
position?: number
): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
const prefix = position
? `컬럼 ${position}(${column.name}): `
: `컬럼 '${column.name}': `;
// 1. 컬럼명 기본 검증
if (!column.name || column.name.trim() === "") {
errors.push(`${prefix}컬럼명은 필수입니다.`);
return { isValid: false, errors };
}
if (!this.isValidColumnName(column.name)) {
errors.push(
`${prefix}유효하지 않은 컬럼명입니다. 영문자로 시작하고 영문자, 숫자, 언더스코어만 사용 가능합니다.`
);
}
// 2. 길이 검증
if (column.name.length > 63) {
errors.push(`${prefix}컬럼명은 63자를 초과할 수 없습니다.`);
}
if (column.name.length < 2) {
errors.push(`${prefix}컬럼명은 최소 2자 이상이어야 합니다.`);
}
// 3. 예약된 컬럼명 검증
if (this.isReservedColumnName(column.name)) {
errors.push(
`${prefix}'${column.name}'은 예약된 컬럼명입니다. 기본 컬럼(id, created_date, updated_date, company_code)과 중복됩니다.`
);
}
// 4. SQL 예약어 검증
if (this.isReservedWord(column.name)) {
errors.push(
`${prefix}'${column.name}'은 SQL 예약어이므로 컬럼명으로 사용할 수 없습니다.`
);
}
// 5. 웹타입 검증
if (!column.webType) {
errors.push(`${prefix}웹타입이 지정되지 않았습니다.`);
}
// 6. 길이 설정 검증 (text, code 타입에서만 허용)
if (column.length !== undefined) {
if (
!["text", "code", "email", "tel", "select", "radio"].includes(
column.webType
)
) {
warnings.push(
`${prefix}${column.webType} 타입에서는 길이 설정이 무시됩니다.`
);
} else if (column.length <= 0 || column.length > 65535) {
errors.push(`${prefix}길이는 1 이상 65535 이하여야 합니다.`);
}
}
// 7. 네이밍 컨벤션 검증
if (column.name.startsWith("_") || column.name.endsWith("_")) {
warnings.push(
`${prefix}컬럼명이 언더스코어로 시작하거나 끝나는 것은 권장하지 않습니다.`
);
}
if (column.name.includes("__")) {
errors.push(`${prefix}컬럼명에 연속된 언더스코어는 사용할 수 없습니다.`);
}
return {
isValid: errors.length === 0,
errors,
warnings,
};
}
/**
* 컬럼명 중복 검증
*/
private static validateColumnDuplication(
columns: CreateColumnDefinition[]
): ValidationResult {
const errors: string[] = [];
const columnNames = columns.map((col) => col.name.toLowerCase());
const seen = new Set<string>();
const duplicates = new Set<string>();
for (const name of columnNames) {
if (seen.has(name)) {
duplicates.add(name);
} else {
seen.add(name);
}
}
if (duplicates.size > 0) {
errors.push(
`중복된 컬럼명이 있습니다: ${Array.from(duplicates).join(", ")}`
);
}
return {
isValid: errors.length === 0,
errors,
};
}
/**
* 테이블명 유효성 검증 (정규식)
*/
private static isValidTableName(tableName: string): boolean {
const tableNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
return tableNameRegex.test(tableName);
}
/**
* 컬럼명 유효성 검증 (정규식)
*/
private static isValidColumnName(columnName: string): boolean {
const columnNameRegex = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
return columnNameRegex.test(columnName) && columnName.length <= 63;
}
/**
* 시스템 테이블 확인
*/
private static isSystemTable(tableName: string): boolean {
return SYSTEM_TABLES.includes(tableName.toLowerCase() as any);
}
/**
* SQL 예약어 확인
*/
private static isReservedWord(word: string): boolean {
return RESERVED_WORDS.includes(word.toLowerCase() as any);
}
/**
* 예약된 컬럼명 확인
*/
private static isReservedColumnName(columnName: string): boolean {
return RESERVED_COLUMNS.includes(columnName.toLowerCase() as any);
}
/**
* 전체 검증 통계 생성
*/
static generateValidationReport(
tableName: string,
columns: CreateColumnDefinition[]
): {
tableName: string;
totalColumns: number;
validationResult: ValidationResult;
summary: string;
} {
const validationResult = this.validateTableCreation(tableName, columns);
let summary = `테이블 '${tableName}' 검증 완료. `;
summary += `컬럼 ${columns.length}개 중 `;
if (validationResult.isValid) {
summary += "모든 검증 통과.";
} else {
summary += `${validationResult.errors.length}개 오류 발견.`;
}
if (validationResult.warnings && validationResult.warnings.length > 0) {
summary += ` ${validationResult.warnings.length}개 경고 있음.`;
}
return {
tableName,
totalColumns: columns.length,
validationResult,
summary,
};
}
}