Studio 모달 완료

This commit is contained in:
logonkhi
2025-12-16 19:35:43 +09:00
parent 0df2f0d8da
commit b5fdf7faeb
49 changed files with 10777 additions and 876 deletions

View File

@@ -17,7 +17,7 @@ namespace UVC.Factory.Modal.Settings
public async void Execute(object? parameter = null)
{
FactoryCameraController.Instance.Enable = false; // 카메라 컨트롤러 비활성화
var modalContent = new ModalContent("Prefabs/UI/Modal/SettingModal")
var modalContent = new ModalContent("Factory/Prefabs/Modal/Setting/SettingModal")
{
Title = "설정 카테고리"
};

View 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
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 618dd606311f7cf4a9d818235d3263ba

View 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
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 892ce8db40260e348bdf934c524d4ab6

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 954cbb1fe9711f348b5980ab49c3c50b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 687ff5a74bf63014bb78b7b7a92762bf
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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.");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 695dd0b8bdbe7de4aa8f2a5b277d36dc

View 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;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fc195687d019c1f489347f9fccd121ee

View 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);
});
}
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 10d5fbdacba4bb94da5a9e990aa5cc4e

View 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e84cce89739e864293b95906dd8b996

View File

@@ -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();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b0b19d94e5b555e449775fbe3edbac84

View File

@@ -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.");
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0cd2a999fb90a1842bc1b34fc30f49c4

View File

@@ -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);
}
}
}

View File

@@ -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();

View File

@@ -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();

View File

@@ -76,6 +76,7 @@ namespace UVC.Data
/// <returns></returns>
public static async UniTask LoadFromAppData()
{
//C:\Users\[user name]\AppData\LocalLow\[company name]\[product name]
string persistentDataPath = UnityEngine.Application.persistentDataPath;
#if UNITY_WEBGL && !UNITY_EDITOR
// WebGL: 스레드 사용 불가. 메인 스레드에서 파일 작업 수행하되 프레임 양보로 스톨 방지.

View File

@@ -260,6 +260,20 @@ namespace UVC.UI.Tab
return activeTab?.tabID;
}
/// <summary>
/// 현재 활성화된 탭의 ITabContent를 반환합니다.
/// </summary>
/// <returns>활성화된 탭의 ITabContent, 없으면 null</returns>
public ITabContent? GetActiveITabContent()
{
string? activeTabID = GetActiveTabID();
if (string.IsNullOrEmpty(activeTabID)) return null;
GameObject? tabInstance = GetTabInstance(activeTabID);
if (tabInstance == null) return null;
return tabInstance.GetComponent<ITabContent>();
}
/// <summary>
/// 특정 탭의 데이터를 설정하고 갱신합니다.
/// </summary>

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace UVC.Core
@@ -18,7 +19,7 @@ namespace UVC.Core
/// <para><b>[ 역할 ]</b></para>
/// <list type="bullet">
/// <item><description>Injector 인스턴스 생성 및 관리</description></item>
/// <item><description>App 라이프사이클 서비스 등록</description></item>
/// <item><description>App 라이프사이클 서비스 등록 (동기/비동기)</description></item>
/// <item><description>씬 로드 시 자동 의존성 주입 (선택적)</description></item>
/// <item><description>서비스 해결을 위한 편의 메서드 제공</description></item>
/// </list>
@@ -30,31 +31,41 @@ namespace UVC.Core
/// <item><description>Inspector에서 App Prefab 목록 설정 (선택)</description></item>
/// </list>
///
/// <para><b>[ 사용 방법 - 상속 ]</b></para>
/// <para><b>[ 사용 방법 - 동기 등록 ]</b></para>
/// <code>
/// public class MyAppContext : InjectorAppContext
/// {
/// [SerializeField] private UIManager uiManagerPrefab;
///
/// protected override void RegisterServices()
/// {
/// base.RegisterServices();
///
/// // Type A: 순수 C# 클래스
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour 동적 생성
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Type C: Prefab 기반
/// Injector.RegisterPrefab&lt;IUIManager&gt;(uiManagerPrefab.gameObject, ServiceLifetime.App);
///
/// // Type D: Singleton 연동
/// Injector.RegisterSingleton&lt;SettingsManager&gt;();
/// }
/// }
/// </code>
///
/// <para><b>[ 사용 방법 - 비동기 등록 ]</b></para>
/// <code>
/// public class MyAppContext : InjectorAppContext
/// {
/// protected override async UniTask RegisterServicesAsync()
/// {
/// // 비동기 로드가 필요한 서비스
/// var setting = new Setting();
/// var library = new Library();
///
/// await UniTask.WhenAll(
/// setting.LoadAsync(),
/// library.LoadAllParallelAsync()
/// );
///
/// Injector.RegisterInstance&lt;Setting&gt;(setting);
/// Injector.RegisterInstance&lt;Library&gt;(library);
/// }
/// }
/// </code>
///
/// <para><b>[ 서비스 접근 방법 ]</b></para>
/// <code>
/// // 방법 1: [Inject] 어트리뷰트 (권장)
@@ -69,8 +80,20 @@ namespace UVC.Core
///
/// <para><b>[ 이벤트 ]</b></para>
/// <list type="bullet">
/// <item><description>OnInjectorInitialized: Injector 생성 후 발생</description></item>
/// <item><description>OnServicesRegistered: 서비스 등록 완료 후 발생</description></item>
/// <item><description>OnInjectorInitialized: Injector 생성 및 모든 서비스 등록 완료 후 발생</description></item>
/// <item><description>OnServicesRegistered: 동기 서비스 등록 완료 후 발생</description></item>
/// <item><description>OnServicesRegisteredAsync: 비동기 서비스 등록 완료 후 발생</description></item>
/// </list>
///
/// <para><b>[ 초기화 순서 ]</b></para>
/// <list type="number">
/// <item><description>Injector 인스턴스 생성</description></item>
/// <item><description>RegisterServices() 호출 (동기)</description></item>
/// <item><description>OnServicesRegistered 이벤트 발생</description></item>
/// <item><description>RegisterServicesAsync() 호출 (비동기) - await 대기</description></item>
/// <item><description>OnServicesRegisteredAsync 이벤트 발생</description></item>
/// <item><description>IsInitialized = true</description></item>
/// <item><description>OnInjectorInitialized 이벤트 발생</description></item>
/// </list>
/// </remarks>
/// <seealso cref="Injector"/>
@@ -108,7 +131,7 @@ namespace UVC.Core
/// 초기화 완료 여부
/// </summary>
/// <remarks>
/// <para>Init() 완료 후 true가 됩니다.</para>
/// <para>동기 및 비동기 서비스 등록이 모두 완료 후 true가 됩니다.</para>
/// <para>InjectorSceneContext는 이 값이 true가 될 때까지 대기합니다.</para>
/// </remarks>
public bool IsInitialized { get; private set; }
@@ -121,19 +144,27 @@ namespace UVC.Core
/// Injector 초기화 완료 시 발생
/// </summary>
/// <remarks>
/// <para>Injector 인스턴스가 생성되고 서비스 등록이 완료된 직후 발생합니다.</para>
/// <para>동기 및 비동기 서비스 등록이 모두 완료된 직후 발생합니다.</para>
/// </remarks>
public event System.Action OnInjectorInitialized;
/// <summary>
/// 서비스 등록 완료 시 발생
/// 동기 서비스 등록 완료 시 발생
/// </summary>
/// <remarks>
/// <para>RegisterServices() 메서드 완료 후 발생합니다.</para>
/// <para>이 이벤트 이후부터 등록된 서비스들을 Resolve할 수 있습니다.</para>
/// <para>비동기 등록 전에 발생합니다.</para>
/// </remarks>
public event System.Action OnServicesRegistered;
/// <summary>
/// 비동기 서비스 등록 완료 시 발생
/// </summary>
/// <remarks>
/// <para>RegisterServicesAsync() 메서드 완료 후 발생합니다.</para>
/// </remarks>
public event System.Action OnServicesRegisteredAsync;
#endregion
#region Lifecycle
@@ -142,12 +173,24 @@ namespace UVC.Core
{
base.Init();
// 비동기 초기화 시작
InitializeAsync().Forget();
}
/// <summary>
/// 비동기 초기화 수행
/// </summary>
private async UniTaskVoid InitializeAsync()
{
// Injector 인스턴스 생성
Injector = new Injector();
// 서비스 등록
// 동기 서비스 등록
RegisterServices();
// 비동기 서비스 등록
await RegisterServicesAsync();
// 씬 로드 이벤트 구독
if (autoInjectOnSceneLoad)
{
@@ -175,11 +218,12 @@ namespace UVC.Core
#region Service Registration
/// <summary>
/// 서비스 등록을 수행합니다. 자식 클래스에서 오버라이드하여 추가 서비스를 등록하세요.
/// 동기 서비스 등록을 수행합니다. 자식 클래스에서 오버라이드하여 추가 서비스를 등록하세요.
/// </summary>
/// <remarks>
/// <para><b>[ 호출 시점 ]</b></para>
/// <para>Init() 메서드에서 Injector 인스턴스 생성 직후 호출됩니다.</para>
/// <para>InitializeAsync()에서 Injector 인스턴스 생성 직후 호출됩니다.</para>
/// <para>RegisterServicesAsync() 이전에 호출됩니다.</para>
///
/// <para><b>[ 오버라이드 시 주의사항 ]</b></para>
/// <para>base.RegisterServices()를 먼저 호출하여 Inspector에서 할당한 Prefab들이 등록되도록 하세요.</para>
@@ -190,13 +234,8 @@ namespace UVC.Core
/// {
/// base.RegisterServices(); // Inspector Prefab 등록
///
/// // Type A: 순수 C# 클래스
/// // 동기 등록이 적합한 서비스들
/// Injector.Register&lt;ILogService, ConsoleLogger&gt;(ServiceLifetime.App);
///
/// // Type B: MonoBehaviour 동적 생성
/// Injector.Register&lt;IAudioManager, AudioManager&gt;(ServiceLifetime.App);
///
/// // Type D: Singleton 연동
/// Injector.RegisterSingleton&lt;SettingsManager&gt;();
/// }
/// </code>
@@ -218,6 +257,45 @@ namespace UVC.Core
OnServicesRegistered?.Invoke();
}
/// <summary>
/// 비동기 서비스 등록을 수행합니다. 자식 클래스에서 오버라이드하여 비동기 로드가 필요한 서비스를 등록하세요.
/// </summary>
/// <remarks>
/// <para><b>[ 호출 시점 ]</b></para>
/// <para>RegisterServices() 완료 후 호출됩니다.</para>
/// <para>이 메서드가 완료될 때까지 IsInitialized는 false입니다.</para>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// protected override async UniTask RegisterServicesAsync()
/// {
/// // JSON 설정 파일 비동기 로드
/// var setting = new Setting();
/// var library = new Library();
///
/// // 병렬 로드로 성능 최적화
/// await UniTask.WhenAll(
/// setting.LoadAsync(),
/// library.LoadAllParallelAsync()
/// );
///
/// // 로드 완료 후 등록
/// Injector.RegisterInstance&lt;Setting&gt;(setting);
/// Injector.RegisterInstance&lt;Library&gt;(library);
/// }
/// </code>
///
/// <para><b>[ 주의사항 ]</b></para>
/// <para>base.RegisterServicesAsync()를 호출할 필요는 없습니다 (기본 구현은 비어있음).</para>
/// <para>이 메서드 완료 전까지 InjectorSceneContext는 대기합니다.</para>
/// </remarks>
/// <returns>비동기 작업</returns>
protected virtual UniTask RegisterServicesAsync()
{
OnServicesRegisteredAsync?.Invoke();
return UniTask.CompletedTask;
}
/// <summary>
/// Prefab을 해당 타입으로 등록합니다.
/// </summary>
@@ -318,6 +396,22 @@ namespace UVC.Core
return Injector?.TryResolve<T>();
}
/// <summary>
/// 초기화가 완료될 때까지 대기합니다.
/// </summary>
/// <returns>초기화 완료 대기 태스크</returns>
/// <remarks>
/// <para>비동기 서비스 등록이 완료될 때까지 대기해야 할 때 사용합니다.</para>
/// <code>
/// await InjectorAppContext.Instance.WaitForInitializationAsync();
/// var setting = InjectorAppContext.Instance.Get&lt;Setting&gt;();
/// </code>
/// </remarks>
public async UniTask WaitForInitializationAsync()
{
await UniTask.WaitUntil(() => IsInitialized);
}
#endregion
}
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using UnityEngine;
using Cysharp.Threading.Tasks;
namespace UVC.Core
{
@@ -149,6 +150,15 @@ namespace UVC.Core
/// </remarks>
public event System.Action OnSceneInjectionCompleted;
/// <summary>
/// Scene 서비스 비동기 등록 완료 시 발생
/// </summary>
/// <remarks>
/// <para>RegisterSceneServicesAsync() 메서드 완료 후 발생합니다.</para>
/// <para>비동기로 로드가 필요한 Scene 서비스들의 등록이 완료된 상태를 나타냅니다.</para>
/// </remarks>
public event System.Action OnSceneServicesRegisteredAsync;
#endregion
#region Lifecycle
@@ -157,44 +167,44 @@ namespace UVC.Core
/// Unity Awake - InjectorAppContext 준비 확인 후 초기화
/// </summary>
protected virtual void Awake()
{
InitializeAsync().Forget();
}
/// <summary>
/// 비동기 초기화 수행 - AppContext 대기 후 서비스 등록 및 의존성 주입
/// </summary>
/// <remarks>
/// <para><b>[ 실행 순서 ]</b></para>
/// <list type="number">
/// <item><description>InjectorAppContext 초기화 대기</description></item>
/// <item><description>동기 서비스 등록 (RegisterSceneServices)</description></item>
/// <item><description>비동기 서비스 등록 (RegisterSceneServicesAsync) - await</description></item>
/// <item><description>자동 의존성 주입 (옵션)</description></item>
/// <item><description>IsInitialized = true</description></item>
/// </list>
/// </remarks>
private async UniTaskVoid InitializeAsync()
{
// InjectorAppContext가 준비될 때까지 대기
if (InjectorAppContext.Instance == null || !InjectorAppContext.Instance.IsInitialized)
{
Debug.LogWarning("[InjectorSceneContext] InjectorAppContext is not initialized. Waiting...");
StartCoroutine(WaitForAppContext());
return;
await UniTask.WaitUntil(() => InjectorAppContext.Instance != null && InjectorAppContext.Instance.IsInitialized);
}
Initialize();
}
/// <summary>
/// InjectorAppContext가 준비될 때까지 대기하는 코루틴
/// </summary>
private System.Collections.IEnumerator WaitForAppContext()
{
while (InjectorAppContext.Instance == null || !InjectorAppContext.Instance.IsInitialized)
{
yield return null;
}
Initialize();
}
/// <summary>
/// 실제 초기화 수행 - 서비스 등록 및 의존성 주입
/// </summary>
private void Initialize()
{
if (Injector == null)
{
Debug.LogError("[InjectorSceneContext] Injector is null. Make sure InjectorAppContext exists in the scene.");
return;
}
// Scene 서비스 등록
// Scene 서비스 등록 (동기)
RegisterSceneServices();
// Scene 서비스 등록 (비동기) - 완료될 때까지 대기
await RegisterSceneServicesAsync();
// 자동 의존성 주입
if (autoInjectSceneObjects)
{
@@ -204,6 +214,32 @@ namespace UVC.Core
IsInitialized = true;
}
/// <summary>
/// 초기화가 완료될 때까지 대기합니다.
/// </summary>
/// <returns>초기화 완료 대기 UniTask</returns>
/// <remarks>
/// <para>다른 컴포넌트에서 SceneContext 초기화 완료를 기다릴 때 사용합니다.</para>
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// public class MyComponent : MonoBehaviour
/// {
/// private async void Start()
/// {
/// var sceneContext = FindObjectOfType&lt;InjectorSceneContext&gt;();
/// await sceneContext.WaitForInitializationAsync();
///
/// // 이제 모든 Scene 서비스가 준비됨
/// var service = sceneContext.Injector.Resolve&lt;IMyService&gt;();
/// }
/// }
/// </code>
/// </remarks>
public async UniTask WaitForInitializationAsync()
{
await UniTask.WaitUntil(() => IsInitialized);
}
/// <summary>
/// Unity OnDestroy - Scene 서비스 정리
/// </summary>
@@ -266,6 +302,48 @@ namespace UVC.Core
OnSceneServicesRegistered?.Invoke();
}
/// <summary>
/// Scene 서비스 비동기 등록을 수행합니다. 자식 클래스에서 오버라이드하여 비동기 서비스를 등록하세요.
/// </summary>
/// <returns>비동기 등록 완료 UniTask</returns>
/// <remarks>
/// <para><b>[ 호출 시점 ]</b></para>
/// <para>InitializeAsync()에서 RegisterSceneServices() 완료 후 호출됩니다.</para>
/// <para>이 메서드는 await되므로 완료될 때까지 IsInitialized가 false로 유지됩니다.</para>
///
/// <para><b>[ 사용 시나리오 ]</b></para>
/// <list type="bullet">
/// <item><description>씬별 설정 파일 비동기 로드</description></item>
/// <item><description>씬별 리소스 비동기 로드</description></item>
/// <item><description>씬별 네트워크 초기화</description></item>
/// </list>
///
/// <para><b>[ 사용 예시 ]</b></para>
/// <code>
/// public class BattleSceneContext : InjectorSceneContext
/// {
/// protected override async UniTask RegisterSceneServicesAsync()
/// {
/// await base.RegisterSceneServicesAsync();
///
/// // 씬별 설정 비동기 로드
/// var battleConfig = new BattleConfig();
/// await battleConfig.LoadAsync();
/// Injector.RegisterInstance&lt;BattleConfig&gt;(battleConfig, ServiceLifetime.Scene);
///
/// // 씬별 리소스 비동기 로드
/// var enemyData = await Resources.LoadAsync&lt;EnemyDatabase&gt;("EnemyData");
/// Injector.RegisterInstance&lt;EnemyDatabase&gt;(enemyData as EnemyDatabase, ServiceLifetime.Scene);
/// }
/// }
/// </code>
/// </remarks>
protected virtual UniTask RegisterSceneServicesAsync()
{
OnSceneServicesRegisteredAsync?.Invoke();
return UniTask.CompletedTask;
}
/// <summary>
/// Prefab을 해당 타입으로 등록합니다. (리플렉션 사용)
/// </summary>