using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UVC.Log; namespace UVC.Data.Core { /// /// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다. /// /// /// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다. /// 소스 JSON 객체의 속성을 Mask 객체에 정의된 타입에 따라 적절히 변환합니다. /// /// /// 기본 사용 예시: /// /// // 소스 JSON 데이터 /// var sourceJson = JObject.Parse(@"{ /// ""name"": ""김철수"", /// ""age"": 30, /// ""isActive"": true /// }"); /// /// // Mask 객체 (타입 지정용) /// var maskJson = DataMask.Parse(@"{ /// ""name"": """", /// ""age"": 0, /// ""isActive"": false /// }"); /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// DataObject result = mapper.Map(sourceJson); /// /// // result는 원본과 동일한 구조이며 각 속성이 Mask에 따라 타입 변환됨 /// /// public class DataMapper { // 재귀 호출 제한하기 위한 설정 추가 private int maxRecursionDepth = 10; /// /// 재귀 최대 깊이를 설정하거나 가져옵니다. 이 값을 초과하는 중첩 객체는 간소화된 처리가 적용됩니다. /// public int MaxRecursionDepth { get => maxRecursionDepth; set => maxRecursionDepth = Math.Max(1, value); // 최소 1 이상 보장 } /// /// 타입 변환을 위한 마스크 객체 /// private DataMask mask; public DataMask Mask => mask; /// /// 병렬 처리를 적용할 배열의 최소 크기 /// private int parallelProcessingThreshold = 1000; /// /// 병렬 처리를 적용할 배열의 최소 크기를 설정하거나 가져옵니다. /// public int ParallelProcessingThreshold { get => parallelProcessingThreshold; set => parallelProcessingThreshold = Math.Max(10, value); // 최소 10 이상 보장 } /// /// 매핑 중 발생한 변환 오류를 추적하는 딕셔너리 /// private Dictionary conversionErrors; /// /// 매핑 중 발생한 모든 변환 오류에 접근할 수 있는 읽기 전용 속성 /// public IReadOnlyDictionary ConversionErrors => conversionErrors; /// /// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다. /// public bool SupportsStreamParsing { get; internal set; } = true; /// /// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다. /// public int SupportsStreamLength { get; internal set; } = 10000; /// /// DataMapper 클래스의 새 인스턴스를 초기화합니다. /// /// 타입 변환을 위한 Mask JSON 객체 /// /// Mask 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만, /// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다. /// public DataMapper(DataMask mask) { this.mask = mask ?? throw new ArgumentNullException(nameof(mask)); this.conversionErrors = new Dictionary(); } /// /// 소스 객체를 Mask 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다. /// /// 매핑할 원본 JSON 객체 /// 매핑된 DataObject 객체 /// /// /// var mapper = new DataMapper(maskJson); /// DataObject result = mapper.Map(sourceJson); /// ULog.Debug(result["name"].ToString()); // "김철수" /// ULog.Debug(result["age"].ToObject()); // 30 /// /// public DataObject Map(JObject source) { return MapObject(source, mask); } /// /// 소스 배열을 Mask 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다. /// /// 매핑할 원본 JSON 배열 /// 매핑된 DataArray 객체 /// /// 이 메서드는 Mask 객체를 JArray로 변환하여 소스 배열의 각 항목을 매핑합니다. /// Mask 배열이 비어있으면 원본 배열의 각 항목을 그대로 변환하고, /// 그렇지 않으면 Mask 배열이 하나인 경우 첫 번째 항목을 템플릿으로 사용하고 /// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다. /// /// /// /// var sourceArray = JArray.Parse(@"[ /// { ""name"": ""김철수"", ""age"": 30 }, /// { ""name"": ""이영희"", ""age"": 25 } /// ]"); /// /// var maskJson = DataMask.Parse(@"{ /// ""name"": """", /// ""age"": 0 /// }"); /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// DataArray result = mapper.Map(sourceArray); /// // result는 원본 배열과 동일한 구조의 DataArray /// /// public DataArray Map(JArray source) { List arr = new List() { mask }; return MapArray(source, arr); } /// /// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다. /// /// JSON 데이터 스트림 /// 매핑된 DataObject public DataObject MapObjectStream(System.IO.Stream jsonStream) { if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream)); // 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만, // 현재는 Newtonsoft.Json의 기본 역직렬화 사용 using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream))) { // 청크 읽기 설정 - 메모리 사용량 최적화 reader.SupportMultipleContent = true; var serializer = new Newtonsoft.Json.JsonSerializer(); var sourceObject = serializer.Deserialize(reader); return Map(sourceObject); } } /// /// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다. /// /// JSON 데이터 스트림 /// 매핑된 DataObject public DataArray MapArrayStream(System.IO.Stream jsonStream) { if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream)); using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream))) { // 청크 읽기 설정 - 메모리 사용량 최적화 reader.SupportMultipleContent = true; var serializer = new Newtonsoft.Json.JsonSerializer(); var sourceArray = serializer.Deserialize(reader); return Map(sourceArray); } } /// /// 객체를 재귀적으로 매핑합니다. /// /// 원본 JSON 객체 /// Mask JSON 객체 /// 현재 재귀 깊이 /// 현재 속성 경로(오류 추적용) /// 매핑된 DataObject 객체 /// /// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다. /// Mask 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다. /// /// /// 중첩 객체 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""user"": { /// ""name"": ""김철수"", /// ""address"": { /// ""city"": ""서울"", /// ""zipcode"": ""12345"" /// } /// } /// }"); /// /// var maskJson = DataMask.Parse(@"{ /// ""user"": { /// ""name"": """", /// ""address"": { /// ""city"": """", /// ""zipcode"": """" /// } /// } /// }"); /// maskJson.ObjectIdKey = "user"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result는 sourceJson과 동일한 중첩 구조를 유지 /// /// private DataObject MapObject(JObject sourceObject, DataMask maskObject, int depth = 0, string path = "") { if (maskObject == null) { var dataObj = DataObjectPool.Get(); dataObj.FromJObject(sourceObject); return dataObj; } // Mask가 비어있으면 원본 객체를 그대로 사용, 깊이 제한에 도달하면 간소화된 처리 if (maskObject.Count == 0 || depth >= maxRecursionDepth) { DataObject dObj = DataObjectPool.Get(); dObj.IdKey = maskObject.ObjectIdKey; dObj.Name = maskObject.ObjectName; // 속성 이름 변환 처리 foreach (var property in sourceObject.Properties()) { string propertyName = property.Name; if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName)) { propertyName = maskObject.NamesForReplace[propertyName]; } dObj[propertyName] = ConvertJTokenToObject(property.Value); } return dObj; } DataObject target = DataObjectPool.Get(); target.IdKey = maskObject.ObjectIdKey; target.Name = maskObject.ObjectName; foreach (var property in sourceObject.Properties()) { string propertyName = property.Name; string currentPath = string.IsNullOrEmpty(path) ? propertyName : $"{path}.{propertyName}"; if (maskObject.ContainsKey(propertyName)) { object maskValue = maskObject[propertyName]; JToken sourceValue = property.Value; // 속성 이름 변환 처리 if (maskObject.NamesForReplace != null && maskObject.NamesForReplace.ContainsKey(propertyName)) { propertyName = maskObject.NamesForReplace[propertyName]; } try { // 중첩된 객체 처리 if (sourceValue.Type == JTokenType.Object && maskValue is DataMask maskSubObject) { target[propertyName] = MapObject((JObject)sourceValue, maskSubObject, depth + 1, currentPath); } // 중첩된 배열 처리 else if (sourceValue.Type == JTokenType.Array && maskValue is List maskArrayTemplate) { var arr = MapArray((JArray)sourceValue, maskArrayTemplate, currentPath); target[propertyName] = arr; } // 일반 속성 처리 else { MapProperty(propertyName, sourceValue, maskValue, target, currentPath); } } catch (Exception ex) { // 예외 정보 저장 후 기본값 설정 conversionErrors[currentPath] = ex; ULog.Warning($"매핑 중 오류 발생 - 경로: {currentPath}", ex); // 마스크 값 타입에 맞는 기본값 설정 SetDefaultValueForProperty(propertyName, maskValue, target); } } else { // Mask에 없는 속성은 무시 (필요시 설정을 통해 포함 가능) continue; } } return target; } /// /// 예외 발생 시 마스크 값 타입에 맞는 기본값을 설정합니다. /// private void SetDefaultValueForProperty(string propertyName, object maskValue, DataObject target) { if (maskValue is string) { target[propertyName] = string.Empty; } else if (maskValue is int || maskValue is long || maskValue is short || maskValue is byte) { target[propertyName] = 0; } else if (maskValue is double || maskValue is float || maskValue is decimal) { target[propertyName] = 0.0; } else if (maskValue is bool) { target[propertyName] = false; } else if (maskValue is DateTime) { target[propertyName] = null; } else if (maskValue is DataMask) { target[propertyName] = DataObjectPool.Get(); } else if (maskValue is List) { target[propertyName] = DataArrayPool.Get(); } else { target[propertyName] = null; } } /// /// JToken을 실제 객체로 변환하는 헬퍼 메서드 /// /// 변환할 JToken 객체 /// 변환된 .NET 객체 /// /// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다. /// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다. /// private object ConvertJTokenToObject(JToken token) { if (token == null) return null; switch (token.Type) { case JTokenType.Object: var dataObject = DataObjectPool.Get(); dataObject.FromJObject((JObject)token); return dataObject; case JTokenType.Array: JArray array = (JArray)token; if (array.All(item => item.Type == JTokenType.Object)) { return DataArrayPool.Get().FromJArray(array); } return array.ToObject(); case JTokenType.Integer: return token.ToObject(); case JTokenType.Float: return token.ToObject(); case JTokenType.String: return token.ToObject(); case JTokenType.Boolean: return token.ToObject(); case JTokenType.Date: return token.ToObject(); case JTokenType.Null: return null; default: return token.ToObject(); } } /// /// 배열을 재귀적으로 매핑합니다. /// /// 원본 JSON 배열 /// Mask JSON 배열 /// 현재 속성 경로(오류 추적용) /// 매핑된 DataArray 객체 /// /// Mask 배열이 비어있으면, 원본 배열의 각 항목을 DataObject로 변환하여 복사합니다. /// 단순 값(문자열, 숫자 등)은 "value" 키를 가진 DataObject로 래핑됩니다. /// /// Mask 배열이 비어있지 않으면, Mask 배열의 첫 번째 항목을 템플릿으로 사용하고 /// 하나 이상인 경우 Mask 배열 개수에 맞춰 템플릿으로 사용(배열 길이가 3이고, 소스 배열 길이가 5일때, 소스배열 3까지만 매핑) 합니다. /// 원본 배열의 각 항목을 매핑합니다. 중첩 배열은 "items" 키를 가진 DataObject로 래핑됩니다. /// /// /// 배열 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""contacts"": [ /// { ""type"": ""mobile"", ""number"": ""010-1234-5678"" }, /// { ""type"": ""home"", ""number"": ""02-123-4567"" } /// ] /// }"); /// /// var maskJson = DataMask.Parse(@"{ /// ""contacts"": [ /// { ""type"": """", ""number"": """" } /// ] /// }"); /// maskJson.ObjectIdKey = "contacts"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result.contacts는 원본 배열과 동일한 구조의 DataArray /// /// private DataArray MapArray(JArray sourceArray, List maskTemplates, string path = "") { // 빠른 초기 크기 할당으로 재할당 방지 DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count); // 특정 크기 이상일 경우 병렬 처리 적용 if (sourceArray.Count > parallelProcessingThreshold && maskTemplates.Count <= 1) { return MapArrayInParallel(sourceArray, maskTemplates, path); } // Mask 배열이 비어있으면 원본 배열을 그대로 사용 if (maskTemplates == null || maskTemplates.Count == 0) { for (int i = 0; i < sourceArray.Count; i++) { JToken sourceItem = sourceArray[i]; string itemPath = $"{path}[{i}]"; try { if (sourceItem.Type == JTokenType.Object) { var dataObject = DataObjectPool.Get(); dataObject.FromJObject((JObject)sourceItem); targetArray.Add(dataObject); } else { // DataObject가 아닌 경우, 새 DataObject를 만들고 값을 넣어줍니다 var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); targetArray.Add(dataObject); } } catch (Exception ex) { conversionErrors[itemPath] = ex; ULog.Warning($"배열 항목 변환 오류 - 경로: {itemPath}", ex); // 오류 발생 시 빈 객체 추가 targetArray.Add(DataObjectPool.Get()); } } return targetArray; } // Mask 템플릿 사용 - 반복 사용 혹은 일치 사용 for (int i = 0; i < sourceArray.Count; i++) { JToken sourceItem = sourceArray[i]; string itemPath = $"{path}[{i}]"; // 현재 인덱스에 맞는 마스크 템플릿 선택 (또는 첫 번째 템플릿 재사용) DataMask maskTemplate; if (maskTemplates.Count > 1) { // maskTemplates.Count > 1이면서 소스 항목 개수 보다 적은 경우 // maskTemplates.Count 보다 큰 인덱스는 무시 // 예: maskTemplates.Count가 3이고, sourceArray.Count가 5인 경우 // 0, 1, 2 인덱스는 maskTemplates[0], maskTemplates[1], maskTemplates[2] 사용 // 3, 4 인덱스는 저장 않함 if (i >= maskTemplates.Count) continue; int templateIndex = Math.Min(i, maskTemplates.Count - 1); maskTemplate = maskTemplates[templateIndex]; } else { // 단일 템플릿 반복 사용 maskTemplate = maskTemplates[0]; } try { if (sourceItem.Type == JTokenType.Object) { JObject sourceItem2 = (JObject)sourceItem; targetArray.Add(MapObject(sourceItem2, maskTemplate, 0, itemPath)); } else if (sourceItem.Type == JTokenType.Array) { // DataArray는 DataObject만 담을 수 있으므로, 배열을 처리하는 방식을 변경해야 함 var nestedArray = MapArray((JArray)sourceItem, maskTemplates, itemPath); var container = DataObjectPool.Get(); container.Add("items", nestedArray); targetArray.Add(container); } else { // 단순 값을 DataObject로 래핑 var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); targetArray.Add(dataObject); } } catch (Exception ex) { conversionErrors[itemPath] = ex; ULog.Warning($"배열 항목 변환 오류 - 경로: {itemPath}", ex); // 오류 발생 시 빈 객체 추가 targetArray.Add(DataObjectPool.Get()); } } return targetArray; } /// /// 대형 배열을 병렬로 처리하는 배열 매핑 메서드 /// private DataArray MapArrayInParallel(JArray sourceArray, List maskTemplates, string path) { DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count); DataMask maskTemplate = maskTemplates.Count > 0 ? maskTemplates[0] : new DataMask(); var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount }; // 결과를 담을 배열 (순서 보존을 위해) var results = new DataObject[sourceArray.Count]; try { Parallel.For(0, sourceArray.Count, parallelOptions, i => { var sourceItem = sourceArray[i]; string itemPath = $"{path}[{i}]"; try { if (sourceItem.Type == JTokenType.Object && maskTemplate != null) { results[i] = MapObject((JObject)sourceItem, maskTemplate, 0, itemPath); } else { // 단순 값 처리 var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); results[i] = dataObject; } } catch (Exception ex) { lock (conversionErrors) { conversionErrors[itemPath] = ex; } ULog.Warning($"병렬 배열 처리 오류 - 경로: {itemPath}", ex); // 오류 발생 시 빈 객체 생성 results[i] = DataObjectPool.Get(); } }); // 결과를 순서대로 추가 targetArray.AddRange(results); } catch (Exception ex) { ULog.Error($"병렬 배열 매핑 중 치명적 오류 발생: {ex.Message}", ex); // 병렬 처리 실패 시 일반 순차 처리로 대체 return MapArraySequential(sourceArray, maskTemplates, path); } return targetArray; } /// /// 병렬 처리가 실패할 경우 대체 사용할 순차적 배열 처리 메서드 /// private DataArray MapArraySequential(JArray sourceArray, List maskTemplates, string path) { DataArray targetArray = DataArrayPool.Get().FromCapacity(sourceArray.Count); DataMask maskTemplate = maskTemplates.Count > 0 ? maskTemplates[0] : new DataMask(); for (int i = 0; i < sourceArray.Count; i++) { JToken sourceItem = sourceArray[i]; string itemPath = $"{path}[{i}]"; try { if (sourceItem.Type == JTokenType.Object) { targetArray.Add(MapObject((JObject)sourceItem, maskTemplate, 0, itemPath)); } else { var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); targetArray.Add(dataObject); } } catch (Exception ex) { conversionErrors[itemPath] = ex; ULog.Warning($"순차 배열 처리 오류 - 경로: {itemPath}", ex); targetArray.Add(DataObjectPool.Get()); } } return targetArray; } /// /// 개별 속성을 매핑합니다. /// /// 속성 이름 /// 매핑할 원본 값 /// 타입을 결정하는 Mask 값 /// 값을 추가할 대상 DataObject /// 현재 속성 경로(오류 추적용) /// /// 이 메서드는 Mask 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다. /// /// 지원되는 타입: /// - 문자열 (string) /// - 정수 (int) /// - 실수 (double, float) /// - 불리언 (bool) /// - 날짜/시간 (DateTime) /// - 열거형 (Enum) /// - DataMap (문자열 매핑 딕셔너리) /// - DataMask (중첩된 마스크 객체) /// - List (배열 마스크) /// /// 타입 변환이 불가능한 경우 원본 값이 그대로 사용됩니다. /// /// /// 다양한 타입 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""name"": ""김철수"", /// ""age"": 30, /// ""height"": 175.5, /// ""isActive"": true, /// ""birthDate"": ""1990-01-01T00:00:00"", /// ""status"": ""Active"" /// }"); /// /// // Mask 객체 설정 (열거형 포함) /// var maskJson = new DataMask(); /// maskJson["name"] = ""; /// maskJson["age"] = 0; /// maskJson["height"] = 0.0; /// maskJson["isActive"] = false; /// maskJson["birthDate"] = JToken.FromObject(DateTime.Now); /// maskJson["status"] = JToken.FromObject(UserStatus.Inactive); /// /// maskJson.ObjectIdKey = "name"; // DataObject의 Id로 사용할 속성 지정 /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result에는 모든 속성이 적절한 타입으로 변환됨 /// /// private void MapProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target, string path) { // 소스 값이 널이면 바로 널 설정 if (sourceValue == null || sourceValue.Type == JTokenType.Null) { target[propertyName] = null; return; } // 타입별 매핑 처리를 분리된 메서드로 호출 if (maskValue is string) { MapStringProperty(propertyName, sourceValue, target); } else if (maskValue is int || maskValue is long || maskValue is short || maskValue is byte) { MapIntegerProperty(propertyName, sourceValue, target); } else if (maskValue is double || maskValue is float || maskValue is decimal) { MapFloatingPointProperty(propertyName, sourceValue, target); } else if (maskValue is bool) { MapBooleanProperty(propertyName, sourceValue, target); } else if (maskValue is DateTime) { MapDateTimeProperty(propertyName, sourceValue, target); } else if (maskValue is Enum || (maskValue?.GetType()?.IsEnum ?? false)) { MapEnumProperty(propertyName, sourceValue, maskValue, target); } else if (maskValue is Dictionary dicObj && dicObj.GetType() == typeof(DataMask)) { MapNestedObjectProperty(propertyName, sourceValue, (DataMask)maskValue, target, path); } else if (maskValue is List) { MapNestedArrayProperty(propertyName, sourceValue, (List)maskValue, target, path); } else if (maskValue is Dictionary || maskValue is DataMap) { MapDataMapProperty(propertyName, sourceValue, maskValue, target); } else if (maskValue is JToken jToken) { MapJTokenProperty(propertyName, sourceValue, jToken, target); } else { // 기타 타입은 그대로 객체로 변환 target[propertyName] = ConvertJTokenToObject(sourceValue); } } #region 타입별 속성 매핑 메서드 /// /// 문자열 타입 속성 매핑 /// private void MapStringProperty(string propertyName, JToken sourceValue, DataObject target) { if (sourceValue.Type == JTokenType.String || sourceValue.Type == JTokenType.Integer || sourceValue.Type == JTokenType.Float || sourceValue.Type == JTokenType.Boolean) { target[propertyName] = sourceValue.ToObject(); } else { target[propertyName] = sourceValue.ToString(); } } /// /// 정수 타입 속성 매핑 /// private void MapIntegerProperty(string propertyName, JToken sourceValue, DataObject target) { if (sourceValue.Type == JTokenType.Integer || sourceValue.Type == JTokenType.Float || sourceValue.Type == JTokenType.String) { try { // 명시적으로 값 범위 검사를 통해 오버플로우 방지 var doubleValue = sourceValue.ToObject(); if (doubleValue > int.MaxValue || doubleValue < int.MinValue) { throw new OverflowException($"값 {doubleValue}가 Int32 범위를 벗어남"); } target[propertyName] = Convert.ToInt32(doubleValue); } catch (Exception) { target[propertyName] = 0; // 변환 실패시 기본값 } } else { target[propertyName] = 0; } } /// /// 부동소수점 타입 속성 매핑 /// private void MapFloatingPointProperty(string propertyName, JToken sourceValue, DataObject target) { if (sourceValue.Type == JTokenType.Float || sourceValue.Type == JTokenType.Integer || sourceValue.Type == JTokenType.String) { try { target[propertyName] = sourceValue.ToObject(); } catch { target[propertyName] = 0.0; // 변환 실패시 기본값 } } else { target[propertyName] = 0.0; } } /// /// 불리언 타입 속성 매핑 /// private void MapBooleanProperty(string propertyName, JToken sourceValue, DataObject target) { if (sourceValue.Type == JTokenType.Boolean) { target[propertyName] = sourceValue.ToObject(); } else if (sourceValue.Type == JTokenType.Integer) { // 0은 false, 나머지는 true로 처리 target[propertyName] = sourceValue.ToObject() != 0; } else if (sourceValue.Type == JTokenType.String) { string strValue = sourceValue.ToString().ToLower(); // 일반적인 참/거짓 문자열 처리 if (strValue == "true" || strValue == "yes" || strValue == "y" || strValue == "1") { target[propertyName] = true; } else if (strValue == "false" || strValue == "no" || strValue == "n" || strValue == "0") { target[propertyName] = false; } else { // 나머지는 bool.Parse 시도 try { target[propertyName] = bool.Parse(strValue); } catch { target[propertyName] = false; // 변환 실패시 기본값 } } } else { target[propertyName] = false; } } /// /// DateTime 타입 속성 매핑 /// private void MapDateTimeProperty(string propertyName, JToken sourceValue, DataObject target) { if (sourceValue.Type == JTokenType.Date) { target[propertyName] = sourceValue.ToObject(); } else if (sourceValue.Type == JTokenType.String) { string dateStr = sourceValue.ToString(); if (DateTime.TryParse(dateStr, out DateTime dateValue)) { target[propertyName] = dateValue; } else { target[propertyName] = null; } } else { target[propertyName] = null; } } /// /// 열거형 타입 속성 매핑 /// private void MapEnumProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target) { try { Type enumType = maskValue.GetType(); if (sourceValue.Type == JTokenType.String) { target[propertyName] = Enum.Parse(enumType, sourceValue.ToString(), true); } else if (sourceValue.Type == JTokenType.Integer) { target[propertyName] = Enum.ToObject(enumType, sourceValue.ToObject()); } else { target[propertyName] = maskValue; // 변환 불가능하면 마스크 기본값 사용 } } catch { target[propertyName] = maskValue; // 변환 실패시 마스크 기본값 사용 } } /// /// 중첩된 객체 타입 속성 매핑 /// private void MapNestedObjectProperty(string propertyName, JToken sourceValue, DataMask nestedMask, DataObject target, string path) { if (sourceValue.Type == JTokenType.Object) { target[propertyName] = MapObject((JObject)sourceValue, nestedMask, 0, $"{path}.{propertyName}"); } else if (sourceValue.Type == JTokenType.String) { // 문자열을 JSON으로 파싱 시도 try { JObject jObj = JObject.Parse(sourceValue.ToString()); target[propertyName] = MapObject(jObj, nestedMask, 0, $"{path}.{propertyName}"); } catch { target[propertyName] = sourceValue.ToString(); } } else { target[propertyName] = ConvertJTokenToObject(sourceValue); } } /// /// 중첩된 배열 타입 속성 매핑 /// private void MapNestedArrayProperty(string propertyName, JToken sourceValue, List maskTemplates, DataObject target, string path) { if (sourceValue.Type == JTokenType.Array) { target[propertyName] = MapArray((JArray)sourceValue, maskTemplates, $"{path}.{propertyName}"); } else if (sourceValue.Type == JTokenType.String) { // 문자열을 JSON 배열로 파싱 시도 try { JArray jArr = JArray.Parse(sourceValue.ToString()); target[propertyName] = MapArray(jArr, maskTemplates, $"{path}.{propertyName}"); } catch { // 배열 파싱 실패시 단일 항목 배열로 처리 var dataObject = DataObjectPool.Get(); dataObject.Add("value", sourceValue.ToString()); var array = DataArrayPool.Get().FromCapacity(1); array.Add(dataObject); target[propertyName] = array; } } else { // 다른 타입은 단일 항목 배열로 변환 var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(sourceValue)); var array = DataArrayPool.Get().FromCapacity(1); array.Add(dataObject); target[propertyName] = array; } } /// /// DataMap 타입 속성 매핑 (문자열 매핑) /// private void MapDataMapProperty(string propertyName, JToken sourceValue, object maskValue, DataObject target) { Dictionary dataMap; if (maskValue is DataMap dm) { dataMap = dm; } else { dataMap = (Dictionary)maskValue; } if (sourceValue.Type == JTokenType.String) { string strValue = sourceValue.ToString(); if (dataMap.ContainsKey(strValue)) { target[propertyName] = dataMap[strValue]; } else { target[propertyName] = strValue; } } else { target[propertyName] = sourceValue.ToObject(); } } /// /// JToken 타입 속성 매핑 (이전 코드와의 호환성) /// private void MapJTokenProperty(string propertyName, JToken sourceValue, JToken jToken, DataObject target) { if (sourceValue.Type == jToken.Type) { switch (sourceValue.Type) { case JTokenType.String: target[propertyName] = sourceValue.ToObject(); return; case JTokenType.Integer: target[propertyName] = sourceValue.ToObject(); return; case JTokenType.Float: target[propertyName] = sourceValue.ToObject(); return; case JTokenType.Boolean: target[propertyName] = sourceValue.ToObject(); return; case JTokenType.Date: target[propertyName] = sourceValue.ToObject(); return; } } if (jToken.Type == JTokenType.Date && sourceValue.Type == JTokenType.String) { string dateStr = sourceValue.ToObject(); if (DateTime.TryParse(dateStr, out DateTime dateValue)) { target[propertyName] = dateValue; } else { target[propertyName] = null; } } else if (jToken.Type == JTokenType.Object && sourceValue.Type == JTokenType.String) { try { // 먼저 DataMap로 변환 시도 var dataMap = jToken.ToObject(); if (dataMap != null) { string strValue = sourceValue.ToObject(); if (dataMap.ContainsKey(strValue)) { target[propertyName] = dataMap[strValue]; } else { target[propertyName] = strValue; } } else { // DataMap가 아니면 소스 값 그대로 사용 target[propertyName] = sourceValue.ToObject(); } } catch { // 변환 실패 시 소스 값 그대로 사용 target[propertyName] = sourceValue.ToObject(); } } else { // 기타 타입은 그대로 객체로 변환 target[propertyName] = ConvertJTokenToObject(sourceValue); } } #endregion } }