개발 완료

This commit is contained in:
logonkhi
2025-11-03 19:07:04 +09:00
parent a5a3f7d553
commit 5292879aaf
6 changed files with 349 additions and 299 deletions

View File

@@ -1,6 +1,7 @@
#nullable enable
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System;
using System.Linq;
using System.Threading;
using TMPro;
@@ -10,6 +11,25 @@ using UVC.UI.List.Tree;
namespace UVC.UI.Window
{
/// <summary>
/// 계층 데이터를 표시/검색/선택하는 창(View)입니다.
///
/// 책임:
/// - 메인 트리(`treeList`)와 검색 트리(`treeListSearch`)를 관리
/// - 입력창으로 검색을 수행하고 결과를 검색 트리에 표시(청크 처리 + 로딩 애니메이션)
/// - `TreeList.OnItemSelectionChanged`를 구독해 외부로 선택/해제 이벤트를 전달
/// - 외부에서 호출 가능한 간단한 항목 추가/삭제 API 제공(실제 렌더링/상태는 `TreeList`가 담당)
///
/// 사용 예:
/// <example>
/// <![CDATA[
/// // 외부에서 창을 참조했다고 가정
/// hierarchyWindow.OnItemSelected += item => Debug.Log($"Selected: {item.Name}");
/// hierarchyWindow.AddItem(new TreeListItemData("Root A"));
/// hierarchyWindow.AddItemAt(new TreeListItemData("Root B"),0);
/// ]]>
/// </example>
/// </summary>
public class HierarchyWindow : MonoBehaviour
{
[SerializeField]
@@ -30,22 +50,31 @@ namespace UVC.UI.Window
[SerializeField]
protected Image loadingImage;
/// <summary>
/// 메인/검색 리스트에서 항목이 선택될 때 발생합니다.
/// </summary>
public System.Action<TreeListItemData>? OnItemSelected;
/// <summary>
/// 메인/검색 리스트에서 항목이 선택 해제될 때 발생합니다.
/// </summary>
public System.Action<TreeListItemData>? OnItemDeselected;
// 검색 목록에서 선택된 항목(클론된 데이터)
private TreeListItemData? selectedSearchItem;
// 검색 작업 상태
private CancellationTokenSource? searchCts;
private bool isSearching = false;
private float searchProgress = 0f; //unused for visual progress now but kept for future
private float searchProgress =0f; // 내부 진행도 추적용
[SerializeField]
[Tooltip("로딩 아이콘 회전 속도(도/초)")]
private float loadingRotateSpeed = 360f;
private float loadingRotateSpeed =360f;
[SerializeField]
[Tooltip("로딩 이미지의 채우기 애니메이션 속도(사이클/초)")]
private float loadingFillCycle = 0.5f; // cycles per second (full0->1->0 cycle per second)
//loadingRotateSpeed 360 일때, loadingFillCycle를 0.5 보다 높게 설정하면 이상해 보임
private float loadingFillCycle =0.5f; // cycles per second. loadingRotateSpeed360 일때, loadingFillCycle를0.5 보다 높게 설정하면 이상해 보임
// DOTween tweens
private Tween? loadingRotationTween;
@@ -58,7 +87,13 @@ namespace UVC.UI.Window
treeListSearch.gameObject.SetActive(false);
inputField.onSubmit.AddListener(OnInputFieldSubmit);
// 검색 리스트 선택 변경을 감지
// 메인 리스트 선택 변경을 외부 이벤트로 전달
if (treeList != null)
{
treeList.OnItemSelectionChanged += HandleMainSelectionChanged;
}
// 검색 리스트의 선택 변경을 감지 (선택 결과를 원본 트리에 반영하는 용도)
if (treeListSearch != null)
{
treeListSearch.OnItemSelectionChanged += OnSearchSelectionChanged;
@@ -76,11 +111,11 @@ namespace UVC.UI.Window
// 검색에서 선택한 항목이 있으면 원본 트리에서 동일 항목을 선택하고 펼침
if (selectedSearchItem != null && treeList != null)
{
// 원본 데이터 찾기 (이 프로젝트의 Equals는 Name 기반이므로 Name으로 검색)
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i.Name == selectedSearchItem.Name);
// 원본 데이터 찾기 (TreeListItemData == 연산자는 Id 기반)
var target = treeList.AllItemDataFlattened.FirstOrDefault(i => i == selectedSearchItem);
if (target != null)
{
// TreeList에 새로 추가된 유틸리티를 이용해 부모 체인을 펼치고 선택 처리
// 부모 체인을 펼치고 선택 처리
treeList.RevealAndSelectItem(target, true);
}
@@ -89,33 +124,65 @@ namespace UVC.UI.Window
});
}
private void Update()
/// <summary>
/// 메인 트리에 항목을 추가합니다.
/// </summary>
/// <param name="data">추가할 데이터.</param>
public void AddItem(TreeListItemData data)
{
// DOTween handles the animations; no per-frame logic needed here
treeList.AddItem(data);
}
/// <summary>
/// 메인 트리에 항목을 특정 인덱스에 삽입합니다.
/// </summary>
/// <param name="data">삽입할 데이터.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddItemAt(TreeListItemData data, int index)
{
treeList.AddItemAt(data, index);
}
/// <summary>
/// 메인 트리에서 항목을 제거합니다(뷰만 제거, 데이터는 호출 측 정책에 따름).
/// </summary>
/// <param name="data">제거할 데이터.</param>
public void RemoveItem(TreeListItemData data)
{
treeList.RemoveItem(data);
}
/// <summary>
/// 메인 트리에서 항목을 완전히 삭제합니다(뷰+데이터 정리).
/// </summary>
/// <param name="data">삭제할 데이터.</param>
public void DeleteItem(TreeListItemData data)
{
treeList.DeleteItem(data);
}
private void StartLoadingAnimation()
{
if (loadingImage == null) return;
// Ensure any previous tweens are killed
// 기존 트윈 정리
StopLoadingAnimation();
loadingImage.fillAmount = 0f;
loadingImage.fillAmount =0f;
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.gameObject.SetActive(true);
// Rotation: rotate360 degrees repeatedly. Duration for one360 rotation (seconds)
float rotDuration = (loadingRotateSpeed != 0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) : 1f;
// Use LocalAxisAdd to rotate continuously
loadingRotationTween = loadingImage.transform.DOLocalRotate(new Vector3(0f, 0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
// 회전 트윈
float rotDuration = (loadingRotateSpeed !=0f) ? (360f / Mathf.Abs(loadingRotateSpeed)) :1f;
loadingRotationTween = loadingImage.transform
.DOLocalRotate(new Vector3(0f,0f, -360f), rotDuration, RotateMode.LocalAxisAdd)
.SetEase(Ease.Linear)
.SetLoops(-1, LoopType.Restart);
// Fill animation:0 ->1 ->0 in one cycle. Forward duration = half cycle
// For a0->1 then jump-to-0 repeat, use Restart loop and full cycle duration
float fullDuration = (loadingFillCycle > 0f) ? (1f / loadingFillCycle) :1f;
loadingFillTween = DOTween.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x, 1f, fullDuration)
// 채우기 트윈
float fullDuration = (loadingFillCycle >0f) ? (1f / loadingFillCycle) :1f;
loadingFillTween = DOTween
.To(() => loadingImage.fillAmount, x => loadingImage.fillAmount = x,1f, fullDuration)
.SetEase(Ease.InOutSine)
.SetLoops(-1, LoopType.Yoyo);
}
@@ -137,9 +204,8 @@ namespace UVC.UI.Window
if (loadingImage != null)
{
loadingImage.gameObject.SetActive(false);
// reset transform / fill
loadingImage.transform.localRotation = Quaternion.identity;
loadingImage.fillAmount = 0f;
loadingImage.fillAmount =0f;
}
}
@@ -152,9 +218,8 @@ namespace UVC.UI.Window
searchCts = null;
}
isSearching = false;
searchProgress = 0f;
searchProgress =0f;
// stop DOTween animations
StopLoadingAnimation();
}
@@ -163,10 +228,24 @@ namespace UVC.UI.Window
if (isSelected)
{
selectedSearchItem = data;
OnItemSelected?.Invoke(data);
}
else if (selectedSearchItem == data)
{
selectedSearchItem = null;
OnItemDeselected?.Invoke(data);
}
}
private void HandleMainSelectionChanged(TreeListItemData data, bool isSelected)
{
if (isSelected)
{
OnItemSelected?.Invoke(data);
}
else
{
OnItemDeselected?.Invoke(data);
}
}
@@ -202,7 +281,7 @@ namespace UVC.UI.Window
private async UniTaskVoid PerformSearchAsync(string text, CancellationToken token)
{
isSearching = true;
searchProgress = 0f;
searchProgress =0f;
var results = new System.Collections.Generic.List<TreeListItemData>();
@@ -215,7 +294,7 @@ namespace UVC.UI.Window
}
int total = sourceList.Count;
if (total == 0)
if (total ==0)
{
isSearching = false;
StopLoadingAnimation();
@@ -226,8 +305,8 @@ namespace UVC.UI.Window
string lower = text.ToLowerInvariant();
// 분할 처리: 일정 갯수마다 await으로 제어권을 반환
const int chunk = 100; // 한 번에 처리할 항목 수 (조절 가능)
for (int i = 0; i < total; i++)
const int chunk =100;
for (int i =0; i < total; i++)
{
token.ThrowIfCancellationRequested();
@@ -238,7 +317,7 @@ namespace UVC.UI.Window
}
// 진행도 업데이트 (내부 사용)
if ((i % chunk) == 0)
if ((i % chunk) ==0)
{
searchProgress = (float)i / (float)total;
await UniTask.Yield(PlayerLoopTiming.Update);
@@ -246,7 +325,7 @@ namespace UVC.UI.Window
}
// 최종 진행도
searchProgress = 1f;
searchProgress =1f;
// UI 반영은 메인 스레드에서
await UniTask.SwitchToMainThread();
@@ -291,12 +370,12 @@ namespace UVC.UI.Window
treeListSearch.OnItemSelectionChanged -= OnSearchSelectionChanged;
}
CancelSearch();
}
if (treeList != null)
{
treeList.OnItemSelectionChanged -= HandleMainSelectionChanged;
}
public void AddItem(TreeListItemData data)
{
treeList.AddItem(data);
CancelSearch();
}
}
}