ERP-node/frontend/lib/services/enhancedFormService.ts

481 lines
14 KiB
TypeScript
Raw Normal View History

/**
*
*
*/
import { ComponentData, ColumnInfo, ScreenDefinition } from "@/types/screen";
import { validateFormData, ValidationResult } from "@/lib/utils/formValidation";
import { dynamicFormApi } from "@/lib/api/dynamicForm";
import { tableManagementApi } from "@/lib/api/tableManagement";
import { screenApi } from "@/lib/api/screen";
// 저장 결과 타입
export interface EnhancedSaveResult {
success: boolean;
message: string;
data?: any;
validationResult?: ValidationResult;
performance?: {
validationTime: number;
saveTime: number;
totalTime: number;
};
warnings?: string[];
}
// 저장 옵션
export interface SaveOptions {
skipClientValidation?: boolean;
skipServerValidation?: boolean;
transformData?: boolean;
showProgress?: boolean;
retryOnError?: boolean;
maxRetries?: number;
}
// 저장 컨텍스트
export interface SaveContext {
tableName: string;
screenInfo: ScreenDefinition;
components: ComponentData[];
formData: Record<string, any>;
options?: SaveOptions;
}
/**
*
*/
export class EnhancedFormService {
private static instance: EnhancedFormService;
private columnCache = new Map<string, ColumnInfo[]>();
private validationCache = new Map<string, ValidationResult>();
public static getInstance(): EnhancedFormService {
if (!EnhancedFormService.instance) {
EnhancedFormService.instance = new EnhancedFormService();
}
return EnhancedFormService.instance;
}
/**
*
*/
async saveFormData(context: SaveContext): Promise<EnhancedSaveResult> {
const startTime = performance.now();
let validationTime = 0;
let saveTime = 0;
try {
const { tableName, screenInfo, components, formData, options = {} } = context;
console.log("🚀 향상된 폼 저장 시작:", {
tableName,
screenId: screenInfo.screenId,
dataKeys: Object.keys(formData),
componentsCount: components.length,
});
// 1. 사전 검증 수행
let validationResult: ValidationResult | undefined;
if (!options.skipClientValidation) {
const validationStart = performance.now();
validationResult = await this.performClientValidation(formData, components, tableName);
validationTime = performance.now() - validationStart;
if (!validationResult.isValid) {
console.error("❌ 클라이언트 검증 실패:", validationResult.errors);
return {
success: false,
message: this.formatValidationMessage(validationResult),
validationResult,
performance: {
validationTime,
saveTime: 0,
totalTime: performance.now() - startTime,
},
};
}
}
// 2. 데이터 변환 및 정제
let processedData = formData;
if (options.transformData !== false) {
processedData = await this.transformFormData(formData, components, tableName);
}
// 3. 서버 저장 수행
const saveStart = performance.now();
const saveResult = await this.performServerSave(screenInfo.screenId, tableName, processedData, options);
saveTime = performance.now() - saveStart;
if (!saveResult.success) {
console.error("❌ 서버 저장 실패:", saveResult.message);
return {
success: false,
message: saveResult.message || "저장 중 서버 오류가 발생했습니다.",
data: saveResult.data,
validationResult,
performance: {
validationTime,
saveTime,
totalTime: performance.now() - startTime,
},
};
}
console.log("✅ 폼 저장 성공:", {
validationTime: `${validationTime.toFixed(2)}ms`,
saveTime: `${saveTime.toFixed(2)}ms`,
totalTime: `${(performance.now() - startTime).toFixed(2)}ms`,
});
return {
success: true,
message: "데이터가 성공적으로 저장되었습니다.",
data: saveResult.data,
validationResult,
performance: {
validationTime,
saveTime,
totalTime: performance.now() - startTime,
},
warnings: validationResult?.warnings.map((w) => w.message),
};
} catch (error: any) {
console.error("❌ 폼 저장 중 예외 발생:", error);
return {
success: false,
message: `저장 중 오류가 발생했습니다: ${error.message || error}`,
performance: {
validationTime,
saveTime,
totalTime: performance.now() - startTime,
},
};
}
}
/**
*
*/
private async performClientValidation(
formData: Record<string, any>,
components: ComponentData[],
tableName: string,
): Promise<ValidationResult> {
try {
// 캐시된 검증 결과 확인
const cacheKey = this.generateValidationCacheKey(formData, components, tableName);
const cached = this.validationCache.get(cacheKey);
if (cached) {
console.log("📋 캐시된 검증 결과 사용");
return cached;
}
// 테이블 컬럼 정보 조회 (캐시 사용)
const tableColumns = await this.getTableColumns(tableName);
// 폼 데이터 검증 수행
const validationResult = await validateFormData(formData, components, tableColumns, tableName);
// 결과 캐시 저장 (5분간)
setTimeout(
() => {
this.validationCache.delete(cacheKey);
},
5 * 60 * 1000,
);
this.validationCache.set(cacheKey, validationResult);
return validationResult;
} catch (error) {
console.error("❌ 클라이언트 검증 중 오류:", error);
return {
isValid: false,
errors: [
{
field: "validation",
code: "VALIDATION_ERROR",
message: `검증 중 오류가 발생했습니다: ${error}`,
severity: "error",
},
],
warnings: [],
};
}
}
/**
* ( )
*/
private async getTableColumns(tableName: string): Promise<ColumnInfo[]> {
// 캐시 확인
const cached = this.columnCache.get(tableName);
if (cached) {
return cached;
}
try {
const response = await tableManagementApi.getColumnList(tableName);
if (response.success && response.data) {
const columns = response.data.map((col) => ({
tableName: col.tableName || tableName,
columnName: col.columnName,
columnLabel: col.displayName,
dataType: col.dataType,
webType: col.webType,
inputType: col.inputType,
isNullable: col.isNullable,
required: col.isNullable === "N",
detailSettings: col.detailSettings,
codeCategory: col.codeCategory,
referenceTable: col.referenceTable,
referenceColumn: col.referenceColumn,
displayColumn: col.displayColumn,
isVisible: col.isVisible,
displayOrder: col.displayOrder,
description: col.description,
})) as ColumnInfo[];
// 캐시 저장 (10분간)
this.columnCache.set(tableName, columns);
setTimeout(
() => {
this.columnCache.delete(tableName);
},
10 * 60 * 1000,
);
return columns;
} else {
throw new Error(response.message || "컬럼 정보 조회 실패");
}
} catch (error) {
console.error("❌ 컬럼 정보 조회 실패:", error);
throw error;
}
}
/**
*
*/
private async transformFormData(
formData: Record<string, any>,
components: ComponentData[],
tableName: string,
): Promise<Record<string, any>> {
const transformed = { ...formData };
const tableColumns = await this.getTableColumns(tableName);
const columnMap = new Map(tableColumns.map((col) => [col.columnName, col]));
for (const [key, value] of Object.entries(transformed)) {
const column = columnMap.get(key);
if (!column) continue;
// 빈 문자열을 null로 변환 (nullable 컬럼인 경우)
if (value === "" && column.isNullable === "Y") {
transformed[key] = null;
continue;
}
// 데이터 타입별 변환
if (value !== null && value !== undefined) {
transformed[key] = this.convertValueByDataType(value, column.dataType);
}
}
// 시스템 필드 자동 추가
const now = new Date().toISOString();
if (!transformed.created_date && tableColumns.some((col) => col.columnName === "created_date")) {
transformed.created_date = now;
}
if (!transformed.updated_date && tableColumns.some((col) => col.columnName === "updated_date")) {
transformed.updated_date = now;
}
console.log("🔄 데이터 변환 완료:", {
original: Object.keys(formData).length,
transformed: Object.keys(transformed).length,
changes: Object.keys(transformed).filter((key) => transformed[key] !== formData[key]),
});
return transformed;
}
/**
*
*/
private convertValueByDataType(value: any, dataType: string): any {
const lowerDataType = dataType.toLowerCase();
// 숫자 타입
if (lowerDataType.includes("integer") || lowerDataType.includes("bigint") || lowerDataType.includes("serial")) {
const num = parseInt(value);
return isNaN(num) ? null : num;
}
if (
lowerDataType.includes("numeric") ||
lowerDataType.includes("decimal") ||
lowerDataType.includes("real") ||
lowerDataType.includes("double")
) {
const num = parseFloat(value);
return isNaN(num) ? null : num;
}
// 불린 타입
if (lowerDataType.includes("boolean")) {
if (typeof value === "boolean") return value;
if (typeof value === "string") {
return value.toLowerCase() === "true" || value === "1" || value === "Y";
}
return Boolean(value);
}
// 날짜/시간 타입
if (lowerDataType.includes("timestamp") || lowerDataType.includes("datetime")) {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date.toISOString();
}
if (lowerDataType.includes("date")) {
const date = new Date(value);
return isNaN(date.getTime()) ? null : date.toISOString().split("T")[0];
}
if (lowerDataType.includes("time")) {
// 시간 형식 변환 로직
return value;
}
// JSON 타입
if (lowerDataType.includes("json")) {
if (typeof value === "string") {
try {
JSON.parse(value);
return value;
} catch {
return JSON.stringify(value);
}
}
return JSON.stringify(value);
}
// 기본값: 문자열
return String(value);
}
/**
*
*/
private async performServerSave(
screenId: number,
tableName: string,
formData: Record<string, any>,
options: SaveOptions,
): Promise<{ success: boolean; message?: string; data?: any }> {
try {
const result = await dynamicFormApi.saveData({
screenId,
tableName,
data: formData,
});
return {
success: result.success,
message: result.message || "저장이 완료되었습니다.",
data: result.data,
};
} catch (error: any) {
console.error("❌ 서버 저장 오류:", error);
// 에러 타입별 처리
if (error.response?.status === 400) {
return {
success: false,
message: "잘못된 요청: " + (error.response.data?.message || "데이터 형식을 확인해주세요."),
};
}
if (error.response?.status === 500) {
return {
success: false,
message: "서버 오류: " + (error.response.data?.message || "잠시 후 다시 시도해주세요."),
};
}
return {
success: false,
message: error.message || "저장 중 알 수 없는 오류가 발생했습니다.",
};
}
}
/**
*
*/
private generateValidationCacheKey(
formData: Record<string, any>,
components: ComponentData[],
tableName: string,
): string {
const dataHash = JSON.stringify(formData);
const componentsHash = JSON.stringify(
components.map((c) => ({ id: c.id, type: c.type, columnName: (c as any).columnName })),
);
return `${tableName}:${btoa(dataHash + componentsHash).substring(0, 32)}`;
}
/**
*
*/
private formatValidationMessage(validationResult: ValidationResult): string {
const errorMessages = validationResult.errors.filter((e) => e.severity === "error").map((e) => e.message);
if (errorMessages.length === 0) {
return "알 수 없는 검증 오류가 발생했습니다.";
}
if (errorMessages.length === 1) {
return errorMessages[0];
}
return `다음 오류들을 수정해주세요:\n• ${errorMessages.join("\n• ")}`;
}
/**
*
*/
public clearCache(): void {
this.columnCache.clear();
this.validationCache.clear();
console.log("🧹 폼 서비스 캐시가 클리어되었습니다.");
}
/**
*
*/
public clearTableCache(tableName: string): void {
this.columnCache.delete(tableName);
console.log(`🧹 테이블 '${tableName}' 캐시가 클리어되었습니다.`);
}
}
// 싱글톤 인스턴스 내보내기
export const enhancedFormService = EnhancedFormService.getInstance();
// 편의 함수들
export const saveFormDataEnhanced = (context: SaveContext): Promise<EnhancedSaveResult> => {
return enhancedFormService.saveFormData(context);
};
export const clearFormCache = (): void => {
enhancedFormService.clearCache();
};
export const clearTableFormCache = (tableName: string): void => {
enhancedFormService.clearTableCache(tableName);
};