#nullable enable using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; using Cysharp.Threading.Tasks; using UVC.Extention; namespace UVC.Data.Core { /// /// 키-값 쌍의 데이터를 관리하고 변경 사항을 추적하는 동적 데이터 객체입니다. /// SortedDictionary를 상속하여 키를 기준으로 정렬된 데이터를 제공합니다. /// /// /// 이 클래스는 JSON과 호환되는 데이터 구조를 표현하며, 데이터 변경 추적 기능을 통해 /// 효율적인 데이터 동기화를 지원합니다. /// /// /// /// // DataMask 생성 및 설정 /// var mask = new DataMask(); /// mask.ObjectIdKey = "Id"; /// mask.ObjectName = "users"; /// /// // 필드 이름 변환 설정 /// mask.NamesForReplace = new Dictionary /// { /// { "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는 제외됨 /// /// public partial class DataObject : OrderedDictionary, IDataObject { private bool isInPool = false; /// /// 이 객체가 객체 풀에 있는지 여부를 나타냅니다. /// 중복 반환을 방지하기 위해 DataObjectPool에서 내부적으로 사용됩니다. /// internal bool IsInPool { get => isInPool; set { isInPool = value; foreach (var item in this) { if (item.Value is DataObject dataObject) { dataObject.isInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다. } else if (item.Value is DataArray dataArray) { dataArray.IsInPool = value; // 내부 DataArray도 풀에 있다고 표시합니다. } } } } private bool createdFromPool = false; /// /// 객체가 풀에서 생성되었는지 여부를 나타냅니다. /// internal bool CreatedFromPool { get => createdFromPool; set { createdFromPool = value; foreach (var item in this) { if (item.Value is DataObject dataObject) { dataObject.CreatedFromPool = value; // 내부 DataObject도 풀에 있다고 표시합니다. } else if (item.Value is DataArray dataArray) { dataArray.CreatedFromPool = value; // 내부 DataArray도 풀에 있다고 표시합니다. } } } } /// /// 객체의 고유 식별자를 나타내는 속성입니다. DataArray에서 데이터를 식별하는 데 사용됩니다. /// public string Id { get { if (IdKey != null && ContainsKey(IdKey)) { return this[IdKey]?.ToString() ?? string.Empty; } if (Count > 0) { return this.First().Value?.ToString() ?? string.Empty; } return string.Empty; } } /// /// Id에 해당하는 key 문자열 /// public string? IdKey { get; set; } = null; /// /// DataObject의 이름을 나타내는 속성입니다. /// public string Name { get; internal set; } = string.Empty; /// /// 직접적인 변경이 있었던 키를 저장하는 리스트입니다. /// protected HashSet changedProperies = new HashSet(); /// /// 변경된 속성의 키 목록을 읽기 전용으로 반환합니다. /// public IReadOnlyCollection ChangedProperies => changedProperies; // 객체 생성 중에는 변경 추적을 비활성화하기 위한 플래그 private bool _isInitializing = false; /// /// 기본 생성자입니다. 빈 데이터 객체를 생성합니다. /// public DataObject() { } public DataObject(string jsonString) : this(JObject.Parse(jsonString)) { } public DataObject(System.IO.Stream jsonStream) { if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream)); // 스트림 처리 최적화를 위해 청크 단위로 읽을 수 있지만, // 현재는 Newtonsoft.Json의 기본 역직렬화 사용 using (var reader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(jsonStream))) { // 청크 읽기 설정 - 메모리 사용량 최적화 reader.SupportMultipleContent = true; var serializer = new Newtonsoft.Json.JsonSerializer(); var sourceObject = serializer.Deserialize(reader); // 수정된 코드: 생성자를 호출하는 대신 FromJObject 메서드를 사용 if (sourceObject != null) FromJObject(sourceObject); } } /// /// JObject로부터 데이터 객체를 생성합니다. /// /// 복사할 JObject 객체 public DataObject(JObject other) { _isInitializing = true; try { // JObject로부터 속성을 복사 foreach (var prop in other.Properties()) { // 인덱서를 사용하여 JToken을 객체로 변환하고 추가 this[prop.Name] = ConvertJTokenToObject(prop.Value); } } finally { _isInitializing = false; } } /// /// Dictionary로 데이터 객체를 초기화합니다. /// /// 초기화에 사용할 Dictionary 객체 public DataObject(Dictionary dictionary) : base() { _isInitializing = true; try { // 생성자에서 초기 속성들을 등록 foreach (var item in dictionary) { this[item.Key] = item.Value; } } finally { _isInitializing = false; } } /// /// 지정된 의 속성과 값으로 현재 객체를 채웁니다. /// /// 이 메서드는 제공된 의 모든 속성을 반복하고 /// 해당 값을 현재 객체의 해당 속성에 할당합니다. 현재 객체에 속성이 없는 경우 /// 동적으로 추가됩니다. 이 메서드는 채우기 프로세스 중 적절한 처리를 보장하기 위해 내부 초기화 /// 플래그를 임시로 설정합니다. /// 복사할 속성과 값이 포함된 입니다. public DataObject FromJObject(JObject other) { IdKey = null; // IdKey를 초기화합니다. Name = string.Empty; // Name을 초기화합니다. _isInitializing = true; try { // JObject로부터 속성을 복사 foreach (var prop in other.Properties()) { // 인덱서를 사용하여 JToken을 객체로 변환하고 추가 this[prop.Name] = ConvertJTokenToObject(prop.Value); } } finally { _isInitializing = false; } return this; } /// /// 지정된 사전의 키-값 쌍으로 현재 객체를 채웁니다. /// /// 이 메서드는 제공된 사전을 기반으로 객체의 속성을 업데이트합니다. /// 사전의 키가 속성 이름과 일치하면 속성 값이 해당 값으로 설정됩니다. /// 사전에 예상되는 속성 유형과 일치하는 유효한 키와 값이 포함되어 있는지 확인합니다. /// 객체의 속성을 초기화하는 키-값 쌍을 포함하는 사전입니다. 키는 속성 /// 이름을 나타내고 값은 해당 속성 값을 나타냅니다. public DataObject FromDictionary(Dictionary dictionary) { IdKey = null; // IdKey를 초기화합니다. Name = string.Empty; // Name을 초기화합니다. _isInitializing = true; try { // 생성자에서 초기 속성들을 등록 foreach (var item in dictionary) { this[item.Key] = item.Value; } } finally { _isInitializing = false; } return this; } /// /// JSON 문자열 표현에서 인스턴스를 생성합니다. /// /// 이 메서드를 사용하여 JSON 문자열을 로 역직렬화합니다. /// 입력 문자열이 예상 객체 구조의 유효한 JSON 표현인지 확인합니다. /// 로 파싱할 JSON 문자열입니다. null이거나 비어 있으면 안 됩니다. /// JSON 문자열의 데이터로 채워진 새 인스턴스를 반환합니다. 이 null이거나 비어 있으면 현재 인스턴스를 반환합니다. public DataObject FromJsonString(string jsonString) { if (!string.IsNullOrEmpty(jsonString)) { return FromJObject(JObject.Parse(jsonString)); } return this; } /// /// UniTask 기반 비차단 방식으로 JObject로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서 /// UI 블로킹을 피하기 위해 일정 수의 키마다 await로 제어권을 양보합니다. /// await myDataObject.FromJObjectIncrementalAsync(jObject, 200, cancellationToken); /// /// JObject 소스 /// 한 번에 처리할 속성 수 /// 취소 토큰 public async UniTask FromJObjectIncrementalAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default) { if (other == null) return; IdKey = null; Name = string.Empty; _isInitializing = true; int count =0; try { foreach (var prop in other.Properties()) { cancellationToken.ThrowIfCancellationRequested(); #if UNITY_WEBGL && !UNITY_EDITOR var value = await ConvertJTokenToObjectAsync(prop.Value, batchSize, cancellationToken); this[prop.Name] = value; #else this[prop.Name] = ConvertJTokenToObject(prop.Value); #endif count++; if (count % batchSize ==0) { await UniTask.Yield(cancellationToken); } } } finally { _isInitializing = false; } } /// /// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다. /// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다. /// await myDataObject.FromJsonStringIncrementalAsync(jsonString, 200, cancellationToken); /// /// JSON 문자열 /// 한 번에 처리할 속성 수 /// 취소 토큰 public async UniTask FromJsonStringIncrementalAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(jsonString)) return; using (var sr = new System.IO.StringReader(jsonString)) using (var reader = new Newtonsoft.Json.JsonTextReader(sr)) { reader.SupportMultipleContent = false; // 기대: StartObject if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartObject) return; IdKey = null; Name = string.Empty; _isInitializing = true; int count =0; while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); if (reader.TokenType == Newtonsoft.Json.JsonToken.PropertyName) { string propName = (string)reader.Value!; // 다음 토큰은 값의 시작이어야 함. JToken.ReadFrom를 사용하여 값 전체를 읽는다. if (!reader.Read()) break; JToken token = JToken.ReadFrom(reader); #if UNITY_WEBGL && !UNITY_EDITOR var value = await ConvertJTokenToObjectAsync(token, batchSize, cancellationToken); this[propName] = value; #else this[propName] = ConvertJTokenToObject(token); #endif count++; if (count % batchSize ==0) { await UniTask.Yield(cancellationToken); } } else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndObject) { break; } } _isInitializing = false; } } /// /// JToken을 적절한 C# 객체 타입으로 변환하는 헬퍼 메서드입니다. /// /// 변환할 JToken 객체 /// 변환된 C# 객체 private async UniTask MaterializeJTokenAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default) { switch (token.Type) { case JTokenType.String: return token.ToString(); case JTokenType.Integer: return token.ToObject(); case JTokenType.Float: return token.ToObject(); case JTokenType.Boolean: return token.ToObject(); case JTokenType.Object: // 점진적으로 DataObject로 채우기 var dataObject = DataObjectPool.Get(); await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken); return dataObject; case JTokenType.Array: JArray array = (JArray)token; var dataArray = DataArrayPool.Get(); await dataArray.FromJArrayIncrementalAsync(array, batchSize, cancellationToken); return dataArray; default: return token.ToString(); } } /// /// 기존 ConvertJTokenToObject는 동기 API를 유지하되, 호출자가 비동기 전환을 원하면 /// MaterializeJTokenAsync를 직접 호출하도록 합니다. 기본 동작은 기존과 동일합니다. /// private object? ConvertJTokenToObject(JToken token) { switch (token.Type) { case JTokenType.String: return token.ToString(); case JTokenType.Integer: return token.ToObject(); case JTokenType.Float: return token.ToObject(); case JTokenType.Boolean: return token.ToObject(); case JTokenType.Object: var dataObject = DataObjectPool.Get(); dataObject.FromJObject((JObject)token); return dataObject; case JTokenType.Array: JArray array = (JArray)token; return DataArrayPool.Get().FromJArray(array); default: return token.ToString(); } } /// /// 새 비동기 전환용 헬퍼: 동기 ConvertJTokenToObject 대신 사용하여 비동기적으로 토큰을 재질화합니다. /// 메서드 이름: ConvertJTokenToObjectAsync /// public UniTask ConvertJTokenToObjectAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default) { return MaterializeJTokenAsync(token, batchSize, cancellationToken); } /// /// 모든 프로퍼티를 변경된 것으로 표시합니다. /// 전체 데이터가 갱신되었을 때 사용합니다. /// public void MarkAllAsUpdated() { changedProperies.Clear(); changedProperies.UnionWith(this.Keys); } /// /// 새 속성을 추가하고 변경 사항을 추적합니다. /// /// 추가할 속성의 이름 /// 속성의 값 public new void Add(string propertyName, object value) { // 인덱서를 사용하여 일관된 변경 추적 로직을 보장합니다. this[propertyName] = value; } /// /// 인덱서를 통해 속성값에 접근하고 설정합니다. /// 속성값이 변경될 때마다 변경 사항을 자동으로 추적합니다. /// /// 접근할 속성의 키 /// 속성값 public new object? this[string key] { get => base[key]; set { // 키가 새로 추가되거나 기존 값이 변경되었는지 확인합니다. bool hasChanged = !TryGetValue(key, out object? oldValue) || !Equals(oldValue, value); if (hasChanged) { // 기존에 풀링 가능한 객체가 있었다면 풀에 반환합니다. if (oldValue is DataObject oldDataObject) { if (oldDataObject.IsInPool) oldDataObject.ReturnToPool(); } else if (oldValue is DataArray oldDataArray) { if (oldDataArray.IsInPool) oldDataArray.ReturnToPool(); } // 기본 딕셔너리에 값을 설정합니다. base[key] = value; // 객체 초기화 중이 아닐 때만 변경 사항을 추적합니다. if (!_isInitializing) { changedProperies.Add(key); } } } } /// /// 지정된 속성의 값을 정수(int)로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 정수 값 또는 기본값 public int? GetInt(string propertyName, int? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is int intValue) return intValue; return Convert.ToInt32(value); } return defaultValue; } /// /// 지정된 속성의 값을 문자열(string)로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 null인 경우 반환할 기본값 /// 변환된 문자열 값 또는 기본값 public string? GetString(string propertyName, string? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { return value.ToString(); } return defaultValue; } /// /// 지정된 속성의 값을 불리언(bool)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 불리언 값 또는 기본값 public bool? GetBool(string propertyName, bool? defaultValue = false) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is bool boolValue) return boolValue; return Convert.ToBoolean(value); } return defaultValue; } /// /// 지정된 속성의 값을 부동 소수점(float)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 부동 소수점 값 또는 기본값 public float? GetFloat(string propertyName, float? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is float floatValue) return floatValue; return Convert.ToSingle(value); } return defaultValue; } /// /// 지정된 속성의 값을 더블 정밀도 부동 소수점(double)으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 더블 값 또는 기본값 public double? GetDouble(string propertyName, double? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is double doubleValue) return doubleValue; return Convert.ToDouble(value); } return defaultValue; } /// /// 지정된 속성의 값을 long으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 long 값 또는 기본값 public long? GetLong(string propertyName, long? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is long longValue) return longValue; return Convert.ToInt64(value); } return defaultValue; } /// /// 지정된 속성의 값을 DateTime으로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 DateTime 값 또는 기본값 public DateTime? GetDateTime(string propertyName, DateTime? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is DateTime dateTime) return dateTime; return Convert.ToDateTime(value); } return defaultValue; } /// /// 지정된 속성의 값을 열거형(Enum)으로 변환하여 반환합니다. /// /// 변환할 열거형 타입 /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 변환된 열거형 값 또는 기본값 public T? GetEnum(string propertyName, T? defaultValue = default) where T : Enum { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is T enumValue) return enumValue; return (T)Enum.Parse(typeof(T), value.ToString()); } return defaultValue; } /// /// 지정된 속성의 값을 IDataObject로 반환합니다. /// /// 속성 이름 /// 속성이 없거나 IDataObject가 아닌 경우 반환할 기본값 /// IDataObject 인터페이스를 구현한 객체 또는 기본값 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; } /// /// 지정된 속성의 값을 DataArray로 변환하여 반환합니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// DataArray 객체 또는 기본값 public DataArray? GetDataArray(string propertyName, DataArray? defaultValue = null) { if (TryGetValue(propertyName, out object? value) && value != null) { if (value is DataArray dataArray) return dataArray; if (value is JArray jArray) return DataArrayPool.Get().FromJArray(jArray); } return defaultValue; } /// /// 지정된 속성의 값을 DataObject로 변환하여 반환합니다. /// 이 메서드는 먼저 현재 객체에서 속성을 찾습니다. 찾지 못하면 중첩된 DataObject를 재귀적으로 탐색합니다. /// 과도한 재귀로 인한 문제를 방지하기 위해 최대 탐색 깊이가 제한됩니다. /// /// 속성 이름 /// 속성이 없거나 변환할 수 없는 경우 반환할 기본값 /// 재귀 탐색의 최대 깊이 /// DataObject 객체 또는 기본값 public DataObject? GetDataObject(string propertyName, DataObject? defaultValue = null, int maxDepth = 8) { // 내부 재귀 탐색을 시작하고, 결과를 찾지 못하면 기본값을 반환합니다. return GetDataObjectInternal(propertyName, 0, maxDepth) ?? defaultValue; } /// /// 재귀 깊이 제한을 두고 내부적으로 DataObject를 탐색하는 private 헬퍼 메서드입니다. /// /// 찾을 속성 이름 /// 현재 재귀 깊이 /// 최대 재귀 깊이 private DataObject? GetDataObjectInternal(string propertyName, int currentDepth, int maxDepth) { // StackOverflow 방지를 위한 최대 재귀 깊이 if (currentDepth >= maxDepth) { return null; // 깊이 제한 초과 시 탐색 중단 } //1. 현재 객체에서 직접 속성 검색 if (TryGetValue(propertyName, out object? value) && value != null) { if (value is DataObject dataObject) return dataObject; if (value is JObject jObject) { var dataObj = DataObjectPool.Get(); dataObj.FromJObject(jObject); return dataObj; } if (value is Dictionary dict) { var dataObj = DataObjectPool.Get(); dataObj.FromDictionary(dict); return dataObj; } } //2. 중첩된 DataObject에서 재귀적으로 검색 foreach (KeyValuePair keyValue in this) { if (keyValue.Value is DataObject nestedDataObject) { // 재귀 호출 시 깊이를1 증가 DataObject? foundInNested = nestedDataObject.GetDataObjectInternal(propertyName, currentDepth + 1, maxDepth); if (foundInNested != null) { return foundInNested; } } } return null; // 이 경로에서 객체를 찾지 못함 } /// /// 지정된 경로를 사용하여 중첩된 DataObject를 찾습니다. /// 경로는 점(.)으로 구분된 속성 이름과 배열 인덱스로 구성됩니다. /// /// /// /// var userProfile = data.FindDataObjectByPath("users.0.profile"); /// /// /// "data.users.0.profile"과 같은 객체 경로 /// 경로를 찾을 수 없는 경우 반환할 기본값 /// 찾은 DataObject 또는 기본값 public DataObject? FindDataObjectByPath(string path, DataObject? defaultValue = null) { var segments = path.Split('.'); object? current = this; foreach (var segment in segments) { if (current == null) return defaultValue; switch (current) { case DataObject currentObject: if (!currentObject.TryGetValue(segment, out current)) { return defaultValue; } break; case DataArray currentArray: if (int.TryParse(segment, out int index) && index >= 0 && index < currentArray.Count) { current = currentArray[index]; } else { return defaultValue; } break; default: // Path leads to a primitive value, cannot traverse further. return defaultValue; } } return current as DataObject ?? defaultValue; } /// /// 속성이 제거될 때 기존 속성 목록에서도 제거합니다. /// /// 제거할 속성의 키 /// 제거 성공 여부 public new bool Remove(string key) { // 제거하기 전에 이전 값을 가져옵니다. TryGetValue(key, out object? oldValue); bool result = base.Remove(key); if (result) { // 제거된 객체가 풀링 가능한 경우 풀에 반환합니다. if (oldValue is DataObject dataObject) { if (dataObject.IsInPool) dataObject.ReturnToPool(); } else if (oldValue is DataArray dataArray) { if (dataArray.IsInPool) dataArray.ReturnToPool(); } // 변경 추적 목록에서도 제거합니다. changedProperies.Remove(key); } return result; } /// /// 모든 속성을 제거하고 추적 리스트를 초기화합니다. /// public void RemoveAll() { foreach (var value in Values.ToList()) { if (value is DataObject dataObject) { dataObject.ReturnToPool(); // DataObject를 풀에 반환합니다. } else if (value is DataArray dataArray) { dataArray.ReturnToPool(); // DataArray를 풀에 반환합니다. } } base.Clear(); changedProperies.Clear(); } /// /// 객체를 풀에 반환하기 전에 초기 상태로 리셋합니다. /// 모든 속성을 제거하고, 이름과 ID 키를 기본값으로 설정합니다. /// public void Reset() { RemoveAll(); Name = string.Empty; IdKey = null; isUpdateImmediately = false; } /// /// 동일한 상태와 값을 가진 현재 데이터 객체의 새 인스턴스를 생성합니다. /// /// 복제된 객체는 원본 객체와 독립적이므로, 한 객체를 변경해도 다른 객체에는 영향을 미치지 않습니다. /// /// 복제된 객체가 풀에 있는지 여부를 나타냅니다. 기본값은 true입니다. /// 현재 객체의 복사본인 새 인스턴스를 반환합니다. public IDataObject Clone(bool fromPool = true) { return Copy(fromPool); } /// /// 모든 키-값 쌍을 포함하여 현재 인스턴스의 깊은 복사본을 생성합니다. /// /// 이 메서드는 와 그 내용의 깊은 복사본을 수행합니다. /// 키-값 쌍의 값이 다른 또는 인 경우, /// 메서드는 해당 객체를 재귀적으로 복제합니다. 기본 유형 및 기타 복제 불가능한 값은 /// 직접 복사됩니다. /// 복제된 객체가 풀에 있는지 여부를 나타냅니다. 기본값은 true입니다. /// 현재 객체의 깊은 복사본인 새로운 인스턴스를 반환합니다. public DataObject Copy(bool fromPool = true) { DataObject clone; if (fromPool) clone = DataObjectPool.Get(); else clone = new DataObject(); clone.Name = Name; clone.IdKey = IdKey; // 모든 키-값 쌍을 순회하며 깊은 복사를 수행합니다. foreach (var pair in this) { object? clonedValue; switch (pair.Value) { // 값이 DataObject인 경우, 재귀적으로 Copy을 호출합니다. case DataObject dataObjectValue: clonedValue = dataObjectValue.Copy(fromPool); break; // 값이 DataArray인 경우, 재귀적으로 Clone을 호출합니다. case DataArray dataArrayValue: clonedValue = dataArrayValue.Copy(fromPool); break; // 그 외의 경우 (primitive 타입 등)는 그대로 복사합니다. default: clonedValue = pair.Value; break; } // 복제된 값을 새 DataObject에 추가합니다. clone.Add(pair.Key, clonedValue); } clone.changedProperies = new HashSet(changedProperies); return clone; } /// /// 현재 DataObject를 JObject로 변환합니다. /// /// 현재 객체의 데이터를 담은 JObject public JObject ToJObject() { JObject result = new JObject(); foreach (var kvp in this) { result[kvp.Key] = JToken.FromObject(kvp.Value); } return result; } /// /// 변경된 것으로 표시된 속성 목록을 지웁니다. /// /// 이 메서드는 변경된 속성의 내부 추적을 재설정하여 /// 기록된 모든 수정 사항을 효과적으로 지웁니다. 모든 변경 내용 추적을 삭제하고 /// 새로 시작하려면 이 메서드를 사용하세요. public void ClearChangedProperties() { // 변경 추적 목록을 초기화합니다. changedProperies.Clear(); } /// /// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다. /// 변경된 키는 자동으로 추적됩니다. /// /// 비교할 DataObject /// true인 경우, 변경된 속성만 업데이트합니다. false인 경우, 모든 속성을 동기화합니다. public void UpdateDifferent(IDataObject other, bool updatedDataOnly) { if (other == null || other is not DataObject otherDataObject) return; // 변경 추적 목록을 초기화하여 'other' 객체의 상태를 기준으로 새로 설정합니다. changedProperies.Clear(); foreach (var keyValue in otherDataObject) { if (!this.ContainsKey(keyValue.Key) || (this[keyValue.Key] == null && keyValue.Value != null) || (this[keyValue.Key] != null && keyValue.Value == null) || (this[keyValue.Key] != null && keyValue.Value != null && !this[keyValue.Key]!.Equals(keyValue.Value))) { //참조 타입과 값 타입 구분하여 복사 object? valueToSet; switch (keyValue.Value) { // DataObject나 DataArray는 풀을 사용하지 않는 깊은 복사를 수행합니다. case DataObject dataObjectValue: valueToSet = dataObjectValue.Copy(fromPool: false); break; case DataArray dataArrayValue: valueToSet = dataArrayValue.Copy(fromPool: false); break; // 그 외 타입은 값을 그대로 할당합니다. default: valueToSet = keyValue.Value; break; } this[keyValue.Key] = valueToSet; changedProperies.Add(keyValue.Key); } } if (updatedDataOnly) return; // 현재 객체에만 있는 속성은 제거합니다. var keysToRemove = this.Keys.Except(otherDataObject.Keys).ToList(); foreach (var key in keysToRemove) { this.Remove(key); changedProperies.Remove(key); } } /// /// 현재 객체의 상태를 기반으로 업데이트된 인스턴스를 생성하고 반환합니다. /// /// 이 메서드는 유형의 속성은 깊이 복사되고, 다른 유형의 속성은 직접 복사되도록 합니다. IdKey가 없으면 /// 현재 객체의 첫 번째 키-값 쌍이 업데이트된 객체에 포함됩니다. /// /// 업데이트된 객체가 풀에서 검색해야 하는지 여부를 나타내는 값입니다. 인 경우 /// 에서 객체를 가져옵니다. 그렇지 않으면 새 /// 인스턴스가 생성됩니다. /// 현재 객체에서 복사된 속성과 값을 포함하는 업데이트된 인스턴스입니다. /// 반환된 객체에는 변경된 모든 속성, IdKeyName이 포함됩니다. /// IdKey가 현재 객체에 있는 경우 해당 값도 업데이트된 객체에 포함됩니다. public IDataObject GetUpdatedObject(bool fromPool = true) { DataObject updated = fromPool ? DataObjectPool.Get() : new DataObject(); foreach (var key in changedProperies) { if (this.ContainsKey(key)) { if (this[key] is DataObject dataObject) { updated[key] = dataObject.Copy(fromPool); // DataObject는 복사합니다. } else if (this[key] is DataArray dataArray) { updated[key] = dataArray.Copy(fromPool); // DataArray는 복사합니다. } else { updated[key] = this[key]; // 그 외의 값은 그대로 복사합니다. } } } updated.IdKey = IdKey; // ID 키를 복사합니다. updated.Name = Name; // 이름을 복사합니다. //id에 해당하는 속성이 있다면, 업데이트된 객체에도 포함시킵니다. if (IdKey != null && ContainsKey(IdKey)) { updated[IdKey] = this[IdKey]; } else if (Count > 0) { var keyValue = this.First(); updated[keyValue.Key] = keyValue.Value; } return updated; } public void ReturnToPool() { if (CreatedFromPool) { Reset(); } else { DataObjectPool.Return(this); } } /// /// 업데이트 된 속성의 수. /// /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다. public int UpdatedCount { get => changedProperies.Count; } private bool isUpdateImmediately = false; /// /// 업데이트가 즉시 적용되어야 하는지 여부를 나타냅니다. /// /// public bool IsUpdateImmediately { get => isUpdateImmediately; set { if (isUpdateImmediately == value) return; isUpdateImmediately = value; foreach (var item in this) { if (item.Value is DataObject dataObject) { dataObject.IsUpdateImmediately = value; } else if (item.Value is DataArray dataArray) { dataArray.IsUpdateImmediately = value; } } } } /// /// DataObject의 내용을 문자열로 반환합니다. /// 각 키-값 쌍이 "키:값" 형식으로 쉼표로 구분되어 표시됩니다. /// /// DataObject의 내용을 나타내는 문자열 public override string ToString() { return string.Join(", ", this.Select(kvp => $"{kvp.Key}:{kvp.Value}")); } /// /// 현재 객체를 JSON 문자열 표현으로 변환합니다. /// /// 현재 객체를 나타내는 JSON 문자열입니다. public string ToJson() { return ToJObject().ToString(Newtonsoft.Json.Formatting.None); } /// /// 비동기 팩토리: JObject로부터 완전히 점진 재질화된 DataObject를 생성합니다. /// public static async UniTask CreateFromJObjectAsync(JObject other, int batchSize =50, CancellationToken cancellationToken = default) { var obj = new DataObject(); await obj.FromJObjectIncrementalAsync(other, batchSize, cancellationToken); return obj; } /// /// 비동기 팩토리: JSON 문자열로부터 완전히 점진 재질화된 DataObject를 생성합니다. /// public static async UniTask CreateFromJsonStringAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(jsonString)) return new DataObject(); #if UNITY_WEBGL && !UNITY_EDITOR var obj = new DataObject(); await obj.FromJsonStringIncrementalAsync(jsonString, batchSize, cancellationToken); return obj; // WebGL: 스레드가 없으므로 incremental 파서를 사용해 메인스레드에서 점진 처리 #else // non-WebGL: 스레드풀에서 전체 파싱 후 비동기 재질화 var jObj = await UniTask.RunOnThreadPool(() => JObject.Parse(jsonString)); return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken); #endif } /// /// 비동기 팩토리: 스트림으로부터 완전히 점진 재질화된 DataObject를 생성합니다. /// public static async UniTask CreateFromStreamAsync(System.IO.Stream jsonStream, int batchSize =50, CancellationToken cancellationToken = default) { if (jsonStream == null) throw new ArgumentNullException(nameof(jsonStream)); using (var sr = new System.IO.StreamReader(jsonStream)) using (var reader = new Newtonsoft.Json.JsonTextReader(sr)) { reader.SupportMultipleContent = true; var serializer = new Newtonsoft.Json.JsonSerializer(); var jObj = serializer.Deserialize(reader); if (jObj == null) return new DataObject(); return await CreateFromJObjectAsync(jObj, batchSize, cancellationToken); } } } }