Files

565 lines
20 KiB
C#

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