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
}