#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
{
///
/// 장비 라이브러리 데이터 관리 클래스
///
///
/// [ 개요 ]
/// StreamingAssets 폴더의 Library JSON 파일들을 로드하여 장비 정보를 관리합니다.
/// 지원 파일: LibraryStakerCrane.json, LibraryRackSingle.json, LibraryAGV.json
///
/// [ JSON 파일 구조 ]
///
/// LibraryXXX.json
/// └── [equipmentType]: List
/// └── 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")
///
///
/// [ 사용 예시 (비동기) ]
///
/// // 비동기 로드 - 모든 라이브러리
/// 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}");
/// }
///
///
/// [ 사용 예시 (동기) ]
///
/// // 동기 로드
/// var library = new Library();
/// library.LoadAll();
///
/// // 개별 로드
/// library.LoadStakerCrane();
///
///
/// [ 지원 장비 타입 ]
///
/// - StakerCrane: 스태커 크레인 - 자동 창고 시스템의 입출고 장비
/// - RackSingle: 단일 랙 - 물품 보관 선반
/// - AGV: 무인 운반차 (Automated Guided Vehicle)
///
///
/// [ Injector 등록 및 사용 ]
/// AppContext에서 Library를 등록하고, 다른 컴포넌트에서 주입받아 사용합니다.
///
/// 1. AppContext에서 등록 (비동기 - 병렬 로드)
///
/// public class StudioAppContext : InjectorAppContext
/// {
/// protected override async UniTask RegisterServicesAsync()
/// {
/// // Library 생성 및 병렬 비동기 로드 (가장 빠름)
/// var library = new Library();
/// await library.LoadAllParallelAsync();
///
/// // Injector에 인스턴스 등록
/// Injector.RegisterInstance(library);
/// }
/// }
///
///
/// 2. AppContext에서 등록 (동기)
///
/// public class StudioAppContext : InjectorAppContext
/// {
/// protected override void RegisterServices()
/// {
/// // Library 생성 및 동기 로드
/// var library = new Library();
/// library.LoadAll();
///
/// // Injector에 인스턴스 등록
/// Injector.RegisterInstance(library);
/// }
/// }
///
///
/// 3. 컴포넌트에서 주입받아 사용
///
/// 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);
/// }
/// }
///
///
/// 4. 직접 접근 (Inject 없이)
///
/// // InjectorAppContext를 통해 직접 접근
/// var library = InjectorAppContext.Instance.Get();
/// var craneList = library.StakerCraneData.stakerCrane;
///
///
/// 5. Setting과 Library 함께 등록
///
/// 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);
/// Injector.RegisterInstance(library);
/// }
/// }
///
///
public class Library
{
/// 스태커 크레인 라이브러리 파일명
private const string StakerCraneFileName = "StakerCrane.json";
/// 단일 랙 라이브러리 파일명
private const string RackFileName = "Rack.json";
/// AGV 라이브러리 파일명
private const string AGVFileName = "AGV.json";
/// 스태커 크레인 데이터
public LibraryStakerCraneData StakerCraneData { get; private set; }
/// 단일 랙 데이터
public LibraryRackSingleData RackSingleData { get; private set; }
/// AGV 데이터
public LibraryAGVData AGVData { get; private set; }
/// 모든 라이브러리 로드 완료 여부
public bool IsLoaded { get; private set; }
///
/// 라이브러리 파일 경로
///
public string LibraryPath
{
get
{
if (!useAppDataPath)
{
return Path.Combine(Application.streamingAssetsPath, "Studio", "Library") + "/";
}
else
{
return Path.Combine(Application.persistentDataPath, "Library") + "/";
}
}
}
private bool useAppDataPath = false;
///
/// 생성자 - 기본 데이터로 초기화합니다.
///
///
/// 비동기 로드를 사용하려면 생성 후 LoadAllAsync()를 호출하세요.
/// 동기 로드를 원하면 LoadAll()을 호출하세요.
///
public Library(bool useAppDataPath = false)
{
this.useAppDataPath = useAppDataPath;
StakerCraneData = new LibraryStakerCraneData();
RackSingleData = new LibraryRackSingleData();
AGVData = new LibraryAGVData();
IsLoaded = false;
}
#region Async Methods
///
/// 모든 라이브러리 파일을 순차적으로 비동기 로드합니다.
///
/// 취소 토큰
public async UniTask LoadAllAsync(CancellationToken cancellationToken = default)
{
await LoadStakerCraneAsync(cancellationToken);
await LoadRackSingleAsync(cancellationToken);
await LoadAGVAsync(cancellationToken);
IsLoaded = true;
}
///
/// 모든 라이브러리 파일을 병렬로 비동기 로드합니다.
///
/// 취소 토큰
///
/// 세 파일을 동시에 로드하여 전체 로딩 시간을 단축합니다.
///
public async UniTask LoadAllParallelAsync(CancellationToken cancellationToken = default)
{
await UniTask.WhenAll(
LoadStakerCraneAsync(cancellationToken),
LoadRackSingleAsync(cancellationToken),
LoadAGVAsync(cancellationToken)
);
IsLoaded = true;
}
///
/// LibraryStakerCrane.json 파일을 비동기로 로드합니다.
///
/// 취소 토큰
/// 로드 성공 여부
public async UniTask LoadStakerCraneAsync(CancellationToken cancellationToken = default)
{
var result = await LoadJsonAsync(StakerCraneFileName, cancellationToken);
StakerCraneData = result ?? new LibraryStakerCraneData();
return result != null;
}
///
/// LibraryRackSingle.json 파일을 비동기로 로드합니다.
///
/// 취소 토큰
/// 로드 성공 여부
public async UniTask LoadRackSingleAsync(CancellationToken cancellationToken = default)
{
var result = await LoadJsonAsync(RackFileName, cancellationToken);
RackSingleData = result ?? new LibraryRackSingleData();
return result != null;
}
///
/// LibraryAGV.json 파일을 비동기로 로드합니다.
///
/// 취소 토큰
/// 로드 성공 여부
public async UniTask LoadAGVAsync(CancellationToken cancellationToken = default)
{
var result = await LoadJsonAsync(AGVFileName, cancellationToken);
AGVData = result ?? new LibraryAGVData();
return result != null;
}
///
/// JSON 파일을 비동기로 로드하여 지정된 타입으로 역직렬화합니다.
///
/// 역직렬화 대상 타입
/// 로드할 파일명
/// 취소 토큰
/// 역직렬화된 데이터, 실패 시 null
///
/// 파일 읽기와 JSON 파싱을 백그라운드 스레드에서 수행합니다.
///
private async UniTask LoadJsonAsync(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(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
///
/// 모든 라이브러리 파일을 동기로 로드합니다.
///
///
/// 메인 스레드를 블로킹하므로, 가능하면 LoadAllAsync() 또는 LoadAllParallelAsync() 사용을 권장합니다.
///
public void LoadAll()
{
LoadStakerCrane();
LoadRackSingle();
LoadAGV();
IsLoaded = true;
}
///
/// LibraryStakerCrane.json 파일을 동기로 로드합니다.
///
/// 로드 성공 여부
public bool LoadStakerCrane()
{
var result = LoadJson(StakerCraneFileName);
StakerCraneData = result ?? new LibraryStakerCraneData();
return result != null;
}
///
/// LibraryRackSingle.json 파일을 동기로 로드합니다.
///
/// 로드 성공 여부
public bool LoadRackSingle()
{
var result = LoadJson(RackFileName);
RackSingleData = result ?? new LibraryRackSingleData();
return result != null;
}
///
/// LibraryAGV.json 파일을 동기로 로드합니다.
///
/// 로드 성공 여부
public bool LoadAGV()
{
var result = LoadJson(AGVFileName);
AGVData = result ?? new LibraryAGVData();
return result != null;
}
///
/// JSON 파일을 동기로 로드하여 지정된 타입으로 역직렬화합니다.
///
/// 역직렬화 대상 타입
/// 로드할 파일명
/// 역직렬화된 데이터, 실패 시 null
private T LoadJson(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(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
///
/// LibraryStakerCrane.json 루트 데이터 클래스
///
[Serializable]
public class LibraryStakerCraneData
{
/// 스태커 크레인 장비 목록
public List list = new();
}
///
/// LibraryRackSingle.json 루트 데이터 클래스
///
///
/// JSON 키가 "AGV"로 되어 있어 필드명이 AGV입니다.
///
[Serializable]
public class LibraryRackSingleData
{
/// 랙 장비 목록 (JSON 키: "list")
public List list = new();
}
///
/// LibraryAGV.json 루트 데이터 클래스
///
[Serializable]
public class LibraryAGVData
{
/// AGV 장비 목록
public List list = new();
}
#endregion
#region Equipment Item
///
/// 개별 장비 항목 데이터
///
///
/// 장비의 기본 정보, 3D 모델 경로, 속성 정의, 상태 정의를 포함합니다.
///
[Serializable]
public class EquipmentItem
{
///
/// 장비 식별자
///
///
/// 고유한 장비 ID (예: "SingleFork", "Rack_Single")
///
public string id;
///
/// 표시 이름
///
///
/// UI에 표시되는 사람이 읽을 수 있는 이름 (예: "Single Fork")
///
public string label;
///
/// glTF/GLB 파일 경로
///
///
/// Resources/Studio 폴더 기준 상대 경로 (예: "staker_crane/SingleFork.glb")
///
public string gltf;
///
/// 썸네일 이미지 경로 (선택)
///
///
/// 라이브러리 UI에서 표시할 미리보기 이미지
///
public string image;
///
/// 속성 정보 목록
///
///
/// 장비의 각 파트별 속성 정의 (크기, 속도 등)
///
public List propertiesInfo = new();
///
/// 상태 정보
///
///
/// 네트워크 연결 상태, 설비 운영 상태 등의 정의
///
public StatusInfo statusInfo = new();
}
#endregion
#region Properties Info
///
/// 속성 정보 섹션
///
///
/// 장비의 특정 파트(Frame, Fork 등)에 대한 속성 그룹을 정의합니다.
///
[Serializable]
public class PropertiesInfo
{
///
/// 섹션 이름
///
///
/// UI에서 그룹핑 표시용 (예: "Frame", "Fork", "Carriage")
///
public string section;
///
/// 3D 모델 파트 이름
///
///
/// glTF 모델 내의 노드/메시 이름 (예: "Fork_01", "Carriage_Body")
/// null이면 모델 전체에 적용
///
public string modelPart;
///
/// 모델 타입
///
///
///
/// - General: 일반 타입 - 단순 속성 표시
/// - Stretch: 스트레치 타입 - 크기 조절 가능
/// - Repeat: 반복 타입 - 반복 배치 가능 (레일 등)
///
///
public string modelType;
///
/// 속성 항목 목록
///
public List properties = new();
}
///
/// 개별 속성 항목
///
[Serializable]
public class PropertyItem
{
///
/// 속성 ID (선택)
///
///
/// 코드에서 속성을 식별하기 위한 고유 키 (예: "length", "forwardSpeed")
///
public string id;
///
/// 표시 레이블
///
///
/// UI에 표시되는 속성 이름 (예: "너비", "주행 속도")
///
public string label;
///
/// 데이터 타입
///
///
/// "Float" 또는 "Int"
///
public string type;
///
/// 속성 값
///
///
/// 숫자 값(int, float) 또는 벡터 문자열 (예: 150, 50.5, "0,0,1")
/// JSON에서 숫자 또는 문자열로 저장될 수 있음
///
public object value;
///
/// 단위
///
///
/// 표시용 단위 문자열 (예: "cm", "cm/s", "")
///
public string unit;
///
/// value를 float로 반환합니다.
///
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;
}
///
/// value를 int로 반환합니다.
///
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;
}
///
/// value를 string으로 반환합니다.
///
public string GetStringValue()
{
return value?.ToString() ?? string.Empty;
}
}
#endregion
#region Status Info
///
/// 상태 정보 컨테이너
///
///
/// 장비의 네트워크 연결 상태와 운영 상태를 정의합니다.
///
[Serializable]
public class StatusInfo
{
///
/// 네트워크 상태 정의
///
///
/// 상위 시스템과의 통신 상태 (connect/disconnect)
/// null이면 네트워크 상태 미지원
///
public StatusSection network;
///
/// 설비 상태 정의
///
///
/// 장비의 운영 상태 (normal, running, error 등)
///
public StatusSection equipment;
}
///
/// 상태 섹션
///
[Serializable]
public class StatusSection
{
///
/// 섹션 이름
///
///
/// UI 표시용 (예: "상위 통신 상태", "설비 상태")
///
public string section;
///
/// 상태 속성 목록
///
public List properties = new();
}
///
/// 개별 상태 속성
///
///
/// 특정 상태 값에 대한 정의 (이름, 색상 등)
///
[Serializable]
public class StatusProperty
{
///
/// 상태 값 (매칭용)
///
///
/// 실제 데이터와 매칭하기 위한 값 (예: "0", "1", "true", "false")
///
public string label;
///
/// 상태 이름
///
///
/// 상태의 의미를 나타내는 이름 (예: "normal", "running", "error", "connect")
///
public string stat;
///
/// 상태 색상 (HEX 코드)
///
///
/// UI 표시용 색상 코드 (예: "#228B22" - 녹색, "#8B0000" - 빨간색)
/// 빈 문자열이면 기본 색상 사용
///
public string? value;
}
#endregion
}