using Newtonsoft.Json.Linq; using System; using System.Linq; namespace UVC.Data { /// /// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다. /// /// /// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다. /// 소스 JSON 객체의 속성을 가이드 객체에 정의된 타입에 따라 적절히 변환합니다. /// /// /// 기본 사용 예시: /// /// // 소스 JSON 데이터 /// var sourceJson = JObject.Parse(@"{ /// ""name"": ""김철수"", /// ""age"": 30, /// ""isActive"": true /// }"); /// /// // 가이드 객체 (타입 지정용) /// var maskJson = JObject.Parse(@"{ /// ""name"": """", /// ""age"": 0, /// ""isActive"": false /// }"); /// /// var mapper = new DataMapper(maskJson); /// DataObject result = mapper.Map(sourceJson); /// /// // result는 원본과 동일한 구조이며 각 속성이 가이드에 따라 타입 변환됨 /// /// public class DataMapper { private JObject mask; /// /// DataMapper 클래스의 새 인스턴스를 초기화합니다. /// /// 타입 변환을 위한 가이드 JSON 객체 /// /// 가이드 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만, /// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다. /// public DataMapper(JObject mask) { this.mask = mask; } /// /// 소스 객체를 가이드 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다. /// /// 매핑할 원본 JSON 객체 /// 매핑된 DataObject 객체 /// /// /// var mapper = new DataMapper(maskJson); /// DataObject result = mapper.Map(sourceJson); /// Debug.Log(result["name"].ToString()); // "김철수" /// Debug.Log(result["age"].ToObject<int>()); // 30 /// /// public DataObject Map(JObject source) { return MapObject(source, mask); } /// /// 소스 배열을 가이드 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다. /// /// 매핑할 원본 JSON 배열 /// 매핑된 DataArray 객체 /// /// 이 메서드는 가이드 객체로부터 새로운 배열을 생성하여 소스 배열의 각 항목을 매핑합니다. /// /// /// /// var sourceArray = JArray.Parse(@"[ /// { ""name"": ""김철수"", ""age"": 30 }, /// { ""name"": ""이영희"", ""age"": 25 } /// ]"); /// /// var maskJson = JObject.Parse(@"{ /// ""name"": """", /// ""age"": 0 /// }"); /// /// var mapper = new DataMapper(maskJson); /// DataArray result = mapper.Map(sourceArray); /// // result는 원본 배열과 동일한 구조의 DataArray /// /// public DataArray Map(JArray source) { JArray arr = new JArray(mask); return MapArray(source, arr); } /// /// 객체를 재귀적으로 매핑합니다. /// /// 원본 JSON 객체 /// 가이드 JSON 객체 /// 매핑된 DataObject 객체 /// /// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다. /// 가이드 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다. /// /// /// 중첩 객체 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""user"": { /// ""name"": ""김철수"", /// ""address"": { /// ""city"": ""서울"", /// ""zipcode"": ""12345"" /// } /// } /// }"); /// /// var maskJson = JObject.Parse(@"{ /// ""user"": { /// ""name"": """", /// ""address"": { /// ""city"": """", /// ""zipcode"": """" /// } /// } /// }"); /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result는 sourceJson과 동일한 중첩 구조를 유지 /// /// private DataObject MapObject(JObject sourceObject, JObject maskObject) { DataObject target = new DataObject(); foreach (var property in sourceObject.Properties()) { if (maskObject.ContainsKey(property.Name)) { JToken maskValue = maskObject[property.Name]; JToken sourceValue = property.Value; // 중첩된 객체 처리 if (sourceValue.Type == JTokenType.Object && maskValue.Type == JTokenType.Object) { target[property.Name] = MapObject((JObject)sourceValue, (JObject)maskValue); } // 중첩된 배열 처리 else if (sourceValue.Type == JTokenType.Array && maskValue.Type == JTokenType.Array) { var arr = MapArray((JArray)sourceValue, (JArray)maskValue); target[property.Name] = arr; } else { MapProperty(property.Name, sourceValue, maskValue, target); } } else { continue; // 가이드에 없는 속성은 무시 } } return target; } /// /// JToken을 실제 객체로 변환하는 헬퍼 메서드 /// private object ConvertJTokenToObject(JToken token) { switch (token.Type) { case JTokenType.Object: return new DataObject((JObject)token); case JTokenType.Array: JArray array = (JArray)token; if (array.All(item => item.Type == JTokenType.Object)) { return new DataArray((JArray)token); } 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 배열 /// 가이드 JSON 배열 /// 매핑된 DataArray 객체 /// /// 가이드 배열이 비어있으면, 원본 배열의 각 항목을 DataObject로 변환하여 복사합니다. /// 단순 값(문자열, 숫자 등)은 "value" 키를 가진 DataObject로 래핑됩니다. /// /// 가이드 배열이 비어있지 않으면, 가이드 배열의 첫 번째 항목을 템플릿으로 사용하여 /// 원본 배열의 각 항목을 매핑합니다. 중첩 배열은 "items" 키를 가진 DataObject로 래핑됩니다. /// /// /// 배열 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""contacts"": [ /// { ""type"": ""mobile"", ""number"": ""010-1234-5678"" }, /// { ""type"": ""home"", ""number"": ""02-123-4567"" } /// ] /// }"); /// /// var maskJson = JObject.Parse(@"{ /// ""contacts"": [ /// { ""type"": """", ""number"": """" } /// ] /// }"); /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result.contacts는 원본 배열과 동일한 구조의 DataArray /// /// private DataArray MapArray(JArray sourceArray, JArray maskArray) { DataArray targetArray = new DataArray(); // 가이드 배열이 비어있으면 원본 배열을 그대로 사용 if (maskArray.Count == 0) { foreach (JToken sourceItem in sourceArray) { if (sourceItem.Type == JTokenType.Object) { targetArray.Add(new DataObject((JObject)sourceItem)); } else { // DataObject가 아닌 경우, 새 DataObject를 만들고 값을 넣어줍니다 var dataObject = new DataObject(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); targetArray.Add(dataObject); } } return targetArray; } // 가이드 배열 개수가 1개면 1개만 템플릿으로 사용 JToken maskTemplate = maskArray.First; int idx = 0; foreach (JToken sourceItem in sourceArray) { if (maskArray.Count > 1) { if (idx > 0 && idx < maskArray.Count) { maskTemplate = maskArray[idx]; } else if (idx >= maskArray.Count) { continue; // 가이드 배열의 범위를 벗어나면 무시 } } if (sourceItem.Type == JTokenType.Object && maskTemplate.Type == JTokenType.Object) { targetArray.Add(MapObject((JObject)sourceItem, (JObject)maskTemplate)); } else if (sourceItem.Type == JTokenType.Array && maskTemplate.Type == JTokenType.Array) { // DataArray는 DataObject만 담을 수 있으므로, 배열을 처리하는 방식을 변경해야 함 var nestedArray = MapArray((JArray)sourceItem, (JArray)maskTemplate); var container = new DataObject(); container.Add("items", nestedArray); targetArray.Add(container); } else { // 단순 값을 DataObject로 래핑 var dataObject = new DataObject(); dataObject.Add("value", ConvertJTokenToObject(sourceItem)); targetArray.Add(dataObject); } idx++; } return targetArray; } /// /// 개별 속성을 매핑합니다. /// /// 속성 이름 /// 매핑할 원본 값 /// 타입을 결정하는 가이드 값 /// 값을 추가할 대상 DataObject /// /// 이 메서드는 가이드 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다. /// /// 지원되는 타입: /// - 문자열 (JTokenType.String) /// - 정수 (JTokenType.Integer) /// - 실수 (JTokenType.Float) /// - 불리언 (JTokenType.Boolean) /// - 날짜/시간 (JTokenType.Date) /// - 열거형 (Enum) /// - DataValueMapper (문자열 매핑 딕셔너리) /// /// 타입 변환이 불가능한 경우 원본 값이 그대로 사용됩니다. /// /// /// 다양한 타입 매핑 예시: /// /// var sourceJson = JObject.Parse(@"{ /// ""name"": ""김철수"", /// ""age"": 30, /// ""height"": 175.5, /// ""isActive"": true, /// ""birthDate"": ""1990-01-01T00:00:00"", /// ""status"": ""Active"" /// }"); /// /// // 가이드 객체 설정 (열거형 포함) /// var maskJson = new JObject(); /// maskJson["name"] = ""; /// maskJson["age"] = 0; /// maskJson["height"] = 0.0; /// maskJson["isActive"] = false; /// maskJson["birthDate"] = JToken.FromObject(DateTime.Now); /// maskJson["status"] = JToken.FromObject(UserStatus.Inactive); /// /// var mapper = new DataMapper(maskJson); /// var result = mapper.Map(sourceJson); /// // result에는 모든 속성이 적절한 타입으로 변환됨 /// /// private void MapProperty(string propertyName, JToken sourceValue, JToken maskValue, DataObject target) { if (maskValue.Type == JTokenType.String && sourceValue.Type == JTokenType.String) { target[propertyName] = sourceValue.ToObject(); } else if (maskValue.Type == JTokenType.Integer && sourceValue.Type == JTokenType.Integer) { target[propertyName] = sourceValue.ToObject(); } else if (maskValue.Type == JTokenType.Float && sourceValue.Type == JTokenType.Float) { target[propertyName] = sourceValue.ToObject(); } else if (maskValue.Type == JTokenType.Boolean && sourceValue.Type == JTokenType.Boolean) { target[propertyName] = sourceValue.ToObject(); } else if (maskValue.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 (maskValue.ToObject()?.GetType()?.IsEnum == true && sourceValue.Type == JTokenType.String) { Type enumType = maskValue.ToObject().GetType(); target[propertyName] = Enum.Parse(enumType, sourceValue.ToObject(), true); } else if (maskValue.Type == JTokenType.Object && sourceValue.Type == JTokenType.String) { try { // 먼저 DataValueMapper로 변환 시도 var dataValueMapper = maskValue.ToObject(); if (dataValueMapper != null) { string strValue = sourceValue.ToObject(); if (dataValueMapper.ContainsKey(strValue)) { target[propertyName] = dataValueMapper[strValue]; } else { target[propertyName] = strValue; } } else { // DataValueMapper가 아니면 소스 값 그대로 사용 target[propertyName] = sourceValue.ToObject(); } } catch { // 변환 실패 시 소스 값 그대로 사용 target[propertyName] = sourceValue.ToObject(); } } else { // 기타 타입은 그대로 객체로 변환 target[propertyName] = ConvertJTokenToObject(sourceValue); } } } }