httppipeline 완료, mqttPipeline 개발 중. test 코드 작성 필요
This commit is contained in:
@@ -212,7 +212,12 @@ namespace UVC.Data
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 업데이트 된 속성의 수.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||||
|
public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 항목을 추가합니다.
|
/// 항목을 추가합니다.
|
||||||
|
|||||||
@@ -41,10 +41,19 @@ namespace UVC.Data
|
|||||||
|
|
||||||
// 재귀 호출 제한하기 위한 설정 추가
|
// 재귀 호출 제한하기 위한 설정 추가
|
||||||
private int maxRecursionDepth = 10;
|
private int maxRecursionDepth = 10;
|
||||||
private int currentDepth = 0;
|
|
||||||
|
|
||||||
private DataMask mask;
|
private DataMask mask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대용량 JSON 데이터를 스트리밍 방식으로 처리할지 여부를 나타내는 속성입니다.
|
||||||
|
/// </summary>
|
||||||
|
public bool SupportsStreamParsing { get; internal set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대용량 JSON 스트림을 판단 할때 스트림 길이가 이 값보다 크면 스트리밍 방식으로 처리합니다.
|
||||||
|
/// </summary>
|
||||||
|
public int SupportsStreamLength { get; internal set; } = 10000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
/// DataMapper 클래스의 새 인스턴스를 초기화합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -116,7 +125,7 @@ namespace UVC.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
||||||
/// <returns>매핑된 DataObject</returns>
|
/// <returns>매핑된 DataObject</returns>
|
||||||
public DataObject MapStream(System.IO.Stream jsonStream)
|
public DataObject MapObjectStream(System.IO.Stream jsonStream)
|
||||||
{
|
{
|
||||||
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||||
{
|
{
|
||||||
@@ -126,6 +135,21 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 대용량 JSON 데이터를 스트리밍 방식으로 매핑합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="jsonStream">JSON 데이터 스트림</param>
|
||||||
|
/// <returns>매핑된 DataObject</returns>
|
||||||
|
public DataArray MapArrayStream(System.IO.Stream jsonStream)
|
||||||
|
{
|
||||||
|
using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream)))
|
||||||
|
{
|
||||||
|
var serializer = new Newtonsoft.Json.JsonSerializer();
|
||||||
|
var sourceArray = serializer.Deserialize<JArray>(reader);
|
||||||
|
return Mapping(sourceArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 객체를 재귀적으로 매핑합니다.
|
/// 객체를 재귀적으로 매핑합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -5,21 +5,96 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 데이터의 구조와 변환 규칙을 정의하는 마스크 클래스입니다.
|
||||||
|
/// Newtonsoft.Json.Linq.JObject를 상속하여 JSON 데이터 구조를 표현하고,
|
||||||
|
/// 데이터 매핑, 필드 이름 변환, 그리고 타입 변환을 위한 메타데이터를 제공합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// DataMask는 주로 DataMapper 클래스와 함께 사용되어 서로 다른 JSON 형식 간의
|
||||||
|
/// 데이터 변환 및 매핑 규칙을 정의합니다. 기본 JSON 구조 외에도 추가 속성들을
|
||||||
|
/// 통해 매핑 과정에서 필요한 메타데이터를 제공합니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// 기본 사용 예시:
|
||||||
|
/// <code>
|
||||||
|
/// // 마스크 객체 생성 및 설정
|
||||||
|
/// var mask = new DataMask();
|
||||||
|
/// mask["name"] = ""; // 문자열 타입 지정
|
||||||
|
/// mask["age"] = 0; // 정수 타입 지정
|
||||||
|
/// mask["isActive"] = false; // 불리언 타입 지정
|
||||||
|
/// mask["height"] = 0.0; // 실수 타입 지정
|
||||||
|
///
|
||||||
|
/// // 메타 속성 설정
|
||||||
|
/// mask.ObjectIdKey = "name"; // DataObject의 ID로 사용할 필드 지정
|
||||||
|
/// mask.ObjectName = "employees"; // 데이터 객체의 이름 지정
|
||||||
|
///
|
||||||
|
/// // 필드 이름 변환 규칙 설정
|
||||||
|
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||||
|
/// {
|
||||||
|
/// { "full_name", "name" }, // JSON의 full_name을 name으로 변환
|
||||||
|
/// { "employee_age", "age" } // JSON의 employee_age를 age로 변환
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // 매핑 예시 (DataMapper 클래스와 함께 사용)
|
||||||
|
/// var sourceJson = JObject.Parse(@"{
|
||||||
|
/// ""full_name"": ""김철수"",
|
||||||
|
/// ""employee_age"": 30,
|
||||||
|
/// ""isActive"": true,
|
||||||
|
/// ""height"": 175.5
|
||||||
|
/// }");
|
||||||
|
///
|
||||||
|
/// var mapper = new DataMapper(mask);
|
||||||
|
/// DataObject result = mapper.Mapping(sourceJson);
|
||||||
|
/// // result는 변환된 필드 이름과 타입을 가진 DataObject 객체
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public class DataMask : JObject
|
public class DataMask : JObject
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DataObject의 Id에 해당하는 key 문자열
|
/// DataObject의 Id에 해당하는 key 문자열입니다.
|
||||||
|
/// 이 속성은 매핑된 DataObject에서 고유 식별자로 사용될 필드를 지정합니다.
|
||||||
|
/// null인 경우 DataObject의 기본 동작을 따릅니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var mask = new DataMask();
|
||||||
|
/// mask.ObjectIdKey = "userId"; // DataObject에서 "userId" 필드가 Id로 사용됨
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public string? ObjectIdKey { get; set; } = null;
|
public string? ObjectIdKey { get; set; } = null;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다.
|
/// DataObject의 이름을 나타내는 속성입니다. DataRepository에서 사용됩니다.
|
||||||
|
/// 이 이름은 매핑된 DataObject를 분류하거나 식별하는 데 사용될 수 있습니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var mask = new DataMask();
|
||||||
|
/// mask.ObjectName = "users"; // 매핑된 DataObject는 "users"라는 이름을 가짐
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public string ObjectName { get; set; } = string.Empty;
|
public string ObjectName { get; set; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 교체 할 이름을 나타내는 딕셔너리입니다.
|
/// 원본 JSON에서 매핑된 DataObject로 필드 이름을 변환하는 규칙을 정의하는 딕셔너리입니다.
|
||||||
|
/// 키는 원본 JSON의 필드 이름이고, 값은 변환될 대상 필드 이름입니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 속성을 통해 서로 다른 명명 규칙을 사용하는 JSON 구조 간의 매핑을 용이하게 할 수 있습니다.
|
||||||
|
/// 예를 들어, snake_case를 사용하는 API 응답을 camelCase로 변환하는 데 사용할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var mask = new DataMask();
|
||||||
|
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||||
|
/// {
|
||||||
|
/// { "first_name", "firstName" },
|
||||||
|
/// { "last_name", "lastName" },
|
||||||
|
/// { "birth_date", "birthDate" }
|
||||||
|
/// };
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public Dictionary<string, string>? NamesForReplace { get; set; }
|
public Dictionary<string, string>? NamesForReplace { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,44 @@ using System.Linq;
|
|||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다.
|
||||||
|
/// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해
|
||||||
|
/// 효율적인 데이터 동기화를 지원합니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // DataMask 생성 및 설정
|
||||||
|
/// var mask = new DataMask();
|
||||||
|
/// mask.ObjectIdKey = "id";
|
||||||
|
/// mask.ObjectName = "users";
|
||||||
|
///
|
||||||
|
/// // 필드 이름 변환 설정
|
||||||
|
/// mask.NamesForReplace = new Dictionary<string, string>
|
||||||
|
/// {
|
||||||
|
/// { "userName", "name" },
|
||||||
|
/// { "userEmail", "email" }
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // JObject 속성으로 마스킹 규칙 추가
|
||||||
|
/// mask["include"] = new JArray("name", "email", "age");
|
||||||
|
/// mask["exclude"] = new JArray("password", "token");
|
||||||
|
///
|
||||||
|
/// // DataObject에 마스크 적용하는 예시
|
||||||
|
/// var dataObject = new DataObject();
|
||||||
|
/// dataObject["userName"] = "홍길동";
|
||||||
|
/// dataObject["userEmail"] = "hong@example.com";
|
||||||
|
/// dataObject["password"] = "secret123";
|
||||||
|
///
|
||||||
|
/// // 마스크 적용 결과 (개념적 예시):
|
||||||
|
/// // - userName은 name으로 변환됨
|
||||||
|
/// // - userEmail은 email로 변환됨
|
||||||
|
/// // - password는 제외됨
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public class DataObject : SortedDictionary<string, object>, IDataObject
|
public class DataObject : SortedDictionary<string, object>, IDataObject
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -27,7 +65,9 @@ namespace UVC.Data
|
|||||||
public string Name { get; internal set; } = string.Empty;
|
public string Name { get; internal set; } = string.Empty;
|
||||||
|
|
||||||
|
|
||||||
// 직접적인 변경이 있었던 키를 저장하는 리스트
|
/// <summary>
|
||||||
|
/// 직접적인 변경이 있었던 키를 저장하는 리스트입니다.
|
||||||
|
/// </summary>
|
||||||
protected List<string> changedProperies = new List<string>();
|
protected List<string> changedProperies = new List<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -132,7 +172,7 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 새 속성을 추가할 때 이벤트 연결을 처리합니다.
|
/// 새 속성을 추가하고 변경 사항을 추적합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="propertyName">추가할 속성의 이름</param>
|
/// <param name="propertyName">추가할 속성의 이름</param>
|
||||||
/// <param name="value">속성의 값</param>
|
/// <param name="value">속성의 값</param>
|
||||||
@@ -152,9 +192,11 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 인덱서를 통한 속성 설정을 처리합니다.
|
/// 인덱서를 통해 속성값에 접근하고 설정합니다.
|
||||||
/// 속성 변경 시 변경 사항을 자동으로 추적합니다.
|
/// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="key">접근할 속성의 키</param>
|
||||||
|
/// <returns>속성값</returns>
|
||||||
public new object this[string key]
|
public new object this[string key]
|
||||||
{
|
{
|
||||||
get => base[key];
|
get => base[key];
|
||||||
@@ -184,7 +226,12 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 정수 값 또는 기본값</returns>
|
||||||
public int GetInt(string propertyName, int defaultValue = 0)
|
public int GetInt(string propertyName, int defaultValue = 0)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -196,6 +243,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 null인 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 문자열 값 또는 기본값</returns>
|
||||||
public string? GetString(string propertyName, string? defaultValue = null)
|
public string? GetString(string propertyName, string? defaultValue = null)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -205,6 +258,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 불리언 값 또는 기본값</returns>
|
||||||
public bool GetBool(string propertyName, bool defaultValue = false)
|
public bool GetBool(string propertyName, bool defaultValue = false)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -216,6 +275,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 부동 소수점 값 또는 기본값</returns>
|
||||||
public float GetFloat(string propertyName, float defaultValue = 0f)
|
public float GetFloat(string propertyName, float defaultValue = 0f)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -227,6 +292,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 더블 값 또는 기본값</returns>
|
||||||
public double GetDouble(string propertyName, double defaultValue = 0.0)
|
public double GetDouble(string propertyName, double defaultValue = 0.0)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -238,6 +309,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 DateTime 값 또는 기본값</returns>
|
||||||
public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
|
public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -249,6 +326,13 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">변환할 열거형 타입</typeparam>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>변환된 열거형 값 또는 기본값</returns>
|
||||||
public T GetEnum<T>(string propertyName, T defaultValue = default) where T : Enum
|
public T GetEnum<T>(string propertyName, T defaultValue = default) where T : Enum
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -260,6 +344,27 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 IDataObject로 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 IDataObject가 아닌 경우 반환할 기본값</param>
|
||||||
|
/// <returns>IDataObject 인터페이스를 구현한 객체 또는 기본값</returns>
|
||||||
|
public IDataObject? Get(string propertyName, IDataObject? defaultValue = null)
|
||||||
|
{
|
||||||
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
|
{
|
||||||
|
if (value is IDataObject dataObject) return dataObject;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 DataArray로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>DataArray 객체 또는 기본값</returns>
|
||||||
public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null)
|
public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -272,6 +377,12 @@ namespace UVC.Data
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 속성의 값을 DataObject로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="propertyName">속성 이름</param>
|
||||||
|
/// <param name="defaultValue">속성이 없거나 변환할 수 없는 경우 반환할 기본값</param>
|
||||||
|
/// <returns>DataObject 객체 또는 기본값</returns>
|
||||||
public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null)
|
public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null)
|
||||||
{
|
{
|
||||||
if (TryGetValue(propertyName, out object value) && value != null)
|
if (TryGetValue(propertyName, out object value) && value != null)
|
||||||
@@ -303,7 +414,7 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 모든 속성을 제거할 때 기존 속성 목록과 변경된 키 목록도 초기화합니다.
|
/// 모든 속성을 제거하고 추적 리스트를 초기화합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void RemoveAll()
|
public void RemoveAll()
|
||||||
{
|
{
|
||||||
@@ -362,6 +473,17 @@ namespace UVC.Data
|
|||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 업데이트 된 속성의 수.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||||
|
public int UpdatedCount { get => changedProperies.Count; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DataObject의 내용을 문자열로 반환합니다.
|
||||||
|
/// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>DataObject의 내용을 나타내는 문자열</returns>
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
{
|
{
|
||||||
return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}"));
|
||||||
|
|||||||
@@ -1,50 +1,398 @@
|
|||||||
using System;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 객체들을 키-값 쌍으로 관리하는 중앙 저장소입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 싱글톤 패턴으로 구현되어 있어 애플리케이션 전체에서
|
||||||
|
/// 하나의 인스턴스만 존재합니다. IDataObject 인터페이스를 구현하는
|
||||||
|
/// 모든 데이터 객체를 저장하고 검색할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
public class DataRepository
|
public class DataRepository
|
||||||
{
|
{
|
||||||
#region Singleton
|
#region Singleton
|
||||||
|
/// <summary>
|
||||||
|
/// DataRepository 싱글톤 인스턴스를 생성하는 지연 초기화 객체입니다.
|
||||||
|
/// </summary>
|
||||||
protected static readonly Lazy<DataRepository> instance = new Lazy<DataRepository>(() => new DataRepository());
|
protected static readonly Lazy<DataRepository> instance = new Lazy<DataRepository>(() => new DataRepository());
|
||||||
|
/// <summary>
|
||||||
|
/// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다.
|
||||||
|
/// </summary>
|
||||||
protected DataRepository() { }
|
protected DataRepository() { }
|
||||||
|
/// <summary>
|
||||||
|
/// DataRepository의 단일 인스턴스에 대한 접근자입니다.
|
||||||
|
/// </summary>
|
||||||
public static DataRepository Instance { get { return instance.Value; } }
|
public static DataRepository Instance { get { return instance.Value; } }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다.
|
||||||
|
/// </summary>
|
||||||
private Dictionary<string, IDataObject> dataObjects = new Dictionary<string, IDataObject>();
|
private Dictionary<string, IDataObject> dataObjects = new Dictionary<string, IDataObject>();
|
||||||
|
|
||||||
public IDataObject AddData(string key, IDataObject dataObject)
|
/// <summary>
|
||||||
|
/// 스레드 동기화를 위한 잠금 객체입니다.
|
||||||
|
/// </summary>
|
||||||
|
private readonly object syncLock = new object();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다.
|
||||||
|
/// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다.
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, Action<IDataObject>> dataUpdateHandlers = new Dictionary<string, Action<IDataObject>>();
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">데이터 객체를 식별하는 고유 키</param>
|
||||||
|
/// <param name="dataObject">저장할 데이터 객체</param>
|
||||||
|
/// <param name="updatedDataOnly">true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환</param>
|
||||||
|
/// <returns>새로 추가된 객체 또는 업데이트된 기존 객체</returns>
|
||||||
|
public IDataObject AddData(string key, IDataObject dataObject, bool updatedDataOnly = true)
|
||||||
{
|
{
|
||||||
if (!dataObjects.ContainsKey(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
|
||||||
|
if (dataObject == null)
|
||||||
|
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
{
|
{
|
||||||
dataObject.InitData();
|
if (!dataObjects.ContainsKey(key))
|
||||||
dataObjects.Add(key, dataObject);
|
{
|
||||||
return dataObject;
|
dataObject.InitData();
|
||||||
}
|
dataObjects.Add(key, dataObject);
|
||||||
else
|
return dataObject;
|
||||||
{
|
}
|
||||||
IDataObject obj = dataObjects[key];
|
else
|
||||||
obj.UpdateDifferent(dataObject);
|
{
|
||||||
return obj.GetUpdatedObject();
|
IDataObject obj = dataObjects[key];
|
||||||
|
obj.UpdateDifferent(dataObject);
|
||||||
|
var newDataObject = dataObject;
|
||||||
|
if (updatedDataOnly) newDataObject = obj.GetUpdatedObject();
|
||||||
|
NotifyDataUpdate(key, newDataObject);
|
||||||
|
return newDataObject;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">제거할 데이터 객체의 키</param>
|
||||||
public void RemoveData(string key)
|
public void RemoveData(string key)
|
||||||
{
|
{
|
||||||
if (dataObjects.ContainsKey(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
{
|
{
|
||||||
dataObjects.Remove(key);
|
if (dataObjects.ContainsKey(key))
|
||||||
|
{
|
||||||
|
dataObjects.Remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">검색할 데이터 객체의 키</param>
|
||||||
|
/// <returns>키가 존재하면 해당 데이터 객체, 존재하지 않으면 null</returns>
|
||||||
public IDataObject GetData(string key)
|
public IDataObject GetData(string key)
|
||||||
{
|
{
|
||||||
if (dataObjects.ContainsKey(key))
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
{
|
{
|
||||||
return dataObjects[key];
|
if (dataObjects.ContainsKey(key))
|
||||||
|
{
|
||||||
|
return dataObjects[key];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">데이터 업데이트를 감시할 키</param>
|
||||||
|
/// <param name="handler">데이터가 업데이트되었을 때 호출될 콜백 함수</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다.
|
||||||
|
/// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
public void AddDataUpdateHandler(string key, Action<IDataObject> handler)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다.");
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
if (!dataUpdateHandlers.ContainsKey(key))
|
||||||
|
{
|
||||||
|
dataUpdateHandlers[key] = handler;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dataUpdateHandlers[key] += handler; // 기존 핸들러에 추가
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키에서 데이터 업데이트 핸들러를 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">핸들러를 제거할 데이터 키</param>
|
||||||
|
/// <param name="handler">제거할 핸들러 함수</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 지정된 키에 연결된 핸들러가 없거나, 제거하려는 핸들러가 등록되지 않은 경우 아무 작업도 수행하지 않습니다.
|
||||||
|
/// 키에 연결된 마지막 핸들러가 제거되면 해당 키는 dataUpdateHandlers 딕셔너리에서 삭제됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
public void RemoveDataUpdateHandler(string key, Action<IDataObject> handler)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
if (handler == null)
|
||||||
|
throw new ArgumentNullException(nameof(handler), "핸들러는 null일 수 없습니다.");
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
if (dataUpdateHandlers.ContainsKey(key))
|
||||||
|
{
|
||||||
|
dataUpdateHandlers[key] -= handler; // 기존 핸들러에서 제거
|
||||||
|
if (dataUpdateHandlers[key] == null)
|
||||||
|
{
|
||||||
|
dataUpdateHandlers.Remove(key); // 핸들러가 없으면 제거
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모든 데이터 업데이트 핸들러를 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 메서드는 모든 키에 대한 모든 핸들러를 한 번에 제거합니다.
|
||||||
|
/// 예를 들어, 씬이 변경되거나 애플리케이션이 종료될 때
|
||||||
|
/// 모든 핸들러를 정리하는 데 유용합니다.
|
||||||
|
/// </remarks>
|
||||||
|
public void ClearDataUpdateHandlers()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
dataUpdateHandlers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키의 데이터가 업데이트되었음을 알리고 등록된 핸들러들을 호출합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">업데이트된 데이터의 키</param>
|
||||||
|
/// <param name="dataObject">업데이트된 데이터 객체</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 메서드는 주로 내부적으로 AddData 메서드에서 호출되어
|
||||||
|
/// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다.
|
||||||
|
/// </remarks>
|
||||||
|
private void NotifyDataUpdate(string key, IDataObject dataObject)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(key))
|
||||||
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
||||||
|
if (dataObject == null)
|
||||||
|
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
if (dataUpdateHandlers.ContainsKey(key))
|
||||||
|
{
|
||||||
|
dataUpdateHandlers[key]?.Invoke(dataObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장소에 저장된 모든 데이터 객체의 키 목록을 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>저장소에 저장된 모든 데이터 객체의 키 목록</returns>
|
||||||
|
public IEnumerable<string> GetAllKeys()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return new List<string>(dataObjects.Keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장된 모든 데이터 객체를 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>저장소의 모든 데이터 객체</returns>
|
||||||
|
public IEnumerable<IDataObject> GetAllData()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return new List<IDataObject>(dataObjects.Values);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키에 대한 데이터 객체가 저장소에 존재하는지 확인합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">확인할 키</param>
|
||||||
|
/// <returns>키가 존재하면 true, 그렇지 않으면 false</returns>
|
||||||
|
public bool ContainsKey(string key)
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
return dataObjects.ContainsKey(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장소의 모든 데이터를 삭제합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
dataObjects.Clear();
|
||||||
|
dataUpdateHandlers.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 키의 데이터 객체를 특정 타입으로 변환하여 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">변환할 타입 (IDataObject를 구현해야 함)</typeparam>
|
||||||
|
/// <param name="key">검색할 키</param>
|
||||||
|
/// <returns>변환된 객체 또는 null</returns>
|
||||||
|
public T GetDataAs<T>(string key) where T : class, IDataObject
|
||||||
|
{
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
if (dataObjects.ContainsKey(key) && dataObjects[key] is T typedObject)
|
||||||
|
{
|
||||||
|
return typedObject;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 조건을 만족하는 첫 번째 데이터 객체를 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="predicate">데이터 객체를 필터링할 조건</param>
|
||||||
|
/// <returns>조건을 만족하는 첫 번째 데이터 객체와 해당 키, 없으면 null</returns>
|
||||||
|
public (string Key, IDataObject Data) FindFirst(Func<IDataObject, bool> predicate)
|
||||||
|
{
|
||||||
|
if (predicate == null)
|
||||||
|
throw new ArgumentNullException(nameof(predicate));
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
foreach (var pair in dataObjects)
|
||||||
|
{
|
||||||
|
if (predicate(pair.Value))
|
||||||
|
{
|
||||||
|
return (pair.Key, pair.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 조건을 만족하는 모든 데이터 객체를 찾습니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="predicate">데이터 객체를 필터링할 조건</param>
|
||||||
|
/// <returns>조건을 만족하는 모든 데이터 객체와 해당 키</returns>
|
||||||
|
public IEnumerable<(string Key, IDataObject Data)> FindAll(Func<IDataObject, bool> predicate)
|
||||||
|
{
|
||||||
|
if (predicate == null)
|
||||||
|
throw new ArgumentNullException(nameof(predicate));
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
List<(string, IDataObject)> result = new List<(string, IDataObject)>();
|
||||||
|
foreach (var pair in dataObjects)
|
||||||
|
{
|
||||||
|
if (predicate(pair.Value))
|
||||||
|
{
|
||||||
|
result.Add((pair.Key, pair.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 저장소의 전체 내용을 JSON 문자열로 직렬화합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>저장소 데이터의 JSON 표현</returns>
|
||||||
|
public string ToJson()
|
||||||
|
{
|
||||||
|
JObject result = new JObject();
|
||||||
|
|
||||||
|
lock (syncLock)
|
||||||
|
{
|
||||||
|
foreach (var pair in dataObjects)
|
||||||
|
{
|
||||||
|
if (pair.Value is DataObject dataObject)
|
||||||
|
{
|
||||||
|
result[pair.Key] = JObject.FromObject(dataObject);
|
||||||
|
}
|
||||||
|
else if (pair.Value is DataArray dataArray)
|
||||||
|
{
|
||||||
|
result[pair.Key] = JArray.FromObject(dataArray);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// JSON 문자열에서 데이터를 불러와 저장소에 추가합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="json">저장소 데이터의 JSON 표현</param>
|
||||||
|
public void LoadFromJson(string json)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(json))
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject data = JObject.Parse(json);
|
||||||
|
|
||||||
|
foreach (var property in data.Properties())
|
||||||
|
{
|
||||||
|
string key = property.Name;
|
||||||
|
JToken value = property.Value;
|
||||||
|
|
||||||
|
if (value is JObject jObject)
|
||||||
|
{
|
||||||
|
DataObject dataObject = new DataObject(jObject);
|
||||||
|
AddData(key, dataObject, false);
|
||||||
|
}
|
||||||
|
else if (value is JArray jArray)
|
||||||
|
{
|
||||||
|
DataArray dataArray = new DataArray(jArray);
|
||||||
|
AddData(key, dataArray, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"JSON 파싱 오류: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,114 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using UnityEngine;
|
||||||
using UVC.Network;
|
using UVC.Network;
|
||||||
|
using UVC.Tests;
|
||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 파이프라인을 관리하는 클래스입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 HTTP 요청의 실행 및 반복 요청을 관리합니다.
|
||||||
|
/// 등록된 요청(HttpPipeLineInfo)을 키 값으로 관리하며,
|
||||||
|
/// 주기적 데이터 수집을 위한 반복 요청 기능을 제공합니다.
|
||||||
|
///
|
||||||
|
/// 주요 기능:
|
||||||
|
/// - 단일 및 반복 HTTP 요청 관리
|
||||||
|
/// - 요청 결과의 JSON 데이터를 IDataObject로 변환
|
||||||
|
/// - 안전한 요청 취소 및 자원 정리
|
||||||
|
/// - 테스트를 위한 목업 기능 지원
|
||||||
|
///
|
||||||
|
/// 모든 반복 실행은 CancellationTokenSource를 통해 취소할 수 있으며,
|
||||||
|
/// 취소 후 현재 진행 중인 모든 요청이 안전하게 완료되는 것을 보장합니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // HttpPipeLine 인스턴스 생성
|
||||||
|
/// var httpPipeline = new HttpPipeLine();
|
||||||
|
///
|
||||||
|
/// // 데이터 매퍼 설정 (응답 데이터 변환용)
|
||||||
|
/// var dataMask = new DataMask();
|
||||||
|
/// dataMask["name"] = "이름";
|
||||||
|
/// dataMask["value"] = 0;
|
||||||
|
/// var dataMapper = new DataMapper(dataMask);
|
||||||
|
///
|
||||||
|
/// // 단일 요청 설정 및 등록
|
||||||
|
/// var singleRequest = new HttpPipeLineInfo("https://api.example.com/data")
|
||||||
|
/// .setDataMapper(dataMapper)
|
||||||
|
/// .setHandler(data => {
|
||||||
|
/// // 데이터 처리 로직
|
||||||
|
/// Debug.Log($"데이터 수신: {data?.ToString() ?? "null"}");
|
||||||
|
/// });
|
||||||
|
/// httpPipeline.Add("dataRequest", singleRequest);
|
||||||
|
///
|
||||||
|
/// // 반복 요청 설정 및 등록
|
||||||
|
/// var repeatingRequest = new HttpPipeLineInfo("https://api.example.com/status")
|
||||||
|
/// .setDataMapper(dataMapper)
|
||||||
|
/// .setHandler(data => {
|
||||||
|
/// // 상태 데이터 처리
|
||||||
|
/// Debug.Log($"상태 업데이트: {data?.ToString() ?? "null"}");
|
||||||
|
/// })
|
||||||
|
/// .setRepeat(true, 0, 5000); // 5초마다 무한 반복
|
||||||
|
/// httpPipeline.Add("statusMonitor", repeatingRequest);
|
||||||
|
///
|
||||||
|
/// // 요청 실행
|
||||||
|
/// await httpPipeline.Excute("dataRequest"); // 단일 실행
|
||||||
|
/// await httpPipeline.Excute("statusMonitor"); // 반복 실행 시작
|
||||||
|
///
|
||||||
|
/// // 나중에 반복 요청 중지
|
||||||
|
/// httpPipeline.StopRepeat("statusMonitor");
|
||||||
|
///
|
||||||
|
/// // 더 이상 필요없는 요청 제거
|
||||||
|
/// httpPipeline.Remove("dataRequest");
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public class HttpPipeLine
|
public class HttpPipeLine
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 테스트를 위한 목업 모드 활성화 여부를 설정하거나 가져옵니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// true로 설정하면 실제 HTTP 요청 대신 MockHttpRequester를 사용합니다.
|
||||||
|
/// 테스트 환경에서 외부 의존성 없이 HTTP 요청을 시뮬레이션할 때 유용합니다.
|
||||||
|
/// </remarks>
|
||||||
|
public bool UseMockup { get; internal set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 등록된 HTTP 파이프라인 정보를 저장하는 사전
|
||||||
|
/// </summary>
|
||||||
private Dictionary<string, HttpPipeLineInfo> infoList = new Dictionary<string, HttpPipeLineInfo>();
|
private Dictionary<string, HttpPipeLineInfo> infoList = new Dictionary<string, HttpPipeLineInfo>();
|
||||||
|
/// <summary>
|
||||||
|
/// 실행 중인 반복 작업의 취소 토큰을 관리하는 사전
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, CancellationTokenSource> repeatTokenSources = new Dictionary<string, CancellationTokenSource>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 진행 중인 요청의 상태를 추적하는 사전입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 키는 요청 식별자이고, 값은 현재 요청이 실행 중인지 여부를 나타냅니다.
|
||||||
|
/// 이 상태 추적은 StopRepeat 메서드가 요청의 완전한 종료를 보장하기 위해 사용됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
private Dictionary<string, bool> requestInProgress = new Dictionary<string, bool>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 새로운 HTTP 요청 정보를 추가하거나 기존 정보를 업데이트합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">요청을 식별하는 키</param>
|
||||||
|
/// <param name="info">HTTP 요청 정보</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 동일한 키가 이미 존재하는 경우 새로운 정보로 대체됩니다.
|
||||||
|
/// </remarks>
|
||||||
public void Add(string key, HttpPipeLineInfo info)
|
public void Add(string key, HttpPipeLineInfo info)
|
||||||
{
|
{
|
||||||
if (!infoList.ContainsKey(key))
|
if (!infoList.ContainsKey(key))
|
||||||
@@ -21,42 +121,448 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Remove(string key)
|
/// <summary>
|
||||||
|
/// 지정한 키의 HTTP 요청 정보를 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">제거할 요청의 키</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 실행 중인 반복 작업이 있다면 함께 중지됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
public async UniTask RemoveAsync(string key)
|
||||||
{
|
{
|
||||||
if (infoList.ContainsKey(key))
|
if (infoList.ContainsKey(key))
|
||||||
{
|
{
|
||||||
|
await StopRepeat(key);
|
||||||
infoList.Remove(key);
|
infoList.Remove(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Excute(string key)
|
/// <summary>
|
||||||
|
/// 지정한 키의 HTTP 요청을 실행합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">실행할 요청의 키</param>
|
||||||
|
/// <returns>비동기 작업</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 요청 정보의 repeat 속성에 따라 단일 실행 또는 반복 실행을 시작합니다.
|
||||||
|
/// 이미 실행 중인 반복 작업이 있다면 먼저 중지하고 완료를 대기한 후 새로운 요청을 시작합니다.
|
||||||
|
/// 단일 실행의 경우 완료될 때까지 대기하지만, 반복 실행은 백그라운드에서 실행됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||||
|
public async UniTask Excute(string key)
|
||||||
{
|
{
|
||||||
if (infoList.ContainsKey(key))
|
if (infoList.ContainsKey(key))
|
||||||
{
|
{
|
||||||
HttpPipeLineInfo info = infoList[key];
|
HttpPipeLineInfo info = infoList[key];
|
||||||
|
|
||||||
string result = await HttpRequester.Request<string>(info.url, info.method, info.body, info.headers);
|
|
||||||
|
|
||||||
IDataObject? dataObject = null;
|
// 반복 설정에 관계없이 이전에 실행 중인 반복 작업이 있다면 중지
|
||||||
if (!string.IsNullOrEmpty(result))
|
await StopRepeat(key);
|
||||||
|
|
||||||
|
if (!info.Repeat)
|
||||||
{
|
{
|
||||||
result = result.Trim();
|
// 단일 실행 로직 호출
|
||||||
if (result.StartsWith("{"))
|
await ExecuteSingle(key, info);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 반복 설정이 있는 경우에만 StartRepeat 호출
|
||||||
|
StartRepeat(key).Forget();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 단일 HTTP 요청을 실행하고 결과를 처리합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">요청을 식별하는 키</param>
|
||||||
|
/// <param name="info">HTTP 요청 정보</param>
|
||||||
|
/// <param name="cancellationToken">요청 취소를 위한 취소 토큰</param>
|
||||||
|
/// <returns>비동기 작업</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 메서드는 HTTP 요청을 보내고, 응답 데이터를 파싱하여 IDataObject로 변환합니다.
|
||||||
|
/// JSON 객체 또는 배열 형식의 응답을 처리할 수 있으며, 취소 토큰을 통해 언제든지 작업을 취소할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="OperationCanceledException">작업이 취소된 경우 발생</exception>
|
||||||
|
/// <exception cref="JsonException">JSON 응답 파싱 중 오류가 발생한 경우</exception>
|
||||||
|
/// <exception cref="Exception">HTTP 요청 중 다른 예외가 발생한 경우</exception>
|
||||||
|
private async UniTask ExecuteSingle(string key, HttpPipeLineInfo info, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
|
||||||
|
int retryCount = 0;
|
||||||
|
Exception lastException = null;
|
||||||
|
|
||||||
|
while (retryCount <= info.MaxRetryCount)
|
||||||
|
{
|
||||||
|
// 취소 요청 확인
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (requestInProgress)
|
||||||
|
{
|
||||||
|
requestInProgress[key] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string result = string.Empty;
|
||||||
|
if (!UseMockup)
|
||||||
{
|
{
|
||||||
JObject source = JObject.Parse(result);
|
result = await HttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
|
||||||
}
|
}
|
||||||
else if(result.StartsWith("["))
|
else
|
||||||
{
|
{
|
||||||
JArray source = JArray.Parse(result);
|
result = await MockHttpRequester.Request<string>(info.Url, info.Method, info.Body, info.Headers);
|
||||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
}
|
||||||
|
|
||||||
|
// 응답 처리 전에 다시 취소 요청 확인
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
IDataObject? dataObject = null;
|
||||||
|
if (!string.IsNullOrEmpty(result))
|
||||||
|
{
|
||||||
|
result = result.Trim();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (result.StartsWith("{"))
|
||||||
|
{
|
||||||
|
if (info.DataMapper != null)
|
||||||
|
{
|
||||||
|
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||||
|
{
|
||||||
|
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||||
|
{
|
||||||
|
dataObject = info.DataMapper.MapObjectStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JObject source = JObject.Parse(result);
|
||||||
|
dataObject = info.DataMapper.Mapping(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (result.StartsWith("["))
|
||||||
|
{
|
||||||
|
if (info.DataMapper != null)
|
||||||
|
{
|
||||||
|
if (info.DataMapper.SupportsStreamParsing && result.Length > info.DataMapper.SupportsStreamLength)
|
||||||
|
{
|
||||||
|
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(result)))
|
||||||
|
{
|
||||||
|
dataObject = info.DataMapper.MapArrayStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JArray source = JArray.Parse(result);
|
||||||
|
dataObject = info.DataMapper.Mapping(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
Debug.LogError($"JSON parsing error in ExecuteSingle for {key}: {ex.Message}\nResponse: {result}");
|
||||||
|
throw; // 상위에서 처리하도록 다시 throw
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 핸들러 호출 전에 다시 취소 요청 확인
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||||
|
}
|
||||||
|
if (dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject, info.UpdatedDataOnly);
|
||||||
|
|
||||||
|
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||||
|
if (info.UpdatedDataOnly)
|
||||||
|
{
|
||||||
|
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
info.Handler?.Invoke(dataObject);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 취소 예외는 그대로 전파
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
lastException = ex;
|
||||||
|
retryCount++;
|
||||||
|
|
||||||
|
if (retryCount <= info.MaxRetryCount)
|
||||||
|
{
|
||||||
|
// 재시도 전에 취소 요청 확인
|
||||||
|
if (cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
throw new OperationCanceledException("Operation cancelled", cancellationToken);
|
||||||
|
}
|
||||||
|
Debug.LogWarning($"Request failed for '{key}', retry {retryCount}/{info.MaxRetryCount} after {info.RetryDelay}ms: {ex.Message}");
|
||||||
|
await UniTask.Delay(info.RetryDelay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (requestInProgress)
|
||||||
|
{
|
||||||
|
requestInProgress[key] = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(dataObject != null) dataObject = DataRepository.Instance.AddData(key, dataObject);
|
|
||||||
info.handler?.Invoke(dataObject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 모든 재시도 후에도 실패
|
||||||
|
Debug.LogError($"Request failed for '{key}' after {info.MaxRetryCount} retries: {lastException?.Message}");
|
||||||
|
throw lastException;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 실행을 시작합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">반복 실행할 요청의 키</param>
|
||||||
|
/// <returns>비동기 작업</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 지정된 간격(repeatInterval)으로 HTTP 요청을 반복 실행합니다.
|
||||||
|
/// repeatCount가 0인 경우 무한 반복하며, 0보다 큰 경우 지정된 횟수만큼만 실행합니다.
|
||||||
|
/// 작업 실행 중 예외가 발생하면 로그를 기록하고 다음 실행을 시도합니다.
|
||||||
|
/// 취소 요청이 있거나 최대 실행 횟수에 도달하면 반복이 종료됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <exception cref="KeyNotFoundException">지정된 키가 등록되어 있지 않은 경우</exception>
|
||||||
|
private async UniTask StartRepeat(string key)
|
||||||
|
{
|
||||||
|
if (!infoList.ContainsKey(key))
|
||||||
|
{
|
||||||
|
throw new KeyNotFoundException($"No HTTP request found with key '{key}'.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpPipeLineInfo info = infoList[key];
|
||||||
|
if (!info.Repeat) return;
|
||||||
|
|
||||||
|
// 새 취소 토큰 생성
|
||||||
|
CancellationTokenSource cts = new CancellationTokenSource();
|
||||||
|
repeatTokenSources[key] = cts;
|
||||||
|
|
||||||
|
int executionCount = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 단일 실행 로직 호출
|
||||||
|
await ExecuteSingle(key, info, cts.Token);
|
||||||
|
|
||||||
|
// 지정된 횟수만큼 반복한 경우 중지
|
||||||
|
if (info.RepeatCount > 0)
|
||||||
|
{
|
||||||
|
executionCount++;
|
||||||
|
if (executionCount >= info.RepeatCount)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 토큰이 취소되지 않은 경우에만 지연
|
||||||
|
if (!cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
// 지정된 간격만큼 대기
|
||||||
|
await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 취소된 경우 루프 종료
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 다른 예외 처리
|
||||||
|
Debug.LogError($"Error in repeat execution for '{key}': {ex.Message}");
|
||||||
|
if (cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UniTask.Delay(info.RepeatInterval, cancellationToken: cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// 취소된 경우 무시하고 루프 종료
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (repeatTokenSources) // 스레드 안전성 확보
|
||||||
|
{
|
||||||
|
if (repeatTokenSources.TryGetValue(key, out var currentCts) && currentCts == cts)
|
||||||
|
{
|
||||||
|
repeatTokenSources.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cts.Dispose(); // 여기서 최종적으로 Dispose 호출
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 실행 중인 요청을 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">중지할 요청의 키</param>
|
||||||
|
/// <returns>요청 중지 작업을 나타내는 비동기 작업</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 해당 키로 실행 중인 반복 작업이 없는 경우 아무 작업도 수행하지 않습니다.
|
||||||
|
/// 요청이 중지되었더라도 현재 실행 중인 작업이 완전히 종료될 때까지 대기합니다.
|
||||||
|
/// 이를 통해 작업 중단 후 자원이 안전하게 정리되는 것을 보장합니다.
|
||||||
|
/// </remarks>
|
||||||
|
public async UniTask StopRepeat(string key)
|
||||||
|
{
|
||||||
|
CancellationTokenSource cts = null;
|
||||||
|
lock (repeatTokenSources) // 스레드 안전성 확보
|
||||||
|
{
|
||||||
|
if (repeatTokenSources.TryGetValue(key, out cts) && !cts.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
cts.Cancel();
|
||||||
|
repeatTokenSources.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 진행 중인 요청이 완료될 때까지 대기
|
||||||
|
if (cts != null)
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
lock (requestInProgress)
|
||||||
|
{
|
||||||
|
if (!requestInProgress.ContainsKey(key) || !requestInProgress[key])
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await UniTask.Delay(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 모든 반복 실행 중인 요청을 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 애플리케이션 종료 시 또는 모든 반복 작업을 일괄 중지해야 할 때 사용합니다.
|
||||||
|
/// 이 메서드는 비동기적으로 작동하지만 완료를 대기하지 않습니다.
|
||||||
|
/// 모든 작업이 완전히 종료될 때까지 기다려야 하는 경우, 각 키에 대해 개별적으로 StopRepeat를 호출하고 대기해야 합니다.
|
||||||
|
/// </remarks>
|
||||||
|
public async UniTask StopAllRepeats()
|
||||||
|
{
|
||||||
|
foreach (var key in new List<string>(repeatTokenSources.Keys))
|
||||||
|
{
|
||||||
|
await StopRepeat(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 활성화된 요청 목록과 상태 정보를 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>키와 요청 상태 정보를 포함하는 딕셔너리</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 반환되는 딕셔너리는 등록된 모든 HTTP 요청에 대한 상태 정보를 제공합니다.
|
||||||
|
/// 각 요청에 대해 활성 상태, 반복 설정, 반복 횟수, 반복 간격을 확인할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var httpPipeline = new HttpPipeLine();
|
||||||
|
/// // 파이프라인에 요청 추가 후...
|
||||||
|
///
|
||||||
|
/// // 모든 활성 요청 확인
|
||||||
|
/// var activeRequests = httpPipeline.GetActiveRequests();
|
||||||
|
/// foreach (var request in activeRequests)
|
||||||
|
/// {
|
||||||
|
/// Debug.Log($"요청 키: {request.Key}, 활성 상태: {request.Value.IsActive}, " +
|
||||||
|
/// $"반복 중: {request.Value.IsRepeating}, 반복 간격: {request.Value.RepeatInterval}ms");
|
||||||
|
/// }
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public Dictionary<string, HttpPipeLineRequestStatus> GetActiveRequests()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, HttpPipeLineRequestStatus>();
|
||||||
|
foreach (var key in infoList.Keys)
|
||||||
|
{
|
||||||
|
bool isRepeating = repeatTokenSources.ContainsKey(key);
|
||||||
|
result[key] = new HttpPipeLineRequestStatus
|
||||||
|
{
|
||||||
|
IsActive = isRepeating,
|
||||||
|
IsRepeating = isRepeating,
|
||||||
|
RepeatCount = isRepeating ? infoList[key].RepeatCount : 0,
|
||||||
|
RepeatInterval = isRepeating ? infoList[key].RepeatInterval : 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||||
|
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// 모든 반복 작업 중지
|
||||||
|
StopAllRepeats().Forget();
|
||||||
|
// 요청 상태 초기화
|
||||||
|
requestInProgress.Clear();
|
||||||
|
// 등록된 요청 정보 초기화
|
||||||
|
infoList.Clear();
|
||||||
|
// 취소 토큰 소스 정리
|
||||||
|
foreach (var cts in repeatTokenSources.Values)
|
||||||
|
{
|
||||||
|
cts.Dispose();
|
||||||
|
}
|
||||||
|
repeatTokenSources.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청의 현재 상태 정보를 나타내는 클래스입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 HTTP 파이프라인에 등록된 요청의 활성 상태, 반복 설정,
|
||||||
|
/// 반복 횟수, 반복 간격에 관한 정보를 제공합니다.
|
||||||
|
/// </remarks>
|
||||||
|
public class HttpPipeLineRequestStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 요청이 현재 활성 상태인지 여부를 나타냅니다.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 요청이 반복 실행 중인지 여부를 나타냅니다.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsRepeating { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 설정된 횟수를 나타냅니다. 0은 무한 반복을 의미합니다.
|
||||||
|
/// </summary>
|
||||||
|
public int RepeatCount { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 요청 간의 간격을 밀리초 단위로 나타냅니다.
|
||||||
|
/// </summary>
|
||||||
|
public int RepeatInterval { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,47 +5,185 @@ using System.Collections.Generic;
|
|||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 파이프라인 정보를 관리하는 클래스입니다.
|
||||||
|
/// 요청 URL, 메서드, 헤더, 본문과 같은 HTTP 요청 정보 및
|
||||||
|
/// 반복 실행, 재시도, 데이터 매핑 등 파이프라인 동작을 구성합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 파이프라인 빌더 패턴을 사용하여 HTTP 요청 설정을 구성할 수 있도록 합니다.
|
||||||
|
/// 각 설정 메서드는 체이닝을 통해 유연하게 요청을 정의할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET")
|
||||||
|
/// .setDataMapper(new DataMapper(dataMask))
|
||||||
|
/// .setHandler(data => Console.WriteLine(data))
|
||||||
|
/// .setRetry(5, 2000)
|
||||||
|
/// .setRepeat(true, 10, 5000);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public class HttpPipeLineInfo
|
public class HttpPipeLineInfo
|
||||||
{
|
{
|
||||||
public string url;
|
private string _url;
|
||||||
public string method;
|
private string _method;
|
||||||
public Dictionary<string, string>? headers = null;
|
private Dictionary<string, string>? _headers = null;
|
||||||
public string? body = null;
|
private string? _body = null;
|
||||||
public Action<IDataObject?>? handler = null;
|
private Action<IDataObject?>? _handler = null;
|
||||||
public bool repeat = false; // 반복 실행 여부
|
private bool _repeat = false; // 반복 실행 여부
|
||||||
public int repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
private int _repeatCount = 0; // 반복 횟수, 0은 무한 반복
|
||||||
public int repeatInterval = 1000; // 반복 간격 (ms)
|
private int _repeatInterval = 1000; // 반복 간격 (ms)
|
||||||
|
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||||
|
private int _maxRetryCount = 3;
|
||||||
|
private int _retryDelay = 1000; // 밀리초
|
||||||
|
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||||
|
|
||||||
public DataMapper? dataMapper = null; // 데이터 매퍼
|
/// <summary>
|
||||||
|
/// 요청을 보낼 URL 주소
|
||||||
|
/// </summary>
|
||||||
|
public string Url => _url;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 메서드 (GET, POST 등)
|
||||||
|
/// </summary>
|
||||||
|
public string Method => _method;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 헤더
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string>? Headers => _headers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 본문
|
||||||
|
/// </summary>
|
||||||
|
public string? Body => _body;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 요청 완료 후 호출될 핸들러
|
||||||
|
/// </summary>
|
||||||
|
public Action<IDataObject?>? Handler => _handler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 실행 여부
|
||||||
|
/// </summary>
|
||||||
|
public bool Repeat => _repeat;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 횟수, 0은 무한 반복
|
||||||
|
/// </summary>
|
||||||
|
public int RepeatCount => _repeatCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 반복 간격 (ms)
|
||||||
|
/// </summary>
|
||||||
|
public int RepeatInterval => _repeatInterval;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 매퍼 객체
|
||||||
|
/// </summary>
|
||||||
|
public DataMapper? DataMapper => _dataMapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 최대 재시도 횟수
|
||||||
|
/// </summary>
|
||||||
|
public int MaxRetryCount => _maxRetryCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 재시도 간격 (ms)
|
||||||
|
/// </summary>
|
||||||
|
public int RetryDelay => _retryDelay;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다)
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdatedDataOnly => _updatedDataOnly;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HttpPipeLineInfo 클래스의 새 인스턴스를 초기화합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">HTTP 요청을 보낼 URL</param>
|
||||||
|
/// <param name="method">HTTP 요청 메서드 (기본값: "post")</param>
|
||||||
|
/// <param name="headers">HTTP 요청 헤더 (선택사항)</param>
|
||||||
|
/// <param name="body">HTTP 요청 본문 (선택사항)</param>
|
||||||
public HttpPipeLineInfo(string url, string method = "post", Dictionary<string, string>? headers = null, string? body = null)
|
public HttpPipeLineInfo(string url, string method = "post", Dictionary<string, string>? headers = null, string? body = null)
|
||||||
{
|
{
|
||||||
this.url = url;
|
_url = url;
|
||||||
this.method = method;
|
_method = method;
|
||||||
this.headers = headers;
|
_headers = headers;
|
||||||
this.body = body;
|
_body = body;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청 응답을 처리할 데이터 매퍼를 설정합니다.
|
||||||
|
/// 데이터 매퍼는 HTTP 응답을 IDataObject로 변환하는 역할을 합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataMapper">사용할 데이터 매퍼 객체</param>
|
||||||
|
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
public HttpPipeLineInfo setDataMapper(DataMapper dataMapper)
|
public HttpPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||||
{
|
{
|
||||||
this.dataMapper = dataMapper;
|
_dataMapper = dataMapper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpPipeLineInfo setHandler(Action<IDataObject> handler)
|
/// <summary>
|
||||||
|
/// HTTP 요청이 완료된 후 호출될 핸들러를 설정합니다.
|
||||||
|
/// 변경 된 데이터는 IDataObject로 전달됩니다.
|
||||||
|
/// 변경 된 데이터가 없으면 호출 되지 않습니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">응답 데이터를 처리할 콜백 함수</param>
|
||||||
|
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
|
public HttpPipeLineInfo setHandler(Action<IDataObject?>? handler)
|
||||||
{
|
{
|
||||||
this.handler = handler;
|
_handler = handler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000)
|
/// <summary>
|
||||||
|
/// HTTP 요청 실패 시 재시도 정책을 설정합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxRetryCount">최대 재시도 횟수 (기본값: 3)</param>
|
||||||
|
/// <param name="retryDelay">재시도 간 대기 시간(밀리초) (기본값: 1000)</param>
|
||||||
|
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
|
public HttpPipeLineInfo setRetry(int maxRetryCount = 3, int retryDelay = 1000)
|
||||||
{
|
{
|
||||||
this.repeat = repeat;
|
_maxRetryCount = maxRetryCount;
|
||||||
this.repeatCount = count;
|
_retryDelay = retryDelay;
|
||||||
this.repeatInterval = interval;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HTTP 요청의 반복 실행 설정을 구성합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="repeat">반복 실행 여부</param>
|
||||||
|
/// <param name="count">반복 횟수 (0은 무한 반복) (기본값: 0)</param>
|
||||||
|
/// <param name="interval">반복 실행 간 대기 시간(밀리초) (기본값: 1000)</param>
|
||||||
|
/// <param name="updatedDataOnly">변경된 데이터만 처리할지 여부 (기본값: true)</param>
|
||||||
|
/// <returns>현재 HttpPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 반복 요청 시 updatedDataOnly가 true인 경우, 서버 응답에서 데이터가 변경된 경우에만 핸들러가 호출됩니다.
|
||||||
|
/// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시키는 데 도움이 됩니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // 5초마다 10번 반복 요청, 변경된 데이터만 처리
|
||||||
|
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/data", "GET")
|
||||||
|
/// .setHandler(data => ProcessData(data))
|
||||||
|
/// .setRepeat(true, 10, 5000, true);
|
||||||
|
///
|
||||||
|
/// // 3초마다 무한 반복, 모든 응답 데이터 처리
|
||||||
|
/// var pipelineInfo = new HttpPipeLineInfo("https://api.example.com/status", "GET")
|
||||||
|
/// .setHandler(data => UpdateStatus(data))
|
||||||
|
/// .setRepeat(true, 0, 3000, false);
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public HttpPipeLineInfo setRepeat(bool repeat, int count = 0, int interval = 1000, bool updatedDataOnly = true)
|
||||||
|
{
|
||||||
|
_repeat = repeat;
|
||||||
|
_repeatCount = count;
|
||||||
|
_repeatInterval = interval;
|
||||||
|
_updatedDataOnly = updatedDataOnly; // true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다.
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,31 @@
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IDataObject
|
public interface IDataObject
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 모든 프로퍼티를 변경된 것으로 표시합니다.
|
||||||
|
/// 전체 데이터가 갱신되었을 때 사용합니다.
|
||||||
|
/// </summary>
|
||||||
public void InitData();
|
public void InitData();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다.
|
||||||
|
/// 변경된 키는 자동으로 추적됩니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="other">비교할 DataObject</param>
|
||||||
public void UpdateDifferent(IDataObject other);
|
public void UpdateDifferent(IDataObject other);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 업데이트된 속성만 포함하는 새로운 DataObject를 반환합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>업데이트 된 항목만 가지고 있는 DataObject</returns>
|
||||||
public IDataObject GetUpdatedObject();
|
public IDataObject GetUpdatedObject();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// /// <summary>
|
||||||
|
/// 업데이트 된 속성의 수.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면 0을 반환합니다.</returns>
|
||||||
|
public int UpdatedCount { get; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,30 +3,76 @@
|
|||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using SampleProject.Config;
|
using SampleProject.Config;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
using UVC.network;
|
using UVC.network;
|
||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 통신을 통해 데이터를 수신하고 처리하는 파이프라인을 관리하는 클래스입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 MQTT 브로커에 연결하여 등록된 토픽으로 들어오는 메시지를 수신하고,
|
||||||
|
/// 해당 메시지를 지정된 데이터 매퍼를 통해 변환한 후 핸들러에게 전달합니다.
|
||||||
|
/// 여러 MQTT 토픽을 동시에 관리하고 각각에 대한 처리 방식을 개별적으로 설정할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
public class MQTTPipeLine
|
public class MQTTPipeLine
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 브로커의 도메인 주소
|
||||||
|
/// </summary>
|
||||||
|
private string domain;
|
||||||
|
|
||||||
private Dictionary<string, MQTTPipeLineInfo> infoList = new Dictionary<string, MQTTPipeLineInfo>();
|
/// <summary>
|
||||||
|
/// MQTT 브로커의 포트 번호
|
||||||
|
/// </summary>
|
||||||
|
public int port;
|
||||||
|
|
||||||
private MQTTService mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT);
|
/// <summary>
|
||||||
|
/// 토픽별 파이프라인 정보를 저장하는 딕셔너리
|
||||||
|
/// </summary>
|
||||||
|
private Dictionary<string, MQTTPipeLineInfo> infoList;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 통신을 처리하는 서비스 객체
|
||||||
|
/// </summary>
|
||||||
|
private MQTTService mqtt;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTTPipeLine 인스턴스를 생성합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="domain">MQTT 브로커의 도메인 주소, 기본값은 "localhost"입니다.</param>
|
||||||
|
/// <param name="port">MQTT 브로커의 포트 번호, 기본값은 1883입니다.</param>
|
||||||
|
public MQTTPipeLine(string domain = "localhost", int port = 1883)
|
||||||
|
{
|
||||||
|
this.domain = string.IsNullOrEmpty(domain) ? Constants.MQTT_DOMAIN : domain;
|
||||||
|
this.port = port;
|
||||||
|
mqtt = new MQTTService(Constants.MQTT_DOMAIN, Constants.MQTT_PORT);
|
||||||
|
infoList = new Dictionary<string, MQTTPipeLineInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 토픽에 대한 파이프라인 정보를 추가합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="info">추가할 MQTTPipeLineInfo 객체</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 동일한 토픽에 대한 정보가 이미 존재하는 경우 덮어씁니다.
|
||||||
|
/// </remarks>
|
||||||
public void Add(MQTTPipeLineInfo info)
|
public void Add(MQTTPipeLineInfo info)
|
||||||
{
|
{
|
||||||
if (!infoList.ContainsKey(info.topic))
|
if (!infoList.ContainsKey(info.Topic))
|
||||||
{
|
{
|
||||||
infoList.Add(info.topic, info);
|
infoList.Add(info.Topic, info);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
infoList[info.topic] = info;
|
infoList[info.Topic] = info;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 지정된 토픽에 대한 파이프라인 정보를 제거합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">제거할 토픽 이름</param>
|
||||||
public void Remove(string topic)
|
public void Remove(string topic)
|
||||||
{
|
{
|
||||||
if (infoList.ContainsKey(topic))
|
if (infoList.ContainsKey(topic))
|
||||||
@@ -35,6 +81,9 @@ namespace UVC.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 파이프라인을 실행하여 MQTT 브로커에 연결하고 등록된 모든 토픽을 구독합니다.
|
||||||
|
/// </summary>
|
||||||
public void Execute()
|
public void Execute()
|
||||||
{
|
{
|
||||||
foreach (var topic in infoList.Keys)
|
foreach (var topic in infoList.Keys)
|
||||||
@@ -44,6 +93,16 @@ namespace UVC.Data
|
|||||||
mqtt.Connect();
|
mqtt.Connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 토픽으로 메시지가 수신되었을 때 호출되는 콜백 메서드입니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">수신된 메시지의 토픽</param>
|
||||||
|
/// <param name="message">수신된 메시지 내용</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 메서드는 수신된 메시지의 형식(JSON 객체 또는 배열)에 따라 적절한 파싱을 수행하고,
|
||||||
|
/// 등록된 데이터 매퍼를 통해 메시지를 변환한 후, 해당 토픽에 등록된 핸들러에게 전달합니다.
|
||||||
|
/// 'UpdatedDataOnly' 설정에 따라 데이터가 변경된 경우에만 핸들러를 호출할 수도 있습니다.
|
||||||
|
/// </remarks>
|
||||||
private void OnTopicMessage(string topic, string message)
|
private void OnTopicMessage(string topic, string message)
|
||||||
{
|
{
|
||||||
if (infoList.ContainsKey(topic))
|
if (infoList.ContainsKey(topic))
|
||||||
@@ -56,19 +115,49 @@ namespace UVC.Data
|
|||||||
if (message.StartsWith("{"))
|
if (message.StartsWith("{"))
|
||||||
{
|
{
|
||||||
JObject source = JObject.Parse(message);
|
JObject source = JObject.Parse(message);
|
||||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source);
|
||||||
}
|
}
|
||||||
else if (message.StartsWith("["))
|
else if (message.StartsWith("["))
|
||||||
{
|
{
|
||||||
JArray source = JArray.Parse(message);
|
JArray source = JArray.Parse(message);
|
||||||
if (info.dataMapper != null) dataObject = info.dataMapper.Mapping(source);
|
if (info.DataMapper != null) dataObject = info.DataMapper.Mapping(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject);
|
if (dataObject != null) dataObject = DataRepository.Instance.AddData(topic, dataObject, info.UpdatedDataOnly);
|
||||||
|
// 갱신 된 데이터가 있는 경우 핸들러 호출
|
||||||
info.handler?.Invoke(dataObject);
|
if (info.UpdatedDataOnly)
|
||||||
|
{
|
||||||
|
if (dataObject != null && dataObject.UpdatedCount > 0) info.Handler?.Invoke(dataObject);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
info.Handler?.Invoke(dataObject);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 파이프라인을 중지하고 모든 토픽 구독을 해제한 후 MQTT 브로커와의 연결을 종료합니다.
|
||||||
|
/// </summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
foreach (var topic in infoList.Keys)
|
||||||
|
{
|
||||||
|
mqtt.RemoveTopicHandler(topic, OnTopicMessage);
|
||||||
|
}
|
||||||
|
mqtt.Disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||||
|
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
mqtt.Disconnect();
|
||||||
|
infoList.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,103 @@ using System;
|
|||||||
|
|
||||||
namespace UVC.Data
|
namespace UVC.Data
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 파이프라인 정보를 관리하는 클래스입니다.
|
||||||
|
/// MQTT 토픽 구독, 메시지 핸들링 및 데이터 매핑에 필요한 설정을 제공합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// 이 클래스는 빌더 패턴을 사용하여 MQTT 구독 설정을 유연하게 구성할 수 있게 합니다.
|
||||||
|
/// 각 설정 메서드는 체이닝을 통해 파이프라인을 정의할 수 있도록 합니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// var pipelineInfo = new MQTTPipeLineInfo("device/status")
|
||||||
|
/// .setDataMapper(new DataMapper(dataMask))
|
||||||
|
/// .setHandler(data => Console.WriteLine(data));
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public class MQTTPipeLineInfo
|
public class MQTTPipeLineInfo
|
||||||
{
|
{
|
||||||
public string topic; // MQTT 토픽
|
private string _topic; // MQTT 토픽
|
||||||
public Action<IDataObject?>? handler = null; // 메시지 핸들러
|
private Action<IDataObject?>? _handler = null; // 메시지 핸들러
|
||||||
public DataMapper? dataMapper = null; // 데이터 매퍼
|
private DataMapper? _dataMapper = null; // 데이터 매퍼
|
||||||
|
private bool _updatedDataOnly = false; // 업데이트된 데이터만 받을 여부
|
||||||
|
|
||||||
public MQTTPipeLineInfo(string topic)
|
/// <summary>
|
||||||
|
/// MQTT 토픽
|
||||||
|
/// </summary>
|
||||||
|
public string Topic => _topic;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 메시지 핸들러
|
||||||
|
/// </summary>
|
||||||
|
public Action<IDataObject?>? Handler => _handler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 데이터 매퍼
|
||||||
|
/// </summary>
|
||||||
|
public DataMapper? DataMapper => _dataMapper;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 업데이트된 데이터만 받을 여부 (true로 설정하면, 데이터가 변경된 경우에만 핸들러가 호출됩니다)
|
||||||
|
/// </summary>
|
||||||
|
public bool UpdatedDataOnly => _updatedDataOnly;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTTPipeLineInfo 클래스의 새 인스턴스를 초기화합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="topic">구독할 MQTT 토픽</param>
|
||||||
|
/// <param name="updatedDataOnly">변경된 데이터만 처리할지 여부 (기본값: true)</param>
|
||||||
|
/// <remarks>
|
||||||
|
/// updatedDataOnly가 true인 경우, 이전 데이터와 동일한 메시지는 핸들러에 전달되지 않습니다.
|
||||||
|
/// 이는 불필요한 데이터 처리를 방지하고 성능을 향상시킵니다.
|
||||||
|
/// </remarks>
|
||||||
|
public MQTTPipeLineInfo(string topic, bool updatedDataOnly = true)
|
||||||
{
|
{
|
||||||
this.topic = topic;
|
_topic = topic;
|
||||||
|
_updatedDataOnly = updatedDataOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 메시지를 수신했을 때 호출될 핸들러를 설정합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="handler">메시지 데이터를 처리할 콜백 함수</param>
|
||||||
|
/// <returns>현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 핸들러는 메시지가 수신되고 DataMapper에 의해 변환된 후 호출됩니다.
|
||||||
|
/// UpdatedDataOnly 속성이 true인 경우, 데이터가 변경된 경우에만 호출됩니다.
|
||||||
|
/// </remarks>
|
||||||
public MQTTPipeLineInfo setHandler(Action<IDataObject?> handler)
|
public MQTTPipeLineInfo setHandler(Action<IDataObject?> handler)
|
||||||
{
|
{
|
||||||
this.handler = handler;
|
_handler = handler;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MQTT 메시지 데이터를 처리할 데이터 매퍼를 설정합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dataMapper">사용할 데이터 매퍼 객체</param>
|
||||||
|
/// <returns>현재 MQTTPipeLineInfo 인스턴스 (메서드 체이닝용)</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// 데이터 매퍼는 수신된 MQTT 메시지(JSON 형식)를 IDataObject로 변환하는 역할을 합니다.
|
||||||
|
/// DataMask를 사용하여 특정 필드에 대한 타입 변환 및 필드 이름 매핑을 처리할 수 있습니다.
|
||||||
|
/// </remarks>
|
||||||
|
/// <example>
|
||||||
|
/// <code>
|
||||||
|
/// // DataMask를 사용한 데이터 매퍼 설정
|
||||||
|
/// var dataMask = new DataMask();
|
||||||
|
/// dataMask["temperature"] = 0.0; // 실수 타입 지정
|
||||||
|
/// dataMask["humidity"] = 0; // 정수 타입 지정
|
||||||
|
/// dataMask["timestamp"] = ""; // 문자열 타입 지정
|
||||||
|
///
|
||||||
|
/// var pipelineInfo = new MQTTPipeLineInfo("sensor/data")
|
||||||
|
/// .setDataMapper(new DataMapper(dataMask))
|
||||||
|
/// .setHandler(data => ProcessSensorData(data));
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
public MQTTPipeLineInfo setDataMapper(DataMapper dataMapper)
|
public MQTTPipeLineInfo setDataMapper(DataMapper dataMapper)
|
||||||
{
|
{
|
||||||
this.dataMapper = dataMapper;
|
_dataMapper = dataMapper;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -363,5 +363,17 @@ namespace UVC.network
|
|||||||
.WithPayload(message)
|
.WithPayload(message)
|
||||||
.BeginPublish();
|
.BeginPublish();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 현재 인스턴스에서 사용되는 모든 리소스를 해제하고 진행 중인 모든 작업을 중지합니다.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>이 메서드는 모든 반복 작업을 중단하고, 내부 상태를 지우고, 관련 리소스를 삭제합니다.
|
||||||
|
/// <see cref="Dispose"/>를 호출한 후에는 해당 인스턴스를 더 이상 사용할 수 없습니다.</remarks>
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disconnect();
|
||||||
|
topicHandler.Clear();
|
||||||
|
client = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
154
Assets/Scripts/UVC/Tests/MockHttpRequester.cs
Normal file
154
Assets/Scripts/UVC/Tests/MockHttpRequester.cs
Normal file
File diff suppressed because one or more lines are too long
2
Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta
Normal file
2
Assets/Scripts/UVC/Tests/MockHttpRequester.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0826ba2b5da8fad48aff9e3542115630
|
||||||
Reference in New Issue
Block a user