using System; using System.IO; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UVC.Json; namespace UVC.Studio.Config { /// /// Studio 애플리케이션 설정 관리 클래스 /// /// /// [ 개요 ] /// StreamingAssets/Settings.json 파일을 로드/저장하여 애플리케이션 설정을 관리합니다. /// /// [ JSON 파일 구조 ] /// /// Settings.json /// ├── database - 데이터베이스 연결 설정 /// │ ├── ip - DB 서버 IP 주소 /// │ ├── port - DB 서버 포트 /// │ ├── id - 접속 계정 ID /// │ └── password - 접속 계정 비밀번호 /// ├── general - 일반 설정 /// │ ├── autoSaveInterval - 자동 저장 간격 (분) /// │ ├── gridSize - 그리드 크기 /// │ ├── snapPosition - 위치 스냅 단위 /// │ ├── snapRotation - 회전 스냅 단위 (도) /// │ └── snapScale - 스케일 스냅 단위 /// └── shortcuts - 단축키 설정 /// ├── menu - 메뉴 단축키 (newProject, openProject, saveProject, ...) /// └── tools - 도구 단축키 (select, move, rotate, scale, ...) /// /// /// [ 사용 예시 (비동기) ] /// /// // 비동기 로드 /// var setting = new Setting(); /// await setting.LoadAsync(); /// /// // 데이터베이스 설정 접근 /// string dbIp = setting.Data.database.ip; /// int dbPort = setting.Data.database.port; /// /// // 일반 설정 접근 /// float gridSize = setting.Data.general.gridSize; /// float snapPos = setting.Data.general.snapPosition; /// /// // 단축키 설정 접근 /// string moveKey = setting.Data.shortcuts.tools.move.key; // "2" /// string moveLabel = setting.Data.shortcuts.tools.move.label; // "Move Tool" /// string saveDisplay = setting.Data.shortcuts.menu.saveProject.GetDisplayString(); // "Ctrl+Shift+S" /// /// // 설정 변경 후 비동기 저장 /// setting.Data.general.gridSize = 2.0f; /// await setting.SaveAsync(); /// /// /// [ 사용 예시 (동기) ] /// /// // 동기 로드 /// var setting = new Setting(); /// setting.Load(); /// /// // 동기 저장 /// setting.Save(); /// /// /// [ Injector 등록 및 사용 ] /// AppContext에서 Setting을 등록하고, 다른 컴포넌트에서 주입받아 사용합니다. /// /// 1. AppContext에서 등록 (비동기) /// /// public class StudioAppContext : InjectorAppContext /// { /// protected override async UniTask RegisterServicesAsync() /// { /// // Setting 생성 및 비동기 로드 /// var setting = new Setting(); /// await setting.LoadAsync(); /// /// // Injector에 인스턴스 등록 /// Injector.RegisterInstance(setting); /// } /// } /// /// /// 2. AppContext에서 등록 (동기) /// /// public class StudioAppContext : InjectorAppContext /// { /// protected override void RegisterServices() /// { /// // Setting 생성 및 동기 로드 /// var setting = new Setting(); /// setting.Load(); /// /// // Injector에 인스턴스 등록 /// Injector.RegisterInstance(setting); /// } /// } /// /// /// 3. 컴포넌트에서 주입받아 사용 /// /// public class EditorManager : MonoBehaviour /// { /// [Inject] private Setting _setting; /// /// private void Start() /// { /// // 설정 데이터 접근 /// float gridSize = _setting.Data.general.gridSize; /// string moveDisplay = _setting.Data.shortcuts.tools.move.GetDisplayString(); /// /// // 설정 변경 및 저장 /// _setting.Data.general.gridSize = 2.0f; /// _setting.SaveAsync().Forget(); /// } /// } /// /// /// 4. 직접 접근 (Inject 없이) /// /// // InjectorAppContext를 통해 직접 접근 /// var setting = InjectorAppContext.Instance.Get(); /// float gridSize = setting.Data.general.gridSize; /// /// public class Setting { /// 설정 파일 이름 private const string FileName = "Settings.json"; /// 로드된 설정 데이터 public SettingData Data { get; private set; } /// 로드 완료 여부 public bool IsLoaded { get; private set; } private bool useAppDataPath = false; /// /// 생성자 - 기본 데이터로 초기화합니다. /// /// /// 비동기 로드를 사용하려면 생성 후 LoadAsync()를 호출하세요. /// 동기 로드를 원하면 Load()를 호출하세요. /// public Setting(bool useAppDataPath = false) { this.useAppDataPath = useAppDataPath; Data = new SettingData(); IsLoaded = false; } #region Async Methods /// /// StreamingAssets/Settings.json 파일에서 설정을 비동기로 로드합니다. /// /// 취소 토큰 /// 로드 성공 여부 /// /// 파일 읽기와 JSON 파싱을 백그라운드 스레드에서 수행합니다. /// 파일이 없거나 파싱 실패 시 기본값으로 초기화됩니다. /// public async UniTask LoadAsync(CancellationToken cancellationToken = default) { string path = Path.Combine(Application.streamingAssetsPath, "Studio", FileName); if (useAppDataPath) path = Path.Combine(Application.persistentDataPath, FileName); if (!File.Exists(path)) { Debug.LogWarning($"[Setting] File not found: {path}"); Data = new SettingData(); IsLoaded = true; return false; } try { // 파일 읽기를 백그라운드 스레드에서 수행 string json = await UniTask.RunOnThreadPool( () => File.ReadAllText(path), cancellationToken: cancellationToken ); // JSON 파싱을 백그라운드 스레드에서 수행 Data = await UniTask.RunOnThreadPool( () => JsonHelper.FromJson(json), cancellationToken: cancellationToken ); IsLoaded = true; Debug.Log($"[Setting] Loaded successfully from {path}"); return true; } catch (OperationCanceledException) { Debug.Log("[Setting] Load cancelled"); throw; } catch (Exception e) { Debug.LogError($"[Setting] Failed to load: {e.Message}"); Data = new SettingData(); IsLoaded = true; return false; } } /// /// 현재 설정을 StreamingAssets/Settings.json 파일에 비동기로 저장합니다. /// /// 취소 토큰 /// 저장 성공 여부 public async UniTask SaveAsync(CancellationToken cancellationToken = default) { string path = Path.Combine(Application.streamingAssetsPath, "Studio", FileName); if (useAppDataPath) { path = Path.Combine(Application.persistentDataPath, FileName); } try { // JSON 직렬화를 백그라운드 스레드에서 수행 (Newtonsoft.Json 사용으로 float 정밀도 문제 해결) string json = await UniTask.RunOnThreadPool( () => Newtonsoft.Json.JsonConvert.SerializeObject(Data, Newtonsoft.Json.Formatting.Indented), cancellationToken: cancellationToken ); // 파일 쓰기를 백그라운드 스레드에서 수행 await UniTask.RunOnThreadPool( () => File.WriteAllText(path, json), cancellationToken: cancellationToken ); Debug.Log($"[Setting] Saved successfully to {path}"); return true; } catch (OperationCanceledException) { Debug.Log("[Setting] Save cancelled"); throw; } catch (Exception e) { Debug.LogError($"[Setting] Failed to save: {e.Message}"); return false; } } #endregion #region Sync Methods /// /// StreamingAssets/Studio/Settings.json 파일에서 설정을 동기로 로드합니다. /// /// 로드 성공 여부 /// /// 파일이 없거나 파싱 실패 시 기본값으로 초기화됩니다. /// 메인 스레드를 블로킹하므로, 가능하면 LoadAsync() 사용을 권장합니다. /// public bool Load() { string path = Path.Combine(Application.streamingAssetsPath, "Studio", FileName); if (useAppDataPath) { path = Path.Combine(Application.persistentDataPath, FileName); } if (!File.Exists(path)) { Debug.LogWarning($"[Setting] File not found: {path}"); Data = new SettingData(); IsLoaded = true; return false; } try { string json = File.ReadAllText(path); Data = JsonHelper.FromJson(json); IsLoaded = true; Debug.Log($"[Setting] Loaded successfully from {path}"); return true; } catch (Exception e) { Debug.LogError($"[Setting] Failed to load: {e.Message}"); Data = new SettingData(); IsLoaded = true; return false; } } /// /// 현재 설정을 StreamingAssets/Settings.json 파일에 동기로 저장합니다. /// /// 저장 성공 여부 /// /// 메인 스레드를 블로킹하므로, 가능하면 SaveAsync() 사용을 권장합니다. /// public bool Save() { string path = Path.Combine(Application.streamingAssetsPath, "Studio", FileName); if (useAppDataPath) { path = Path.Combine(Application.persistentDataPath, FileName); } try { // Newtonsoft.Json 사용으로 float 정밀도 문제 해결 string json = JsonHelper.ToJson(Data); File.WriteAllText(path, json); Debug.Log($"[Setting] Saved successfully to {path}"); return true; } catch (Exception e) { Debug.LogError($"[Setting] Failed to save: {e.Message}"); return false; } } #endregion } #region Data Classes /// /// Settings.json 루트 데이터 클래스 /// [Serializable] public class SettingData { /// 데이터베이스 연결 설정 public DatabaseSetting database = new(); /// 일반 설정 (그리드, 스냅 등) public GeneralSetting general = new(); /// 단축키 설정 public ShortcutsSetting shortcuts = new(); } /// /// 데이터베이스 연결 설정 /// /// /// MySQL 등 외부 데이터베이스 연결에 필요한 정보를 저장합니다. /// [Serializable] public class DatabaseSetting { /// 데이터베이스 서버 IP 주소 (기본값: 127.0.0.1) public string ip = "127.0.0.1"; /// 데이터베이스 서버 포트 (기본값: 3306 - MySQL 기본 포트) public int port = 3306; /// 접속 계정 ID public string id = "admin"; /// 접속 계정 비밀번호 public string password = "password"; } /// /// 일반 설정 (에디터 동작 관련) /// [Serializable] public class GeneralSetting { /// 자동 저장 간격 (분 단위, 기본값: 5분) public int autoSaveInterval = 5; /// 그리드 표시 크기 (기본값: 1) public float gridSize = 1f; /// 위치 스냅 단위 (기본값: 0.1) public float snapPosition = 0.1f; /// 회전 스냅 단위 (도 단위, 기본값: 5도) public float snapRotation = 5f; /// 스케일 스냅 단위 (기본값: 0.1) public float snapScale = 0.1f; } /// /// 단축키 설정 컨테이너 /// [Serializable] public class ShortcutsSetting { /// 메뉴 관련 단축키 (File, Edit, Create 등) public MenuShortcuts menu = new(); /// 도구 관련 단축키 (Select, Move, Rotate 등) public ToolShortcuts tools = new(); } /// /// 메뉴 단축키 설정 /// /// /// File, Edit, Create 메뉴의 단축키를 정의합니다. /// [Serializable] public class MenuShortcuts { /// 새 프로젝트 (File > New Project) public ShortcutItem newProject = new("N", "File > New Project", ctrl: true, shift: true); /// 프로젝트 열기 (File > Open Project) public ShortcutItem openProject = new("O", "File > Open Project", ctrl: true, shift: true); /// 프로젝트 저장 (File > Save Project) public ShortcutItem saveProject = new("S", "File > Save Project", ctrl: true, shift: true); /// 다른 이름으로 저장 (File > Save As...) public ShortcutItem saveAsProject = new("S", "File > Save As...", ctrl: true, shift: true, alt: true); /// 데이터베이스 삽입 (File > Insert Database) public ShortcutItem insertDb = new("I", "File > Insert Database", ctrl: true, shift: true); /// 레이아웃 내보내기 (File > Export > Layout) public ShortcutItem exportLayout = new("L", "File > Export > Layout", ctrl: true, shift: true); /// 메타데이터 내보내기 (File > Export > Metadata) public ShortcutItem exportMeta = new("M", "File > Export > Metadata", ctrl: true, shift: true); /// glTF 내보내기 (File > Export > glTF) public ShortcutItem exportGltf = new("G", "File > Export > glTF", ctrl: true, shift: true); /// 실행 취소 (Edit > Undo) public ShortcutItem undo = new("Z", "Edit > Undo", ctrl: true, shift: true); /// 다시 실행 (Edit > Redo) public ShortcutItem redo = new("Y", "Edit > Redo", ctrl: true, shift: true); /// 복제 (Edit > Duplicate) public ShortcutItem duplicate = new("D", "Edit > Duplicate", ctrl: true, shift: true); /// 삭제 (Edit > Delete) public ShortcutItem delete = new("DELETE", "Edit > Delete", shift: true); /// 평면 생성 (Create > Plane) public ShortcutItem createPlane = new("V", "Create > Plane", ctrl: true, shift: true); } /// /// 도구 단축키 설정 /// /// /// 에디터 도구 전환에 사용되는 단축키를 정의합니다. /// [Serializable] public class ToolShortcuts { /// 선택 도구 public ShortcutItem select = new("1", "Select Tool"); /// 이동 도구 public ShortcutItem move = new("2", "Move Tool"); /// 회전 도구 public ShortcutItem rotate = new("3", "Rotate Tool"); /// 스케일 도구 public ShortcutItem scale = new("4", "Scale Tool"); /// 스냅 도구 public ShortcutItem snap = new("5", "Snap Tool"); /// 가이드 도구 public ShortcutItem guide = new("6", "Guide Tool"); /// 노드 도구 public ShortcutItem node = new("7", "Node Tool"); /// 링크 도구 public ShortcutItem link = new("8", "Link Tool"); /// 아크 도구 public ShortcutItem arc = new("9", "Arc Tool"); } /// /// 개별 단축키 항목 /// [Serializable] public class ShortcutItem { /// /// 단축키 문자 (1글자, 예: "S", "N", "1") /// public string key; /// /// Ctrl 키 사용 여부 /// public bool ctrl; /// /// Shift 키 사용 여부 /// public bool shift; /// /// Alt 키 사용 여부 /// public bool alt; /// /// 단축키 설명 레이블 /// /// /// UI 표시용 (예: "File > Save Project", "Move Tool") /// public string label; /// 기본 생성자 (JSON 역직렬화용) public ShortcutItem() { } /// /// 단축키 항목 생성 /// /// 단축키 문자 (예: "S") /// 설명 레이블 (예: "File > Save Project") /// Ctrl 키 사용 여부 /// Shift 키 사용 여부 /// Alt 키 사용 여부 public ShortcutItem(string key, string label, bool ctrl = false, bool shift = false, bool alt = false) { this.key = key; this.label = label; this.ctrl = ctrl; this.shift = shift; this.alt = alt; } /// /// 단축키 조합 문자열을 반환합니다. /// /// 표시용 단축키 문자열 (예: "Ctrl+Shift+S") public string GetDisplayString() { var parts = new System.Collections.Generic.List(); if (ctrl) parts.Add("Ctrl"); if (shift) parts.Add("Shift"); if (alt) parts.Add("Alt"); parts.Add(key); return string.Join("+", parts); } } #endregion }