/** * 매핑 제약사항 검증 유틸리티 * INSERT/UPDATE/DELETE 액션의 매핑 규칙을 검증합니다. */ export interface ValidationResult { isValid: boolean; error?: string; warnings?: string[]; } export interface ColumnMapping { id?: string; fromColumnName?: string; toColumnName: string; sourceTable?: string; targetTable?: string; defaultValue?: string; transformFunction?: string; } export interface UpdateCondition { id: string; fromColumn: string; operator: string; value: string | string[]; logicalOperator?: "AND" | "OR"; } export interface WhereCondition { id: string; toColumn: string; operator: string; valueSource: string; fromColumn?: string; staticValue?: string; logicalOperator?: "AND" | "OR"; } export interface DeleteCondition { id: string; fromColumn: string; operator: string; value: string | string[]; logicalOperator?: "AND" | "OR"; } /** * 전체 매핑 제약사항 검증 */ export const validateMappingConstraints = ( actionType: "insert" | "update" | "delete", newMapping: ColumnMapping, existingMappings: ColumnMapping[], ): ValidationResult => { switch (actionType) { case "insert": return validateInsertMapping(newMapping, existingMappings); case "update": return validateUpdateMapping(newMapping, existingMappings); case "delete": return validateDeleteConditions(newMapping, existingMappings); default: return { isValid: false, error: "지원하지 않는 액션 타입입니다." }; } }; /** * INSERT 매핑 검증 * 규칙: 1:N 매핑 허용, N:1 매핑 금지 */ export const validateInsertMapping = ( newMapping: ColumnMapping, existingMappings: ColumnMapping[], ): ValidationResult => { // TO 컬럼이 이미 다른 FROM 컬럼과 매핑되어 있는지 확인 const existingToMapping = existingMappings.find((mapping) => mapping.toColumnName === newMapping.toColumnName); if ( existingToMapping && existingToMapping.fromColumnName && existingToMapping.fromColumnName !== newMapping.fromColumnName ) { return { isValid: false, error: `대상 컬럼 '${newMapping.toColumnName}'은 이미 '${existingToMapping.fromColumnName}'과 매핑되어 있습니다.`, }; } // 기본값이 설정된 경우와 FROM 컬럼이 동시에 설정되어 있는지 확인 if (newMapping.fromColumnName && newMapping.defaultValue && newMapping.defaultValue.trim()) { return { isValid: false, error: `'${newMapping.toColumnName}' 컬럼에는 FROM 컬럼과 기본값을 동시에 설정할 수 없습니다.`, }; } return { isValid: true }; }; /** * UPDATE 매핑 검증 */ export const validateUpdateMapping = ( newMapping: ColumnMapping, existingMappings: ColumnMapping[], ): ValidationResult => { // 기본 INSERT 규칙 적용 const baseValidation = validateInsertMapping(newMapping, existingMappings); if (!baseValidation.isValid) { return baseValidation; } // UPDATE 특화 검증 로직 추가 가능 return { isValid: true }; }; /** * DELETE 조건 검증 */ export const validateDeleteConditions = ( newMapping: ColumnMapping, existingMappings: ColumnMapping[], ): ValidationResult => { // DELETE는 기본적으로 조건 기반이므로 매핑 제약이 다름 return { isValid: true }; }; /** * 자기 자신 테이블 UPDATE 작업 검증 */ export const validateSelfTableUpdate = ( fromTable: string, toTable: string, updateConditions: UpdateCondition[], whereConditions: WhereCondition[], ): ValidationResult => { if (fromTable === toTable) { // 1. WHERE 조건 필수 if (!whereConditions.length) { return { isValid: false, error: "자기 자신 테이블 업데이트 시 WHERE 조건이 필수입니다.", }; } // 2. 업데이트 조건과 WHERE 조건이 겹치지 않도록 체크 const conditionColumns = updateConditions.map((c) => c.fromColumn); const whereColumns = whereConditions.map((c) => c.toColumn); const overlap = conditionColumns.filter((col) => whereColumns.includes(col)); if (overlap.length > 0) { return { isValid: false, error: `업데이트 조건과 WHERE 조건에서 같은 컬럼(${overlap.join(", ")})을 사용하면 예상치 못한 결과가 발생할 수 있습니다.`, }; } // 3. 무한 루프 방지 체크 const hasInfiniteLoopRisk = updateConditions.some((condition) => whereConditions.some( (where) => where.fromColumn === condition.toColumn && where.toColumn === condition.fromColumn, ), ); if (hasInfiniteLoopRisk) { return { isValid: false, error: "자기 참조 업데이트로 인한 무한 루프 위험이 있습니다.", }; } } return { isValid: true }; }; /** * 자기 자신 테이블 DELETE 작업 검증 */ export const validateSelfTableDelete = ( fromTable: string, toTable: string, deleteConditions: DeleteCondition[], whereConditions: WhereCondition[], maxDeleteCount: number, ): ValidationResult => { if (fromTable === toTable) { // 1. WHERE 조건 필수 체크 if (!whereConditions.length) { return { isValid: false, error: "자기 자신 테이블 삭제 시 WHERE 조건이 필수입니다.", }; } // 2. 강화된 안전장치: 더 엄격한 제한 const selfDeleteMaxCount = Math.min(maxDeleteCount, 10); if (maxDeleteCount > selfDeleteMaxCount) { return { isValid: false, error: `자기 자신 테이블 삭제 시 최대 ${selfDeleteMaxCount}개까지만 허용됩니다.`, }; } // 3. 삭제 조건이 너무 광범위한지 체크 const hasBroadCondition = deleteConditions.some( (condition) => condition.operator === "!=" || condition.operator === "NOT IN" || condition.operator === "NOT EXISTS", ); if (hasBroadCondition) { return { isValid: false, error: "자기 자신 테이블 삭제 시 부정 조건(!=, NOT IN, NOT EXISTS)은 위험합니다.", }; } // 4. WHERE 조건이 충분히 구체적인지 체크 if (whereConditions.length < 2) { return { isValid: false, error: "자기 자신 테이블 삭제 시 WHERE 조건을 2개 이상 설정하는 것을 권장합니다.", warnings: ["안전을 위해 더 구체적인 조건을 설정하세요."], }; } } return { isValid: true }; }; /** * 컬럼 데이터 타입 호환성 검증 */ export const validateDataTypeCompatibility = (fromColumnType: string, toColumnType: string): ValidationResult => { // 기본 호환성 규칙 const fromType = normalizeDataType(fromColumnType); const toType = normalizeDataType(toColumnType); // 같은 타입이면 호환 if (fromType === toType) { return { isValid: true }; } // 숫자 타입 간 호환성 const numericTypes = ["int", "integer", "bigint", "smallint", "decimal", "numeric", "float", "double"]; if (numericTypes.includes(fromType) && numericTypes.includes(toType)) { return { isValid: true, warnings: ["숫자 타입 간 변환 시 정밀도 손실이 발생할 수 있습니다."], }; } // 문자열 타입 간 호환성 const stringTypes = ["varchar", "char", "text", "string"]; if (stringTypes.includes(fromType) && stringTypes.includes(toType)) { return { isValid: true, warnings: ["문자열 길이 제한을 확인하세요."], }; } // 날짜/시간 타입 간 호환성 const dateTypes = ["date", "datetime", "timestamp", "time"]; if (dateTypes.includes(fromType) && dateTypes.includes(toType)) { return { isValid: true, warnings: ["날짜/시간 형식 변환 시 데이터 손실이 발생할 수 있습니다."], }; } // 호환되지 않는 타입 return { isValid: false, error: `'${fromColumnType}' 타입과 '${toColumnType}' 타입은 호환되지 않습니다.`, }; }; /** * 데이터 타입 정규화 */ const normalizeDataType = (dataType: string): string => { const lowerType = dataType.toLowerCase(); // 정수 타입 if (lowerType.includes("int") || lowerType.includes("serial")) { return "int"; } // 실수 타입 if ( lowerType.includes("decimal") || lowerType.includes("numeric") || lowerType.includes("float") || lowerType.includes("double") ) { return "decimal"; } // 문자열 타입 if ( lowerType.includes("varchar") || lowerType.includes("char") || lowerType.includes("text") || lowerType.includes("string") ) { return "varchar"; } // 날짜 타입 if (lowerType.includes("date")) { return "date"; } // 시간 타입 if (lowerType.includes("time")) { return "datetime"; } // 불린 타입 if (lowerType.includes("bool")) { return "boolean"; } return lowerType; }; /** * 매핑 완성도 검증 */ export const validateMappingCompleteness = (requiredColumns: string[], mappings: ColumnMapping[]): ValidationResult => { const mappedColumns = mappings.map((m) => m.toColumnName); const unmappedRequired = requiredColumns.filter((col) => !mappedColumns.includes(col)); if (unmappedRequired.length > 0) { return { isValid: false, error: `필수 컬럼이 매핑되지 않았습니다: ${unmappedRequired.join(", ")}`, }; } return { isValid: true }; }; /** * 전체 액션 설정 검증 */ export const validateActionConfiguration = ( actionType: "insert" | "update" | "delete", fromTable?: string, toTable?: string, mappings?: ColumnMapping[], conditions?: any[], ): ValidationResult => { // 기본 필수 정보 체크 if (!toTable) { return { isValid: false, error: "대상 테이블을 선택해야 합니다.", }; } // 액션 타입별 검증 switch (actionType) { case "insert": if (!mappings || mappings.length === 0) { return { isValid: false, error: "INSERT 작업에는 최소 하나의 필드 매핑이 필요합니다.", }; } break; case "update": if (!fromTable) { return { isValid: false, error: "UPDATE 작업에는 조건 확인용 소스 테이블이 필요합니다.", }; } if (!conditions || conditions.length === 0) { return { isValid: false, error: "UPDATE 작업에는 WHERE 조건이 필요합니다.", }; } break; case "delete": if (!fromTable) { return { isValid: false, error: "DELETE 작업에는 조건 확인용 소스 테이블이 필요합니다.", }; } if (!conditions || conditions.length === 0) { return { isValid: false, error: "DELETE 작업에는 WHERE 조건이 필요합니다.", }; } break; } return { isValid: true }; };