data pipeline 개발
This commit is contained in:
318
Assets/Scripts/UVC/Data/DataArray.cs
Normal file
318
Assets/Scripts/UVC/Data/DataArray.cs
Normal file
@@ -0,0 +1,318 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// DataObject 객체 컬렉션의 변경사항을 추적하는 데이터 배열 클래스
|
||||
/// </summary>
|
||||
public class DataArray : List<DataObject>, IDataObject
|
||||
{
|
||||
// 추가 된 항목 목록
|
||||
protected List<DataObject> addedList = new List<DataObject>();
|
||||
// 제거 된 항목 목록
|
||||
protected List<DataObject> removedList = new List<DataObject>();
|
||||
// 수정 된 항목 목록
|
||||
protected List<DataObject> modifiedList = new List<DataObject>();
|
||||
|
||||
|
||||
// 추가된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
||||
public ReadOnlyCollection<DataObject> AddedItems => addedList.AsReadOnly();
|
||||
|
||||
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
||||
public ReadOnlyCollection<DataObject> RemovedItems => removedList.AsReadOnly();
|
||||
|
||||
// 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션
|
||||
public ReadOnlyCollection<DataObject> ModifiedList => modifiedList.AsReadOnly();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 변경 추적 활성화 여부
|
||||
/// </summary>
|
||||
public bool IsChangeTracking { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// 기본 생성자
|
||||
/// </summary>
|
||||
public DataArray() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 초기 용량을 지정하는 생성자
|
||||
/// </summary>
|
||||
/// <param name="capacity">초기 용량</param>
|
||||
public DataArray(int capacity) : base(capacity)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 기존 컬렉션으로부터 생성하는 생성자
|
||||
/// </summary>
|
||||
/// <param name="collection">초기 항목을 포함하는 컬렉션</param>
|
||||
public DataArray(IEnumerable<DataObject> collection) : base(collection)
|
||||
{
|
||||
}
|
||||
|
||||
public DataArray(string jsonString) : base()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(jsonString))
|
||||
{
|
||||
try
|
||||
{
|
||||
JArray jArray = JArray.Parse(jsonString);
|
||||
foreach (var item in jArray)
|
||||
{
|
||||
Add(ConvertToDataObject(item));
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JArray로부터 DataArray를 생성하는 생성자
|
||||
/// </summary>
|
||||
/// <param name="jArray">JSON 배열</param>
|
||||
public DataArray(JArray jArray) : base()
|
||||
{
|
||||
if (jArray != null)
|
||||
{
|
||||
foreach (var item in jArray)
|
||||
{
|
||||
Add(ConvertToDataObject(item));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JToken을 DataObject로 변환합니다.
|
||||
/// </summary>
|
||||
private DataObject ConvertToDataObject(JToken token)
|
||||
{
|
||||
if (token.Type == JTokenType.Object)
|
||||
{
|
||||
return new DataObject((JObject)token);
|
||||
}
|
||||
else
|
||||
{
|
||||
// JObject가 아닌 경우, 새 DataObject를 만들고 값을 넣어줍니다
|
||||
var dataObject = new DataObject();
|
||||
dataObject.Add("value", ConvertJTokenToObject(token));
|
||||
return dataObject;
|
||||
}
|
||||
}
|
||||
|
||||
private object ConvertJTokenToObject(JToken token)
|
||||
{
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.String:
|
||||
return token.ToString();
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<int>();
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<float>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Object:
|
||||
return new DataObject((JObject)token);
|
||||
case JTokenType.Array:
|
||||
JArray array = (JArray)token;
|
||||
return new DataArray(array);
|
||||
default:
|
||||
return token.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 항목을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">추가할 항목</param>
|
||||
public new void Add(DataObject item)
|
||||
{
|
||||
base.Add(item);
|
||||
|
||||
// 변경 사항을 추적하고 이벤트 발생
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
int index = Count - 1;
|
||||
addedList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 항목을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">제거할 항목</param>
|
||||
/// <returns>제거 성공 여부</returns>
|
||||
public new bool Remove(DataObject item)
|
||||
{
|
||||
int index = IndexOf(item);
|
||||
if (index >= 0)
|
||||
{
|
||||
RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 인덱스의 항목을 제거합니다.
|
||||
/// </summary>
|
||||
/// <param name="index">제거할 항목의 인덱스</param>
|
||||
public new void RemoveAt(int index)
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
DataObject removedItem = this[index];
|
||||
base.RemoveAt(index);
|
||||
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
removedList.Add(removedItem);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컬렉션의 모든 항목을 제거합니다.
|
||||
/// </summary>
|
||||
public new void Clear()
|
||||
{
|
||||
if (Count > 0)
|
||||
{
|
||||
var oldItems = new List<DataObject>(this);
|
||||
base.Clear();
|
||||
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
// 모든 항목을 제거 목록에 추가
|
||||
removedList.AddRange(oldItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 인덱스에 항목을 삽입합니다.
|
||||
/// </summary>
|
||||
/// <param name="index">삽입할 위치</param>
|
||||
/// <param name="item">삽입할 항목</param>
|
||||
public new void Insert(int index, DataObject item)
|
||||
{
|
||||
base.Insert(index, item);
|
||||
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
addedList.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 인덱스의 항목을 교체합니다.
|
||||
/// </summary>
|
||||
/// <param name="index">교체할 위치</param>
|
||||
/// <param name="item">새 항목</param>
|
||||
public new DataObject this[int index]
|
||||
{
|
||||
get { return base[index]; }
|
||||
set
|
||||
{
|
||||
if (index < 0 || index >= Count)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
DataObject oldItem = base[index];
|
||||
base[index] = value;
|
||||
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
addedList.Add(value);
|
||||
removedList.Add(oldItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 범위의 항목을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="collection">추가할 항목의 컬렉션</param>
|
||||
public new void AddRange(IEnumerable<DataObject> collection)
|
||||
{
|
||||
if (collection == null)
|
||||
throw new ArgumentNullException(nameof(collection));
|
||||
|
||||
int startIndex = Count;
|
||||
base.AddRange(collection);
|
||||
|
||||
if (IsChangeTracking)
|
||||
{
|
||||
var items = collection.ToList();
|
||||
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
addedList.Add(items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 변경된 인덱스 목록을 초기화합니다.
|
||||
/// </summary>
|
||||
public void ResetChangedIndices()
|
||||
{
|
||||
addedList.Clear();
|
||||
removedList.Clear();
|
||||
modifiedList.Clear();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 변경 내역을 저장하지 않고 항목을 추가합니다.
|
||||
/// </summary>
|
||||
/// <param name="item">추가할 항목</param>
|
||||
public void AddWithoutTracking(DataObject item)
|
||||
{
|
||||
bool oldTracking = IsChangeTracking;
|
||||
IsChangeTracking = false;
|
||||
Add(item);
|
||||
IsChangeTracking = oldTracking;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// DataArray를 JArray로 변환합니다.
|
||||
/// </summary>
|
||||
public JArray ToJArray()
|
||||
{
|
||||
JArray array = new JArray();
|
||||
foreach (var item in this)
|
||||
{
|
||||
array.Add(item);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DataArray를 JArray로 암시적 변환합니다.
|
||||
/// </summary>
|
||||
public static implicit operator JArray(DataArray dataArray)
|
||||
{
|
||||
return dataArray.ToJArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JArray를 DataArray로 암시적 변환합니다.
|
||||
/// </summary>
|
||||
public static implicit operator DataArray(JArray jArray)
|
||||
{
|
||||
return new DataArray(jArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/DataArray.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/DataArray.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ea1772f89851a04fa8a65b43169561b
|
||||
@@ -1,5 +1,6 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
@@ -7,7 +8,7 @@ namespace UVC.Data
|
||||
/// 서로 다른 JSON 데이터 구조 간에 매핑 기능을 제공하는 클래스입니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 JSON 데이터를 원하는 형식으로 변환하거나, 중첩된 구조(nested structure)도 처리할 수 있습니다.
|
||||
/// 이 클래스는 JSON 데이터를 DataObject와 DataArray 형식으로 변환하며, 중첩된 구조(nested structure)도 처리할 수 있습니다.
|
||||
/// 소스 JSON 객체의 속성을 가이드 객체에 정의된 타입에 따라 적절히 변환합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
@@ -21,63 +22,93 @@ namespace UVC.Data
|
||||
/// }");
|
||||
///
|
||||
/// // 가이드 객체 (타입 지정용)
|
||||
/// var guideJson = JObject.Parse(@"{
|
||||
/// var maskJson = JObject.Parse(@"{
|
||||
/// ""name"": """",
|
||||
/// ""age"": 0,
|
||||
/// ""isActive"": false
|
||||
/// }");
|
||||
///
|
||||
/// var mapper = new DataMapper(sourceJson, guideJson);
|
||||
/// JObject result = mapper.Map();
|
||||
/// var mapper = new DataMapper(maskJson);
|
||||
/// DataObject result = mapper.Map(sourceJson);
|
||||
///
|
||||
/// // result는 원본과 동일한 구조이며 각 속성이 가이드에 따라 타입 변환됨
|
||||
/// </code>
|
||||
/// </example>
|
||||
public class DataMapper
|
||||
{
|
||||
private JObject source;
|
||||
private JObject guide;
|
||||
private JObject mask;
|
||||
|
||||
/// <summary>
|
||||
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
||||
/// </summary>
|
||||
/// <param name="source">매핑할 원본 JSON 객체</param>
|
||||
/// <param name="guide">타입 변환을 위한 가이드 JSON 객체</param>
|
||||
/// <param name="mask">타입 변환을 위한 가이드 JSON 객체</param>
|
||||
/// <remarks>
|
||||
/// 가이드 객체는 원본 JSON 객체와 동일한 구조를 가질 필요는 없지만,
|
||||
/// 변환하려는 속성들에 대한 타입 정보를 제공해야 합니다.
|
||||
/// </remarks>
|
||||
public DataMapper(JObject source, JObject target)
|
||||
public DataMapper(JObject mask)
|
||||
{
|
||||
this.source = source;
|
||||
this.guide = target;
|
||||
this.mask = mask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 객체를 가이드 객체를 기반으로 매핑하여 새로운 JSON 객체를 생성합니다.
|
||||
/// 소스 객체를 가이드 객체를 기반으로 매핑하여 새로운 DataObject를 생성합니다.
|
||||
/// </summary>
|
||||
/// <returns>매핑된 JSON 객체</returns>
|
||||
/// <param name="source">매핑할 원본 JSON 객체</param>
|
||||
/// <returns>매핑된 DataObject 객체</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var mapper = new DataMapper(sourceJson, guideJson);
|
||||
/// JObject result = mapper.Map();
|
||||
/// var mapper = new DataMapper(maskJson);
|
||||
/// DataObject result = mapper.Map(sourceJson);
|
||||
/// Debug.Log(result["name"].ToString()); // "김철수"
|
||||
/// Debug.Log(result["age"].ToObject<int>()); // 30
|
||||
/// </code>
|
||||
/// </example>
|
||||
public JObject Map()
|
||||
public DataObject Map(JObject source)
|
||||
{
|
||||
return MapObject(source, guide);
|
||||
return MapObject(source, mask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 배열을 가이드 객체를 기반으로 매핑하여 새로운 DataArray를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="source">매핑할 원본 JSON 배열</param>
|
||||
/// <returns>매핑된 DataArray 객체</returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 가이드 객체로부터 새로운 배열을 생성하여 소스 배열의 각 항목을 매핑합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// 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
|
||||
/// </code>
|
||||
/// </example>
|
||||
public DataArray Map(JArray source)
|
||||
{
|
||||
JArray arr = new JArray(mask);
|
||||
return MapArray(source, arr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 객체를 재귀적으로 매핑합니다.
|
||||
/// </summary>
|
||||
/// <param name="sourceObject">원본 JSON 객체</param>
|
||||
/// <param name="guideObject">가이드 JSON 객체</param>
|
||||
/// <returns>매핑된 JSON 객체</returns>
|
||||
/// <param name="maskObject">가이드 JSON 객체</param>
|
||||
/// <returns>매핑된 DataObject 객체</returns>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 중첩된 객체와 배열을 포함하여 JSON 구조를 재귀적으로 처리합니다.
|
||||
/// 가이드 객체에 포함되지 않은 속성은 원본 값을 그대로 사용합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 중첩 객체 매핑 예시:
|
||||
@@ -92,7 +123,7 @@ namespace UVC.Data
|
||||
/// }
|
||||
/// }");
|
||||
///
|
||||
/// var guideJson = JObject.Parse(@"{
|
||||
/// var maskJson = JObject.Parse(@"{
|
||||
/// ""user"": {
|
||||
/// ""name"": """",
|
||||
/// ""address"": {
|
||||
@@ -102,53 +133,90 @@ namespace UVC.Data
|
||||
/// }
|
||||
/// }");
|
||||
///
|
||||
/// var mapper = new DataMapper(sourceJson, guideJson);
|
||||
/// var result = mapper.Map();
|
||||
/// var mapper = new DataMapper(maskJson);
|
||||
/// var result = mapper.Map(sourceJson);
|
||||
/// // result는 sourceJson과 동일한 중첩 구조를 유지
|
||||
/// </code>
|
||||
/// </example>
|
||||
private JObject MapObject(JObject sourceObject, JObject guideObject)
|
||||
private DataObject MapObject(JObject sourceObject, JObject maskObject)
|
||||
{
|
||||
JObject target = new JObject();
|
||||
DataObject target = new DataObject();
|
||||
foreach (var property in sourceObject.Properties())
|
||||
{
|
||||
if (guideObject.ContainsKey(property.Name))
|
||||
if (maskObject.ContainsKey(property.Name))
|
||||
{
|
||||
JToken guideValue = guideObject[property.Name];
|
||||
JToken maskValue = maskObject[property.Name];
|
||||
JToken sourceValue = property.Value;
|
||||
|
||||
// 중첩된 객체 처리
|
||||
if (sourceValue.Type == JTokenType.Object && guideValue.Type == JTokenType.Object)
|
||||
if (sourceValue.Type == JTokenType.Object && maskValue.Type == JTokenType.Object)
|
||||
{
|
||||
target[property.Name] = MapObject((JObject)sourceValue, (JObject)guideValue);
|
||||
target[property.Name] = MapObject((JObject)sourceValue, (JObject)maskValue);
|
||||
}
|
||||
// 중첩된 배열 처리
|
||||
else if (sourceValue.Type == JTokenType.Array && guideValue.Type == JTokenType.Array)
|
||||
else if (sourceValue.Type == JTokenType.Array && maskValue.Type == JTokenType.Array)
|
||||
{
|
||||
target[property.Name] = MapArray((JArray)sourceValue, (JArray)guideValue);
|
||||
var arr = MapArray((JArray)sourceValue, (JArray)maskValue);
|
||||
target[property.Name] = arr;
|
||||
}
|
||||
else
|
||||
{
|
||||
MapProperty(property.Name, sourceValue, guideValue, target);
|
||||
MapProperty(property.Name, sourceValue, maskValue, target);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
target[property.Name] = property.Value;
|
||||
continue; // 가이드에 없는 속성은 무시
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JToken을 실제 객체로 변환하는 헬퍼 메서드
|
||||
/// </summary>
|
||||
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<object>();
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<int>();
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<double>();
|
||||
case JTokenType.String:
|
||||
return token.ToObject<string>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Date:
|
||||
return token.ToObject<DateTime>();
|
||||
case JTokenType.Null:
|
||||
return null;
|
||||
default:
|
||||
return token.ToObject<object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배열을 재귀적으로 매핑합니다.
|
||||
/// </summary>
|
||||
/// <param name="sourceArray">원본 JSON 배열</param>
|
||||
/// <param name="guideArray">가이드 JSON 배열</param>
|
||||
/// <returns>매핑된 JSON 배열</returns>
|
||||
/// <param name="maskArray">가이드 JSON 배열</param>
|
||||
/// <returns>매핑된 DataArray 객체</returns>
|
||||
/// <remarks>
|
||||
/// 가이드 배열이 비어있으면, 원본 배열을 그대로 복사합니다.
|
||||
/// 그렇지 않으면, 가이드 배열의 첫 번째 항목을 템플릿으로 사용하여 원본 배열의 각 항목을 매핑합니다.
|
||||
/// 가이드 배열이 비어있으면, 원본 배열의 각 항목을 DataObject로 변환하여 복사합니다.
|
||||
/// 단순 값(문자열, 숫자 등)은 "value" 키를 가진 DataObject로 래핑됩니다.
|
||||
///
|
||||
/// 가이드 배열이 비어있지 않으면, 가이드 배열의 첫 번째 항목을 템플릿으로 사용하여
|
||||
/// 원본 배열의 각 항목을 매핑합니다. 중첩 배열은 "items" 키를 가진 DataObject로 래핑됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 배열 매핑 예시:
|
||||
@@ -160,44 +228,78 @@ namespace UVC.Data
|
||||
/// ]
|
||||
/// }");
|
||||
///
|
||||
/// var guideJson = JObject.Parse(@"{
|
||||
/// var maskJson = JObject.Parse(@"{
|
||||
/// ""contacts"": [
|
||||
/// { ""type"": """", ""number"": """" }
|
||||
/// ]
|
||||
/// }");
|
||||
///
|
||||
/// var mapper = new DataMapper(sourceJson, guideJson);
|
||||
/// var result = mapper.Map();
|
||||
/// // result.contacts는 원본 배열과 동일한 구조의 배열
|
||||
/// var mapper = new DataMapper(maskJson);
|
||||
/// var result = mapper.Map(sourceJson);
|
||||
/// // result.contacts는 원본 배열과 동일한 구조의 DataArray
|
||||
/// </code>
|
||||
/// </example>
|
||||
private JArray MapArray(JArray sourceArray, JArray guideArray)
|
||||
private DataArray MapArray(JArray sourceArray, JArray maskArray)
|
||||
{
|
||||
JArray targetArray = new JArray();
|
||||
DataArray targetArray = new DataArray();
|
||||
|
||||
// 가이드 배열이 비어있으면 원본 배열을 그대로 사용
|
||||
if (guideArray.Count == 0)
|
||||
if (maskArray.Count == 0)
|
||||
{
|
||||
return new JArray(sourceArray);
|
||||
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;
|
||||
}
|
||||
|
||||
// 가이드 배열의 첫 번째 항목을 템플릿으로 사용
|
||||
JToken guideTemplate = guideArray.First;
|
||||
|
||||
// 가이드 배열 개수가 1개면 1개만 템플릿으로 사용
|
||||
JToken maskTemplate = maskArray.First;
|
||||
int idx = 0;
|
||||
foreach (JToken sourceItem in sourceArray)
|
||||
{
|
||||
if (sourceItem.Type == JTokenType.Object && guideTemplate.Type == JTokenType.Object)
|
||||
if (maskArray.Count > 1)
|
||||
{
|
||||
targetArray.Add(MapObject((JObject)sourceItem, (JObject)guideTemplate));
|
||||
if (idx > 0 && idx < maskArray.Count)
|
||||
{
|
||||
maskTemplate = maskArray[idx];
|
||||
}
|
||||
else if (idx >= maskArray.Count)
|
||||
{
|
||||
continue; // 가이드 배열의 범위를 벗어나면 무시
|
||||
}
|
||||
}
|
||||
else if (sourceItem.Type == JTokenType.Array && guideTemplate.Type == JTokenType.Array)
|
||||
|
||||
if (sourceItem.Type == JTokenType.Object && maskTemplate.Type == JTokenType.Object)
|
||||
{
|
||||
targetArray.Add(MapArray((JArray)sourceItem, (JArray)guideTemplate));
|
||||
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
|
||||
{
|
||||
targetArray.Add(sourceItem);
|
||||
// 단순 값을 DataObject로 래핑
|
||||
var dataObject = new DataObject();
|
||||
dataObject.Add("value", ConvertJTokenToObject(sourceItem));
|
||||
targetArray.Add(dataObject);
|
||||
}
|
||||
idx++;
|
||||
}
|
||||
|
||||
return targetArray;
|
||||
@@ -208,11 +310,21 @@ namespace UVC.Data
|
||||
/// </summary>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="sourceValue">매핑할 원본 값</param>
|
||||
/// <param name="guideValue">타입을 결정하는 가이드 값</param>
|
||||
/// <param name="target">값을 추가할 대상 객체</param>
|
||||
/// <param name="maskValue">타입을 결정하는 가이드 값</param>
|
||||
/// <param name="target">값을 추가할 대상 DataObject</param>
|
||||
/// <remarks>
|
||||
/// 이 메서드는 가이드 값의 타입에 따라 원본 값을 적절한 타입으로 변환합니다.
|
||||
/// 지원되는 타입: 문자열, 정수, 실수, 불리언, 날짜/시간, 열거형, DataValueMapper
|
||||
///
|
||||
/// 지원되는 타입:
|
||||
/// - 문자열 (JTokenType.String)
|
||||
/// - 정수 (JTokenType.Integer)
|
||||
/// - 실수 (JTokenType.Float)
|
||||
/// - 불리언 (JTokenType.Boolean)
|
||||
/// - 날짜/시간 (JTokenType.Date)
|
||||
/// - 열거형 (Enum)
|
||||
/// - DataValueMapper (문자열 매핑 딕셔너리)
|
||||
///
|
||||
/// 타입 변환이 불가능한 경우 원본 값이 그대로 사용됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// 다양한 타입 매핑 예시:
|
||||
@@ -227,89 +339,91 @@ namespace UVC.Data
|
||||
/// }");
|
||||
///
|
||||
/// // 가이드 객체 설정 (열거형 포함)
|
||||
/// 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 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(sourceJson, guideJson);
|
||||
/// var result = mapper.Map();
|
||||
/// var mapper = new DataMapper(maskJson);
|
||||
/// var result = mapper.Map(sourceJson);
|
||||
/// // result에는 모든 속성이 적절한 타입으로 변환됨
|
||||
/// </code>
|
||||
/// </example>
|
||||
private void MapProperty(string propertyName, JToken sourceValue, JToken guideValue, JObject target)
|
||||
private void MapProperty(string propertyName, JToken sourceValue, JToken maskValue, DataObject target)
|
||||
{
|
||||
if (guideValue.Type == JTokenType.String && sourceValue.Type == JTokenType.String)
|
||||
if (maskValue.Type == JTokenType.String && sourceValue.Type == JTokenType.String)
|
||||
{
|
||||
target[propertyName] = sourceValue.ToObject<string>();
|
||||
}
|
||||
else if (guideValue.Type == JTokenType.Integer && sourceValue.Type == JTokenType.Integer)
|
||||
else if (maskValue.Type == JTokenType.Integer && sourceValue.Type == JTokenType.Integer)
|
||||
{
|
||||
target[propertyName] = sourceValue.ToObject<int>();
|
||||
}
|
||||
else if (guideValue.Type == JTokenType.Float && sourceValue.Type == JTokenType.Float)
|
||||
else if (maskValue.Type == JTokenType.Float && sourceValue.Type == JTokenType.Float)
|
||||
{
|
||||
target[propertyName] = sourceValue.ToObject<double>();
|
||||
}
|
||||
else if (guideValue.Type == JTokenType.Boolean && sourceValue.Type == JTokenType.Boolean)
|
||||
else if (maskValue.Type == JTokenType.Boolean && sourceValue.Type == JTokenType.Boolean)
|
||||
{
|
||||
target[propertyName] = sourceValue.ToObject<bool>();
|
||||
}
|
||||
else if (guideValue.Type == JTokenType.Date && sourceValue.Type == JTokenType.String)
|
||||
else if (maskValue.Type == JTokenType.Date && sourceValue.Type == JTokenType.String)
|
||||
{
|
||||
string dateStr = sourceValue.ToObject<string>();
|
||||
if (DateTime.TryParse(dateStr, out DateTime dateValue))
|
||||
{
|
||||
target[propertyName] = JToken.FromObject(dateValue);
|
||||
target[propertyName] = dateValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
target[propertyName] = null;
|
||||
}
|
||||
}
|
||||
else if (guideValue.ToObject<object>()?.GetType()?.IsEnum == true && sourceValue.Type == JTokenType.String)
|
||||
else if (maskValue.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));
|
||||
Type enumType = maskValue.ToObject<object>().GetType();
|
||||
target[propertyName] = Enum.Parse(enumType, sourceValue.ToObject<string>(), true);
|
||||
}
|
||||
else if (guideValue.Type == JTokenType.Object && sourceValue.Type == JTokenType.String)
|
||||
else if (maskValue.Type == JTokenType.Object && sourceValue.Type == JTokenType.String)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 먼저 DataValueMapper로 변환 시도
|
||||
var dataValueMapper = guideValue.ToObject<DataValueMapper>();
|
||||
var dataValueMapper = maskValue.ToObject<DataValueMapper>();
|
||||
if (dataValueMapper != null)
|
||||
{
|
||||
string strValue = sourceValue.ToObject<string>();
|
||||
if (dataValueMapper.ContainsKey(strValue))
|
||||
{
|
||||
target[propertyName] = new JValue(dataValueMapper[strValue]);
|
||||
target[propertyName] = dataValueMapper[strValue];
|
||||
}
|
||||
else
|
||||
{
|
||||
target[propertyName] = new JValue(strValue);
|
||||
target[propertyName] = strValue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// DataValueMapper가 아니면 소스 값 그대로 사용
|
||||
target[propertyName] = sourceValue;
|
||||
target[propertyName] = sourceValue.ToObject<object>();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 변환 실패 시 소스 값 그대로 사용
|
||||
target[propertyName] = sourceValue;
|
||||
target[propertyName] = sourceValue.ToObject<object>();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
target[propertyName] = sourceValue;
|
||||
// 기타 타입은 그대로 객체로 변환
|
||||
target[propertyName] = ConvertJTokenToObject(sourceValue);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
309
Assets/Scripts/UVC/Data/DataObject.cs
Normal file
309
Assets/Scripts/UVC/Data/DataObject.cs
Normal file
@@ -0,0 +1,309 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class DataObject : Dictionary<string, object>, IDataObject
|
||||
{
|
||||
// 직접적인 변경이 있었던 키를 저장하는 리스트
|
||||
protected List<string> changedProperies = new List<string>();
|
||||
|
||||
public ReadOnlyCollection<string> ChangedProperies => changedProperies.AsReadOnly();
|
||||
|
||||
// 기존에 있던 속성 키를 추적하기 위한 집합
|
||||
private HashSet<string> existingProperties = new HashSet<string>();
|
||||
|
||||
public DataObject()
|
||||
{
|
||||
}
|
||||
|
||||
public DataObject(JObject other)
|
||||
{
|
||||
// JObject로부터 속성을 복사
|
||||
foreach (var prop in other.Properties())
|
||||
{
|
||||
// JToken을 object로 변환
|
||||
this[prop.Name] = ConvertJTokenToObject(prop.Value);
|
||||
existingProperties.Add(prop.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Dictionary로 초기화하는 생성자 추가
|
||||
public DataObject(Dictionary<string, object> dictionary) : base(dictionary)
|
||||
{
|
||||
// 생성자에서 초기 속성들을 기존 속성으로 등록
|
||||
foreach (var key in dictionary.Keys)
|
||||
{
|
||||
existingProperties.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
// JToken을 실제 객체로 변환하는 헬퍼 메서드
|
||||
private object ConvertJTokenToObject(JToken token)
|
||||
{
|
||||
switch (token.Type)
|
||||
{
|
||||
case JTokenType.String:
|
||||
return token.ToString();
|
||||
case JTokenType.Integer:
|
||||
return token.ToObject<int>();
|
||||
case JTokenType.Float:
|
||||
return token.ToObject<float>();
|
||||
case JTokenType.Boolean:
|
||||
return token.ToObject<bool>();
|
||||
case JTokenType.Object:
|
||||
return new DataObject((JObject)token);
|
||||
case JTokenType.Array:
|
||||
JArray array = (JArray)token;
|
||||
return new DataArray(array);
|
||||
default:
|
||||
return token.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 프로퍼티를 변경된 것으로 표시합니다.
|
||||
/// </summary>
|
||||
public void ChangeAll()
|
||||
{
|
||||
changedProperies.Clear();
|
||||
|
||||
foreach (var key in this.Keys)
|
||||
{
|
||||
changedProperies.Add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 변경된 키 목록을 초기화합니다.
|
||||
/// </summary>
|
||||
public void ClearChangedKeys()
|
||||
{
|
||||
changedProperies.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성이 변경될 때 호출되는 메서드입니다.
|
||||
/// </summary>
|
||||
protected virtual void OnPropertyChanged(string propertyName)
|
||||
{
|
||||
// 기존에 존재하던 속성인 경우에만 변경된 것으로 추적
|
||||
if (existingProperties.Contains(propertyName))
|
||||
{
|
||||
if (!changedProperies.Contains(propertyName))
|
||||
{
|
||||
changedProperies.Add(propertyName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 새로 추가된 속성은 기존 속성 목록에 추가
|
||||
existingProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 새 속성을 추가할 때 이벤트 연결을 처리합니다.
|
||||
/// </summary>
|
||||
public new void Add(string propertyName, object value)
|
||||
{
|
||||
// 추가하기 전에 확인 - 속성이 이미 있는지 확인
|
||||
bool isExisting = ContainsKey(propertyName);
|
||||
|
||||
// 기본 구현 호출
|
||||
base.Add(propertyName, value);
|
||||
|
||||
// 새로 추가된 속성이면 기존 속성 목록에 추가
|
||||
if (!isExisting)
|
||||
{
|
||||
existingProperties.Add(propertyName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인덱서를 통한 속성 설정을 처리합니다.
|
||||
/// </summary>
|
||||
public new object this[string key]
|
||||
{
|
||||
get => base[key];
|
||||
set
|
||||
{
|
||||
// 속성 설정 전에 기존에 있는 속성인지 확인
|
||||
bool isExisting = ContainsKey(key) || existingProperties.Contains(key);
|
||||
|
||||
// 기본 구현 호출
|
||||
base[key] = value;
|
||||
|
||||
// 기존 속성이었다면 변경된 것으로 처리
|
||||
if (isExisting)
|
||||
{
|
||||
if (!changedProperies.Contains(key))
|
||||
{
|
||||
changedProperies.Add(key);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 새로 추가된 속성은 기존 속성으로 등록
|
||||
existingProperties.Add(key);
|
||||
}
|
||||
|
||||
OnPropertyChanged(key);
|
||||
}
|
||||
}
|
||||
|
||||
public int GetInt(string propertyName, int defaultValue = 0)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is int intValue)
|
||||
return intValue;
|
||||
return Convert.ToInt32(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public string GetString(string propertyName, string defaultValue = "")
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
return value.ToString();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public bool GetBool(string propertyName, bool defaultValue = false)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
return boolValue;
|
||||
return Convert.ToBoolean(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public float GetFloat(string propertyName, float defaultValue = 0f)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is float floatValue)
|
||||
return floatValue;
|
||||
return Convert.ToSingle(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public double GetDouble(string propertyName, double defaultValue = 0.0)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is double doubleValue)
|
||||
return doubleValue;
|
||||
return Convert.ToDouble(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is DateTime dateTime)
|
||||
return dateTime;
|
||||
return Convert.ToDateTime(value);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public T GetEnum<T>(string propertyName, T defaultValue = default) where T : Enum
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is T enumValue)
|
||||
return enumValue;
|
||||
return (T)Enum.Parse(typeof(T), value.ToString());
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public DataArray GetDataArray(string propertyName, DataArray? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is DataArray dataArray)
|
||||
return dataArray;
|
||||
if (value is JArray jArray)
|
||||
return new DataArray(jArray);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public DataObject GetDataObject(string propertyName, DataObject? defaultValue = null)
|
||||
{
|
||||
if (TryGetValue(propertyName, out object value) && value != null)
|
||||
{
|
||||
if (value is DataObject dataObject)
|
||||
return dataObject;
|
||||
if (value is JObject jObject)
|
||||
return new DataObject(jObject);
|
||||
if (value is Dictionary<string, object> dict)
|
||||
return new DataObject(dict);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성이 제거될 때 기존 속성 목록에서도 제거합니다.
|
||||
/// </summary>
|
||||
public new bool Remove(string key)
|
||||
{
|
||||
bool result = base.Remove(key);
|
||||
if (result)
|
||||
{
|
||||
existingProperties.Remove(key);
|
||||
changedProperies.Remove(key);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 속성을 제거할 때 기존 속성 목록과 변경된 키 목록도 초기화합니다.
|
||||
/// </summary>
|
||||
public void RemoveAll()
|
||||
{
|
||||
base.Clear();
|
||||
existingProperties.Clear();
|
||||
changedProperies.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JObject로 변환
|
||||
/// </summary>
|
||||
public JObject ToJObject()
|
||||
{
|
||||
JObject result = new JObject();
|
||||
foreach (var kvp in this)
|
||||
{
|
||||
result[kvp.Key] = JToken.FromObject(kvp.Value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void SetDifferent(DataObject other)
|
||||
{
|
||||
changedProperies.Clear();
|
||||
foreach (var keyValue in other)
|
||||
{
|
||||
if(!this.ContainsKey(keyValue.Key) || !this[keyValue.Key].Equals(keyValue.Value))
|
||||
{
|
||||
this[keyValue.Key] = keyValue.Value;
|
||||
changedProperies.Add(keyValue.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/DataObject.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/DataObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ca43f1365775ee443b6290a4079de628
|
||||
45
Assets/Scripts/UVC/Data/DataRepository.cs
Normal file
45
Assets/Scripts/UVC/Data/DataRepository.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class DataRepository
|
||||
{
|
||||
#region Singleton
|
||||
protected static readonly Lazy<DataRepository> instance = new Lazy<DataRepository>(() => new DataRepository());
|
||||
protected DataRepository() { }
|
||||
public static DataRepository Instance { get { return instance.Value; } }
|
||||
#endregion
|
||||
|
||||
private Dictionary<string, DataObject> dataObjects = new Dictionary<string, DataObject>();
|
||||
|
||||
public void AddData(string key, DataObject dataObject)
|
||||
{
|
||||
if (!dataObjects.ContainsKey(key))
|
||||
{
|
||||
dataObject.ChangeAll();
|
||||
dataObjects.Add(key, dataObject);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataObject obj = dataObjects[key];
|
||||
obj.SetDifferent(obj);
|
||||
}
|
||||
}
|
||||
public void RemoveData(string key)
|
||||
{
|
||||
if (dataObjects.ContainsKey(key))
|
||||
{
|
||||
dataObjects.Remove(key);
|
||||
}
|
||||
}
|
||||
public DataObject GetData(string key)
|
||||
{
|
||||
if (dataObjects.ContainsKey(key))
|
||||
{
|
||||
return dataObjects[key];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/DataRepository.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/DataRepository.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8654f538ded07d44385d806ada0cf1b5
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class DataRequestInfo
|
||||
{
|
||||
private string url;
|
||||
private string method;
|
||||
private Dictionary<string, string> headers;
|
||||
private string body;
|
||||
|
||||
}
|
||||
}
|
||||
54
Assets/Scripts/UVC/Data/HttpPipeLine.cs
Normal file
54
Assets/Scripts/UVC/Data/HttpPipeLine.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System.Collections.Generic;
|
||||
using UVC.Network;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class HttpPipeLine
|
||||
{
|
||||
private Dictionary<string, HttpPipeLineInfo> infoList = new Dictionary<string, HttpPipeLineInfo>();
|
||||
public void Add(string key, HttpPipeLineInfo info)
|
||||
{
|
||||
if (!infoList.ContainsKey(key))
|
||||
{
|
||||
infoList.Add(key, info);
|
||||
}
|
||||
else
|
||||
{
|
||||
infoList[key] = info; // Update existing entry
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
if (infoList.ContainsKey(key))
|
||||
{
|
||||
infoList.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public async void Excute(string key)
|
||||
{
|
||||
if (infoList.ContainsKey(key))
|
||||
{
|
||||
HttpPipeLineInfo info = infoList[key];
|
||||
|
||||
string result = await HttpRequester.Request<string>(info.url, info.method, info.body, info.headers);
|
||||
JObject jsonResult = JObject.Parse(result);
|
||||
DataObject dataObject = new DataObject(jsonResult);
|
||||
|
||||
if (info.dataMapper != null)
|
||||
{
|
||||
dataObject = info.dataMapper.Map(jsonResult);
|
||||
}
|
||||
DataRepository.Instance.AddData(key, dataObject);
|
||||
if (info.handler != null)
|
||||
{
|
||||
info.handler(dataObject);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/HttpPipeLine.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/HttpPipeLine.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0780f29b8d71ea46938f4804b5b7ca2
|
||||
50
Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs
Normal file
50
Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class HttpPipeLineInfo
|
||||
{
|
||||
public string url;
|
||||
public string method;
|
||||
public Dictionary<string, string> headers;
|
||||
public string body;
|
||||
public Action<IDataObject> handler = null;
|
||||
public bool repeat = false; // 반복 실행 여부
|
||||
public int repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
||||
public int repeatInterval = 1000; // 반복 간격 (ms)
|
||||
|
||||
public DataMapper dataMapper = null; // 데이터 매퍼
|
||||
|
||||
public HttpPipeLineInfo(string url, string method = "post", Dictionary<string, string> headers = null, string body = null)
|
||||
{
|
||||
this.url = url;
|
||||
this.method = method;
|
||||
this.headers = headers;
|
||||
this.body = body;
|
||||
}
|
||||
|
||||
public HttpPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||
{
|
||||
this.dataMapper = dataMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpPipeLineInfo setHandler(Action<IDataObject> handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000)
|
||||
{
|
||||
this.repeat = repeat;
|
||||
this.repeatCount = count;
|
||||
this.repeatInterval = interval;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/HttpPipeLineInfo.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 15e27eb73804cd548972be4bdd89a058
|
||||
6
Assets/Scripts/UVC/Data/IDataObject.cs
Normal file
6
Assets/Scripts/UVC/Data/IDataObject.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace UVC.Data
|
||||
{
|
||||
public interface IDataObject
|
||||
{
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/IDataObject.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/IDataObject.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 73168fc72a50dab44adc23095e0341be
|
||||
8
Assets/Scripts/UVC/Data/MQTTPipeLine.cs
Normal file
8
Assets/Scripts/UVC/Data/MQTTPipeLine.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class MQTTPipeLine
|
||||
{
|
||||
}
|
||||
}
|
||||
29
Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs
Normal file
29
Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace UVC.Data
|
||||
{
|
||||
public class MQTTPipeLineInfo
|
||||
{
|
||||
public string topic; // MQTT 토픽
|
||||
public Action<DataObject> handler = null; // 메시지 핸들러
|
||||
public DataMapper dataMapper = null; // 데이터 매퍼
|
||||
|
||||
public MQTTPipeLineInfo(string topic)
|
||||
{
|
||||
this.topic = topic;
|
||||
}
|
||||
|
||||
public MQTTPipeLineInfo setHandler(Action<DataObject> handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public MQTTPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||
{
|
||||
this.dataMapper = dataMapper;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs.meta
Normal file
2
Assets/Scripts/UVC/Data/MQTTPipeLineInfo.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d808f1610804b40429477dd3193c93e5
|
||||
166
Assets/Scripts/UVC/Network/DebounceRequester.cs
Normal file
166
Assets/Scripts/UVC/Network/DebounceRequester.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using UVC.Network;
|
||||
|
||||
namespace AssetsUVC.Network
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP 요청을 디바운스(debounce) 처리하기 위한 유틸리티 클래스
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스는 짧은 시간 내에 반복적으로 발생하는 HTTP 요청을 제어하여
|
||||
/// 마지막 요청만 실행되도록 합니다. 사용자 입력에 따른 API 호출이나
|
||||
/// 실시간 검색과 같이 연속된 요청을 최적화할 때 유용합니다.
|
||||
///
|
||||
/// 기본 사용법:
|
||||
/// <code>
|
||||
/// // DebounceRequester 인스턴스 생성
|
||||
/// var requester = new DebounceRequester();
|
||||
///
|
||||
/// // 연속된 검색 요청 예제
|
||||
/// void OnSearchTextChanged(string searchText)
|
||||
/// {
|
||||
/// requester.Request<SearchResult>(
|
||||
/// 300, // 300ms 디바운스
|
||||
/// "/api/search",
|
||||
/// "get",
|
||||
/// $"{{\"query\": \"{searchText}\"}}",
|
||||
/// null,
|
||||
/// false,
|
||||
/// result => {
|
||||
/// // 검색 결과 처리
|
||||
/// DisplaySearchResults(result);
|
||||
/// }
|
||||
/// );
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public class DebounceRequester
|
||||
{
|
||||
/// <summary>
|
||||
/// 취소 토큰 소스. 이전 요청을 취소하는 데 사용됩니다.
|
||||
/// </summary>
|
||||
private CancellationTokenSource cts = new CancellationTokenSource();
|
||||
|
||||
/// <summary>
|
||||
/// 취소 토큰을 재설정하여 이전 요청을 취소합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메소드는 새로운 요청이 시작될 때마다 호출되어 이전에 예약된 요청을 취소합니다.
|
||||
/// </remarks>
|
||||
private void resetCancellationToken()
|
||||
{
|
||||
cts.Cancel();
|
||||
cts = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DebounceRequester가 사용한 리소스를 해제합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 클래스의 사용이 끝나면 항상 Dispose를 호출하여 리소스 누수를 방지해야 합니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 리소스 해제 예제
|
||||
/// public class SearchManager : IDisposable
|
||||
/// {
|
||||
/// private DebounceRequester requester = new DebounceRequester();
|
||||
///
|
||||
/// public void Dispose()
|
||||
/// {
|
||||
/// requester.Dispose();
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void Dispose()
|
||||
{
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 지정된 시간(밀리초) 동안 디바운스된 HTTP 요청을 수행합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
||||
/// <param name="milliseconds">디바운스 시간(밀리초)</param>
|
||||
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
||||
/// <param name="method">HTTP 메소드 (get, post 등)</param>
|
||||
/// <param name="body">요청 본문으로 전송할 JSON 문자열 (null 가능)</param>
|
||||
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
||||
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
||||
/// <param name="action">요청 완료 후 실행할 콜백 함수 (null 가능)</param>
|
||||
/// <remarks>
|
||||
/// 이 메소드는 호출된 후 지정된 밀리초 동안 대기하고, 그 시간 내에 다시 호출되면
|
||||
/// 이전 요청을 취소하고 새로운 타이머를 시작합니다. 디바운스 시간이 경과한 후에만
|
||||
/// 실제 HTTP 요청이 수행됩니다.
|
||||
/// </remarks>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 실시간 타이핑 검색 예제
|
||||
/// private DebounceRequester searchRequester = new DebounceRequester();
|
||||
///
|
||||
/// public void OnSearchInputChanged(string searchText)
|
||||
/// {
|
||||
/// // 사용자가 타이핑을 멈춘 후 500ms 후에 검색 요청
|
||||
/// searchRequester.Request<List<ProductData>>(
|
||||
/// 500,
|
||||
/// "/api/products/search",
|
||||
/// "get",
|
||||
/// $"{{\"query\": \"{searchText}\"}}",
|
||||
/// null,
|
||||
/// true,
|
||||
/// results => {
|
||||
/// // 검색 결과 UI 업데이트
|
||||
/// UpdateSearchResultsUI(results);
|
||||
/// }
|
||||
/// );
|
||||
/// }
|
||||
///
|
||||
/// // 자동 저장 기능 예제
|
||||
/// private DebounceRequester autoSaveRequester = new DebounceRequester();
|
||||
///
|
||||
/// public void OnDocumentChanged(string documentId, string content)
|
||||
/// {
|
||||
/// // 문서 변경 후 2초 동안 추가 변경이 없으면 저장
|
||||
/// var body = $"{{\"id\": \"{documentId}\", \"content\": \"{content}\"}}";
|
||||
///
|
||||
/// autoSaveRequester.Request<SaveResult>(
|
||||
/// 2000,
|
||||
/// "/api/documents/save",
|
||||
/// "post",
|
||||
/// body,
|
||||
/// null,
|
||||
/// true,
|
||||
/// result => {
|
||||
/// if (result.success) {
|
||||
/// ShowSavedNotification();
|
||||
/// }
|
||||
/// }
|
||||
/// );
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public void Request<T>(int milliseconds, string url, string method, string body = null, Dictionary<string, string> header = null, bool useAuth = false, Action<T> action = null)
|
||||
{
|
||||
resetCancellationToken();
|
||||
UniTask.Delay(TimeSpan.FromMilliseconds(milliseconds), cancellationToken: cts.Token, ignoreTimeScale: false).ContinueWith(async () =>
|
||||
{
|
||||
if (action != null)
|
||||
{
|
||||
T result = await HttpRequester.Request<T>(url, method, body, header, useAuth);
|
||||
action.Invoke(result);
|
||||
}
|
||||
else
|
||||
{
|
||||
HttpRequester.Request<T>(url, method, body, header, useAuth).Forget();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/UVC/Network/DebounceRequester.cs.meta
Normal file
2
Assets/Scripts/UVC/Network/DebounceRequester.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84bd91fe0c1db234ea850ff5a747f322
|
||||
@@ -21,13 +21,88 @@ namespace UVC.Network
|
||||
/// 이 클래스는 REST API 호출을 위한 메소드들을 제공합니다.
|
||||
/// GET, POST 요청 및 파일 다운로드를 지원하며 요청/응답 로깅 기능도 포함합니다.
|
||||
/// 요청은 비동기(UniTask)로 처리됩니다.
|
||||
///
|
||||
/// 기본 사용법:
|
||||
/// <code>
|
||||
/// // 도메인 설정
|
||||
/// HttpRequester.Domain = "https://api.example.com";
|
||||
///
|
||||
/// // GET 요청 예제
|
||||
/// var products = await HttpRequester.RequestGet<List<Product>>("/api/products");
|
||||
///
|
||||
/// // POST 요청 예제
|
||||
/// var loginData = new Dictionary<string, object>
|
||||
/// {
|
||||
/// { "username", "user@example.com" },
|
||||
/// { "password", "password123" }
|
||||
/// };
|
||||
/// var response = await HttpRequester.RequestPost<LoginResponse>("/api/login", loginData);
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public static class HttpRequester
|
||||
public class HttpRequester
|
||||
{
|
||||
/// <summary>
|
||||
/// 기본 도메인 주소입니다.
|
||||
/// </summary>
|
||||
public static string Domain {get; set; } = ""; // Replace with your actual API domain
|
||||
public static string Domain { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
||||
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
||||
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
|
||||
/// <param name="body">요청 본문으로 전송할 Dictionary 데이터 (null 가능)</param>
|
||||
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
||||
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
||||
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // PUT 요청 예제
|
||||
/// public async UniTask<ProductResponse> UpdateProductAsync(int productId, string name, float price)
|
||||
/// {
|
||||
/// var body = new Dictionary<string, object>
|
||||
/// {
|
||||
/// { "id", productId },
|
||||
/// { "name", name },
|
||||
/// { "price", price }
|
||||
/// };
|
||||
///
|
||||
/// return await HttpRequester.Request<ProductResponse>("/api/products", "put", body);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static async UniTask<T> Request<T>(string url, string method, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), method, header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 형태의 본문을 가진 사용자 정의 HTTP 메소드로 API 요청을 수행합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
||||
/// <param name="url">요청할 URL (http가 포함되지 않은 경우 Domain과 결합)</param>
|
||||
/// <param name="method">HTTP 메소드 (get, post, put 등)</param>
|
||||
/// <param name="body">요청 본문으로 전송할 JSON 문자열 (null 가능)</param>
|
||||
/// <param name="header">추가할 헤더 정보 (null 가능)</param>
|
||||
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
||||
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // DELETE 요청 예제
|
||||
/// public async UniTask<bool> DeleteResourceAsync(string resourceId)
|
||||
/// {
|
||||
/// string jsonBody = $"{{\"id\": \"{resourceId}\"}}";
|
||||
///
|
||||
/// return await HttpRequester.Request<bool>("/api/resources", "delete", jsonBody);
|
||||
/// }
|
||||
/// </code>
|
||||
/// </example>
|
||||
public static async UniTask<T> Request<T>(string url, string method, string body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
return await Request_<T>(url, body, method, header, useAuth);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Dictionary를 JSON body로 변환하여 POST 요청을 수행합니다.
|
||||
@@ -63,7 +138,7 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<T> RequestPost<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
return await Request<T>(url, body, HTTPMethods.Post, header, useAuth);
|
||||
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -88,7 +163,7 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<T> RequestPost<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
return await Request<T>(url, body, HTTPMethods.Post, header, useAuth);
|
||||
return await Request_<T>(url, body, "post", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -126,26 +201,31 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<T> RequestGet<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
return await Request<T>(url, body, HTTPMethods.Get, header, useAuth);
|
||||
return await Request_<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청을 처리하는 내부 메소드
|
||||
/// </summary>
|
||||
private static async UniTask<T> Request<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = false)
|
||||
/// <remarks>
|
||||
/// 이 메소드는 모든 HTTP 요청의 공통 로직을 처리합니다:
|
||||
/// - URL 구성 (Domain과 상대 경로 결합)
|
||||
/// - HTTP 메소드 설정
|
||||
/// - 헤더 설정
|
||||
/// - 요청 로깅
|
||||
/// - 응답 처리 및 역직렬화
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
||||
/// <param name="url">요청할 URL</param>
|
||||
/// <param name="body">요청 본문 (JSON 문자열)</param>
|
||||
/// <param name="methodString">HTTP 메소드 문자열</param>
|
||||
/// <param name="header">추가할 헤더 정보</param>
|
||||
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
||||
/// <returns>지정된 타입으로 변환된 응답 데이터</returns>
|
||||
private static async UniTask<T> Request_<T>(string url, string body = null, string methodString = "post", Dictionary<string, string> header = null, bool useAuth = false)
|
||||
{
|
||||
string bodyString = "";
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
bodyString = body.ToString();
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
bodyString = JsonHelper.ToJson(body);
|
||||
}
|
||||
}
|
||||
HTTPMethods method = StringToMethod(methodString);
|
||||
|
||||
if (!url.Contains("http")) url = $"{Domain}{url}";
|
||||
|
||||
var request = SelectHTTPRequest(method, url);
|
||||
@@ -165,20 +245,12 @@ namespace UVC.Network
|
||||
headerObject.Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
}
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
//Debug.Log($"Request APIToken :{AuthService.Instance.Entiti.accessToken}");
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
|
||||
}
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body));
|
||||
}
|
||||
//var response = await request.GetFromJsonResultAsync<T>();
|
||||
var response = await request.GetAsStringAsync();
|
||||
@@ -224,7 +296,7 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
|
||||
{
|
||||
return await RequestWithDuration<T>(url, body, HTTPMethods.Post, header, useAuth);
|
||||
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "post", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -256,7 +328,7 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<(T, TimeSpan)> RequestPostWithDuration<T>(string url, string body, Dictionary<string, string> header = null, bool useAuth = true)
|
||||
{
|
||||
return await RequestWithDuration<T>(url, body, HTTPMethods.Post, header, useAuth);
|
||||
return await RequestWithDuration<T>(url, body, "post", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -295,26 +367,31 @@ namespace UVC.Network
|
||||
/// </example>
|
||||
public static async UniTask<(T, TimeSpan)> RequestGetWithDuration<T>(string url, Dictionary<string, object> body = null, Dictionary<string, string> header = null, bool useAuth = true)
|
||||
{
|
||||
return await RequestWithDuration<T>(url, body, HTTPMethods.Get, header, useAuth);
|
||||
return await RequestWithDuration<T>(url, body == null ? null : JsonHelper.ToJson(body), "get", header, useAuth);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// HTTP 요청을 처리하고 처리 시간을 측정하는 내부 메소드
|
||||
/// </summary>
|
||||
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, object body = null, HTTPMethods method = HTTPMethods.Post, Dictionary<string, string> header = null, bool useAuth = true)
|
||||
/// <remarks>
|
||||
/// 이 메소드는 모든 성능 측정이 필요한 HTTP 요청의 공통 로직을 처리합니다:
|
||||
/// - 요청 처리 시간 측정
|
||||
/// - URL 구성
|
||||
/// - HTTP 메소드 설정
|
||||
/// - 헤더 설정
|
||||
/// - 요청 로깅
|
||||
/// - 응답 처리 및 역직렬화
|
||||
/// </remarks>
|
||||
/// <typeparam name="T">응답 데이터를 변환할 타입</typeparam>
|
||||
/// <param name="url">요청할 URL</param>
|
||||
/// <param name="body">요청 본문 (JSON 문자열)</param>
|
||||
/// <param name="methodString">HTTP 메소드 문자열</param>
|
||||
/// <param name="header">추가할 헤더 정보</param>
|
||||
/// <param name="useAuth">인증 토큰 사용 여부</param>
|
||||
/// <returns>지정된 타입으로 변환된 응답 데이터와 요청 처리 시간의 튜플</returns>
|
||||
private static async UniTask<(T, TimeSpan)> RequestWithDuration<T>(string url, string body = null, string methodString = "", Dictionary<string, string> header = null, bool useAuth = true)
|
||||
{
|
||||
string bodyString = "";
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
bodyString = body.ToString();
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
bodyString = JsonHelper.ToJson(body);
|
||||
}
|
||||
}
|
||||
HTTPMethods method = StringToMethod(methodString);
|
||||
if (!url.Contains("http")) url = $"{Domain}{url}";
|
||||
var request = SelectHTTPRequest(method, url);
|
||||
request.DownloadSettings = new Best.HTTP.Request.Settings.DownloadSettings() { ContentStreamMaxBuffered = 1024 * 1024 * 200 };
|
||||
@@ -334,19 +411,11 @@ namespace UVC.Network
|
||||
//if (useAuth) request.SetHeader("access-token", AuthService.Instance.Entiti.accessToken);
|
||||
if (body != null)
|
||||
{
|
||||
if (body is string)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body as string));
|
||||
}
|
||||
else if (body is Dictionary<string, object>)
|
||||
{
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(JsonHelper.ToJson(body)));
|
||||
}
|
||||
request.UploadSettings.UploadStream =
|
||||
new MemoryStream(Encoding.UTF8.GetBytes(body));
|
||||
}
|
||||
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, method.ToString(), headerObject.ToString(Formatting.None), bodyString, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
HttpLogEntry log = ServerLog.LogHttpRequest(url, methodString, headerObject.ToString(Formatting.None), body, DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
|
||||
//var response = await request.GetFromJsonResultAsync<T>();
|
||||
var now = DateTime.UtcNow;
|
||||
var response = await request.GetAsStringAsync();
|
||||
@@ -415,7 +484,7 @@ namespace UVC.Network
|
||||
req.DownloadSettings.OnDownloadStarted = null;
|
||||
req.DownloadSettings.OnDownloadProgress = null;
|
||||
req.DownloadSettings.DownloadStreamFactory = null;
|
||||
|
||||
|
||||
switch (req.State)
|
||||
{
|
||||
case HTTPRequestStates.Finished:
|
||||
@@ -463,6 +532,20 @@ namespace UVC.Network
|
||||
/// <summary>
|
||||
/// 다운로드 스트림을 처리하여 파일로 저장합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 이 메소드는 스트림 데이터를 지정된 파일로 저장하며, 다운로드가 완료되거나 중단될 때까지 블로킹 방식으로 데이터를 처리합니다.
|
||||
/// 다운로드에 실패한 경우 (길이가 0인 경우) 빈 파일은 삭제됩니다.
|
||||
/// </remarks>
|
||||
/// <param name="savePath">다운로드한 데이터를 저장할 파일 경로</param>
|
||||
/// <param name="blockingStream">다운로드 콘텐츠 스트림</param>
|
||||
/// <returns>저장된 파일의 크기(바이트)</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 내부적으로 Download 메소드에서 사용됩니다
|
||||
/// long fileSize = await UniTask.RunOnThreadPool<long>(() =>
|
||||
/// ConsumeDownloadStream(savePath, downloadStream as BlockingDownloadContentStream));
|
||||
/// </code>
|
||||
/// </example>
|
||||
private static long ConsumeDownloadStream(string savePath, BlockingDownloadContentStream blockingStream)
|
||||
{
|
||||
long len = 0;
|
||||
@@ -502,6 +585,18 @@ namespace UVC.Network
|
||||
/// <summary>
|
||||
/// HTTP 메소드와 URL을 기반으로 적절한 HTTPRequest 객체를 생성합니다.
|
||||
/// </summary>
|
||||
/// <param name="methods">요청에 사용할 HTTP 메소드</param>
|
||||
/// <param name="url">요청할 URL</param>
|
||||
/// <param name="onRequest">요청 완료 시 호출될 콜백 (선택 사항)</param>
|
||||
/// <returns>생성된 HTTPRequest 객체</returns>
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // 내부 사용 예제 (직접 사용하지 않음)
|
||||
/// HTTPRequest request = SelectHTTPRequest(HTTPMethods.Get, "https://api.example.com/data");
|
||||
/// request.SetHeader("Custom-Header", "HeaderValue");
|
||||
/// request.Send();
|
||||
/// </code>
|
||||
/// </example>
|
||||
private static HTTPRequest SelectHTTPRequest(HTTPMethods methods, string url, OnRequestFinishedDelegate onRequest = null)
|
||||
{
|
||||
switch (methods)
|
||||
@@ -518,5 +613,22 @@ namespace UVC.Network
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 문자열 형태의 HTTP 메소드를 Best.HTTP.HTTPMethods 열거형으로 변환합니다.
|
||||
/// </summary>
|
||||
/// <param name="method">변환할 HTTP 메소드 문자열 ("get", "post" 등)</param>
|
||||
/// <returns>해당하는 HTTPMethods 열거형 값</returns>
|
||||
/// <exception cref="ArgumentException">지원하지 않는 HTTP 메소드인 경우 발생</exception>
|
||||
private static HTTPMethods StringToMethod(string method)
|
||||
{
|
||||
return method.ToLower() switch
|
||||
{
|
||||
"get" => HTTPMethods.Get,
|
||||
"post" => HTTPMethods.Post,
|
||||
_ => throw new ArgumentException($"Unsupported HTTP method: {method}"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace UVC.Tests.Data
|
||||
[TestFixture]
|
||||
public class DataMapperTests
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 모든 테스트 메서드를 실행하는 메서드입니다.
|
||||
/// </summary>
|
||||
@@ -37,7 +36,7 @@ namespace UVC.Tests.Data
|
||||
RunTest(nameof(Map_ComplexObject_MapsAllProperties), Map_ComplexObject_MapsAllProperties);
|
||||
RunTest(nameof(Map_NestedObject_MapsCorrectly), Map_NestedObject_MapsCorrectly);
|
||||
RunTest(nameof(Map_ArrayMapping_MapsCorrectly), Map_ArrayMapping_MapsCorrectly);
|
||||
RunTest(nameof(Map_EmptyGuideArray_CopiesSourceArray), Map_EmptyGuideArray_CopiesSourceArray);
|
||||
RunTest(nameof(Map_EmptyMaskArray_CopiesSourceArray), Map_EmptyMaskArray_CopiesSourceArray);
|
||||
RunTest(nameof(Map_ComplexNestedStructure_MapsCorrectly), Map_ComplexNestedStructure_MapsCorrectly);
|
||||
RunTest(nameof(Map_MixedArrayTypes_HandlesCorrectly), Map_MixedArrayTypes_HandlesCorrectly);
|
||||
|
||||
@@ -70,19 +69,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_StringProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Name"] = "홍길동";
|
||||
var mask = new JObject();
|
||||
mask["Name"] = "홍길동";
|
||||
|
||||
var source = JObject.Parse(@"{ ""Name"": ""김철수"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Name"));
|
||||
Assert.AreEqual("김철수", result["Name"].ToString());
|
||||
Assert.AreEqual("김철수", result.GetString("Name"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -92,19 +91,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_IntProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Age"] = 25;
|
||||
var mask = new JObject();
|
||||
mask["Age"] = 25;
|
||||
|
||||
var source = JObject.Parse(@"{ ""Age"": 30 }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Age"));
|
||||
Assert.AreEqual(30, result["Age"].ToObject<int>());
|
||||
Assert.AreEqual(30, result.GetInt("Age"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -114,19 +113,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_DoubleProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Height"] = 175.5;
|
||||
var mask = new JObject();
|
||||
mask["Height"] = 175.5;
|
||||
|
||||
var source = JObject.Parse(@"{ ""Height"": 180.5 }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Height"));
|
||||
Assert.AreEqual(180.5, result["Height"].ToObject<double>());
|
||||
Assert.AreEqual(180.5, result.GetDouble("Height"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,19 +135,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_BoolProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["IsActive"] = false;
|
||||
var mask = new JObject();
|
||||
mask["IsActive"] = false;
|
||||
|
||||
var source = JObject.Parse(@"{ ""IsActive"": true }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("IsActive"));
|
||||
Assert.IsTrue(result["IsActive"].ToObject<bool>());
|
||||
Assert.IsTrue(result.GetBool("IsActive"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -158,20 +157,20 @@ namespace UVC.Tests.Data
|
||||
public void Map_DateTimeProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["BirthDate"] = JToken.FromObject(DateTime.Now);
|
||||
var mask = new JObject();
|
||||
mask["BirthDate"] = JToken.FromObject(DateTime.Now);
|
||||
|
||||
var expectedDate = new DateTime(1990, 1, 1);
|
||||
var source = JObject.Parse(@"{ ""BirthDate"": ""1990-01-01T00:00:00.000"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("BirthDate"));
|
||||
Assert.AreEqual(expectedDate, result["BirthDate"].ToObject<DateTime>());
|
||||
Assert.AreEqual(expectedDate, result.GetDateTime("BirthDate"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -181,22 +180,22 @@ namespace UVC.Tests.Data
|
||||
public void Map_DataValueMapperProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
var mask = new JObject();
|
||||
var valueMapper = new DataValueMapper();
|
||||
valueMapper["ON"] = "활성화";
|
||||
valueMapper["OFF"] = "비활성화";
|
||||
guide["Status"] = JToken.FromObject(valueMapper);
|
||||
mask["Status"] = JToken.FromObject(valueMapper);
|
||||
|
||||
var source = JObject.Parse(@"{ ""Status"": ""ON"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Status"));
|
||||
Assert.AreEqual("활성화", result["Status"].ToString());
|
||||
Assert.AreEqual("활성화", result.GetString("Status"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -206,22 +205,22 @@ namespace UVC.Tests.Data
|
||||
public void Map_DataValueMapperWithUnmappedValue_ReturnsOriginal()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
var mask = new JObject();
|
||||
var valueMapper = new DataValueMapper();
|
||||
valueMapper["ON"] = "활성화";
|
||||
valueMapper["OFF"] = "비활성화";
|
||||
guide["Status"] = JToken.FromObject(valueMapper);
|
||||
mask["Status"] = JToken.FromObject(valueMapper);
|
||||
|
||||
var source = JObject.Parse(@"{ ""Status"": ""UNKNOWN"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Status"));
|
||||
Assert.AreEqual("UNKNOWN", result["Status"].ToString());
|
||||
Assert.AreEqual("UNKNOWN", result.GetString("Status"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -231,19 +230,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_EnumProperty_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Status"] = JToken.FromObject(UserStatus.Inactive);
|
||||
var mask = new JObject();
|
||||
mask["Status"] = JToken.FromObject(UserStatus.Inactive);
|
||||
|
||||
var source = JObject.Parse(@"{ ""Status"": ""Active"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Status"));
|
||||
Assert.AreEqual(UserStatus.Active, result["Status"].ToObject<UserStatus>());
|
||||
Assert.AreEqual(UserStatus.Active, result.GetEnum<UserStatus>("Status"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -253,19 +252,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_AdditionalProperty_AddsToResult()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Name"] = "홍길동";
|
||||
var mask = new JObject();
|
||||
mask["Name"] = "홍길동";
|
||||
|
||||
var source = JObject.Parse(@"{ ""Name"": ""김철수"", ""Email"": ""kim@example.com"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Email"));
|
||||
Assert.AreEqual("kim@example.com", result["Email"].ToString());
|
||||
Assert.IsFalse(result.ContainsKey("Email"));
|
||||
Assert.AreEqual(null, result.GetString("Email"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -275,19 +274,19 @@ namespace UVC.Tests.Data
|
||||
public void Map_InvalidDateTimeString_ReturnsNull()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["BirthDate"] = JToken.FromObject(DateTime.Now);
|
||||
var mask = new JObject();
|
||||
mask["BirthDate"] = JToken.FromObject(DateTime.Now);
|
||||
|
||||
var source = JObject.Parse(@"{ ""BirthDate"": ""InvalidDateTime"" }");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("BirthDate"));
|
||||
Assert.AreEqual(JTokenType.Null, result["BirthDate"].Type);
|
||||
Assert.IsNull(result["BirthDate"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -297,13 +296,13 @@ namespace UVC.Tests.Data
|
||||
public void Map_ComplexObject_MapsAllProperties()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject();
|
||||
guide["Id"] = 1;
|
||||
guide["Name"] = "홍길동";
|
||||
guide["IsActive"] = false;
|
||||
guide["JoinDate"] = JToken.FromObject(DateTime.Now);
|
||||
guide["Height"] = 175.5;
|
||||
guide["Status"] = JToken.FromObject(UserStatus.Inactive);
|
||||
var mask = new JObject();
|
||||
mask["Id"] = 1;
|
||||
mask["Name"] = "홍길동";
|
||||
mask["IsActive"] = false;
|
||||
mask["JoinDate"] = JToken.FromObject(DateTime.Now);
|
||||
mask["Height"] = 175.5;
|
||||
mask["Status"] = JToken.FromObject(UserStatus.Inactive);
|
||||
|
||||
string json = @"{
|
||||
""Id"": 100,
|
||||
@@ -316,30 +315,30 @@ namespace UVC.Tests.Data
|
||||
}";
|
||||
|
||||
var source = JObject.Parse(json);
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.AreEqual(7, result.Count);
|
||||
Assert.AreEqual(100, result["Id"].ToObject<int>());
|
||||
Assert.AreEqual("김철수", result["Name"].ToString());
|
||||
Assert.IsTrue(result["IsActive"].ToObject<bool>());
|
||||
Assert.AreEqual(new DateTime(2023, 1, 1), result["JoinDate"].ToObject<DateTime>());
|
||||
Assert.AreEqual(180.0, result["Height"].ToObject<double>());
|
||||
Assert.AreEqual(UserStatus.Active, result["Status"].ToObject<UserStatus>());
|
||||
Assert.AreEqual("kim@example.com", result["Email"].ToString());
|
||||
Assert.AreEqual(6, result.Count);
|
||||
Assert.AreEqual(100, result.GetInt("Id"));
|
||||
Assert.AreEqual("김철수", result.GetString("Name"));
|
||||
Assert.IsTrue(result.GetBool("IsActive"));
|
||||
Assert.AreEqual(new DateTime(2023, 1, 1), result.GetDateTime("JoinDate"));
|
||||
Assert.AreEqual(180.0, result.GetDouble("Height"));
|
||||
Assert.AreEqual(UserStatus.Active, result.GetEnum<UserStatus>("Status"));
|
||||
Assert.AreEqual(null, result.GetString("Email"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중첩된 객체 매핑 테스트 - 새로 추가
|
||||
/// 중첩된 객체 매핑 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_NestedObject_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject
|
||||
var mask = new JObject
|
||||
{
|
||||
["User"] = new JObject
|
||||
{
|
||||
@@ -363,32 +362,33 @@ namespace UVC.Tests.Data
|
||||
}
|
||||
}");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("User"));
|
||||
var user = result["User"] as JObject;
|
||||
Assert.IsNotNull(user);
|
||||
Assert.AreEqual("김철수", user["Name"].ToString());
|
||||
|
||||
var address = user["Address"] as JObject;
|
||||
var user = result.GetDataObject("User");
|
||||
Assert.IsNotNull(user);
|
||||
Assert.AreEqual("김철수", user.GetString("Name"));
|
||||
|
||||
var address = user.GetDataObject("Address");
|
||||
Assert.IsNotNull(address);
|
||||
Assert.AreEqual("부산", address["City"].ToString());
|
||||
Assert.AreEqual("67890", address["ZipCode"].ToString());
|
||||
Assert.AreEqual("대한민국", address["Country"].ToString());
|
||||
Assert.AreEqual("부산", address.GetString("City"));
|
||||
Assert.AreEqual("67890", address.GetString("ZipCode"));
|
||||
Assert.AreEqual(null, address.GetString("Country"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배열 매핑 테스트 - 새로 추가
|
||||
/// 배열 매핑 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_ArrayMapping_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject
|
||||
var mask = new JObject
|
||||
{
|
||||
["Contacts"] = new JArray
|
||||
{
|
||||
@@ -407,37 +407,37 @@ namespace UVC.Tests.Data
|
||||
]
|
||||
}");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Contacts"));
|
||||
var contacts = result["Contacts"] as JArray;
|
||||
var contacts = result.GetDataArray("Contacts");
|
||||
Assert.IsNotNull(contacts);
|
||||
Assert.AreEqual(2, contacts.Count);
|
||||
Assert.AreEqual(1, contacts.Count);
|
||||
|
||||
var contact1 = contacts[0] as JObject;
|
||||
var contact1 = contacts[0];
|
||||
Assert.IsNotNull(contact1);
|
||||
Assert.AreEqual("mobile", contact1["Type"].ToString());
|
||||
Assert.AreEqual("010-1234-5678", contact1["Number"].ToString());
|
||||
Assert.AreEqual("mobile", contact1.GetString("Type"));
|
||||
Assert.AreEqual("010-1234-5678", contact1.GetString("Number"));
|
||||
|
||||
var contact2 = contacts[1] as JObject;
|
||||
var contact2 = contacts[1];
|
||||
Assert.IsNotNull(contact2);
|
||||
Assert.AreEqual("home", contact2["Type"].ToString());
|
||||
Assert.AreEqual("02-123-4567", contact2["Number"].ToString());
|
||||
Assert.AreEqual("123", contact2["Extension"].ToString());
|
||||
Assert.AreEqual("home", contact2.GetString("Type"));
|
||||
Assert.AreEqual("02-123-4567", contact2.GetString("Number"));
|
||||
Assert.AreEqual("123", contact2.GetString("Extension"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 빈 가이드 배열 테스트 - 새로 추가
|
||||
/// 빈 가이드 배열 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_EmptyGuideArray_CopiesSourceArray()
|
||||
public void Map_EmptyMaskArray_CopiesSourceArray()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject
|
||||
var mask = new JObject
|
||||
{
|
||||
["Tags"] = new JArray()
|
||||
};
|
||||
@@ -446,29 +446,35 @@ namespace UVC.Tests.Data
|
||||
""Tags"": [""개발"", ""테스트"", ""배포""]
|
||||
}");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(result.ContainsKey("Tags"));
|
||||
var tags = result["Tags"] as JArray;
|
||||
var tags = result.GetDataArray("Tags");
|
||||
Assert.IsNotNull(tags);
|
||||
Assert.AreEqual(3, tags.Count);
|
||||
Assert.AreEqual("개발", tags[0].ToString());
|
||||
Assert.AreEqual("테스트", tags[1].ToString());
|
||||
Assert.AreEqual("배포", tags[2].ToString());
|
||||
|
||||
// DataArray의 항목들을 검증
|
||||
for (int i = 0; i < tags.Count; i++)
|
||||
{
|
||||
var item = tags[i];
|
||||
Assert.IsNotNull(item);
|
||||
// DataObject는 "value" 키에 실제 값을 저장
|
||||
Assert.AreEqual(((JArray)source["Tags"])[i].ToString(), item.GetString("value"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 복잡한 중첩 구조 매핑 테스트 - 새로 추가
|
||||
/// 복잡한 중첩 구조 매핑 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_ComplexNestedStructure_MapsCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject
|
||||
var mask = new JObject
|
||||
{
|
||||
["Company"] = new JObject
|
||||
{
|
||||
@@ -517,55 +523,56 @@ namespace UVC.Tests.Data
|
||||
}
|
||||
}");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
var company = result["Company"] as JObject;
|
||||
var company = result.GetDataObject("Company");
|
||||
Assert.IsNotNull(company);
|
||||
Assert.AreEqual("XYZ 주식회사", company["Name"].ToString());
|
||||
Assert.AreEqual(new DateTime(2000, 1, 1), company["Founded"].ToObject<DateTime>());
|
||||
Assert.AreEqual("서울시 강남구", company["Address"].ToString());
|
||||
Assert.AreEqual("XYZ 주식회사", company.GetString("Name"));
|
||||
Assert.AreEqual(new DateTime(2000, 1, 1), company.GetDateTime("Founded"));
|
||||
Assert.AreEqual("서울시 강남구", company.GetString("Address"));
|
||||
|
||||
var departments = company["Departments"] as JArray;
|
||||
var departments = company.GetDataArray("Departments");
|
||||
Assert.IsNotNull(departments);
|
||||
Assert.AreEqual(2, departments.Count);
|
||||
|
||||
var devDept = departments[0] as JObject;
|
||||
Assert.AreEqual("개발부", devDept["Name"].ToString());
|
||||
var devEmployees = devDept["Employees"] as JArray;
|
||||
var devDept = departments[0];
|
||||
Assert.AreEqual("개발부", devDept.GetString("Name"));
|
||||
var devEmployees = devDept.GetDataArray("Employees");
|
||||
Assert.AreEqual(2, devEmployees.Count);
|
||||
Assert.AreEqual("김개발", devEmployees[0]["Name"].ToString());
|
||||
Assert.AreEqual(35, devEmployees[0]["Age"].ToObject<int>());
|
||||
Assert.AreEqual(UserStatus.Active, devEmployees[0]["Status"].ToObject<UserStatus>());
|
||||
Assert.AreEqual("이테스트", devEmployees[1]["Name"].ToString());
|
||||
Assert.AreEqual(UserStatus.Inactive, devEmployees[1]["Status"].ToObject<UserStatus>());
|
||||
Assert.AreEqual("김개발", devEmployees[0].GetString("Name"));
|
||||
Assert.AreEqual(35, devEmployees[0].GetInt("Age"));
|
||||
Assert.AreEqual(UserStatus.Active, devEmployees[0].GetEnum<UserStatus>("Status"));
|
||||
Assert.AreEqual("이테스트", devEmployees[1].GetString("Name"));
|
||||
Assert.AreEqual(UserStatus.Inactive, devEmployees[1].GetEnum<UserStatus>("Status"));
|
||||
|
||||
var marketingDept = departments[1] as JObject;
|
||||
Assert.AreEqual("마케팅부", marketingDept["Name"].ToString());
|
||||
Assert.AreEqual(500000, marketingDept["Budget"].ToObject<int>());
|
||||
var marketingEmployees = marketingDept["Employees"] as JArray;
|
||||
var marketingDept = departments[1];
|
||||
Assert.AreEqual("마케팅부", marketingDept.GetString("Name"));
|
||||
Assert.AreEqual(500000, marketingDept.GetInt("Budget"));
|
||||
var marketingEmployees = marketingDept.GetDataArray("Employees");
|
||||
Assert.AreEqual(1, marketingEmployees.Count);
|
||||
Assert.AreEqual("박마케팅", marketingEmployees[0]["Name"].ToString());
|
||||
Assert.AreEqual("박마케팅", marketingEmployees[0].GetString("Name"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 다양한 형식의 배열 테스트 - 새로 추가
|
||||
/// 다양한 형식의 배열 테스트
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void Map_MixedArrayTypes_HandlesCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var guide = new JObject
|
||||
var mask = new JObject
|
||||
{
|
||||
["MixedArray"] = new JArray
|
||||
{
|
||||
"문자열",
|
||||
123,
|
||||
true,
|
||||
new JObject { ["Key"] = "Value" }
|
||||
new JObject { ["Key"] = "Value" },
|
||||
new JArray{1, 1}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -575,31 +582,43 @@ namespace UVC.Tests.Data
|
||||
456,
|
||||
false,
|
||||
{ ""Key"": ""NewValue"", ""Extra"": ""ExtraValue"" },
|
||||
[1, 2, 3]
|
||||
[1, 2, 3],
|
||||
{ ""items"": [1, 2, 3] }
|
||||
]
|
||||
}");
|
||||
|
||||
var mapper = new DataMapper(source, guide);
|
||||
var mapper = new DataMapper(mask);
|
||||
|
||||
// Act
|
||||
var result = mapper.Map();
|
||||
var result = mapper.Map(source);
|
||||
|
||||
// Assert
|
||||
var mixedArray = result["MixedArray"] as JArray;
|
||||
Assert.IsTrue(result.ContainsKey("MixedArray"));
|
||||
var mixedArray = result.GetDataArray("MixedArray");
|
||||
Assert.IsNotNull(mixedArray);
|
||||
Assert.AreEqual(5, mixedArray.Count);
|
||||
Assert.AreEqual("테스트", mixedArray[0].ToString());
|
||||
Assert.AreEqual(456, mixedArray[1].ToObject<int>());
|
||||
Assert.AreEqual(false, mixedArray[2].ToObject<bool>());
|
||||
|
||||
var objInArray = mixedArray[3] as JObject;
|
||||
// 문자열 항목 검증
|
||||
Assert.AreEqual("테스트", mixedArray[0].GetString("value"));
|
||||
// 숫자 항목 검증
|
||||
Assert.AreEqual(456, mixedArray[1].GetInt("value"));
|
||||
// 불리언 항목 검증
|
||||
Assert.AreEqual(false, mixedArray[2].GetBool("value"));
|
||||
|
||||
// 객체 항목 검증
|
||||
var objInArray = mixedArray[3];
|
||||
Assert.IsNotNull(objInArray);
|
||||
Assert.AreEqual("NewValue", objInArray["Key"].ToString());
|
||||
Assert.AreEqual("ExtraValue", objInArray["Extra"].ToString());
|
||||
Assert.AreEqual("NewValue", objInArray.GetString("Key"));
|
||||
|
||||
var nestedArray = mixedArray[4] as JArray;
|
||||
// 중첩 배열 항목 검증
|
||||
var nestedItem = mixedArray[4];
|
||||
Assert.IsNotNull(nestedItem);
|
||||
// DataMapper는 중첩 배열을 "items" 키를 가진 DataObject로 포장합니다
|
||||
var nestedArray = nestedItem.GetDataArray("items");
|
||||
Assert.IsNotNull(nestedArray);
|
||||
Assert.AreEqual(3, nestedArray.Count);
|
||||
Assert.AreEqual(2, nestedArray.Count);
|
||||
Assert.AreEqual(1, nestedArray[0].GetInt("value"));
|
||||
Assert.AreEqual(2, nestedArray[1].GetInt("value"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user