using Best.HTTP.JSON.LitJson; using Cysharp.Threading.Tasks; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; using UnityEngine; using UnityEngine.UI; using UVC.Json; namespace SHI.modal { /// /// 모달 패널 내부에서 모델 뷰, 계층 리스트, 간트 차트를 조율하는 컨트롤러입니다. /// 컴포넌트 간 선택 동기화, 레이아웃 분할 제어, 데이터 로딩을 담당합니다. /// public class BlockDetailModal : MonoBehaviour { [Header("References")] [SerializeField] private Button closeButton; [SerializeField] private ModelDetailListView listView; [SerializeField] private ModelDetailView modelView; [SerializeField] private ModelDetailChartView chartView; [Header("UI Controls")] [SerializeField] private Button modelViewExpandButton; [SerializeField] private Button chartViewExpandButton; [SerializeField] private Button dragButton; [SerializeField] private Button showListButton; // split 제어용 캐시 private LayoutElement _modelLayout; private LayoutElement _chartLayout; private enum ExpandedSide { None, Model, Chart } private ExpandedSide _expanded = ExpandedSide.None; private RectTransform ModelRect => modelView != null ? modelView.GetComponent() : null; private RectTransform ChartRect => chartView != null ? chartView.GetComponent() : null; private HorizontalSplitDrag _splitter; // lifecycle private CancellationTokenSource _cts; private bool _suppressSelection; // key<->id 매핑(차트-리스트/모델 동기화) private readonly Dictionary _keyToId = new Dictionary(); private readonly Dictionary _idToKey = new Dictionary(); /// /// UI 이벤트를 연결하고 스플리터를 준비합니다. /// public void Start() { // Close if (closeButton != null) { closeButton.onClick.AddListener(OnCloseClicked); } // 리스트 표시 버튼 if (showListButton != null && listView != null) showListButton.onClick.AddListener(OnShowListClicked); if (showListButton != null) showListButton.gameObject.SetActive(false); // 선택 동기화: 리스트 -> 모델/차트 if (listView != null) { listView.OnItemSelected += OnListItemSelected; listView.OnItemDeselected += OnListItemDeselected; listView.OnClosed += OnListClosed; listView.OnVisibilityChanged += OnListVisibilityChanged; } // 선택 동기화: 모델 -> 리스트/차트 if (modelView != null) { modelView.OnItemSelected += OnModelItemSelected; } // 선택 동기화: 차트 -> 리스트/모델 if (chartView != null) { chartView.OnRowClickedByKey += OnChartRowClickedByKey; chartView.OnRowClicked += OnChartRowClicked; } // 확장 버튼 if (modelViewExpandButton != null) modelViewExpandButton.onClick.AddListener(ToggleExpandModel); if (chartViewExpandButton != null) chartViewExpandButton.onClick.AddListener(ToggleExpandChart); // 드래그 스플리터 SetupSplitControls(); } private void OnCloseClicked() { gameObject.SetActive(false); } private void OnShowListClicked() { if (listView != null) listView.gameObject.SetActive(true); if (showListButton != null) showListButton.gameObject.SetActive(false); if (_splitter != null) _splitter.RefreshPosition(); } private void OnListItemSelected(UVC.UI.List.Tree.TreeListItemData data) { if (data == null) return; HandleSelection(data.Id, "ListView"); } private void OnListItemDeselected(UVC.UI.List.Tree.TreeListItemData data) { HandleDeselection(data.Id, "ListView"); } private void OnListClosed() { if (showListButton != null) showListButton.gameObject.SetActive(true); if (_splitter != null) _splitter.RefreshPosition(); } private void OnListVisibilityChanged(Guid id, bool vis) { if (modelView != null) modelView.SetVisibility(id, vis); } private void OnModelItemSelected(UVC.UI.List.Tree.TreeListItemData data) { if (data == null) return; HandleSelection(data.Id, "ModelView"); } private void OnChartRowClickedByKey(string key) { if (string.IsNullOrEmpty(key)) return; if (_keyToId.TryGetValue(key, out var id)) HandleSelection(id, "ChartView"); } private void OnChartRowClicked(Guid id) { HandleSelection(id, "ChartView"); } /// /// LoadData 호출 시 리스트/모델/차트를 활성화하고 분할 비율을0.5/0.5로 초기화합니다. /// private void InitializePanelsForLoad() { // 활성화 상태 설정 if (listView != null) { listView.gameObject.SetActive(true); listView.Clear(); } if (showListButton != null) showListButton.gameObject.SetActive(false); if (ModelRect != null) ModelRect.gameObject.SetActive(true); if (chartView != null) chartView.gameObject.SetActive(true); _expanded = ExpandedSide.None; // 레이아웃 요소 보장 및50/50 설정 var modelRect = ModelRect; var chartRect = ChartRect; if (modelRect != null) { _modelLayout = modelRect.GetComponent() ?? modelRect.gameObject.AddComponent(); _modelLayout.flexibleWidth = 1f; } if (chartRect != null) { _chartLayout = chartRect.GetComponent() ?? chartRect.gameObject.AddComponent(); _chartLayout.flexibleWidth = 1f; } // 스플리터 보장 및 동기화 if (_splitter == null && dragButton != null && modelRect != null && chartRect != null) { _splitter = dragButton.gameObject.GetComponent(); if (_splitter == null) _splitter = dragButton.gameObject.AddComponent(); var leftFixed = listView != null ? listView.GetComponent() : null; _splitter.Initialize(modelRect, chartRect, leftFixed); } if (_splitter != null) _splitter.gameObject.SetActive(true); Canvas.ForceUpdateCanvases(); _splitter?.RefreshPosition(); } /// /// glTF 모델과 간트 데이터를 하위 뷰에 로드하고, 선택 동기화를 위한 매핑을 구축합니다. /// /// glTF/glb 파일 경로. /// 간트 데이터셋 경로. /// 외부 취소 토큰. public async UniTask LoadData(string gltfPath, string ganttPath, CancellationToken externalCt = default) { Debug.Log($"BlockDetailModal: LoadData {gltfPath}"); // 레이아웃/활성 상태 초기화 (리스트/모델/차트 활성,50/50 비율) InitializePanelsForLoad(); // 이전 작업 취소 if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); } _cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt); var ct = _cts.Token; // 모델/리스트 로드 IEnumerable items = Array.Empty(); if (modelView != null) { try { items = await modelView.LoadModelAsync(gltfPath, ct); } catch (OperationCanceledException) { } } BuildKeyMaps(items); if (listView != null) listView.SetupData(items); if (chartView != null) chartView.LoadFromStreamingAssets(ganttPath); } private void BuildKeyMaps(IEnumerable items) { _keyToId.Clear(); _idToKey.Clear(); if (items == null) return; var stack = new Stack(); foreach (var it in items) { if (it == null) continue; stack.Push(it); while (stack.Count > 0) { var cur = stack.Pop(); if (!string.IsNullOrEmpty(cur.ExternalKey)) { _keyToId[cur.ExternalKey] = cur.Id; _idToKey[cur.Id] = cur.ExternalKey; } if (cur.Children != null) { for (int i = 0; i < cur.Children.Count; i++) { var child = cur.Children[i] as ModelDetailListItemData; if (child != null) stack.Push(child); } } } } } private void SetupSplitControls() { var modelRect = ModelRect; var chartRect = ChartRect; if (modelRect == null || chartRect == null || dragButton == null) return; _modelLayout = modelRect.GetComponent(); if (_modelLayout == null) _modelLayout = modelRect.gameObject.AddComponent(); _chartLayout = chartRect.GetComponent(); if (_chartLayout == null) _chartLayout = chartView.gameObject.AddComponent(); // 초기 분할50/50 _modelLayout.flexibleWidth = 1f; _chartLayout.flexibleWidth = 1f; // 드래그 핸들 부착 _splitter = dragButton.gameObject.GetComponent(); if (_splitter == null) _splitter = dragButton.gameObject.AddComponent(); var leftFixed = listView != null ? listView.GetComponent() : null; _splitter.Initialize(modelRect, chartRect, leftFixed); // 레이아웃 갱신 후 위치 보정 Canvas.ForceUpdateCanvases(); _splitter.RefreshPosition(); } private void HandleSelection(Guid itemId, string source) { if (_suppressSelection) return; _suppressSelection = true; try { if (source != "ListView" && listView != null) listView.SelectByItemId(itemId); if (source != "ModelView" && modelView != null) modelView.FocusItemById(itemId); if (source != "ChartView" && chartView != null) { if (_idToKey.TryGetValue(itemId, out var key)) chartView.SelectByItemKey(key); else chartView.SelectByItemId(itemId); } } finally { _suppressSelection = false; } } private void HandleDeselection(Guid itemId, string source) { if (_suppressSelection) return; if (source != "ModelView" && modelView != null) modelView.UnfocusItem(); } private void ToggleExpandModel() { if (ModelRect == null || chartView == null) return; if (_expanded == ExpandedSide.Model) { ResetSplit(); return; } _expanded = ExpandedSide.Model; ModelRect.gameObject.SetActive(true); chartView.gameObject.SetActive(false); if (_splitter != null) _splitter.gameObject.SetActive(false); } private void ToggleExpandChart() { if (ModelRect == null || chartView == null) return; if (_expanded == ExpandedSide.Chart) { ResetSplit(); return; } _expanded = ExpandedSide.Chart; ModelRect.gameObject.SetActive(false); chartView.gameObject.SetActive(true); if (_splitter != null) _splitter.gameObject.SetActive(false); } private void ResetSplit() { _expanded = ExpandedSide.None; if (ModelRect != null) ModelRect.gameObject.SetActive(true); if (chartView != null) chartView.gameObject.SetActive(true); if (_modelLayout != null) _modelLayout.flexibleWidth = 1f; if (_chartLayout != null) _chartLayout.flexibleWidth = 1f; if (_splitter != null) { _splitter.gameObject.SetActive(true); _splitter.RefreshPosition(); } } private void OnDisable() { if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); _cts = null; } if (chartView != null) chartView.Dispose(); if (modelView != null) modelView.Dispose(); } private void OnDestroy() { // 이벤트 해제(메모리 누수 방지) if (closeButton != null) closeButton.onClick.RemoveListener(OnCloseClicked); if (showListButton != null && listView != null) showListButton.onClick.RemoveListener(OnShowListClicked); if (modelViewExpandButton != null) modelViewExpandButton.onClick.RemoveListener(ToggleExpandModel); if (chartViewExpandButton != null) chartViewExpandButton.onClick.RemoveListener(ToggleExpandChart); if (listView != null) { listView.OnItemSelected -= OnListItemSelected; listView.OnItemDeselected -= OnListItemDeselected; listView.OnClosed -= OnListClosed; listView.OnVisibilityChanged -= OnListVisibilityChanged; } if (modelView != null) { modelView.OnItemSelected -= OnModelItemSelected; } if (chartView != null) { chartView.OnRowClickedByKey -= OnChartRowClickedByKey; chartView.OnRowClicked -= OnChartRowClicked; } if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); _cts = null; } } } }