Files
XRLib/Assets/work.md
2025-11-12 16:48:34 +09:00

7.7 KiB

BlockDetailModal 통합 구현 작업지시서 (최종안)

1. 목표 BlockDetailModal의 3개 뷰(3D 모델, 계층 리스트, 간트 차트)를 통합하고, 모든 뷰가 상호 동기화되도록 완성한다.

2. 핵심 데이터 계약 및 전략

  • 데이터 모델: SHI.modal.ModelDetailListItemData를 핵심 데이터 단위로 사용한다.
  • 고유 식별자: 상속받은 UVC.UI.List.Tree.TreeListItemDatapublic Guid Id 를 모든 동기화(선택, 포커스, 가시성)의 유일한 키로 사용한다. Name 기반의 기존 로직은 모두 Id 기반으로 변경한다.
  • 데이터 정의:
    • ScheduleSegment: Guid ItemId, DateTime Start, DateTime End, float Progress (0-1), string Type
    • GanttChartData: List<ScheduleSegment> Segments
  • 라이브러리: 3D 모델 로딩은 설치된 glTFast (6.14.1)를 사용한다.

3. 샘플 데이터 및 모델 파일 생성

  • 샘플 glTF 모델: Assets/StreamingAssets/block.glb 파일을 사용(또는 배치). 테스트 시 LoadData 호출에 Path.Combine(Application.streamingAssetsPath, "block.glb") 전달.
  • 샘플 간트 데이터: Assets/StreamingAssets/sample_gantt_data.json 파일을 생성한다. (폴더가 없으면 생성)
  • JSON 내용: GanttChartData 구조에 맞는 샘플 JSON 데이터를 작성한다. ItemIdModelDetailView에서 생성될 ModelDetailListItemDataId와 일치해야 하므로, 초기 테스트를 위해 몇 개의 고정된 Guid를 사용한다.
    {
      "Segments": [
        {
          "ItemId": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
          "Start": "2024-07-01T00:00:00Z",
          "End": "2024-07-10T00:00:00Z",
          "Progress": 0.5,
          "Type": "Task"
        },
        {
          "ItemId": "b2a8f0e0-0e3a-4b1a-9b0a-0e1b9b0f0e1b",
          "Start": "2024-07-05T00:00:00Z",
          "End": "2024-07-15T00:00:00Z",
          "Progress": 0.2,
          "Type": "Milestone"
        }
      ]
    }
    

4. BlockDetailModal.cs 수정

  • 필드 추가:
    • private CancellationTokenSource? _cts;
    • private bool _isSelectionSuppressed;
  • 메서드 추가/수정:
    • public async UniTask LoadData(string gltfPath, GanttChartData gantt, CancellationToken externalCt = default):
      1. 기존 _cts가 있다면 취소하고 Dispose 한다. _cts = CancellationTokenSource.CreateLinkedTokenSource(externalCt);로 새로 생성한다.
      2. (구현) 로딩 UI를 표시한다.
      3. modelView.LoadModelAsync(gltfPath, _cts.Token)을 호출하여 IEnumerable<ModelDetailListItemData>를 받는다.
      4. listView.Populate(items)로 리스트를 채운다.
      5. chartView.LoadData(gantt)로 차트 데이터를 전달한다.
      6. (구현) 로딩 UI를 숨긴다.
    • private void OnDisable(): _cts?.Cancel(); _cts?.Dispose();를 호출하여 비활성화 시 모든 비동기 작업을 취소한다. modelViewchartView의 리소스 정리(Dispose) 메서드를 호출한다.
  • 이벤트 핸들러 수정 (Start 메서드 내):
    • 모든 이벤트 연결을 Id 기반으로 변경하고, 재진입 방지 로직을 적용한다.
      // 예시: listView.OnItemSelected += ...
      listView.OnItemSelected += data => {
          if (_isSelectionSuppressed || data is not ModelDetailListItemData item) return;
          HandleSelection(item.Id, "ListView");
      };
      // modelView, chartView도 동일하게 수정
      
    • private void HandleSelection(Guid itemId, string source):
      _isSelectionSuppressed = true;
      if (source != "ListView") listView.SelectByItemId(itemId);
      if (source != "ModelView") modelView.FocusItemById(itemId);
      if (source != "ChartView") chartView.SelectByItemId(itemId);
      // UniTask.Yield() 또는 DelayFrame(1)을 사용하여 한 프레임 뒤 false로 설정, 즉각적인 재진입 방지
      UniTask.Yield(PlayerLoopTiming.PostLateUpdate, this.GetCancellationTokenOnDestroy()).ContinueWith(() => _isSelectionSuppressed = false);
      

5. ModelDetailView.cs 구현

  • 메서드 추가:
    • public async UniTask<IEnumerable<ModelDetailListItemData>> LoadModelAsync(string path, CancellationToken ct): glTFast를 사용하여 모델을 비동기 로드. 각 GameObject에 대해 ModelDetailListItemData를 생성하고 Id를 부여하여 Dictionary<Guid, GameObject>에 매핑 후, 데이터 목록을 반환한다.
    • public void FocusItemById(Guid id): id에 해당하는 GameObjectBounds를 계산하여 카메라를 부드럽게 이동/줌한다. 객체를 하이라이트한다.
    • public void SetVisibility(Guid id, bool isVisible): id에 해당하는 GameObject를 활성화/비활성화한다.
    • public void Dispose(): 로드된 GameObject, 생성된 머티리얼 인스턴스 등 모든 리소스를 파괴한다.
  • 구현 내용:
    • Raycast를 통한 객체 선택 로직을 구현하고, 선택 시 OnItemSelected 이벤트를 Id가 포함된 ModelDetailListItemData와 함께 발생시킨다.
    • 객체 하이라이트를 위한 머티리얼 인스턴싱 및 캐시 관리 로직을 구현한다.

6. ModelDetailListView.cs 수정/구현

  • 메서드 추가:
    • public void Populate(IEnumerable<ModelDetailListItemData> items): 기존 항목을 모두 지우고 새 데이터로 트리를 구성한다.
    • public void SelectByItemId(Guid id): id에 해당하는 아이템을 찾아 선택 상태로 만든다.
  • 가시성 처리:
    • Populate 시, 각 ModelDetailListItemDataOnClickVisibleAction(data, isVisible) => { if(data is ModelDetailListItemData item) OnVisibilityChanged?.Invoke(item.Id, isVisible); } 와 같은 람다를 할당한다.
    • public event Action<Guid, bool> OnVisibilityChanged; 이벤트를 추가하고, BlockDetailModal에서 이 이벤트를 구독하여 modelView.SetVisibility를 호출하도록 연결한다.

7. ModelDetailChartView.cs 구현

  • 메서드 추가:
    • public void LoadData(GanttChartData data): 차트 데이터를 받아 렌더링한다.
    • public void SelectByItemId(Guid id): id에 해당하는 행을 찾아 하이라이트하고, 해당 위치로 스크롤한다.
    • public event Action<Guid> OnRowClicked; (string에서 Guid로 변경)
    • public void Dispose(): UI Toolkit으로 생성된 동적 요소들을 정리한다.
  • 구현 내용:
    • UI Toolkit의 ListView를 사용하여 행 가상화를 구현한다.
    • 행 클릭 시 OnRowClicked 이벤트를 ItemId와 함께 발생시킨다.

8. 실행 우선순위

  1. 데이터 계약: ScheduleSegment/GanttChartData 클래스 생성 및 샘플 JSON/GLB 파일 작성.
  2. ID 기반 리팩토링: BlockDetailModal 및 각 뷰의 이벤트와 메서드 시그니처를 string name에서 Guid Id로 모두 변경.
  3. 뷰 구현: ModelDetailView의 모델 로딩, ModelDetailListViewPopulate, ModelDetailChartViewLoadData를 순서대로 구현.
  4. 동기화 로직: BlockDetailModal에 재진입 방지 로직(_isSelectionSuppressed)을 포함한 HandleSelection 구현.
  5. 세부 기능: 하이라이트, 카메라 포커싱, 가시성 토글, 차트 스크롤링 순으로 구현.
  6. 생명주기: OnDisable과 각 뷰의 Dispose 메서드에 리소스 정리 로직을 철저히 구현.
  7. 테스트: 중복 이름 아이템 선택, 빠른 모달 닫기 등 엣지 케이스를 포함하여 테스트.