ERP-node/frontend/lib/utils/mappingValidation.ts

407 lines
11 KiB
TypeScript
Raw Normal View History

/**
*
* 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 };
};