Studio 모달 완료
This commit is contained in:
699
Assets/Scripts/Studio/Config/Library.cs
Normal file
699
Assets/Scripts/Studio/Config/Library.cs
Normal file
@@ -0,0 +1,699 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.Json;
|
||||
|
||||
namespace UVC.Studio.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// 장비 라이브러리 데이터 관리 클래스
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>[ 개요 ]</b></para>
|
||||
/// <para>StreamingAssets 폴더의 Library JSON 파일들을 로드하여 장비 정보를 관리합니다.</para>
|
||||
/// <para>지원 파일: LibraryStakerCrane.json, LibraryRackSingle.json, LibraryAGV.json</para>
|
||||
///
|
||||
/// <para><b>[ JSON 파일 구조 ]</b></para>
|
||||
/// <code>
|
||||
/// LibraryXXX.json
|
||||
/// └── [equipmentType]: List<EquipmentItem>
|
||||
/// └── EquipmentItem
|
||||
/// ├── model - 모델 식별자 (예: "SingleFork")
|
||||
/// ├── label - 표시 이름 (예: "Single Fork")
|
||||
/// ├── gltf - glTF 파일 경로 (예: "staker_crane/SingleFork.glb")
|
||||
/// ├── image - 썸네일 이미지 경로 (선택)
|
||||
/// ├── propertiesInfo - 속성 정보 목록
|
||||
/// │ └── PropertiesInfo
|
||||
/// │ ├── section - 섹션 이름 (예: "Frame", "Fork")
|
||||
/// │ ├── modelPart - 3D 모델 파트 이름 (예: "Fork_01")
|
||||
/// │ ├── modelType - 모델 타입 (General, Stretch, Repeat)
|
||||
/// │ └── properties - 속성 항목 목록
|
||||
/// │ └── PropertyItem
|
||||
/// │ ├── id - 속성 ID (선택)
|
||||
/// │ ├── label - 표시 이름 (예: "너비")
|
||||
/// │ ├── type - 데이터 타입 (Float, Int)
|
||||
/// │ ├── value - 값 (문자열)
|
||||
/// │ └── unit - 단위 (예: "cm", "cm/s")
|
||||
/// └── statusInfo - 상태 정보
|
||||
/// ├── network - 네트워크 상태 정의
|
||||
/// └── equipment - 설비 상태 정의
|
||||
/// └── StatusSection
|
||||
/// ├── section - 섹션 이름
|
||||
/// └── properties - 상태 항목 목록
|
||||
/// └── StatusProperty
|
||||
/// ├── label - 상태 값 (예: "0", "true")
|
||||
/// ├── stat - 상태 이름 (예: "normal", "running")
|
||||
/// └── value - 색상 코드 (예: "#228B22")
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>[ 사용 예시 (비동기) ]</b></para>
|
||||
/// <code>
|
||||
/// // 비동기 로드 - 모든 라이브러리
|
||||
/// var library = new Library();
|
||||
/// await library.LoadAllAsync();
|
||||
///
|
||||
/// // 비동기 로드 - 개별 라이브러리
|
||||
/// await library.LoadStakerCraneAsync();
|
||||
/// await library.LoadAGVAsync();
|
||||
///
|
||||
/// // 병렬 로드
|
||||
/// await library.LoadAllParallelAsync();
|
||||
///
|
||||
/// // 데이터 접근
|
||||
/// foreach (var crane in library.StakerCraneData.stakerCrane)
|
||||
/// {
|
||||
/// Debug.Log($"Model: {crane.model}, Label: {crane.label}");
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>[ 사용 예시 (동기) ]</b></para>
|
||||
/// <code>
|
||||
/// // 동기 로드
|
||||
/// var library = new Library();
|
||||
/// library.LoadAll();
|
||||
///
|
||||
/// // 개별 로드
|
||||
/// library.LoadStakerCrane();
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>[ 지원 장비 타입 ]</b></para>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>StakerCrane:</b> 스태커 크레인 - 자동 창고 시스템의 입출고 장비</description></item>
|
||||
/// <item><description><b>RackSingle:</b> 단일 랙 - 물품 보관 선반</description></item>
|
||||
/// <item><description><b>AGV:</b> 무인 운반차 (Automated Guided Vehicle)</description></item>
|
||||
/// </list>
|
||||
///
|
||||
/// <para><b>[ Injector 등록 및 사용 ]</b></para>
|
||||
/// <para>AppContext에서 Library를 등록하고, 다른 컴포넌트에서 주입받아 사용합니다.</para>
|
||||
///
|
||||
/// <para><b>1. AppContext에서 등록 (비동기 - 병렬 로드)</b></para>
|
||||
/// <code>
|
||||
/// public class StudioAppContext : InjectorAppContext
|
||||
/// {
|
||||
/// protected override async UniTask RegisterServicesAsync()
|
||||
/// {
|
||||
/// // Library 생성 및 병렬 비동기 로드 (가장 빠름)
|
||||
/// var library = new Library();
|
||||
/// await library.LoadAllParallelAsync();
|
||||
///
|
||||
/// // Injector에 인스턴스 등록
|
||||
/// Injector.RegisterInstance<Library>(library);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>2. AppContext에서 등록 (동기)</b></para>
|
||||
/// <code>
|
||||
/// public class StudioAppContext : InjectorAppContext
|
||||
/// {
|
||||
/// protected override void RegisterServices()
|
||||
/// {
|
||||
/// // Library 생성 및 동기 로드
|
||||
/// var library = new Library();
|
||||
/// library.LoadAll();
|
||||
///
|
||||
/// // Injector에 인스턴스 등록
|
||||
/// Injector.RegisterInstance<Library>(library);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>3. 컴포넌트에서 주입받아 사용</b></para>
|
||||
/// <code>
|
||||
/// public class EquipmentSpawner : MonoBehaviour
|
||||
/// {
|
||||
/// [Inject] private Library _library;
|
||||
///
|
||||
/// private void Start()
|
||||
/// {
|
||||
/// // 스태커 크레인 데이터 접근
|
||||
/// foreach (var crane in _library.StakerCraneData.stakerCrane)
|
||||
/// {
|
||||
/// Debug.Log($"Model: {crane.model}, GLTF: {crane.gltf}");
|
||||
/// }
|
||||
///
|
||||
/// // AGV 데이터 접근
|
||||
/// foreach (var agv in _library.AGVData.AGV)
|
||||
/// {
|
||||
/// Debug.Log($"AGV: {agv.label}");
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// public EquipmentItem GetEquipmentByModel(string model)
|
||||
/// {
|
||||
/// return _library.StakerCraneData.stakerCrane
|
||||
/// .FirstOrDefault(e => e.model == model);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>4. 직접 접근 (Inject 없이)</b></para>
|
||||
/// <code>
|
||||
/// // InjectorAppContext를 통해 직접 접근
|
||||
/// var library = InjectorAppContext.Instance.Get<Library>();
|
||||
/// var craneList = library.StakerCraneData.stakerCrane;
|
||||
/// </code>
|
||||
///
|
||||
/// <para><b>5. Setting과 Library 함께 등록</b></para>
|
||||
/// <code>
|
||||
/// public class StudioAppContext : InjectorAppContext
|
||||
/// {
|
||||
/// protected override async UniTask RegisterServicesAsync()
|
||||
/// {
|
||||
/// // Setting과 Library를 병렬로 로드
|
||||
/// var setting = new Setting();
|
||||
/// var library = new Library();
|
||||
///
|
||||
/// await UniTask.WhenAll(
|
||||
/// setting.LoadAsync(),
|
||||
/// library.LoadAllParallelAsync()
|
||||
/// );
|
||||
///
|
||||
/// // 인스턴스 등록
|
||||
/// Injector.RegisterInstance<Setting>(setting);
|
||||
/// Injector.RegisterInstance<Library>(library);
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// </remarks>
|
||||
public class Library
|
||||
{
|
||||
/// <summary>스태커 크레인 라이브러리 파일명</summary>
|
||||
private const string StakerCraneFileName = "LibraryStakerCrane.json";
|
||||
|
||||
/// <summary>단일 랙 라이브러리 파일명</summary>
|
||||
private const string RackSingleFileName = "LibraryRackSingle.json";
|
||||
|
||||
/// <summary>AGV 라이브러리 파일명</summary>
|
||||
private const string AGVFileName = "LibraryAGV.json";
|
||||
|
||||
/// <summary>스태커 크레인 데이터</summary>
|
||||
public LibraryStakerCraneData StakerCraneData { get; private set; }
|
||||
|
||||
/// <summary>단일 랙 데이터</summary>
|
||||
public LibraryRackSingleData RackSingleData { get; private set; }
|
||||
|
||||
/// <summary>AGV 데이터</summary>
|
||||
public LibraryAGVData AGVData { get; private set; }
|
||||
|
||||
/// <summary>모든 라이브러리 로드 완료 여부</summary>
|
||||
public bool IsLoaded { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 생성자 - 기본 데이터로 초기화합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 비동기 로드를 사용하려면 생성 후 LoadAllAsync()를 호출하세요.
|
||||
/// 동기 로드를 원하면 LoadAll()을 호출하세요.
|
||||
/// </remarks>
|
||||
public Library()
|
||||
{
|
||||
StakerCraneData = new LibraryStakerCraneData();
|
||||
RackSingleData = new LibraryRackSingleData();
|
||||
AGVData = new LibraryAGVData();
|
||||
IsLoaded = false;
|
||||
}
|
||||
|
||||
#region Async Methods
|
||||
|
||||
/// <summary>
|
||||
/// 모든 라이브러리 파일을 순차적으로 비동기 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
public async UniTask LoadAllAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await LoadStakerCraneAsync(cancellationToken);
|
||||
await LoadRackSingleAsync(cancellationToken);
|
||||
await LoadAGVAsync(cancellationToken);
|
||||
IsLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 라이브러리 파일을 병렬로 비동기 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
/// <remarks>
|
||||
/// 세 파일을 동시에 로드하여 전체 로딩 시간을 단축합니다.
|
||||
/// </remarks>
|
||||
public async UniTask LoadAllParallelAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await UniTask.WhenAll(
|
||||
LoadStakerCraneAsync(cancellationToken),
|
||||
LoadRackSingleAsync(cancellationToken),
|
||||
LoadAGVAsync(cancellationToken)
|
||||
);
|
||||
IsLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryStakerCrane.json 파일을 비동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public async UniTask<bool> LoadStakerCraneAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await LoadJsonAsync<LibraryStakerCraneData>(StakerCraneFileName, cancellationToken);
|
||||
StakerCraneData = result ?? new LibraryStakerCraneData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryRackSingle.json 파일을 비동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public async UniTask<bool> LoadRackSingleAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await LoadJsonAsync<LibraryRackSingleData>(RackSingleFileName, cancellationToken);
|
||||
RackSingleData = result ?? new LibraryRackSingleData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryAGV.json 파일을 비동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public async UniTask<bool> LoadAGVAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await LoadJsonAsync<LibraryAGVData>(AGVFileName, cancellationToken);
|
||||
AGVData = result ?? new LibraryAGVData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 파일을 비동기로 로드하여 지정된 타입으로 역직렬화합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">역직렬화 대상 타입</typeparam>
|
||||
/// <param name="fileName">로드할 파일명</param>
|
||||
/// <param name="cancellationToken">취소 토큰</param>
|
||||
/// <returns>역직렬화된 데이터, 실패 시 null</returns>
|
||||
/// <remarks>
|
||||
/// 파일 읽기와 JSON 파싱을 백그라운드 스레드에서 수행합니다.
|
||||
/// </remarks>
|
||||
private async UniTask<T> LoadJsonAsync<T>(string fileName, CancellationToken cancellationToken = default) where T : class
|
||||
{
|
||||
string path = Path.Combine(Application.streamingAssetsPath, fileName);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Debug.LogWarning($"[Library] File not found: {path}");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 파일 읽기를 백그라운드 스레드에서 수행
|
||||
string json = await UniTask.RunOnThreadPool(
|
||||
() => File.ReadAllText(path),
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
// JSON 파싱을 백그라운드 스레드에서 수행
|
||||
T data = await UniTask.RunOnThreadPool(
|
||||
() => JsonHelper.FromJson<T>(json),
|
||||
cancellationToken: cancellationToken
|
||||
);
|
||||
|
||||
Debug.Log($"[Library] Loaded successfully: {fileName}");
|
||||
return data;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Debug.Log($"[Library] Load cancelled: {fileName}");
|
||||
throw;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[Library] Failed to load {fileName}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sync Methods
|
||||
|
||||
/// <summary>
|
||||
/// 모든 라이브러리 파일을 동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 메인 스레드를 블로킹하므로, 가능하면 LoadAllAsync() 또는 LoadAllParallelAsync() 사용을 권장합니다.
|
||||
/// </remarks>
|
||||
public void LoadAll()
|
||||
{
|
||||
LoadStakerCrane();
|
||||
LoadRackSingle();
|
||||
LoadAGV();
|
||||
IsLoaded = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryStakerCrane.json 파일을 동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadStakerCrane()
|
||||
{
|
||||
var result = LoadJson<LibraryStakerCraneData>(StakerCraneFileName);
|
||||
StakerCraneData = result ?? new LibraryStakerCraneData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryRackSingle.json 파일을 동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadRackSingle()
|
||||
{
|
||||
var result = LoadJson<LibraryRackSingleData>(RackSingleFileName);
|
||||
RackSingleData = result ?? new LibraryRackSingleData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryAGV.json 파일을 동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
public bool LoadAGV()
|
||||
{
|
||||
var result = LoadJson<LibraryAGVData>(AGVFileName);
|
||||
AGVData = result ?? new LibraryAGVData();
|
||||
return result != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON 파일을 동기로 로드하여 지정된 타입으로 역직렬화합니다.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">역직렬화 대상 타입</typeparam>
|
||||
/// <param name="fileName">로드할 파일명</param>
|
||||
/// <returns>역직렬화된 데이터, 실패 시 null</returns>
|
||||
private T LoadJson<T>(string fileName) where T : class
|
||||
{
|
||||
string path = Path.Combine(Application.streamingAssetsPath, fileName);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
Debug.LogWarning($"[Library] File not found: {path}");
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
T data = JsonHelper.FromJson<T>(json);
|
||||
Debug.Log($"[Library] Loaded successfully: {fileName}");
|
||||
return data;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[Library] Failed to load {fileName}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Root Data Classes
|
||||
|
||||
/// <summary>
|
||||
/// LibraryStakerCrane.json 루트 데이터 클래스
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class LibraryStakerCraneData
|
||||
{
|
||||
/// <summary>스태커 크레인 장비 목록</summary>
|
||||
public List<EquipmentItem> stakerCrane = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryRackSingle.json 루트 데이터 클래스
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// JSON 키가 "AGV"로 되어 있어 필드명이 AGV입니다.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class LibraryRackSingleData
|
||||
{
|
||||
/// <summary>랙 장비 목록 (JSON 키: "AGV")</summary>
|
||||
public List<EquipmentItem> AGV = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// LibraryAGV.json 루트 데이터 클래스
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class LibraryAGVData
|
||||
{
|
||||
/// <summary>AGV 장비 목록</summary>
|
||||
public List<EquipmentItem> AGV = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Equipment Item
|
||||
|
||||
/// <summary>
|
||||
/// 개별 장비 항목 데이터
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 장비의 기본 정보, 3D 모델 경로, 속성 정의, 상태 정의를 포함합니다.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class EquipmentItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 모델 식별자
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 고유한 모델 ID (예: "SingleFork", "Rack_Single")
|
||||
/// </remarks>
|
||||
public string model;
|
||||
|
||||
/// <summary>
|
||||
/// 표시 이름
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI에 표시되는 사람이 읽을 수 있는 이름 (예: "Single Fork")
|
||||
/// </remarks>
|
||||
public string label;
|
||||
|
||||
/// <summary>
|
||||
/// glTF/GLB 파일 경로
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Resources/Studio 폴더 기준 상대 경로 (예: "staker_crane/SingleFork.glb")
|
||||
/// </remarks>
|
||||
public string gltf;
|
||||
|
||||
/// <summary>
|
||||
/// 썸네일 이미지 경로 (선택)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 라이브러리 UI에서 표시할 미리보기 이미지
|
||||
/// </remarks>
|
||||
public string image;
|
||||
|
||||
/// <summary>
|
||||
/// 속성 정보 목록
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 장비의 각 파트별 속성 정의 (크기, 속도 등)
|
||||
/// </remarks>
|
||||
public List<PropertiesInfo> propertiesInfo = new();
|
||||
|
||||
/// <summary>
|
||||
/// 상태 정보
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 네트워크 연결 상태, 설비 운영 상태 등의 정의
|
||||
/// </remarks>
|
||||
public StatusInfo statusInfo = new();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Properties Info
|
||||
|
||||
/// <summary>
|
||||
/// 속성 정보 섹션
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 장비의 특정 파트(Frame, Fork 등)에 대한 속성 그룹을 정의합니다.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class PropertiesInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 섹션 이름
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI에서 그룹핑 표시용 (예: "Frame", "Fork", "Carriage")
|
||||
/// </remarks>
|
||||
public string section;
|
||||
|
||||
/// <summary>
|
||||
/// 3D 모델 파트 이름
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// glTF 모델 내의 노드/메시 이름 (예: "Fork_01", "Carriage_Body")
|
||||
/// null이면 모델 전체에 적용
|
||||
/// </remarks>
|
||||
public string modelPart;
|
||||
|
||||
/// <summary>
|
||||
/// 모델 타입
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>General:</b> 일반 타입 - 단순 속성 표시</description></item>
|
||||
/// <item><description><b>Stretch:</b> 스트레치 타입 - 크기 조절 가능</description></item>
|
||||
/// <item><description><b>Repeat:</b> 반복 타입 - 반복 배치 가능 (레일 등)</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string modelType;
|
||||
|
||||
/// <summary>
|
||||
/// 속성 항목 목록
|
||||
/// </summary>
|
||||
public List<PropertyItem> properties = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 속성 항목
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class PropertyItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 속성 ID (선택)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 코드에서 속성을 식별하기 위한 고유 키 (예: "length", "forwardSpeed")
|
||||
/// </remarks>
|
||||
public string id;
|
||||
|
||||
/// <summary>
|
||||
/// 표시 레이블
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI에 표시되는 속성 이름 (예: "너비", "주행 속도")
|
||||
/// </remarks>
|
||||
public string label;
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 타입
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// "Float" 또는 "Int"
|
||||
/// </remarks>
|
||||
public string type;
|
||||
|
||||
/// <summary>
|
||||
/// 속성 값 (문자열)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 숫자 값 또는 벡터 문자열 (예: "150", "0,0,1")
|
||||
/// </remarks>
|
||||
public string value;
|
||||
|
||||
/// <summary>
|
||||
/// 단위
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 표시용 단위 문자열 (예: "cm", "cm/s", "")
|
||||
/// </remarks>
|
||||
public string unit;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Status Info
|
||||
|
||||
/// <summary>
|
||||
/// 상태 정보 컨테이너
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 장비의 네트워크 연결 상태와 운영 상태를 정의합니다.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class StatusInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// 네트워크 상태 정의
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 상위 시스템과의 통신 상태 (connect/disconnect)
|
||||
/// null이면 네트워크 상태 미지원
|
||||
/// </remarks>
|
||||
public StatusSection network;
|
||||
|
||||
/// <summary>
|
||||
/// 설비 상태 정의
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 장비의 운영 상태 (normal, running, error 등)
|
||||
/// </remarks>
|
||||
public StatusSection equipment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상태 섹션
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class StatusSection
|
||||
{
|
||||
/// <summary>
|
||||
/// 섹션 이름
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI 표시용 (예: "상위 통신 상태", "설비 상태")
|
||||
/// </remarks>
|
||||
public string section;
|
||||
|
||||
/// <summary>
|
||||
/// 상태 속성 목록
|
||||
/// </summary>
|
||||
public List<StatusProperty> properties = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 상태 속성
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 특정 상태 값에 대한 정의 (이름, 색상 등)
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class StatusProperty
|
||||
{
|
||||
/// <summary>
|
||||
/// 상태 값 (매칭용)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 실제 데이터와 매칭하기 위한 값 (예: "0", "1", "true", "false")
|
||||
/// </remarks>
|
||||
public string label;
|
||||
|
||||
/// <summary>
|
||||
/// 상태 이름
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 상태의 의미를 나타내는 이름 (예: "normal", "running", "error", "connect")
|
||||
/// </remarks>
|
||||
public string stat;
|
||||
|
||||
/// <summary>
|
||||
/// 상태 색상 (HEX 코드)
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI 표시용 색상 코드 (예: "#228B22" - 녹색, "#8B0000" - 빨간색)
|
||||
/// 빈 문자열이면 기본 색상 사용
|
||||
/// </remarks>
|
||||
public string value;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Scripts/Studio/Config/Library.cs.meta
Normal file
2
Assets/Scripts/Studio/Config/Library.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 618dd606311f7cf4a9d818235d3263ba
|
||||
529
Assets/Scripts/Studio/Config/Setting.cs
Normal file
529
Assets/Scripts/Studio/Config/Setting.cs
Normal file
@@ -0,0 +1,529 @@
|
||||
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; // "W"
|
||||
/// string moveLabel = setting.Data.shortcuts.tools.move.label; // "Move Tool"
|
||||
/// string saveKey = setting.Data.shortcuts.menu.saveProject.key; // "Ctrl+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 moveKey = _setting.Data.shortcuts.tools.move.key;
|
||||
///
|
||||
/// // 설정 변경 및 저장
|
||||
/// _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, 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, 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/Settings.json 파일에서 설정을 동기로 로드합니다.
|
||||
/// </summary>
|
||||
/// <returns>로드 성공 여부</returns>
|
||||
/// <remarks>
|
||||
/// 파일이 없거나 파싱 실패 시 기본값으로 초기화됩니다.
|
||||
/// 메인 스레드를 블로킹하므로, 가능하면 LoadAsync() 사용을 권장합니다.
|
||||
/// </remarks>
|
||||
public bool Load()
|
||||
{
|
||||
string path = Path.Combine(Application.streamingAssetsPath, 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, 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("Ctrl+N", "File > New Project");
|
||||
|
||||
/// <summary>프로젝트 열기 (File > Open Project)</summary>
|
||||
public ShortcutItem openProject = new("Ctrl+O", "File > Open Project");
|
||||
|
||||
/// <summary>프로젝트 저장 (File > Save Project)</summary>
|
||||
public ShortcutItem saveProject = new("Ctrl+S", "File > Save Project");
|
||||
|
||||
/// <summary>다른 이름으로 저장 (File > Save As...)</summary>
|
||||
public ShortcutItem saveAsProject = new("Ctrl+Shift+S", "File > Save As...");
|
||||
|
||||
/// <summary>데이터베이스 삽입 (File > Insert Database)</summary>
|
||||
public ShortcutItem insertDb = new("Ctrl+I", "File > Insert Database");
|
||||
|
||||
/// <summary>레이아웃 내보내기 (File > Export > Layout)</summary>
|
||||
public ShortcutItem exportLayout = new("Ctrl+L", "File > Export > Layout");
|
||||
|
||||
/// <summary>메타데이터 내보내기 (File > Export > Metadata)</summary>
|
||||
public ShortcutItem exportMeta = new("Ctrl+M", "File > Export > Metadata");
|
||||
|
||||
/// <summary>glTF 내보내기 (File > Export > glTF)</summary>
|
||||
public ShortcutItem exportGltf = new("Ctrl+G", "File > Export > glTF");
|
||||
|
||||
/// <summary>실행 취소 (Edit > Undo)</summary>
|
||||
public ShortcutItem undo = new("Ctrl+Z", "Edit > Undo");
|
||||
|
||||
/// <summary>다시 실행 (Edit > Redo)</summary>
|
||||
public ShortcutItem redo = new("Ctrl+Shift+Z", "Edit > Redo");
|
||||
|
||||
/// <summary>복제 (Edit > Duplicate)</summary>
|
||||
public ShortcutItem duplicate = new("Ctrl+D", "Edit > Duplicate");
|
||||
|
||||
/// <summary>삭제 (Edit > Delete)</summary>
|
||||
public ShortcutItem delete = new("Delete", "Edit > Delete");
|
||||
|
||||
/// <summary>평면 생성 (Create > Plane)</summary>
|
||||
public ShortcutItem createPlane = new("Ctrl+P", "Create > Plane");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 도구 단축키 설정
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 에디터 도구 전환에 사용되는 단축키를 정의합니다.
|
||||
/// Unity 에디터의 기본 단축키(Q, W, E, R)와 유사한 구조입니다.
|
||||
/// </remarks>
|
||||
[Serializable]
|
||||
public class ToolShortcuts
|
||||
{
|
||||
/// <summary>선택 도구</summary>
|
||||
public ShortcutItem select = new("Q", "Select Tool");
|
||||
|
||||
/// <summary>이동 도구</summary>
|
||||
public ShortcutItem move = new("W", "Move Tool");
|
||||
|
||||
/// <summary>회전 도구</summary>
|
||||
public ShortcutItem rotate = new("E", "Rotate Tool");
|
||||
|
||||
/// <summary>스케일 도구</summary>
|
||||
public ShortcutItem scale = new("R", "Scale Tool");
|
||||
|
||||
/// <summary>스냅 도구</summary>
|
||||
public ShortcutItem snap = new("S", "Snap Tool");
|
||||
|
||||
/// <summary>가이드 도구</summary>
|
||||
public ShortcutItem guide = new("G", "Guide Tool");
|
||||
|
||||
/// <summary>노드 도구</summary>
|
||||
public ShortcutItem node = new("N", "Node Tool");
|
||||
|
||||
/// <summary>링크 도구</summary>
|
||||
public ShortcutItem link = new("L", "Link Tool");
|
||||
|
||||
/// <summary>아크 도구</summary>
|
||||
public ShortcutItem arc = new("A", "Arc Tool");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 개별 단축키 항목
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ShortcutItem
|
||||
{
|
||||
/// <summary>
|
||||
/// 단축키 조합 문자열
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 형식: "[Modifier+]Key" (예: "Ctrl+S", "Ctrl+Shift+Z", "Delete", "W")
|
||||
/// </remarks>
|
||||
public string key;
|
||||
|
||||
/// <summary>
|
||||
/// 단축키 설명 레이블
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// UI 표시용 (예: "File > Save Project", "Move Tool")
|
||||
/// </remarks>
|
||||
public string label;
|
||||
|
||||
/// <summary>기본 생성자 (JSON 역직렬화용)</summary>
|
||||
public ShortcutItem() { }
|
||||
|
||||
/// <summary>
|
||||
/// 단축키 항목 생성
|
||||
/// </summary>
|
||||
/// <param name="key">단축키 조합 (예: "Ctrl+S")</param>
|
||||
/// <param name="label">설명 레이블 (예: "File > Save Project")</param>
|
||||
public ShortcutItem(string key, string label)
|
||||
{
|
||||
this.key = key;
|
||||
this.label = label;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
2
Assets/Scripts/Studio/Config/Setting.cs.meta
Normal file
2
Assets/Scripts/Studio/Config/Setting.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 892ce8db40260e348bdf934c524d4ab6
|
||||
8
Assets/Scripts/Studio/Modal.meta
Normal file
8
Assets/Scripts/Studio/Modal.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 954cbb1fe9711f348b5980ab49c3c50b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
Assets/Scripts/Studio/Modal/Settings.meta
Normal file
8
Assets/Scripts/Studio/Modal/Settings.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 687ff5a74bf63014bb78b7b7a92762bf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,128 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
using UVC.Studio.Config;
|
||||
using UVC.UI.Tab;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingDatabaseTabContent : MonoBehaviour, ITabContent
|
||||
{
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? ipTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? portTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? idTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? pwTxt;
|
||||
|
||||
[Inject]
|
||||
private Setting? setting;
|
||||
|
||||
private bool changedValue = false;
|
||||
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠에 데이터를 전달합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public async void SetContentData(object? data)
|
||||
{
|
||||
Debug.Log($"SettingDatabaseTabContent SetContentData: {data} {setting == null}");
|
||||
|
||||
if (setting == null)
|
||||
{
|
||||
await InjectorAppContext.Instance.WaitForInitializationAsync();
|
||||
setting = InjectorAppContext.Instance.Get<Setting>();
|
||||
}
|
||||
|
||||
Debug.Log($"SettingDatabaseTabContent SetContentData: {data} {setting == null}");
|
||||
|
||||
if (setting != null)
|
||||
{
|
||||
changedValue = false;
|
||||
|
||||
DatabaseSetting database = setting.Data.database;
|
||||
if (ipTxt != null)
|
||||
{
|
||||
ipTxt.text = database.ip;
|
||||
ipTxt.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
setting.Data.database.ip = value;
|
||||
changedValue = true;
|
||||
});
|
||||
}
|
||||
if (portTxt != null)
|
||||
{
|
||||
portTxt.text = database.port.ToString();
|
||||
portTxt.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
if (int.TryParse(value, out int intValue))
|
||||
{
|
||||
setting.Data.database.port = intValue;
|
||||
changedValue = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (idTxt != null)
|
||||
{
|
||||
idTxt.text = database.id;
|
||||
idTxt.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
setting.Data.database.id = value;
|
||||
changedValue = true;
|
||||
});
|
||||
}
|
||||
if (pwTxt != null)
|
||||
{
|
||||
pwTxt.text = database.password;
|
||||
pwTxt.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
setting.Data.database.password = value;
|
||||
changedValue = true;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public void UpdateContentData(object? data)
|
||||
{
|
||||
if (data != null && data is string content)
|
||||
{
|
||||
Debug.Log($"UpdateContentData: {content}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 닫힐 때 실행되는 로직을 처리합니다.
|
||||
/// </summary>
|
||||
/// <returns>비동기 닫기 작업을 나타내는 <see cref="UniTask"/>입니다.</returns>
|
||||
public async UniTask OnCloseAsync()
|
||||
{
|
||||
if (ipTxt != null) ipTxt.onEndEdit.RemoveAllListeners();
|
||||
if (portTxt != null) portTxt.onEndEdit.RemoveAllListeners();
|
||||
if (idTxt != null) idTxt.onEndEdit.RemoveAllListeners();
|
||||
if (pwTxt != null) pwTxt.onEndEdit.RemoveAllListeners();
|
||||
|
||||
Debug.Log($"SettingDatabaseTabContent OnCloseAsync: changedValue={changedValue} setting == null:{setting == null}");
|
||||
if (changedValue && setting != null)
|
||||
{
|
||||
await setting.SaveAsync();
|
||||
Debug.Log("Database settings saved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 695dd0b8bdbe7de4aa8f2a5b277d36dc
|
||||
159
Assets/Scripts/Studio/Modal/Settings/SettingGeneralTabContent.cs
Normal file
159
Assets/Scripts/Studio/Modal/Settings/SettingGeneralTabContent.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UVC.Core;
|
||||
using UVC.Studio.Config;
|
||||
using UVC.UI.Tab;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingGeneralTabContent : MonoBehaviour, ITabContent
|
||||
{
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? autoSaveValueTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? gridValueTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? positionSnapValueTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? RotationSnapValueTxt;
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? ScaleSnapValueTxt;
|
||||
|
||||
[Inject]
|
||||
private Setting? setting;
|
||||
|
||||
private bool changedValue = false;
|
||||
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠에 데이터를 전달합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public async void SetContentData(object? data)
|
||||
{
|
||||
Debug.Log($"SettingGeneralTabContent SetContentData: {data}");
|
||||
|
||||
if (setting == null)
|
||||
{
|
||||
await InjectorAppContext.Instance.WaitForInitializationAsync();
|
||||
setting = InjectorAppContext.Instance.Get<Setting>();
|
||||
}
|
||||
|
||||
if (setting != null)
|
||||
{
|
||||
changedValue = false;
|
||||
|
||||
GeneralSetting general = setting.Data.general;
|
||||
if (autoSaveValueTxt != null)
|
||||
{
|
||||
autoSaveValueTxt.text = general.autoSaveInterval.ToString();
|
||||
autoSaveValueTxt.onEndEdit.AddListener(onChagedTextAutoSave);
|
||||
}
|
||||
if (gridValueTxt != null)
|
||||
{
|
||||
gridValueTxt.text = general.gridSize.ToString();
|
||||
gridValueTxt.onEndEdit.AddListener(onChagedTextGrid);
|
||||
}
|
||||
if (positionSnapValueTxt != null)
|
||||
{
|
||||
positionSnapValueTxt.text = general.snapPosition.ToString();
|
||||
positionSnapValueTxt.onEndEdit.AddListener(onChagedTextSnapPosition);
|
||||
}
|
||||
if (RotationSnapValueTxt != null)
|
||||
{
|
||||
RotationSnapValueTxt.text = general.snapRotation.ToString();
|
||||
RotationSnapValueTxt.onEndEdit.AddListener(onChagedTextRotationSnap);
|
||||
}
|
||||
if (ScaleSnapValueTxt != null)
|
||||
{
|
||||
ScaleSnapValueTxt.text = general.snapScale.ToString();
|
||||
ScaleSnapValueTxt.onEndEdit.AddListener(onChagedTextScaleSnap);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onChagedTextAutoSave(string value)
|
||||
{
|
||||
if (setting != null && int.TryParse(value, out int intValue))
|
||||
{
|
||||
setting.Data.general.autoSaveInterval = intValue;
|
||||
changedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onChagedTextGrid(string value)
|
||||
{
|
||||
if (setting != null && float.TryParse(value, out float floatValue))
|
||||
{
|
||||
setting.Data.general.gridSize = floatValue;
|
||||
changedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onChagedTextSnapPosition(string value)
|
||||
{
|
||||
if (setting != null && float.TryParse(value, out float floatValue))
|
||||
{
|
||||
setting.Data.general.snapPosition = floatValue;
|
||||
changedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onChagedTextRotationSnap(string value)
|
||||
{
|
||||
if (setting != null && float.TryParse(value, out float floatValue))
|
||||
{
|
||||
setting.Data.general.snapRotation = floatValue;
|
||||
changedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void onChagedTextScaleSnap(string value)
|
||||
{
|
||||
if (setting != null && float.TryParse(value, out float floatValue))
|
||||
{
|
||||
setting.Data.general.snapScale = floatValue;
|
||||
changedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public void UpdateContentData(object? data)
|
||||
{
|
||||
if (data != null && data is string content)
|
||||
{
|
||||
Debug.Log($"UpdateContentData: {content}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 닫힐 때 실행되는 로직을 처리합니다.
|
||||
/// </summary>
|
||||
/// <returns>비동기 닫기 작업을 나타내는 <see cref="UniTask"/>입니다.</returns>
|
||||
public async UniTask OnCloseAsync()
|
||||
{
|
||||
if (autoSaveValueTxt != null) autoSaveValueTxt.onEndEdit.RemoveListener(onChagedTextAutoSave);
|
||||
if (gridValueTxt != null) gridValueTxt.onEndEdit.RemoveListener(onChagedTextGrid);
|
||||
if (positionSnapValueTxt != null) positionSnapValueTxt.onEndEdit.RemoveListener(onChagedTextSnapPosition);
|
||||
if (RotationSnapValueTxt != null) RotationSnapValueTxt.onEndEdit.RemoveListener(onChagedTextRotationSnap);
|
||||
if (ScaleSnapValueTxt != null) ScaleSnapValueTxt.onEndEdit.RemoveListener(onChagedTextScaleSnap);
|
||||
|
||||
Debug.Log($"SettingGeneralTabContent OnCloseAsync: changedValue={changedValue} setting == null:{setting == null}");
|
||||
|
||||
if(changedValue && setting != null)
|
||||
{
|
||||
await setting.SaveAsync();
|
||||
changedValue = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc195687d019c1f489347f9fccd121ee
|
||||
78
Assets/Scripts/Studio/Modal/Settings/SettingModal.cs
Normal file
78
Assets/Scripts/Studio/Modal/Settings/SettingModal.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using UVC.Data.Core;
|
||||
using UVC.UI.Modal;
|
||||
using UVC.UI.Tab;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingModal : ModalView
|
||||
{
|
||||
[SerializeField]
|
||||
protected TabController? tabController;
|
||||
|
||||
/// <summary>
|
||||
/// 모달이 열릴 때 호출됩니다. (비동기)
|
||||
/// </summary>
|
||||
/// <param name="content">모달에 표시할 내용/설정</param>
|
||||
public override async UniTask OnOpen(ModalContent content)
|
||||
{
|
||||
await base.OnOpen(content); // 부모의 OnOpen을 먼저 호출해서 기본 UI를 설정해요.
|
||||
if (tabController != null)
|
||||
{
|
||||
// 코드로 탭 설정하기
|
||||
SetupTabs(content);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 모달이 닫힐 때 호출됩니다. (비동기)
|
||||
/// </summary>
|
||||
/// <param name="content">모달에 표시할 내용/설정</param>
|
||||
public override async UniTask OnClose(ModalContent content)
|
||||
{
|
||||
// 현재 활성화된 탭의 OnCloseAsync 호출
|
||||
if (tabController != null)
|
||||
{
|
||||
ITabContent? activeTabContent = tabController.GetActiveITabContent();
|
||||
if (activeTabContent != null) await activeTabContent.OnCloseAsync();
|
||||
}
|
||||
await base.OnClose(content);
|
||||
}
|
||||
|
||||
private void SetupTabs(ModalContent content)
|
||||
{
|
||||
// 1. TabConfig 설정
|
||||
tabController?.AddTabConfig("Database", "Database", "Studio/Prefabs/Modal/Setting/SettingDatabaseTabContent", "Prefabs/UI/Images/icon_db", null, true);
|
||||
tabController?.AddTabConfig("General", "General", "Studio/Prefabs/Modal/Setting/SettingGeneralTabContent", "Prefabs/UI/Images/icon_info", null, true);
|
||||
tabController?.AddTabConfig("Shortcut", "Shortcut", "Studio/Prefabs/Modal/Setting/SettingShortcutTabContent", "Prefabs/UI/Images/icon_shortcut", null, true);
|
||||
|
||||
// 2. 컨트롤러 초기화
|
||||
tabController?.Initialize();
|
||||
|
||||
if (tabController != null)
|
||||
{
|
||||
tabController.OnTabChanged += (index) =>
|
||||
{
|
||||
Debug.Log($"탭이 변경되었습니다: {index}");
|
||||
};
|
||||
if(content.Message.StartsWith("shortcut:"))
|
||||
{
|
||||
// 특정 탭으로 이동
|
||||
string parts = content.Message.Substring("shortcut:".Length);
|
||||
if (parts.Length > 0)
|
||||
{
|
||||
//시간차를 계산해 0.5초 후에 탭을 활성화
|
||||
UniTask.Delay(500).ContinueWith(() => {
|
||||
Debug.Log($"ActivateTab: {parts[0]}");
|
||||
string tabKey = parts;
|
||||
tabController.ActivateTab(tabKey, content.Message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 10d5fbdacba4bb94da5a9e990aa5cc4e
|
||||
29
Assets/Scripts/Studio/Modal/Settings/SettingOpenCommand.cs
Normal file
29
Assets/Scripts/Studio/Modal/Settings/SettingOpenCommand.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
#nullable enable
|
||||
using UVC.UI.Commands;
|
||||
using UVC.UI.Modal;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingOpenCommand : ICommand
|
||||
{
|
||||
private object? _parameter;
|
||||
|
||||
public SettingOpenCommand(object? parameter = null)
|
||||
{
|
||||
_parameter = parameter;
|
||||
}
|
||||
|
||||
public async void Execute(object? parameter = null)
|
||||
{
|
||||
var modalContent = new ModalContent("Studio/Prefabs/Modal/Setting/SettingModal")
|
||||
{
|
||||
Title = "설정 카테고리"
|
||||
};
|
||||
|
||||
if(parameter != null) modalContent.Message = parameter.ToString();
|
||||
else if(_parameter != null) modalContent.Message = _parameter.ToString();
|
||||
|
||||
await UVC.UI.Modal.Modal.Open<object>(modalContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9e84cce89739e864293b95906dd8b996
|
||||
@@ -0,0 +1,13 @@
|
||||
using UVC.UI.Commands.Mono;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingOpenCommandMono : MonoBehaviourCommand
|
||||
{
|
||||
public override void Execute()
|
||||
{
|
||||
var command = new SettingOpenCommand();
|
||||
command.Execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0b19d94e5b555e449775fbe3edbac84
|
||||
@@ -0,0 +1,175 @@
|
||||
#nullable enable
|
||||
using Cysharp.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UVC.Core;
|
||||
using UVC.Studio.Config;
|
||||
using UVC.UI.Tab;
|
||||
|
||||
namespace UVC.Studio.Modal.Settings
|
||||
{
|
||||
public class SettingShortcutTabContent : MonoBehaviour, ITabContent
|
||||
{
|
||||
|
||||
[SerializeField]
|
||||
private LayoutGroup? labelGroup;
|
||||
|
||||
[SerializeField]
|
||||
private LayoutGroup? valueGroup;
|
||||
|
||||
[SerializeField]
|
||||
private TextMeshProUGUI? firstLabelTxt;
|
||||
|
||||
|
||||
[SerializeField]
|
||||
private TMP_InputField? firstValueTxt;
|
||||
|
||||
[Inject]
|
||||
private Setting? setting;
|
||||
|
||||
private List<TextMeshProUGUI> labelTxts = new List<TextMeshProUGUI>();
|
||||
private List<TMP_InputField> valueTxts = new List<TMP_InputField>();
|
||||
|
||||
private bool changedValue = false;
|
||||
|
||||
/// <summary>
|
||||
/// 탭 콘텐츠에 데이터를 전달합니다.
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public async void SetContentData(object? data)
|
||||
{
|
||||
Debug.Log($"SettingShortcutTabContent SetContentData: {data}");
|
||||
|
||||
if (setting == null)
|
||||
{
|
||||
await InjectorAppContext.Instance.WaitForInitializationAsync();
|
||||
setting = InjectorAppContext.Instance.Get<Setting>();
|
||||
}
|
||||
|
||||
labelTxts.Clear();
|
||||
valueTxts.Clear();
|
||||
|
||||
if (setting != null && labelGroup != null && valueGroup != null)
|
||||
{
|
||||
changedValue = false;
|
||||
|
||||
ShortcutsSetting shortcuts = setting.Data.shortcuts;
|
||||
|
||||
// menu와 tools의 모든 ShortcutItem 필드를 순회
|
||||
var shortcutGroups = new object[] { shortcuts.menu, shortcuts.tools };
|
||||
|
||||
foreach (var group in shortcutGroups)
|
||||
{
|
||||
var fields = group.GetType().GetFields();
|
||||
|
||||
for (int i = 0; i < fields.Length; i++)
|
||||
{
|
||||
var field = fields[i];
|
||||
if (field.FieldType == typeof(ShortcutItem))
|
||||
{
|
||||
var shortcut = (ShortcutItem?)field.GetValue(group);
|
||||
if (shortcut == null) continue;
|
||||
if (i == 0)
|
||||
{
|
||||
firstLabelTxt!.text = shortcut.label;
|
||||
firstValueTxt!.text = shortcut.key;
|
||||
labelTxts.Add(firstLabelTxt);
|
||||
valueTxts.Add(firstValueTxt);
|
||||
|
||||
// 영문 대문자만 입력되도록 필터링
|
||||
firstValueTxt.onValueChanged.AddListener((value) =>
|
||||
{
|
||||
firstValueTxt.text = value.ToUpper();
|
||||
});
|
||||
|
||||
// 값 변경 리스너 추가
|
||||
firstValueTxt.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
shortcut.key = value;
|
||||
changedValue = true;
|
||||
});
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// 라벨 복제
|
||||
if (firstLabelTxt != null)
|
||||
{
|
||||
TextMeshProUGUI labelInstance = Instantiate(firstLabelTxt, labelGroup.transform);
|
||||
labelInstance.text = shortcut.label;
|
||||
labelTxts.Add(labelInstance);
|
||||
}
|
||||
|
||||
// 값 복제
|
||||
if (firstValueTxt != null)
|
||||
{
|
||||
TMP_InputField valueInstance = Instantiate(firstValueTxt, valueGroup.transform);
|
||||
valueInstance.text = shortcut.key;
|
||||
valueTxts.Add(valueInstance);
|
||||
|
||||
// 영문 대문자만 입력되도록 필터링
|
||||
valueInstance.onValueChanged.AddListener((value) =>
|
||||
{
|
||||
valueInstance.text = value.ToUpper();
|
||||
});
|
||||
|
||||
valueInstance.onEndEdit.AddListener((value) =>
|
||||
{
|
||||
shortcut.key = value;
|
||||
changedValue = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 탭 전환 시 데이터가 있는 경우 전달 되는 데이터. SetContentData 이후 호출 됨
|
||||
/// </summary>
|
||||
/// <param name="data">전달할 데이터 객체</param>
|
||||
public void UpdateContentData(object? data)
|
||||
{
|
||||
if (data != null && data is string content)
|
||||
{
|
||||
Debug.Log($"UpdateContentData: {content}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 닫힐 때 실행되는 로직을 처리합니다.
|
||||
/// </summary>
|
||||
/// <returns>비동기 닫기 작업을 나타내는 <see cref="UniTask"/>입니다.</returns>
|
||||
public async UniTask OnCloseAsync()
|
||||
{
|
||||
foreach (var labelTxt in labelTxts)
|
||||
{
|
||||
if(labelTxt != firstLabelTxt)
|
||||
{
|
||||
Destroy(labelTxt.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var valueTxt in valueTxts)
|
||||
{
|
||||
valueTxt.onValueChanged.RemoveAllListeners();
|
||||
valueTxt.onEndEdit.RemoveAllListeners();
|
||||
if(valueTxt != firstValueTxt)
|
||||
{
|
||||
Destroy(valueTxt.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
Debug.Log($"SettingShortcutTabContent OnCloseAsync: changedValue={changedValue} setting == null:{setting == null}");
|
||||
if (changedValue && setting != null)
|
||||
{
|
||||
await setting.SaveAsync();
|
||||
Debug.Log("Shortcut settings saved.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0cd2a999fb90a1842bc1b34fc30f49c4
|
||||
@@ -3,27 +3,53 @@ using UnityEngine;
|
||||
using UVC.Core;
|
||||
using UVC.UI.Menu;
|
||||
using UVC.Util;
|
||||
using UVC.Studio.Config;
|
||||
using Cysharp.Threading.Tasks;
|
||||
|
||||
namespace UVC.Studio
|
||||
{
|
||||
public class StudioAppContext : InjectorAppContext
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// App 라이프사이클 서비스들을 등록합니다.
|
||||
/// Awake 시점에 자동 호출되며, 모든 서비스는 씬 전환 시에도 유지됩니다.
|
||||
/// App 라이프사이클 서비스들을 동기적으로 등록합니다.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>비동기 로드가 필요 없는 서비스들만 여기서 등록합니다.</para>
|
||||
/// <para>비동기 로드가 필요한 서비스는 RegisterServicesAsync()에서 등록하세요.</para>
|
||||
/// </remarks>
|
||||
protected override void RegisterServices()
|
||||
{
|
||||
base.RegisterServices();
|
||||
|
||||
// 여기에 StudioAppContext에 등록할 서비스들을 추가하세요.
|
||||
|
||||
// 비동기 로드가 필요 없는 싱글톤 서비스 등록
|
||||
Injector.RegisterSingleton<StudioAppMain>();
|
||||
Injector.RegisterSingleton<CursorManager>();
|
||||
Injector.RegisterSingleton<ContextMenuManager>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// App 라이프사이클 서비스들을 비동기적으로 등록합니다.
|
||||
/// </summary>
|
||||
/// <returns>비동기 등록 완료 UniTask</returns>
|
||||
/// <remarks>
|
||||
/// <para>이 메서드는 RegisterServices() 완료 후 await되어 호출됩니다.</para>
|
||||
/// <para>완료될 때까지 IsInitialized가 false로 유지되므로 안전하게 비동기 로드를 수행할 수 있습니다.</para>
|
||||
/// </remarks>
|
||||
protected override async UniTask RegisterServicesAsync()
|
||||
{
|
||||
await base.RegisterServicesAsync();
|
||||
|
||||
// Setting과 Library를 병렬로 로드
|
||||
var setting = new Setting();
|
||||
var library = new Library();
|
||||
await UniTask.WhenAll(
|
||||
setting.LoadAsync(),
|
||||
library.LoadAllParallelAsync()
|
||||
);
|
||||
|
||||
// 로드 완료 후 인스턴스 등록
|
||||
Injector.RegisterInstance<Setting>(setting);
|
||||
Injector.RegisterInstance<Library>(library);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using UVC.Core;
|
||||
using UVC.Data;
|
||||
using UVC.Locale;
|
||||
using UVC.Util;
|
||||
using UVC.UI.Loading;
|
||||
|
||||
namespace UVC.Studio
|
||||
{
|
||||
@@ -30,6 +31,8 @@ namespace UVC.Studio
|
||||
UVC.Log.Log4netCodeConfigurator.Setup();
|
||||
#endif
|
||||
|
||||
UILoading.Show();
|
||||
|
||||
RTGLite.RTG.get.initialized += OnRTGInit;
|
||||
|
||||
await SettupConfigAsync();
|
||||
|
||||
@@ -6,6 +6,7 @@ using UVC.Core;
|
||||
using UVC.Data;
|
||||
using UVC.Data.Core;
|
||||
using UVC.Locale;
|
||||
using UVC.Studio.Modal.Settings;
|
||||
using UVC.UI.Commands;
|
||||
using UVC.UI.Loading;
|
||||
using UVC.UI.Menu;
|
||||
@@ -20,18 +21,22 @@ namespace UVC.Studio
|
||||
[DefaultExecutionOrder(90)]
|
||||
public class StudioSceneMain : SingletonScene<StudioSceneMain>
|
||||
{
|
||||
[SerializeField]
|
||||
|
||||
[Inject]
|
||||
private TopMenuController topMenu;
|
||||
|
||||
[SerializeField]
|
||||
[Inject]
|
||||
private Toolbox toolBox;
|
||||
|
||||
[SerializeField]
|
||||
[Inject]
|
||||
private StudioSideTabBar sideTabBar;
|
||||
|
||||
|
||||
[Inject]
|
||||
private PropertyWindow propertyWindow;
|
||||
|
||||
|
||||
|
||||
public Action Initialized;
|
||||
|
||||
/// <summary>
|
||||
@@ -40,26 +45,34 @@ namespace UVC.Studio
|
||||
/// </summary>
|
||||
protected override void Init()
|
||||
{
|
||||
|
||||
if (!TooltipManager.Instance.IsInitialized) TooltipManager.Instance.Initialize();
|
||||
|
||||
StudioAppMain.Instance.Initialized += OnAppInitialized;
|
||||
}
|
||||
|
||||
private async void OnAppInitialized()
|
||||
{
|
||||
// SceneContext 초기화 완료 대기 (Injection이 수행된 후)
|
||||
var sceneCtx = FindAnyObjectByType<StudioSceneContext>();
|
||||
if (sceneCtx != null)
|
||||
{
|
||||
await sceneCtx.WaitForInitializationAsync();
|
||||
Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
private void Initialize()
|
||||
{
|
||||
|
||||
|
||||
SetupTopMenu();
|
||||
SetupToolBox();
|
||||
SetupPropertyWindow();
|
||||
|
||||
|
||||
if (Initialized != null)
|
||||
{
|
||||
Initialized.Invoke();
|
||||
}
|
||||
|
||||
await UniTask.Delay(2000);
|
||||
sideTabBar.InitTab();
|
||||
|
||||
Initialized?.Invoke();
|
||||
|
||||
UILoading.Hide();
|
||||
}
|
||||
|
||||
|
||||
@@ -97,10 +110,10 @@ namespace UVC.Studio
|
||||
}));
|
||||
topMenu.AddMenuItem(new MenuItemData("setting", "menu_setting", subMenuItems: new List<MenuItemData>
|
||||
{
|
||||
new MenuItemData("setting_db", "menu_setting_db", new DebugLogCommand("데이터베이스 설정 선택됨 (Command 실행)")),
|
||||
new MenuItemData("setting_general", "menu_setting_general", new DebugLogCommand("일반 설정 선택됨 (Command 실행)")),
|
||||
new MenuItemData("setting_library", "menu_setting_library", new DebugLogCommand("라이브러리 설정 선택됨 (Command 실행)")),
|
||||
new MenuItemData("setting_shortcut", "menu_setting_shortcut", new DebugLogCommand("단축키 설정 선택됨 (Command 실행)")),
|
||||
new MenuItemData("setting_db", "menu_setting_db", new SettingOpenCommand("shortcut:Database")),
|
||||
new MenuItemData("setting_general", "menu_setting_general", new SettingOpenCommand("shortcut:General")),
|
||||
new MenuItemData("setting_library", "menu_setting_library", new SettingOpenCommand("shortcut:Library")),
|
||||
new MenuItemData("setting_shortcut", "menu_setting_shortcut", new SettingOpenCommand("shortcut:Shortcut")),
|
||||
}));
|
||||
|
||||
topMenu.Initialize();
|
||||
|
||||
Reference in New Issue
Block a user