using Newtonsoft.Json.Linq; using System; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading; using Cysharp.Threading.Tasks; namespace UVC.Data.Core { /// /// DataObject 객체 컬렉션의 변경사항을 추적하는 데이터 배열 클래스 /// public partial class DataArray : List, IDataObject { private bool isInPool = false; /// /// 이 객체가 객체 풀에 있는지 여부를 나타냅니다. /// 중복 반환을 방지하기 위해 DataArrayPool에서 내부적으로 사용됩니다. /// internal bool IsInPool { get => isInPool; set { isInPool = value; foreach (var item in this) { item.IsInPool = value; // 내부 DataObject도 풀에 있다고 표시합니다. } } } private bool createdFromPool = false; /// /// 객체가 풀에서 생성되었는지 여부를 나타냅니다. /// internal bool CreatedFromPool { get => createdFromPool; set { createdFromPool = value; foreach (var item in this) { item.CreatedFromPool = value; // 내부 DataObject도 풀에서 생성되었음을 표시합니다. } } } // 추가 된 항목 목록 protected List addedList = new List(); // 제거 된 항목 목록 protected List removedList = new List(); // 수정 된 항목 목록 protected List modifiedList = new List(); // 추가된 항목에 접근할 수 있는 읽기 전용 컬렉션 public ReadOnlyCollection AddedItems => addedList.AsReadOnly(); // 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션 public ReadOnlyCollection RemovedItems => removedList.AsReadOnly(); // 제거된 항목에 접근할 수 있는 읽기 전용 컬렉션 public ReadOnlyCollection ModifiedList => modifiedList.AsReadOnly(); /// /// 기본 생성자 /// public DataArray() : base() { } /// /// 초기 용량을 지정하는 생성자 /// /// 초기 용량 public DataArray(int capacity) : base(capacity) { } /// /// 기존 컬렉션으로부터 생성하는 생성자 /// /// 초기 항목을 포함하는 컬렉션 public DataArray(IEnumerable collection) : base(collection) { } public DataArray(string jsonString) : base() { if (!string.IsNullOrEmpty(jsonString)) { try { JArray jArray = JArray.Parse(jsonString); foreach (var item in jArray) { Add(ConvertToDataObject(item)); } } catch (Exception ex) { throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex); } } } public DataArray(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); // 수정된 코드: 생성자를 호출하는 대신 JArray 메서드를 사용 if (sourceObject != null) FromJArray(sourceObject); } } /// /// JArray로부터 DataArray를 생성하는 생성자 /// /// JSON 배열 public DataArray(JArray jArray) : base() { FromJArray(jArray); } /// /// 배열의 JSON 문자열 표현을 사용하여 현재 인스턴스를 채웁니다. /// /// 이 메서드는 제공된 JSON 문자열을 구문 분석하고 각 요소를 데이터 객체로 변환한 다음 /// 현재 에 추가합니다. 이 null이거나 /// 비어 있는 경우, 메서드는 수정 없이 현재 인스턴스를 반환합니다. /// 구문 분석하여 데이터 객체로 변환할 JSON 문자열입니다. 유효한 JSON 배열을 나타내야 합니다. /// JSON 문자열에서 파싱된 데이터 객체로 채워진 현재 인스턴스입니다. /// 이 유효한 JSON 배열 형식이 아닌 경우 발생합니다. public DataArray FromJsonString(string jsonString) { if (!string.IsNullOrEmpty(jsonString)) { try { JArray jArray = JArray.Parse(jsonString); foreach (var item in jArray) { Add(ConvertToDataObject(item)); } } catch (Exception ex) { throw new ArgumentException("Invalid JSON string format.", nameof(jsonString), ex); } } return this; } /// /// UniTask 기반 비차단(코루틴 대체) 방식으로 JArray로부터 데이터를 채웁니다. WebGL 같은 싱글스레드 환경에서 /// UI 블로킹을 피하기 위해 일정 수의 항목마다 await로 제어권을 양보합니다. /// await myDataArray.FromJArrayIncrementalAsync(jArray, 100, cancellationToken); /// /// JArray 소스 /// 한 번에 처리할 항목 수 /// 취소 토큰 public async UniTask FromJArrayIncrementalAsync(JArray jArray, int batchSize = 50, CancellationToken cancellationToken = default) { if (jArray == null) return; int count = 0; foreach (var item in jArray) { cancellationToken.ThrowIfCancellationRequested(); base.Add(ConvertToDataObject(item)); count++; if (count % batchSize == 0) { await UniTask.Yield(cancellationToken); } } } /// /// UniTask 기반 비차단 방식으로 JSON 문자열을 스트리밍하여 파싱합니다. /// JsonTextReader를 사용해 토큰 단위로 읽고, 일정 수의 항목마다 await로 제어권을 양보합니다. /// await myDataArray.FromJsonStringIncrementalAsync(jArray, 100, 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; // 기대: StartArray if (!reader.Read() || reader.TokenType != Newtonsoft.Json.JsonToken.StartArray) return; int count = 0; while (reader.Read()) { cancellationToken.ThrowIfCancellationRequested(); if (reader.TokenType == Newtonsoft.Json.JsonToken.StartObject || reader.TokenType == Newtonsoft.Json.JsonToken.StartArray || reader.TokenType == Newtonsoft.Json.JsonToken.String || reader.TokenType == Newtonsoft.Json.JsonToken.Integer || reader.TokenType == Newtonsoft.Json.JsonToken.Float || reader.TokenType == Newtonsoft.Json.JsonToken.Boolean) { // JToken.ReadFrom는 현재 토큰에서 전체 값(객체나 배열 등)을 읽습니다. JToken token = JToken.ReadFrom(reader); base.Add(ConvertToDataObject(token)); count++; if (count % batchSize == 0) { await UniTask.Yield(cancellationToken); } } else if (reader.TokenType == Newtonsoft.Json.JsonToken.EndArray) { break; } } } } public DataArray FromCapacity(int capacity) { Capacity = capacity; return this; } public DataArray FromCollection(IEnumerable collection) { if (collection == null) return this; foreach (var item in collection) { // base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다. base.Add(item); } return this; } /// /// JArray로부터 DataArray의 내용을 채웁니다. /// /// JSON 배열 public DataArray FromJArray(JArray jArray) { if (jArray == null) return this; foreach (var item in jArray) { // base.Add를 사용하여 추적 로직을 우회하고 직접 추가합니다. base.Add(ConvertToDataObject(item)); } return this; } /// /// JToken을 DataObject로 변환합니다. /// private DataObject ConvertToDataObject(JToken token) { if (token.Type == JTokenType.Object) { // 풀에서 객체를 가져와 JObject로 초기화합니다. var dataObject = DataObjectPool.Get(); dataObject.FromJObject((JObject)token); return dataObject; } else { // JObject가 아닌 경우, 풀에서 새 DataObject를 가져와 값을 넣어줍니다. var dataObject = DataObjectPool.Get(); dataObject.Add("value", ConvertJTokenToObject(token)); return dataObject; } } private object ConvertJTokenToObject(JToken token) { switch (token.Type) { case JTokenType.String: return token.ToString(); case JTokenType.Integer: return token.ToObject(); 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; var dataArray = DataArrayPool.Get(); dataArray.FromJArray(array); return dataArray; default: return token.ToString(); } } /// /// 모든 아이템이 추가 된것으로 표시합니다. /// 전체 데이터가 갱신되었을 때 사용합니다. /// public void MarkAllAsUpdated() { addedList.Clear(); addedList.AddRange(this); } /// /// 다른 DataObject와 현재 객체를 비교하여 다른 부분만 설정합니다. /// 변경된 키는 자동으로 추적됩니다. /// /// 비교할 DataObject /// true로 설정하면, 다른 속성만 업데이트합니다. false로 설정하면, 모든 속성을 덮어씁니다. public void UpdateDifferent(IDataObject other, bool updatedDataOnly) { if (other == null) return; if (!(other is DataArray otherArray)) return; // 기존 변경 추적 목록을 초기화합니다. ClearTrackedChanges(); //Port에 서도 동일한 Id를 사용하는 경우가 있기 때문에 //GroupBy를 사용하여 Id로 그룹화한 후, 각 그룹의 첫 번째 항목을 선택하여 딕셔너리를 생성 var thisDict = this.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First()); var otherDict = otherArray.GroupBy(item => item.Id).ToDictionary(g => g.Key, g => g.First()); var thisIds = new HashSet(thisDict.Keys); var otherIds = new HashSet(otherDict.Keys); if (!updatedDataOnly) { // 제거된 항목 확인 (현재 배열에는 있지만 다른 배열에는 없는 항목) foreach (var id in thisIds.Where(id => !otherIds.Contains(id))) { removedList.Add(thisDict[id]); } } // 추가된 항목 확인 (다른 배열에는 있지만 현재 배열에는 없는 항목) foreach (var id in otherIds.Where(id => !thisIds.Contains(id))) { addedList.Add(otherDict[id].Copy(fromPool: false)); } // 수정된 항목 확인 (양쪽 모두에 있지만 내용이 다른 항목) foreach (var id in thisIds.Where(id => otherIds.Contains(id))) { var thisItem = thisDict[id]; var otherItem = otherDict[id]; // [성능 개선] ToString() 비교는 매우 비효율적입니다. // DataObject.UpdateDifferent를 직접 호출하여 변경 사항을 적용하고, // UpdatedCount를 통해 실제 변경 여부를 확인합니다. thisItem.UpdateDifferent(otherItem, updatedDataOnly); if (thisItem.UpdatedCount > 0) { modifiedList.Add(thisItem); } } // [성능 개선] RemoveAll과 HashSet을 사용하여 제거 작업의 효율성을 높입니다. if (removedList.Count > 0) { // 제거될 객체들을 먼저 풀에 반환합니다. foreach (var item in removedList) { if (item.IsInPool) item.ReturnToPool(); } // 그 다음 리스트에서 제거합니다. var removedItemIds = new HashSet(removedList.Select(i => i.Id)); this.RemoveAll(item => removedItemIds.Contains(item.Id)); } // [성능 개선] base.AddRange를 사용하여 추적 로직을 우회하고 효율적으로 추가합니다. if (addedList.Count > 0) { base.AddRange(addedList); } } /// /// 현재 데이터 객체의 업데이트된 버전을 생성하고 반환합니다. 선택적으로 메모리 할당 최적화를 위해 풀을 사용합니다. /// /// 이 메서드는 현재 데이터 객체와 관련 요소의 깊은 복사를 수행합니다. /// 이면 메서드는 메모리 사용을 최적화하기 위해 객체 풀에서 새 인스턴스를 검색합니다. 그렇지 않으면 새 인스턴스가 직접 생성됩니다. 반환된 /// 객체는 추가, 제거 및 수정을 포함하여 현재 객체의 상태에 대한 업데이트를 반영합니다. /// 변경 사항은 추적되어 업데이트된 객체의 해당 목록에 적용됩니다. /// 객체 풀에서 새 인스턴스를 검색할지 여부를 나타내는 부울 값입니다. /// 객체 풀을 사용합니다. 를 사용하여 새 인스턴스를 직접 생성합니다. /// 추가, 제거 또는 수정된 목록에서 추적된 모든 변경 사항을 포함하여 현재 객체 데이터의 깊은 복사본을 포함하는 업데이트된 인스턴스입니다. /// public IDataObject GetUpdatedObject(bool fromPool = true) { // 풀에서 새 DataArray 인스턴스를 가져옵니다. var clone = fromPool ? DataArrayPool.Get() : new DataArray(); clone.FromCapacity(this.Count); // 배열의 모든 DataObject를 순회하며 각각을 복제합니다. foreach (var item in this) { // DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고, // base.Add를 사용해 추적 로직 없이 직접 추가합니다. if (item.GetUpdatedObject(fromPool) is DataObject updatedObject) { clone.Add(updatedObject); if (addedList.Contains(item)) { clone.addedList.Add(updatedObject); } else if (removedList.Contains(item)) { clone.removedList.Add(updatedObject); } else if (modifiedList.Contains(item)) { clone.modifiedList.Add(updatedObject); } } } return clone; } /// /// 업데이트 된 속성의 수. /// /// 업데이트된 속성의 총 개수입니다. 업데이트된 속성이 없으면0을 반환합니다. public int UpdatedCount { get => addedList.Count + modifiedList.Count + removedList.Count; } private bool isUpdateImmediately = false; /// /// 업데이트가 즉시 적용되어야 하는지 여부를 나타냅니다. /// /// public bool IsUpdateImmediately { get => isUpdateImmediately; set { if (isUpdateImmediately == value) return; isUpdateImmediately = value; // 내부 DataObject에도 동일한 값을 설정합니다. foreach (var item in this) { item.IsUpdateImmediately = value; } } } /// /// 컬렉션의 모든 항목을 제거합니다. /// public new void Clear() { isUpdateImmediately = false; // 내부 리스트와 변경 추적 리스트를 모두 비웁니다. ReturnToDataObjectPool(); base.Clear(); ClearTrackedChanges(); } /// /// 객체를 풀에 반환하기 전에 초기 상태로 리셋합니다. /// public void Reset() { Clear(); } public void ReturnToPool() { if (CreatedFromPool) { Reset(); } else { // 포함된 DataObject들을 먼저 풀에 반환합니다. DataArrayPool.Return(this); } } /// /// 이 DataArray에 포함된 모든 DataObject를 풀에 반환하고 리스트를 비웁니다. /// DataArray의 사용이 끝났을 때 호출해야 합니다. /// private void ReturnToDataObjectPool() { foreach (var item in this) { item.ReturnToPool(); } } /// /// 변경된 인덱스 목록을 초기화합니다. /// public void ClearTrackedChanges() { addedList.Clear(); removedList.Clear(); modifiedList.Clear(); } /// /// 동일한 상태와 값을 가진 현재 데이터 객체의 새 인스턴스를 생성합니다. /// /// 복제된 객체는 원본 객체와 독립적이므로, 한 객체를 변경해도 다른 객체에는 영향을 미치지 않습니다. /// /// 객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다. /// 현재 객체의 복사본인 새 인스턴스를 반환합니다. public IDataObject Clone(bool fromPool = true) { return Copy(fromPool); } /// /// 현재 인스턴스의 요소 및 관련 상태를 포함한 깊은 복사본을 생성합니다. /// /// /// 이 메서드는 현재 배열에 있는 모든 요소의 깊은 복사본을 포함하는 새로운 인스턴스를 반환합니다. /// 복사된 인스턴스는 내부 목록에서 추적하는 추가, 제거 또는 수정된 요소를 포함하여 원본 인스턴스의 상태도 복제합니다. /// /// 객체 풀에서 복제할지 여부를 지정합니다. 기본값은 true입니다. /// 현재 인스턴스의 깊은 복사본인 새로운 인스턴스를 반환합니다. public DataArray Copy(bool fromPool = true) { // 풀에서 새 DataArray 인스턴스를 가져옵니다. DataArray clone; if (fromPool) clone = DataArrayPool.Get(); else clone = new DataArray(); // 배열의 모든 DataObject를 순회하며 각각을 복제합니다. foreach (var item in this) { // DataObject의 Clone 메서드를 호출하여 깊은 복사를 수행하고, // base.Add를 사용해 추적 로직 없이 직접 추가합니다. DataObject clonedItem = item.Copy(fromPool); clone.Add(clonedItem); if (addedList.Contains(item)) { clone.addedList.Add(clonedItem); } else if (removedList.Contains(item)) { clone.removedList.Add(clonedItem); } else if (modifiedList.Contains(item)) { clone.modifiedList.Add(clonedItem); } } return clone; } /// /// DataArray를 JArray로 변환합니다. /// public JArray ToJArray() { JArray array = new JArray(); foreach (var item in this) { array.Add(item); } return array; } /// /// DataArray를 JArray로 암시적 변환합니다. /// public static implicit operator JArray(DataArray dataArray) { return dataArray.ToJArray(); } /// /// JArray를 DataArray로 암시적 변환합니다. /// public static implicit operator DataArray(JArray jArray) { var dataArray = DataArrayPool.Get(); dataArray.FromJArray(jArray); return dataArray; } /// /// JToken을 DataObject로 비동기 재질화하여 반환합니다. 이름: ConvertTokenToDataObjectAsync /// public async UniTask ConvertTokenToDataObjectAsync(JToken token, int batchSize =50, CancellationToken cancellationToken = default) { if (token.Type == JTokenType.Object) { var dataObject = DataObjectPool.Get(); await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken); return dataObject; } else { var dataObject = DataObjectPool.Get(); var value = await MaterializePrimitiveOrCollectionAsync(token, batchSize, cancellationToken); dataObject.Add("value", value); return dataObject; } } private async UniTask MaterializePrimitiveOrCollectionAsync(JToken token, int batchSize, CancellationToken cancellationToken) { 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.Array: JArray array = (JArray)token; var dataArray = DataArrayPool.Get(); await dataArray.FromJArrayIncrementalAsync(array, batchSize, cancellationToken); return dataArray; case JTokenType.Object: var dataObject = DataObjectPool.Get(); await dataObject.FromJObjectIncrementalAsync((JObject)token, batchSize, cancellationToken); return dataObject; default: return token.ToString(); } } // Note: existing synchronous ConvertToDataObject remains for compatibility /// /// 비동기 팩토리: JArray로부터 완전히 점진 재질화된 DataArray를 생성합니다. /// public static async UniTask CreateFromJArrayAsync(JArray jArray, int batchSize =50, CancellationToken cancellationToken = default) { var arr = new DataArray(); await arr.FromJArrayIncrementalAsync(jArray, batchSize, cancellationToken); return arr; } /// /// 비동기 팩토리: JSON 문자열로부터 완전히 점진 재질화된 DataArray를 생성합니다. /// public static async UniTask CreateFromJsonStringAsync(string jsonString, int batchSize =50, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(jsonString)) return new DataArray(); #if UNITY_WEBGL && !UNITY_EDITOR var arr = new DataArray(); await arr.FromJsonStringIncrementalAsync(jsonString, batchSize, cancellationToken); return arr; #else var jArr = await UniTask.RunOnThreadPool(() => JArray.Parse(jsonString)); return await CreateFromJArrayAsync(jArr, batchSize, cancellationToken); #endif } /// /// 비동기 팩토리: 스트림으로부터 완전히 점진 재질화된 DataArray를 생성합니다. /// 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 jArr = serializer.Deserialize(reader); if (jArr == null) return new DataArray(); return await CreateFromJArrayAsync(jArr, batchSize, cancellationToken); } } } }