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