429 lines
17 KiB
C#
429 lines
17 KiB
C#
#nullable enable
|
|
|
|
using Cysharp.Threading.Tasks;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using UVC.Data.Core;
|
|
using UVC.Data.Http;
|
|
using UVC.Data.Mqtt;
|
|
using UVC.Log;
|
|
|
|
namespace UVC.Data
|
|
{
|
|
/// <summary>
|
|
/// 데이터 객체들을 키-값 쌍으로 관리하는 중앙 저장소입니다.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// 이 클래스는 싱글톤 패턴으로 구현되어 있어 애플리케이션 전체에서
|
|
/// 하나의 인스턴스만 존재합니다. IDataObject 인터페이스를 구현하는
|
|
/// 모든 데이터 객체를 저장하고 검색할 수 있습니다.
|
|
/// </remarks>
|
|
public class DataRepository
|
|
{
|
|
#region Singleton
|
|
/// <summary>
|
|
/// DataRepository 싱글톤 인스턴스를 생성하는 지연 초기화 객체입니다.
|
|
/// </summary>
|
|
protected static readonly Lazy<DataRepository> instance = new Lazy<DataRepository>(() => new DataRepository());
|
|
/// <summary>
|
|
/// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다.
|
|
/// </summary>
|
|
protected DataRepository()
|
|
{
|
|
// Best MQTT 초기화 작업을 Main 스레드에서 호출 해야 한다.
|
|
Best.HTTP.Shared.HTTPManager.Setup();
|
|
}
|
|
/// <summary>
|
|
/// DataRepository의 단일 인스턴스에 대한 접근자입니다.
|
|
/// </summary>
|
|
public static DataRepository Instance { get { return instance.Value; } }
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다.
|
|
/// </summary>
|
|
private Dictionary<string, IDataObject> dataObjects = new Dictionary<string, IDataObject>();
|
|
|
|
/// <summary>
|
|
/// 스레드 동기화를 위한 잠금 객체입니다.
|
|
/// </summary>
|
|
private readonly object syncLock = new object();
|
|
|
|
/// <summary>
|
|
/// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다.
|
|
/// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다.
|
|
/// </summary>
|
|
private Dictionary<string, Action<IDataObject>> dataUpdateHandlers = new Dictionary<string, Action<IDataObject>>();
|
|
|
|
private HttpDataFetcher httpFetcher = new HttpDataFetcher();
|
|
public HttpDataFetcher HttpFetcher => httpFetcher;
|
|
|
|
private MqttDataReceiver mqttReceiver = new MqttDataReceiver();
|
|
public MqttDataReceiver MqttReceiver => mqttReceiver;
|
|
|
|
|
|
/// <summary>
|
|
/// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다.
|
|
/// </summary>
|
|
/// <param name="key">데이터 객체를 식별하는 고유 키</param>
|
|
/// <param name="dataObject">저장할 데이터 객체</param>
|
|
/// <param name="updatedDataOnly">true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환</param>
|
|
/// <returns>새로 추가된 객체 또는 업데이트된 기존 객체</returns>
|
|
internal IDataObject AddOrUpdateData(string key, IDataObject dataObject, bool updatedDataOnly = true)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
|
|
|
if (dataObject == null)
|
|
throw new ArgumentNullException(nameof(dataObject), "데이터 객체는 null일 수 없습니다.");
|
|
|
|
lock (syncLock)
|
|
{
|
|
if (!dataObjects.ContainsKey(key))
|
|
{
|
|
dataObject.MarkAllAsUpdated();
|
|
var newData = dataObject.Clone(fromPool: false);
|
|
dataObjects.Add(key, newData);
|
|
NotifyDataUpdate(key, newData);
|
|
return dataObject;
|
|
}
|
|
else
|
|
{
|
|
IDataObject obj = dataObjects[key];
|
|
obj.UpdateDifferent(dataObject);
|
|
IDataObject newDataObject;
|
|
if (updatedDataOnly)
|
|
{
|
|
newDataObject = obj.GetUpdatedObject(fromPool: false);
|
|
}
|
|
else
|
|
{
|
|
newDataObject = dataObject;
|
|
}
|
|
bool shouldInvoke = !updatedDataOnly || newDataObject.UpdatedCount > 0;
|
|
if(shouldInvoke) NotifyDataUpdate(key, newDataObject);
|
|
return newDataObject;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다.
|
|
/// </summary>
|
|
/// <param name="key">제거할 데이터 객체의 키</param>
|
|
public void RemoveData(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
|
|
|
lock (syncLock)
|
|
{
|
|
if (dataObjects.ContainsKey(key))
|
|
{
|
|
dataObjects.Remove(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다.
|
|
/// </summary>
|
|
/// <param name="key">검색할 데이터 객체의 키</param>
|
|
/// <returns>키가 존재하면 해당 데이터 객체, 존재하지 않으면 null</returns>
|
|
public IDataObject? GetData(string key)
|
|
{
|
|
if (string.IsNullOrEmpty(key))
|
|
throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다.");
|
|
|
|
lock (syncLock)
|
|
{
|
|
if (dataObjects.ContainsKey(key))
|
|
{
|
|
return dataObjects[key];
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다.
|
|
/// </summary>
|
|
/// <param name="key">데이터 업데이트를 감시할 키</param>
|
|
/// <param name="handler">데이터가 업데이트되었을 때 호출될 콜백 함수</param>
|
|
/// <remarks>
|
|
/// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다.
|
|
/// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다.
|
|
/// 전달 되는 데이터 객체는 업데이트된 상태고 DataRepository에 저장된 객체이기에 pool.ReturnToPool()을 호출하면 않됩니다.
|
|
/// </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>
|
|
/// 이 메서드는 주로 내부적으로 AddOrUpdateData 메서드에서 호출되어
|
|
/// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다.
|
|
/// </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일 수 없습니다.");
|
|
//Debug.Log($"NotifyDataUpdate: {key}, {dataObject.GetType().Name}");
|
|
lock (syncLock)
|
|
{
|
|
if (dataUpdateHandlers.ContainsKey(key))
|
|
{
|
|
UniTask.Post(() => 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 = DataObjectPool.Get();
|
|
dataObject.FromJObject(jObject);
|
|
AddOrUpdateData(key, dataObject, false);
|
|
}
|
|
else if (value is JArray jArray)
|
|
{
|
|
DataArray dataArray = DataArrayPool.Get().FromJArray(jArray);
|
|
AddOrUpdateData(key, dataArray, false);
|
|
}
|
|
}
|
|
}
|
|
catch (JsonException ex)
|
|
{
|
|
ULog.Error($"JSON 파싱 오류", ex);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|