DataMapper 중첩 구조 처리

This commit is contained in:
김형인
2025-06-05 00:09:39 +09:00
parent 0eeeb95cac
commit ef11fd4a14
3 changed files with 621 additions and 89 deletions

View File

@@ -4,79 +4,132 @@ using System;
namespace UVC.Data
{
/// <summary>
/// 서로 다른 형식이나 구조 간에 데이터를 매핑하는 기능을 제공합니다.
/// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다.
/// </summary>
/// <remarks>이 클래스는 일반적으로 데이터베이스 레코드를 도메인 객체에 매핑하거나 서로 다른 데이터 모델 간에 변환하는 등 데이터를 한 표현에서 다른 표현으로 변환하거나 매핑하는 데 사용됩니다.
///</remarks>
/// <remarks>
/// 이 클래스는 JSON 데이터를 원하는 형식으로 변환하거나, 중첩된 구조(nested structure)도 처리할 수 있습니다.
/// 소스 JSON 객체의 속성을 가이드 객체에 정의된 타입에 따라 적절히 변환합니다.
/// </remarks>
/// <example>
/// 기본 사용 예시:
/// <code>
/// // 소스 JSON 데이터
/// var sourceJson = JObject.Parse(@"{
/// ""name"": ""김철수"",
/// ""age"": 30,
/// ""isActive"": true
/// }");
///
/// // 가이드 객체 (타입 지정용)
/// var guideJson = JObject.Parse(@"{
/// ""name"": """",
/// ""age"": 0,
/// ""isActive"": false
/// }");
///
/// var mapper = new DataMapper(sourceJson, guideJson);
/// JObject result = mapper.Map();
///
/// // result는 원본과 동일한 구조이며 각 속성이 가이드에 따라 타입 변환됨
/// </code>
/// </example>
public class DataMapper
{
private JObject source;
private JObject guide;
/// <summary>
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
/// </summary>
/// <param name="source">매핑할 원본 JSON 객체</param>
/// <param name="guide">타입 변환을 위한 가이드 JSON 객체</param>
/// <remarks>
/// 가이드 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
/// </remarks>
public DataMapper(JObject source, JObject target)
{
this.source = source;
this.guide = target;
}
/// <summary>
/// 소스 객체를 가이드 객체를 기반으로 매핑하여 새로운 JSON 객체를 생성합니다.
/// </summary>
/// <returns>매핑된 JSON 객체</returns>
/// <example>
/// <code>
/// var mapper = new DataMapper(sourceJson, guideJson);
/// JObject result = mapper.Map();
/// Debug.Log(result["name"].ToString()); // "김철수"
/// Debug.Log(result["age"].ToObject&lt;int&gt;()); // 30
/// </code>
/// </example>
public JObject Map()
{
return MapObject(source, guide);
}
/// <summary>
/// 객체를 재귀적으로 매핑합니다.
/// </summary>
/// <param name="sourceObject">원본 JSON 객체</param>
/// <param name="guideObject">가이드 JSON 객체</param>
/// <returns>매핑된 JSON 객체</returns>
/// <remarks>
/// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다.
/// </remarks>
/// <example>
/// 중첩 객체 매핑 예시:
/// <code>
/// var sourceJson = JObject.Parse(@"{
/// ""user"": {
/// ""name"": ""김철수"",
/// ""address"": {
/// ""city"": ""서울"",
/// ""zipcode"": ""12345""
/// }
/// }
/// }");
///
/// var guideJson = JObject.Parse(@"{
/// ""user"": {
/// ""name"": """",
/// ""address"": {
/// ""city"": """",
/// ""zipcode"": """"
/// }
/// }
/// }");
///
/// var mapper = new DataMapper(sourceJson, guideJson);
/// var result = mapper.Map();
/// // result는 sourceJson과 동일한 중첩 구조를 유지
/// </code>
/// </example>
private JObject MapObject(JObject sourceObject, JObject guideObject)
{
JObject target = new JObject();
foreach (var property in source.Properties())
foreach (var property in sourceObject.Properties())
{
if (guide.ContainsKey(property.Name))
if (guideObject.ContainsKey(property.Name))
{
JToken guideValue = guide[property.Name];
string guideType = guideValue?.Type.ToString() ?? "null";
if (guideType == "String" && property.Value.Type == JTokenType.String)
JToken guideValue = guideObject[property.Name];
JToken sourceValue = property.Value;
// 중첩된 객체 처리
if (sourceValue.Type == JTokenType.Object && guideValue.Type == JTokenType.Object)
{
target[property.Name] = property.Value.ToObject<string>();
target[property.Name] = MapObject((JObject)sourceValue, (JObject)guideValue);
}
else if (guideType == "Integer" && property.Value.Type == JTokenType.Integer)
// 중첩된 배열 처리
else if (sourceValue.Type == JTokenType.Array && guideValue.Type == JTokenType.Array)
{
target[property.Name] = property.Value.ToObject<int>();
}
else if (guideType == "Float" && property.Value.Type == JTokenType.Float)
{
target[property.Name] = property.Value.ToObject<double>();
}
else if (guideType == "Boolean" && property.Value.Type == JTokenType.Boolean)
{
target[property.Name] = property.Value.ToObject<bool>();
}
else if (guideType == "Object" && guideValue.ToObject<object>() is DataValueMapper && property.Value.Type == JTokenType.String)
{
string strValue = property.Value.ToObject<string>();
var dataValueMapper = guideValue.ToObject<DataValueMapper>();
if (dataValueMapper != null && dataValueMapper.ContainsKey(strValue))
{
target[property.Name] = dataValueMapper[strValue];
}
else
{
target[property.Name] = strValue;
}
}
else if (guideType == "Date" && property.Value.Type == JTokenType.String)
{
string dateStr = property.Value.ToObject<string>();
if (DateTime.TryParse(dateStr, out DateTime dateValue))
{
target[property.Name] = JToken.FromObject(dateValue);
}
else
{
target[property.Name] = null;
}
}
else if (guideValue.ToObject<object>()?.GetType()?.IsEnum == true && property.Value.Type == JTokenType.String)
{
Type enumType = guideValue.ToObject<object>().GetType();
target[property.Name] = JToken.FromObject(Enum.Parse(enumType, property.Value.ToObject<string>(), true));
target[property.Name] = MapArray((JArray)sourceValue, (JArray)guideValue);
}
else
{
target[property.Name] = property.Value;
MapProperty(property.Name, sourceValue, guideValue, target);
}
}
else
@@ -87,6 +140,176 @@ namespace UVC.Data
return target;
}
/// <summary>
/// 배열을 재귀적으로 매핑합니다.
/// </summary>
/// <param name="sourceArray">원본 JSON 배열</param>
/// <param name="guideArray">가이드 JSON 배열</param>
/// <returns>매핑된 JSON 배열</returns>
/// <remarks>
/// 가이드 배열이 비어있으면, 원본 배열을 그대로 복사합니다.
/// 그렇지 않으면, 가이드 배열의 첫 번째 항목을 템플릿으로 사용하여 원본 배열의 각 항목을 매핑합니다.
/// </remarks>
/// <example>
/// 배열 매핑 예시:
/// <code>
/// var sourceJson = JObject.Parse(@"{
/// ""contacts"": [
/// { ""type"": ""mobile"", ""number"": ""010-1234-5678"" },
/// { ""type"": ""home"", ""number"": ""02-123-4567"" }
/// ]
/// }");
///
/// var guideJson = JObject.Parse(@"{
/// ""contacts"": [
/// { ""type"": """", ""number"": """" }
/// ]
/// }");
///
/// var mapper = new DataMapper(sourceJson, guideJson);
/// var result = mapper.Map();
/// // result.contacts는 원본 배열과 동일한 구조의 배열
/// </code>
/// </example>
private JArray MapArray(JArray sourceArray, JArray guideArray)
{
JArray targetArray = new JArray();
// 가이드 배열이 비어있으면 원본 배열을 그대로 사용
if (guideArray.Count == 0)
{
return new JArray(sourceArray);
}
// 가이드 배열의 첫 번째 항목을 템플릿으로 사용
JToken guideTemplate = guideArray.First;
foreach (JToken sourceItem in sourceArray)
{
if (sourceItem.Type == JTokenType.Object && guideTemplate.Type == JTokenType.Object)
{
targetArray.Add(MapObject((JObject)sourceItem, (JObject)guideTemplate));
}
else if (sourceItem.Type == JTokenType.Array && guideTemplate.Type == JTokenType.Array)
{
targetArray.Add(MapArray((JArray)sourceItem, (JArray)guideTemplate));
}
else
{
targetArray.Add(sourceItem);
}
}
return targetArray;
}
/// <summary>
/// 개별 속성을 매핑합니다.
/// </summary>
/// <param name="propertyName">속성 이름</param>
/// <param name="sourceValue">매핑할 원본 값</param>
/// <param name="guideValue">타입을 결정하는 가이드 값</param>
/// <param name="target">값을 추가할 대상 객체</param>
/// <remarks>
/// 이 메서드는 가이드 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다.
/// 지원되는 타입: 문자열, 정수, 실수, 불리언, 날짜/시간, 열거형, DataValueMapper
/// </remarks>
/// <example>
/// 다양한 타입 매핑 예시:
/// <code>
/// var sourceJson = JObject.Parse(@"{
/// ""name"": ""김철수"",
/// ""age"": 30,
/// ""height"": 175.5,
/// ""isActive"": true,
/// ""birthDate"": ""1990-01-01T00:00:00"",
/// ""status"": ""Active""
/// }");
///
/// // 가이드 객체 설정 (열거형 포함)
/// var guideJson = new JObject();
/// guideJson["name"] = "";
/// guideJson["age"] = 0;
/// guideJson["height"] = 0.0;
/// guideJson["isActive"] = false;
/// guideJson["birthDate"] = JToken.FromObject(DateTime.Now);
/// guideJson["status"] = JToken.FromObject(UserStatus.Inactive);
///
/// var mapper = new DataMapper(sourceJson, guideJson);
/// var result = mapper.Map();
/// // result에는 모든 속성이 적절한 타입으로 변환됨
/// </code>
/// </example>
private void MapProperty(string propertyName, JToken sourceValue, JToken guideValue, JObject target)
{
if (guideValue.Type == JTokenType.String && sourceValue.Type == JTokenType.String)
{
target[propertyName] = sourceValue.ToObject<string>();
}
else if (guideValue.Type == JTokenType.Integer && sourceValue.Type == JTokenType.Integer)
{
target[propertyName] = sourceValue.ToObject<int>();
}
else if (guideValue.Type == JTokenType.Float && sourceValue.Type == JTokenType.Float)
{
target[propertyName] = sourceValue.ToObject<double>();
}
else if (guideValue.Type == JTokenType.Boolean && sourceValue.Type == JTokenType.Boolean)
{
target[propertyName] = sourceValue.ToObject<bool>();
}
else if (guideValue.Type == JTokenType.Date && sourceValue.Type == JTokenType.String)
{
string dateStr = sourceValue.ToObject<string>();
if (DateTime.TryParse(dateStr, out DateTime dateValue))
{
target[propertyName] = JToken.FromObject(dateValue);
}
else
{
target[propertyName] = null;
}
}
else if (guideValue.ToObject<object>()?.GetType()?.IsEnum == true && sourceValue.Type == JTokenType.String)
{
Type enumType = guideValue.ToObject<object>().GetType();
target[propertyName] = JToken.FromObject(Enum.Parse(enumType, sourceValue.ToObject<string>(), true));
}
else if (guideValue.Type == JTokenType.Object && sourceValue.Type == JTokenType.String)
{
try
{
// 먼저 DataValueMapper로 변환 시도
var dataValueMapper = guideValue.ToObject<DataValueMapper>();
if (dataValueMapper != null)
{
string strValue = sourceValue.ToObject<string>();
if (dataValueMapper.ContainsKey(strValue))
{
target[propertyName] = new JValue(dataValueMapper[strValue]);
}
else
{
target[propertyName] = new JValue(strValue);
}
}
else
{
// DataValueMapper가 아니면 소스 값 그대로 사용
target[propertyName] = sourceValue;
}
}
catch
{
// 변환 실패 시 소스 값 그대로 사용
target[propertyName] = sourceValue;
}
}
else
{
target[propertyName] = sourceValue;
}
}
}
}