#nullable enable using Cysharp.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; using UVC.Data.Core; using UVC.Data.Http; using UVC.Data.Mqtt; using UVC.Log; namespace UVC.Data { /// /// 데이터 객체들을 키-값 쌍으로 관리하는 중앙 저장소입니다. /// /// /// 이 클래스는 싱글톤 패턴으로 구현되어 있어 애플리케이션 전체에서 /// 하나의 인스턴스만 존재합니다. IDataObject 인터페이스를 구현하는 /// 모든 데이터 객체를 저장하고 검색할 수 있습니다. /// public class DataRepository { #region Singleton /// /// DataRepository 싱글톤 인스턴스를 생성하는 지연 초기화 객체입니다. /// protected static readonly Lazy instance = new Lazy(() => new DataRepository()); /// /// 외부에서의 인스턴스 생성을 방지하는 보호된 생성자입니다. /// protected DataRepository() { // Best MQTT 초기화 작업을 Main 스레드에서 호출 해야 한다. Best.HTTP.Shared.HTTPManager.Setup(); } /// /// DataRepository의 단일 인스턴스에 대한 접근자입니다. /// public static DataRepository Instance { get { return instance.Value; } } #endregion /// /// 키로 식별되는 데이터 객체를 저장하는 컬렉션입니다. /// private Dictionary dataObjects = new Dictionary(); /// /// 스레드 동기화를 위한 잠금 객체입니다. /// private readonly object syncLock = new object(); /// /// 데이터 업데이트 시 호출될 핸들러 함수들을 저장하는 딕셔너리입니다. /// 각 키에 연결된 데이터가 업데이트될 때 해당 핸들러가 호출됩니다. /// private Dictionary> dataUpdateHandlers = new Dictionary>(); private HttpDataFetcher httpFetcher = new HttpDataFetcher(); public HttpDataFetcher HttpFetcher => httpFetcher; private MqttDataReceiver mqttReceiver = new MqttDataReceiver(); public MqttDataReceiver MqttReceiver => mqttReceiver; /// /// 저장소에 데이터 객체를 추가하거나 기존 객체를 업데이트합니다. /// /// 데이터 객체를 식별하는 고유 키 /// 저장할 데이터 객체 /// true인 경우 업데이트된 속성만 반환, false인 경우 전체 객체 반환 /// 새로 추가된 객체 또는 업데이트된 기존 객체 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); //데이터 즉시 업데이트 여부 설정 newData.IsUpdateImmediately = dataObject.IsUpdateImmediately; dataObjects.Add(key, newData); var notifiedDataObject = newData.Clone(false); notifiedDataObject.IsUpdateImmediately = dataObject.IsUpdateImmediately; NotifyDataUpdate(key, notifiedDataObject); return dataObject; } else { IDataObject obj = dataObjects[key]; obj.UpdateDifferent(dataObject, updatedDataOnly); IDataObject newDataObject; if (updatedDataOnly) { newDataObject = obj.GetUpdatedObject(fromPool: false); } else { newDataObject = dataObject; } //데이터 즉시 업데이트 여부 설정 newDataObject.IsUpdateImmediately = dataObject.IsUpdateImmediately; bool shouldInvoke = !updatedDataOnly || newDataObject.UpdatedCount > 0; if (shouldInvoke) { var notifiedDataObject = newDataObject; if (newDataObject == dataObject) { notifiedDataObject = newDataObject.Clone(false); notifiedDataObject.IsUpdateImmediately = dataObject.IsUpdateImmediately; } NotifyDataUpdate(key, notifiedDataObject); } return newDataObject; } } } /// /// 지정된 키에 해당하는 데이터 객체를 저장소에서 제거합니다. /// /// 제거할 데이터 객체의 키 public void RemoveData(string key) { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key), "키는 null이거나 빈 문자열일 수 없습니다."); lock (syncLock) { if (dataObjects.ContainsKey(key)) { dataObjects.Remove(key); } } } /// /// 지정된 키에 해당하는 데이터 객체를 저장소에서 검색합니다. /// /// 검색할 데이터 객체의 키 /// 키가 존재하면 해당 데이터 객체, 존재하지 않으면 null 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; } } /// /// 지정된 키에 대한 데이터 업데이트 핸들러를 추가합니다. /// /// 데이터 업데이트를 감시할 키 /// 데이터가 업데이트되었을 때 호출될 콜백 함수 /// /// 같은 키에 대해 여러 핸들러를 등록할 수 있습니다. /// 핸들러는 해당 키의 데이터가 AddData를 통해 업데이트될 때마다 호출됩니다. /// 전달 되는 데이터 객체는 업데이트된 상태고 DataRepository에 저장된 객체이기에 pool.ReturnToPool()을 호출하면 않됩니다. /// public void AddDataUpdateHandler(string key, Action 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; // 기존 핸들러에 추가 } } } /// /// 지정된 키에서 데이터 업데이트 핸들러를 제거합니다. /// /// 핸들러를 제거할 데이터 키 /// 제거할 핸들러 함수 /// /// 지정된 키에 연결된 핸들러가 없거나, 제거하려는 핸들러가 등록되지 않은 경우 아무 작업도 수행하지 않습니다. /// 키에 연결된 마지막 핸들러가 제거되면 해당 키는 dataUpdateHandlers 딕셔너리에서 삭제됩니다. /// public void RemoveDataUpdateHandler(string key, Action 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); // 핸들러가 없으면 제거 } } } } /// /// 모든 데이터 업데이트 핸들러를 제거합니다. /// /// /// 이 메서드는 모든 키에 대한 모든 핸들러를 한 번에 제거합니다. /// 예를 들어, 씬이 변경되거나 애플리케이션이 종료될 때 /// 모든 핸들러를 정리하는 데 유용합니다. /// public void ClearDataUpdateHandlers() { lock (syncLock) { dataUpdateHandlers.Clear(); } } /// /// 지정된 키의 데이터가 업데이트되었음을 알리고 등록된 핸들러들을 호출합니다. /// /// 업데이트된 데이터의 키 /// 업데이트된 데이터 객체 /// /// 이 메서드는 주로 내부적으로 AddOrUpdateData 메서드에서 호출되어 /// 특정 키의 데이터가 변경되었을 때 등록된 핸들러들에게 알립니다. /// 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)) { var handler = dataUpdateHandlers[key]; UniTask.Post(() => handler.Invoke(dataObject)); } } } /// /// 저장소에 저장된 모든 데이터 객체의 키 목록을 반환합니다. /// /// 저장소에 저장된 모든 데이터 객체의 키 목록 public IEnumerable GetAllKeys() { lock (syncLock) { return new List(dataObjects.Keys); } } /// /// 저장된 모든 데이터 객체를 반환합니다. /// /// 저장소의 모든 데이터 객체 public IEnumerable GetAllData() { lock (syncLock) { return new List(dataObjects.Values); } } /// /// 지정된 키에 대한 데이터 객체가 저장소에 존재하는지 확인합니다. /// /// 확인할 키 /// 키가 존재하면 true, 그렇지 않으면 false public bool ContainsKey(string key) { lock (syncLock) { return dataObjects.ContainsKey(key); } } /// /// 저장소의 모든 데이터를 삭제합니다. /// public void Clear() { lock (syncLock) { dataObjects.Clear(); dataUpdateHandlers.Clear(); } } /// /// 지정된 키의 데이터 객체를 특정 타입으로 변환하여 반환합니다. /// /// 변환할 타입 (IDataObject를 구현해야 함) /// 검색할 키 /// 변환된 객체 또는 null public T? GetDataAs(string key) where T : class, IDataObject { lock (syncLock) { if (dataObjects.ContainsKey(key) && dataObjects[key] is T typedObject) { return typedObject; } return null; } } /// /// 지정된 조건을 만족하는 첫 번째 데이터 객체를 찾습니다. /// /// 데이터 객체를 필터링할 조건 /// 조건을 만족하는 첫 번째 데이터 객체와 해당 키, 없으면 null public (string Key, IDataObject Data) FindFirst(Func 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); } } /// /// 지정된 조건을 만족하는 모든 데이터 객체를 찾습니다. /// /// 데이터 객체를 필터링할 조건 /// 조건을 만족하는 모든 데이터 객체와 해당 키 public IEnumerable<(string Key, IDataObject Data)> FindAll(Func 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; } } /// /// 저장소의 전체 내용을 JSON 문자열로 직렬화합니다. /// /// 저장소 데이터의 JSON 표현 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(); } /// /// JSON 문자열에서 데이터를 불러와 저장소에 추가합니다. /// /// 저장소 데이터의 JSON 표현 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); } } } }