#nullable enable using Cysharp.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.IO; using System.Linq; using System.Text; using System.Threading; using UnityEngine; using UVC.Data.Core; using UVC.Log; namespace UVC.Data.Http { /// /// HTTP 응답 데이터를 처리하는 정적 클래스입니다. /// /// /// 이 클래스는 `HttpDataFetcher`로부터 데이터 처리 로직을 분리하여 재사용성을 높입니다. /// HTTP 응답(문자열)을 입력받아, 설정된 규칙(`HttpRequestConfig`)에 따라 파싱, 유효성 검사, /// 데이터 변환(`IDataObject`로 매핑) 및 결과 핸들러 호출까지의 과정을 담당합니다. /// 모든 메서드는 정적(static)이므로 인스턴스 생성 없이 사용할 수 있습니다. /// public static class HttpDataProcessor { /// /// HTTP 응답 문자열을 비동기적으로 처리하고, 결과를 IDataObject로 변환한 후 적절한 핸들러를 호출합니다. /// /// 요청을 식별하는 고유 키. 로그 및 데이터 저장소에서 사용됩니다. /// HTTP 요청에 대한 모든 설정(URL, 매퍼, 핸들러 등)을 담고 있는 객체입니다. /// HTTP 요청으로부터 받은 원시 응답 문자열입니다. /// 비동기 작업을 취소하기 위한 토큰입니다. /// /// 이 메서드는 다음과 같은 순서로 응답을 처리합니다. /// 1. 응답 문자열이 비어 있는지 확인합니다. /// 2. `HttpResponseMask`를 적용하여 응답의 성공 여부를 판단하고 실제 데이터 부분을 추출합니다. /// 3. 데이터가 분할 처리(Split) 대상인지, 단일 객체(Object)인지, 배열(Array)인지 확인하고 각각의 처리 메서드를 호출합니다. /// 4. 처리된 데이터(`IDataObject`)를 `DataRepository`에 추가하거나 업데이트합니다. /// 5. 설정에 따라 성공(`SuccessHandler`) 또는 실패(`FailHandler`) 핸들러를 메인 스레드에서 호출하여 UI 업데이트 등을 안전하게 처리할 수 있도록 합니다. /// 6. 모든 과정에서 발생하는 예외(JSON 파싱, 작업 취소 등)를 처리하고 로그를 남깁니다. /// public static void ProcessResponse(string key, HttpRequestConfig info, string responseContent, CancellationToken cancellationToken) { // 매핑된 최종 데이터 객체를 담을 변수입니다. try-finally 블록에서 자원 해제를 위해 외부에 선언합니다. IDataObject? mappedObject = null; try { // 앞뒤 공백 제거 responseContent = responseContent.Trim(); // 응답이 비어있으면 실패로 간주하고 FailHandler를 호출합니다. if (string.IsNullOrEmpty(responseContent)) { // 핸들러는 UI와 상호작용할 수 있으므로 메인 스레드에서 호출합니다. if (info.FailHandler != null) { UniTask.Post(() => info.FailHandler.Invoke("Response content is null or empty.")); } return; } // ResponseMask를 사용해 응답이 성공적인지 확인하고, 실제 데이터 부분을 추출합니다. // 예를 들어, 응답이 {"status":"OK", "data":{...}} 형태일 때, "status"가 "OK"인지 확인하고 "data" 부분만 가져옵니다. HttpResponseResult responseResult = info.ResponseMask.Apply(responseContent); if (!responseResult.IsSuccess) { if (info.FailHandler != null) { string errorMessage = responseResult.Message!; UniTask.Post(() => info.FailHandler.Invoke(errorMessage)); } return; } // 마스크를 통해 추출된 실제 데이터 responseContent = responseResult.Data!.Trim(); // 응답을 최상위 키를 기준으로 분할 처리해야 하는 경우 if (info.SplitResponseByKey && responseContent.StartsWith("{")) { ProcessSplitResponse(info, responseContent, cancellationToken); return; // 분할 처리가 완료되면 이 메서드의 나머지 로직은 실행하지 않습니다. } // 응답이 JSON 객체 형태인 경우 (e.g., {"id": 1, "name": "item"}) else if (responseContent.StartsWith("{")) { mappedObject = ProcessObjectResponse(info, responseContent, cancellationToken); if (mappedObject == null) return; } // 응답이 JSON 배열 형태인 경우 (e.g., [{"id": 1}, {"id": 2}]) else if (responseContent.StartsWith("[")) { mappedObject = ProcessArrayResponse(info, responseContent, cancellationToken); if (mappedObject == null) return; } // 핸들러 호출 전 작업 취소 요청이 있었는지 다시 확인합니다. cancellationToken.ThrowIfCancellationRequested(); // 매핑된 데이터를 DataRepository에 저장하거나 업데이트합니다. var repoObject = mappedObject; if (mappedObject != null) { // DataRepository는 모든 데이터를 중앙에서 관리하는 저장소입니다. repoObject = DataRepository.Instance.AddOrUpdateData(key, mappedObject, info.UpdatedDataOnly); // 만약 반환된 객체가 원본과 같다면, 핸들러에 전달하기 위해 복제본을 만듭니다. if (repoObject == mappedObject) repoObject = mappedObject.Clone(fromPool: false); } // 'UpdatedDataOnly' 옵션이 켜져 있고, 실제로 데이터가 업데이트되었을 때만 핸들러를 호출합니다. if (info.UpdatedDataOnly) { if (repoObject != null && repoObject.UpdatedCount > 0) { if (info.SuccessHandler != null) { var handlerData = repoObject; UniTask.Post(() => info.SuccessHandler.Invoke(handlerData)); } return; } } // 최종적으로 처리된 데이터가 있는 경우 성공 핸들러를 호출합니다. if (repoObject != null) { if (info.SuccessHandler != null) { var handlerData = repoObject; UniTask.Post(() => info.SuccessHandler.Invoke(handlerData)); } } // 처리된 데이터가 없는 경우 실패 핸들러를 호출합니다. else { if (info.FailHandler != null) { UniTask.Post(() => info.FailHandler.Invoke("Data is Null")); } } } catch (JsonException ex) { // JSON 파싱 중 오류가 발생한 경우 ULog.Error($"JSON parsing error for {key}: {ex.Message}\nResponse: {responseContent}", ex); if (info.FailHandler != null) { UniTask.Post(() => info.FailHandler.Invoke($"JSON parsing error: {ex.Message}")); } } catch (OperationCanceledException) { // 작업이 외부에서 취소된 경우, 예외를 다시 던져 상위 호출자에게 알립니다. throw; } catch (Exception ex) { // 그 외 모든 예외 처리 ULog.Error($"Error processing response for {key}: {ex.Message}", ex); if (info.FailHandler != null) { UniTask.Post(() => info.FailHandler.Invoke($"Error processing response: {ex.Message}")); } } finally { // 매핑 과정에서 생성된 IDataObject 객체는 내부적으로 풀링될 수 있습니다. // 처리가 끝난 객체는 다시 풀에 반환하여 메모리 할당을 줄입니다. mappedObject?.ReturnToPool(); } } /// /// 키-값 형태의 JSON 응답을 각 키별로 분할하여 처리합니다. /// /// HTTP 요청 설정 객체. /// 분할할 JSON 객체 문자열. /// 비동기 작업을 위한 취소 토큰. /// /// 예를 들어, `{"AGV": [...], "ALARM": [...]}` 같은 응답을 받으면, /// "AGV"와 "ALARM"을 각각 별개의 데이터로 간주하고 처리합니다. /// 각 키에 대해 별도의 DataMapper나 Validator를 `HttpRequestConfig`에 설정할 수 있습니다. /// public static void ProcessSplitResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken = null) { JObject responseObject = JObject.Parse(jsonResponse); // JSON 객체의 모든 프로퍼티(키-값 쌍)를 순회합니다. foreach (var property in responseObject.Properties()) { cancellationToken?.ThrowIfCancellationRequested(); string subKey = property.Name; // "AGV", "ALARM" 등 JToken subToken = property.Value; // `[...]` 또는 `{...}` IDataObject? subMappedObject = null; // 현재 키(subKey)에 대한 분할 설정을 가져옵니다. DataMapperValidator? splitConfig = null; if (info.SplitConfigs != null) info.SplitConfigs.TryGetValue(subKey, out splitConfig); // 분할 설정에 지정된 매퍼와 유효성 검사기를 사용합니다. 없으면 null입니다. var subKeyDataMapper = splitConfig?.DataMapper; var subKeyValidator = splitConfig?.Validator; //매퍼가 null인 경우, 해당 키는 처리하지 않습니다. if (subKeyDataMapper == null) continue; try { // 하위 데이터가 배열 또는 객체 형태인지에 따라 적절한 처리 메서드를 호출합니다. if (subToken is JArray && subToken.Count() > 0) { // 분할 처리 중 개별 항목의 실패가 전체 실패로 이어지지 않도록 `invokeFailHandler`를 false로 설정합니다. subMappedObject = ProcessArrayResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false); } else if (subToken is JObject && subToken.Count() > 0) { subMappedObject = ProcessObjectResponse(info, subToken.ToString(), cancellationToken, subKeyDataMapper, subKeyValidator, invokeFailHandler: false); } //Debug.Log($"Processing split response for key: {subKey}, {subToken.Count()}. {subToken.GetType().Name} subMappedObject == null:{subMappedObject == null}"); // 매핑된 결과가 없으면 (예: 유효성 검사 실패) 다음 키로 넘어갑니다. if (subMappedObject == null) { continue; } // 분할된 데이터를 `subKey`를 키로 사용하여 DataRepository에 저장합니다. var repoObject = DataRepository.Instance.AddOrUpdateData(subKey, subMappedObject, info.UpdatedDataOnly); // 핸들러 호출 조건(데이터가 있거나, 업데이트된 경우)을 확인합니다. bool shouldInvokeHandler = repoObject != null && (!info.UpdatedDataOnly || repoObject.UpdatedCount > 0); if (shouldInvokeHandler && info.SuccessHandler != null) { var handlerData = repoObject.Clone(fromPool: false); //await UniTask.SwitchToMainThread(); UniTask.Post(() => info.SuccessHandler.Invoke(handlerData)); // 다음 키 처리를 위해 다시 스레드 풀로 전환합니다. //await UniTask.SwitchToThreadPool(); } } finally { // 사용이 끝난 객체는 풀에 반환합니다. subMappedObject?.ReturnToPool(); } } } private static IDataObject? ProcessObjectResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true) { Action? failHandler = invokeFailHandler ? info.FailHandler : null; return ProcessObjectResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler); } /// /// 단일 JSON 객체 응답을 검증하고 매핑합니다. /// /// 처리할 JSON 객체 문자열. /// 취소 토큰. /// 사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다. /// 사용할 유효성 검사기. null이면 검사를 생략합니다. /// 실패 시 호출할 핸들러. /// 매핑된 IDataObject 또는 null(실패 시). /// /// 이 메서드는 먼저 `DataValidator`를 통해 데이터의 유효성을 검사하고, /// 통과하면 `DataMapper`를 사용해 JSON을 `IDataObject`로 변환합니다. /// 대용량 JSON의 경우 메모리 효율을 위해 스트림 기반 파싱을 지원합니다. /// public static IDataObject? ProcessObjectResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action? failHandler) { // 유효성 검사기가 설정된 경우 if (validator != null) { bool isValid; // 스트림 파싱을 지원하고 데이터가 충분히 큰 경우, 스트림으로 유효성을 검사합니다. if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength) { using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse)); isValid = validator.IsValid(stream); } else { JObject source = JObject.Parse(jsonResponse); isValid = validator.IsValid(source); } // 유효성 검사에 실패하면 실패 핸들러를 호출하고 null을 반환합니다. if (!isValid) { if (failHandler != null) { UniTask.Post(() => failHandler.Invoke("Data is not Valid")); } return null; } } cancellationToken?.ThrowIfCancellationRequested(); // 데이터 매퍼가 설정된 경우 if (dataMapper != null) { // 스트림 파싱 지원 여부에 따라 매핑 방식을 선택합니다. if (dataMapper.SupportsStreamParsing && jsonResponse.Length > dataMapper.SupportsStreamLength) { using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse)); return dataMapper.MapObjectStream(stream); } else { JObject source = JObject.Parse(jsonResponse); return dataMapper.Map(source); } } else { // 매퍼가 없으면 원시 JSON 문자열을 그대로 `DataObject`로 감싸 반환합니다. return new DataObject(jsonResponse); } } private static IDataObject? ProcessArrayResponse(HttpRequestConfig info, string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper = null, DataValidator? validator = null, bool invokeFailHandler = true) { Action? failHandler = invokeFailHandler ? info.FailHandler : null; return ProcessArrayResponse(jsonResponse, cancellationToken, dataMapper ?? info.DataMapper, validator ?? info.Validator, failHandler); } /// /// JSON 배열 응답을 검증하고 매핑합니다. /// /// 처리할 JSON 배열 문자열. /// 취소 토큰. /// 사용할 데이터 매퍼. null이면 원시 데이터를 반환합니다. /// 사용할 유효성 검사기. null이면 검사를 생략합니다. /// 실패 시 호출할 핸들러. /// 매핑된 IDataObject(보통 DataArray) 또는 null(실패 시). /// /// `DataValidator`를 통해 배열의 각 항목을 필터링하여 유효한 데이터만 포함하는 새 배열을 만들 수 있습니다. /// 그 후 `DataMapper`를 통해 배열 전체를 `DataArray` 객체로 변환합니다. /// public static IDataObject? ProcessArrayResponse(string jsonResponse, CancellationToken? cancellationToken, DataMapper? dataMapper, DataValidator? validator, Action? failHandler) { JArray? sourceArray; // 유효성 검사기가 설정된 경우 if (validator != null) { // 스트림 파싱 지원 여부에 따라 유효한 데이터만 필터링합니다. if (validator.SupportsStreamParsing && jsonResponse.Length > validator.SupportsStreamLength) { using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonResponse)); sourceArray = validator.GetValidData(stream); } else { sourceArray = validator.GetValidData(JArray.Parse(jsonResponse)); } // 유효한 데이터가 하나도 없는 경우 if (sourceArray == null || sourceArray.Count == 0) { if (failHandler != null) { UniTask.Post(() => failHandler.Invoke("Data is not Valid or empty after validation")); } return null; } } else { // 검사기가 없으면 원본 배열을 그대로 사용합니다. sourceArray = JArray.Parse(jsonResponse); } cancellationToken?.ThrowIfCancellationRequested(); // 데이터 매퍼가 설정된 경우, 필터링된 배열을 매핑합니다. if (dataMapper != null) { return dataMapper.Map(sourceArray); } else { // 매퍼가 없으면 필터링된 배열을 `DataArray`로 감싸 반환합니다. return new DataArray(sourceArray.ToString()); } } } }