/** * 개선된 폼 데이터 저장 서비스 * 클라이언트 측 사전 검증과 서버 측 검증을 조합한 안전한 저장 로직 */ 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; options?: SaveOptions; } /** * 향상된 폼 데이터 저장 클래스 */ export class EnhancedFormService { private static instance: EnhancedFormService; private columnCache = new Map(); private validationCache = new Map(); public static getInstance(): EnhancedFormService { if (!EnhancedFormService.instance) { EnhancedFormService.instance = new EnhancedFormService(); } return EnhancedFormService.instance; } /** * 메인 저장 메서드 */ async saveFormData(context: SaveContext): Promise { 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, components: ComponentData[], tableName: string, ): Promise { 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 { // tableName이 비어있으면 빈 배열 반환 if (!tableName || tableName.trim() === "") { console.warn("⚠️ getTableColumns: tableName이 비어있음"); return []; } // 캐시 확인 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, components: ComponentData[], tableName: string, ): Promise> { 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, 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, 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 => { return enhancedFormService.saveFormData(context); }; export const clearFormCache = (): void => { enhancedFormService.clearCache(); }; export const clearTableFormCache = (tableName: string): void => { enhancedFormService.clearTableCache(tableName); };