Files

763 lines
26 KiB
C#

#nullable enable
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
/// ├── id - 장비 식별자 (예: "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($"ID: {crane.id}, 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($"ID: {crane.id}, GLTF: {crane.gltf}");
/// }
///
/// // AGV 데이터 접근
/// foreach (var agv in _library.AGVData.AGV)
/// {
/// Debug.Log($"AGV: {agv.label}");
/// }
/// }
///
/// public EquipmentItem GetEquipmentById(string id)
/// {
/// return _library.StakerCraneData.stakerCrane
/// .FirstOrDefault(e => e.id == id);
/// }
/// }
/// </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 = "StakerCrane.json";
/// <summary>단일 랙 라이브러리 파일명</summary>
private const string RackFileName = "Rack.json";
/// <summary>AGV 라이브러리 파일명</summary>
private const string AGVFileName = "AGV.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>
public string LibraryPath
{
get
{
if (!useAppDataPath)
{
return Path.Combine(Application.streamingAssetsPath, "Studio", "Library") + "/";
}
else
{
return Path.Combine(Application.persistentDataPath, "Library") + "/";
}
}
}
private bool useAppDataPath = false;
/// <summary>
/// 생성자 - 기본 데이터로 초기화합니다.
/// </summary>
/// <remarks>
/// 비동기 로드를 사용하려면 생성 후 LoadAllAsync()를 호출하세요.
/// 동기 로드를 원하면 LoadAll()을 호출하세요.
/// </remarks>
public Library(bool useAppDataPath = false)
{
this.useAppDataPath = useAppDataPath;
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>(RackFileName, 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, "Studio", "Library", fileName);
if (useAppDataPath) path = Path.Combine(Application.persistentDataPath, "Library", 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>(RackFileName);
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 (useAppDataPath) path = Path.Combine(Application.persistentDataPath, "Library", 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> list = new();
}
/// <summary>
/// LibraryRackSingle.json 루트 데이터 클래스
/// </summary>
/// <remarks>
/// JSON 키가 "AGV"로 되어 있어 필드명이 AGV입니다.
/// </remarks>
[Serializable]
public class LibraryRackSingleData
{
/// <summary>랙 장비 목록 (JSON 키: "list")</summary>
public List<EquipmentItem> list = new();
}
/// <summary>
/// LibraryAGV.json 루트 데이터 클래스
/// </summary>
[Serializable]
public class LibraryAGVData
{
/// <summary>AGV 장비 목록</summary>
public List<EquipmentItem> list = new();
}
#endregion
#region Equipment Item
/// <summary>
/// 개별 장비 항목 데이터
/// </summary>
/// <remarks>
/// 장비의 기본 정보, 3D 모델 경로, 속성 정의, 상태 정의를 포함합니다.
/// </remarks>
[Serializable]
public class EquipmentItem
{
/// <summary>
/// 장비 식별자
/// </summary>
/// <remarks>
/// 고유한 장비 ID (예: "SingleFork", "Rack_Single")
/// </remarks>
public string id;
/// <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>
/// 숫자 값(int, float) 또는 벡터 문자열 (예: 150, 50.5, "0,0,1")
/// JSON에서 숫자 또는 문자열로 저장될 수 있음
/// </remarks>
public object value;
/// <summary>
/// 단위
/// </summary>
/// <remarks>
/// 표시용 단위 문자열 (예: "cm", "cm/s", "")
/// </remarks>
public string unit;
/// <summary>
/// value를 float로 반환합니다.
/// </summary>
public float GetFloatValue()
{
if (value == null) return 0f;
if (value is float f) return f;
if (value is double d) return (float)d;
if (value is int i) return i;
if (value is long l) return l;
if (float.TryParse(value.ToString(), out float result)) return result;
return 0f;
}
/// <summary>
/// value를 int로 반환합니다.
/// </summary>
public int GetIntValue()
{
if (value == null) return 0;
if (value is int i) return i;
if (value is long l) return (int)l;
if (value is float f) return (int)f;
if (value is double d) return (int)d;
if (int.TryParse(value.ToString(), out int result)) return result;
return 0;
}
/// <summary>
/// value를 string으로 반환합니다.
/// </summary>
public string GetStringValue()
{
return value?.ToString() ?? string.Empty;
}
}
#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
}