#nullable enable using Cysharp.Threading.Tasks; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEngine; using UVC.Data.Core; using UVC.Extention; namespace UVC.Data { public class UserSetting : OrderedDictionary { // 사용자 데이터를 저장하는 컬렉션입니다. private static Dictionary _userDatas = new Dictionary(); public static IReadOnlyDictionary UserDatas => _userDatas; /// /// 사용자 정의 키를 지정된 세팅에 연결합니다. /// /// 동일한 키를 가진 세팅가 이미 있는 경우 새 /// 마스크로 대체됩니다. /// 세팅의 고유 식별자입니다. 이거나 비어 있을 수 없습니다. /// 지정된 키와 연결할 객체입니다. 일 수 없습니다. public static void AddSetting(string key, UserSetting setting) { if (string.IsNullOrEmpty(key) || setting == null) return; _userDatas[key] = setting; } /// /// 지정된 키와 연관된 사용자 마스크를 제거합니다. /// /// 가 null이거나 비어 있으면 메서드는 아무 작업도 수행하지 않습니다. /// /// 제거할 사용자 마스크를 식별하는 키입니다. null이거나 비어 있으면 안 됩니다. public static void RemoveSetting(string key) { if (string.IsNullOrEmpty(key)) return; _userDatas.Remove(key); } /// /// 지정된 키와 연관된 를 검색합니다. /// /// 연관된 를 찾는 데 사용되는 키입니다. null이거나 비어 있을 수 없습니다. /// 지정된 키와 연관된 또는 키를 찾을 수 없거나 null이거나 비어 있는 경우 을 반환합니다. /// public static UserSetting? Get(string key) { if (string.IsNullOrEmpty(key)) return null; _userDatas.TryGetValue(key, out var setting); return setting; } /// /// 내부 컬렉션에서 모든 사용자 세팅을 지웁니다. /// /// 이 메서드는 사용자 세팅 컬렉션에서 모든 항목을 제거하고 빈 상태로 재설정합니다. /// 일반적으로 새 마스크를 적용하기 전에 이전에 적용된 마스크를 지우는 데 사용됩니다. /// public static void ClearSetting() { _userDatas.Clear(); } /// /// 애플리케이션의 영구 데이터 디렉터리(AppData/LocalLow/Company Name/Product Name)에서 사용자 마스크 데이터를 로드합니다. /// /// 이 메서드는 애플리케이션의 영구 데이터 경로에 있는 "user" 하위 디렉터리에서 JSON 파일을 검색합니다. /// 각 JSON 파일을 읽고 그 내용을 사용하여 /// 객체를 생성한 후 애플리케이션의 사용자 마스크 컬렉션에 추가합니다. 이 메서드는 메인 스레드를 차단하지 않도록 백그라운드 /// 스레드에서 실행됩니다. /// public static async UniTask LoadFromAppData() { //C:\Users\[user name]\AppData\LocalLow\[company name]\[product name] string persistentDataPath = UnityEngine.Application.persistentDataPath; #if UNITY_WEBGL && !UNITY_EDITOR // WebGL: 스레드 사용 불가. 메인 스레드에서 파일 작업 수행하되 프레임 양보로 스톨 방지. string folderPath = System.IO.Path.Combine(persistentDataPath, "user"); DirectoryInfo directory = new DirectoryInfo(folderPath); if (!directory.Exists) return; FileInfo[] files = directory.GetFiles("*.json"); foreach (FileInfo file in files) { // 프레임 양보 await UniTask.Yield(); string filePath = file.FullName; string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath); if (string.IsNullOrEmpty(fileName)) continue; string jsonString = System.IO.File.ReadAllText(filePath); AddSetting(fileName, new UserSetting(jsonString)); } #else await UniTask.RunOnThreadPool(() => { string folderPath = System.IO.Path.Combine(persistentDataPath, "user"); DirectoryInfo directory = new DirectoryInfo(folderPath); if (!directory.Exists) return; FileInfo[] files = directory.GetFiles("*.json"); foreach (FileInfo file in files) { string filePath = file.FullName; string fileName = System.IO.Path.GetFileNameWithoutExtension(filePath); if (string.IsNullOrEmpty(fileName)) continue; string jsonString = System.IO.File.ReadAllText(filePath); AddSetting(fileName, new UserSetting(jsonString)); } }); #endif } /// /// 사용자 세팅을 애플리케이션의 영구 데이터 경로(AppData/LocalLow/Company Name/Product Name)에 저장합니다. /// /// 이 메서드는 모든 사용자 세팅을 반복하며 각 마스크를 JSON 파일로 저장합니다. /// 애플리케이션의 영구 데이터 경로에 있는 "user" 하위 디렉터리에 디렉터리가 없으면 /// 생성됩니다. 각 파일의 이름은 해당 키와 ".json" 확장자를 사용하여 지정됩니다. 저장 과정에서 발생하는 모든 오류는 /// Unity 콘솔에 기록됩니다. /// public static async UniTask SaveToAppData() { string persistentDataPath = UnityEngine.Application.persistentDataPath; #if UNITY_WEBGL && !UNITY_EDITOR foreach (var kv in _userDatas) { await UniTask.Yield(); // 긴 루프 스톨 방지 string key = kv.Key; if (string.IsNullOrEmpty(key)) continue; if (!_userDatas.TryGetValue(key, out var setting)) continue; try { string folderPath = Path.Combine(persistentDataPath, "user"); if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); string filePath = Path.Combine(folderPath, $"{key}.json"); File.WriteAllText(filePath, setting.ToJsonString()); } catch (Exception ex) { Debug.LogError($"SaveToAppData Error(WebGL): {ex.Message}"); } } #else await UniTask.RunOnThreadPool(() => { foreach (var keyValue in _userDatas) { string key = keyValue.Key; if (string.IsNullOrEmpty(key)) continue; if (!_userDatas.TryGetValue(key, out var setting)) return; try { string folderPath = System.IO.Path.Combine(persistentDataPath, "user"); if (!Directory.Exists(folderPath)) Directory.CreateDirectory(folderPath); string filePath = System.IO.Path.Combine(folderPath, $"{key}.json"); string jsonString = setting.ToJsonString(); System.IO.File.WriteAllText(filePath, jsonString); } catch (Exception ex) { Debug.LogError($"SaveToAppData Error: {ex.Message}"); } } }); #endif } public static UserSetting? FromDataObject(DataObject dataObject) { if (dataObject == null) return null; UserSetting setting = new UserSetting(); foreach (var item in dataObject) { setting[item.Key] = item.Key; } return setting; } public UserSetting() { } public UserSetting(string jsonString) { if (string.IsNullOrEmpty(jsonString)) return; JObject jObj = JObject.Parse(jsonString); foreach (var property in jObj.Properties()) { this[property.Name] = ConvertJTokenToObject(property); } } public UserSetting(JObject jObj) { if (jObj == null) return; foreach (var property in jObj.Properties()) { this[property.Name] = ConvertJTokenToObject(property.Value); } } /// /// JToken을 실제 객체로 변환하는 헬퍼 메서드 /// /// 변환할 JToken 객체 /// 변환된 .NET 객체 /// /// 이 메서드는 JToken의 타입에 따라 적절한 .NET 타입의 객체로 변환합니다. /// 객체는 DataObject로, 배열은 DataArray로 변환되며, 기본 타입은 해당하는 .NET 타입으로 변환됩니다. /// private object ConvertJTokenToObject(JToken token) { if (token == null) return null; switch (token.Type) { case JTokenType.Property: JProperty prop = (JProperty)token; return ConvertJTokenToObject(prop.Value); case JTokenType.Object: return new UserSetting((JObject)token); case JTokenType.Array: JArray array = (JArray)token; List settings = new List(); if (array.All(item => item.Type == JTokenType.Object)) { foreach (var item in array) { settings.Add(new UserSetting((JObject)item)); } } return settings; case JTokenType.Integer: return token.ToObject(); case JTokenType.Float: return token.ToObject(); case JTokenType.String: return token.ToObject(); case JTokenType.Boolean: return token.ToObject(); case JTokenType.Date: return token.ToObject(); case JTokenType.Null: return null; default: return token.ToObject(); } } /// /// JObject 타입과의 호환성을 위한 메서드입니다. /// Dictionary에 저장된 모든 속성을 JObject 형식으로 반환합니다. /// /// 현재 UserSetting의 내용을 담은 JObject public JObject ToJObject() { JObject result = new JObject(); foreach (var pair in this) { if (pair.Value is UserSetting setting) { result[pair.Key] = ((UserSetting)pair.Value).ToJObject(); } else { result[pair.Key] = JToken.FromObject(pair.Value); } } return result; } /// /// JObject에서 속성을 가져오기 위한 확장 메서드입니다. /// /// 속성을 추출할 JObject /// JObject의 모든 속성 private IEnumerable> GetProperties(JObject jObj) { foreach (var property in jObj.Properties()) { yield return new KeyValuePair(property.Name, property.Value); } } /// /// Json 문자열로 변환합니다. /// /// public string ToJsonString() { return ToJObject().ToString(Newtonsoft.Json.Formatting.Indented); } } }