개발 중

This commit is contained in:
logonkhi
2025-10-30 18:36:26 +09:00
parent e245ee9f96
commit 09a620ff71
35 changed files with 430 additions and 386 deletions

View File

@@ -1,4 +1,5 @@
#nullable enable
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -62,7 +63,7 @@ namespace UVC.UI.List.Tree
///
/// 역할:
/// - AddItem()에서 새 아이템의 부모로 지정
/// - UpdateFlattenedItemList()에서 모든 자식을 순회
/// - UpdateFlattenedItemDataList()에서 모든 자식을 순회
///
/// 구조 예:
/// root (이것)
@@ -150,16 +151,20 @@ namespace UVC.UI.List.Tree
/// - SelectRange()에서 두 아이템 사이의 인덱스를 빠르게 찾기
/// - Shift+클릭 범위 선택 구현
///
/// UpdateFlattenedItemList() 메서드로 항상 최신 상태로 유지됩니다.
/// UpdateFlattenedItemDataList() 메서드로 항상 최신 상태로 유지됩니다.
/// </summary>
protected List<TreeListItemData> allItemsFlattened = new List<TreeListItemData>();
protected List<TreeListItemData> allItemDatasFlattened = new List<TreeListItemData>();
public List<TreeListItemData> AllItemsFlattened => allItemsFlattened;
public List<TreeListItemData> AllItemsFlattened => allItemDatasFlattened;
protected List<TreeListItemData> items = new List<TreeListItemData>();
public List<TreeListItemData> Items => items;
protected List<TreeListItem> allItemFlattened = new List<TreeListItem>();
public List<TreeListItem> AllItemFlattened => allItemFlattened;
/// <summary>
/// 드래그 & 드롭 매니저입니다.
/// </summary>
@@ -174,7 +179,7 @@ namespace UVC.UI.List.Tree
/// 무언가 일어났을 때 다른 코드에 알려주는 메커니즘입니다.
/// 구독자(listener)가 등록되어 있으면 자동으로 호출됩니다.
///
/// 문법: event Action<TreeListItemData, bool>?
/// 문법: Action<TreeListItemData, bool>?
/// = TreeListItemData 1개, bool 1개를 매개변수로 받는 함수들을 등록
///
/// 호출되는 경우:
@@ -186,7 +191,7 @@ namespace UVC.UI.List.Tree
/// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}");
/// };
/// </summary>
public event Action<TreeListItemData, bool>? OnItemSelectionChanged;
public Action<TreeListItemData, bool>? OnItemSelectionChanged;
/// <summary>
/// 현재 선택된 아이템 목록을 반환합니다. (읽기 전용)
@@ -197,7 +202,7 @@ namespace UVC.UI.List.Tree
///
/// AsReadOnly()란?
/// 리스트를 읽기 전용으로 변환합니다.
/// 따라서 외부에서 SelectedItems.Add()같은 수정이 불가능합니다.
/// 따라서 외부에서 SelectedItems.AddChild()같은 수정이 불가능합니다.
///
/// 왜 이렇게 하나?
/// 클래스 내부에서만 selectedItems를 제어하고,
@@ -211,6 +216,8 @@ namespace UVC.UI.List.Tree
/// </summary>
public IReadOnlyList<TreeListItemData> SelectedItems => selectedItems.AsReadOnly();
public List<TreeListItem> TreeLists => root.GetComponentsInChildren<TreeListItem>().ToList();
private void Awake()
{
// 드래그 & 드롭 이벤트 구독
@@ -226,6 +233,54 @@ namespace UVC.UI.List.Tree
{
dragDropManager.OnDropped -= HandleItemDropped;
}
if(OnItemSelectionChanged != null) OnItemSelectionChanged = null;
}
private void Update()
{
// Delete 키 입력 감지
if (Input.GetKeyDown(KeyCode.Delete))
{
HandleDeleteKeyPressed();
}
}
/// <summary>
/// Delete 키가 눌렸을 때 선택된 아이템들을 삭제합니다.
///
/// 동작:
/// 1. 현재 선택된 아이템 목록을 복사 (선택 상태 변경 중 문제 방지)
/// 2. 복사본을 역순으로 순회 (뒤에서 앞으로)
/// 3. 각 아이템을 RemoveItem()으로 삭제
///
/// 역순 삭제 이유:
/// - 앞에서부터 삭제하면 인덱스가 변경되어 뒤의 아이템을 놓칠 수 있음
/// - 뒤에서부터 삭제하면 앞의 인덱스는 영향을 받지 않음
///
/// ToList()로 복사하는 이유:
/// - RemoveItem() 호출 중 selectedItems가 수정될 수 있음
/// - 복사본을 순회하면 안전하게 모든 아이템을 삭제 가능
/// </summary>
private void HandleDeleteKeyPressed()
{
// 선택된 아이템이 없으면 아무것도 하지 않음
if (selectedItems.Count == 0)
{
return;
}
// 선택된 아이템 목록을 복사 (역순 삭제 시 안전)
var itemsToDelete = selectedItems.ToList();
// 디버그 로그
Debug.Log($"Delete key pressed. Removing {itemsToDelete.Count} selected item(s).");
// 각 선택된 아이템을 삭제
foreach (var item in itemsToDelete)
{
DeleteItem(item);
}
}
/// <summary>
@@ -254,66 +309,98 @@ namespace UVC.UI.List.Tree
public void AddItem(TreeListItemData data)
{
data.Parent = null;
// Instantiate(템플릿, 부모 Transform)
// = 템플릿을 복제하고 부모의 자식으로 설정
TreeListItem item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
//data에 해당하는 TreeListItem 찾기
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
{
item.transform.SetParent(root);
item.SetExpand();
// 생성된 아이템 초기화
// 데이터를 UI에 바인딩하고 이벤트 리스너 등록
item.Init(data, this, dragDropManager);
UniTask.DelayFrame(1).ContinueWith(() =>
{
//gameObject 순서 조절
item.transform.SetAsLastSibling();
items.Add(data);
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemDataList();
});
}
else
{
// Instantiate(템플릿, 부모 Transform)
// = 템플릿을 복제하고 부모의 자식으로 설정
item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemList();
// 생성된 아이템 초기화
// 데이터를 UI에 바인딩하고 이벤트 리스너 등록
item.Init(data, this, dragDropManager);
items.Add(data);
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemDataList();
}
}
public void AddItemAt(TreeListItemData data, int index)
{
data.Parent = null;
TreeListItem item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
item.Init(data, this, dragDropManager);
items.Insert(index, data);
//data에 해당하는 TreeListItem 찾기
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
{
item.transform.SetParent(root);
item.SetExpand();
}
else
{
item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
item.Init(data, this, dragDropManager);
items.Insert(index, data);
}
//gameObject 순서 조절
item.transform.SetSiblingIndex(index);
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemList();
UniTask.DelayFrame(1).ContinueWith(() =>
{
//gameObject 순서 조절
item.transform.SetSiblingIndex(index);
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemDataList();
});
}
/// <summary>
/// 해당 항목을 트리 리스트에서 제거합니다. date 자체를 삭제하지 않습니다.
/// </summary>
/// <param name="data"></param>
public void RemoveItem(TreeListItemData data)
{
if (data.Parent != null)
{
data.Parent.RemoveChild(data);
}
data.Parent = null;
items.Remove(data);
//gameObject 삭제
// 🎯 root가 부모인 TreeListItem을 찾아 Destroy하기
// 1. root의 모든 직접 자식을 순회
for (int i = 0; i < root.childCount; i++)
// selectedItems에서도 제거
if (selectedItems.Contains(data))
{
// 자식 Transform 가져오기
Transform childTransform = root.GetChild(i);
// TreeListItem 컴포넌트 검색
TreeListItem treeListItem = childTransform.GetComponent<TreeListItem>();
// TreeListItem이 있고, 그 데이터가 제거할 데이터와 같으면 삭제
if (treeListItem != null && treeListItem.Data == data)
{
// TreeListItem의 Delete() 메서드로 정리 작업 수행
// (이벤트 구독 해제 및 GameObject 삭제)
treeListItem.Delete();
break; // 해당 아이템을 찾았으므로 루프 종료
}
selectedItems.Remove(data);
}
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
if (item != null)
{
item.Delete();
}
// 범위 선택에 필요한 평탄화 리스트 업데이트
UpdateFlattenedItemList();
UpdateFlattenedItemDataList();
}
/// <summary>
/// 해당 항목을 트리 리스트에서 제거며 date 자체도 삭제합니다.
/// </summary>
/// <param name="data"></param>
public void DeleteItem(TreeListItemData data)
{
RemoveItem(data);
data.Dispose();
}
/// <summary>
@@ -323,7 +410,7 @@ namespace UVC.UI.List.Tree
/// 1. 기존 평탄화 리스트를 비움 (Clear)
/// 2. root의 모든 직접 자식을 순회 (foreach)
/// 3. 각 자식의 TreeListItem 컴포넌트 획득 (GetComponent)
/// 4. AddItemToFlattened() 호출해서 자식과 손자까지 재귀 추가
/// 4. AddItemDataToFlattened() 호출해서 자식과 손자까지 재귀 추가
///
/// 호출되는 시점:
/// - AddItem() 실행 후 (새 아이템 추가됨)
@@ -342,17 +429,27 @@ namespace UVC.UI.List.Tree
/// 예: child.GetComponent<TreeListItem>()
/// = child 게임 오브젝트에서 TreeListItem 컴포넌트 찾기
/// </summary>
internal void UpdateFlattenedItemList()
internal void UpdateFlattenedItemDataList()
{
// 기존 평탄화 리스트 비우기
allItemsFlattened.Clear();
allItemDatasFlattened.Clear();
// root의 모든 직접 자식을 순회
// (손자, 증손자는 재귀로 처리됨)
foreach (TreeListItemData itemData in items)
{
// 이 아이템과 자식들을 재귀적으로 평탄화 리스트에 추가
AddItemToFlattened(itemData);
AddItemDataToFlattened(itemData);
}
allItemFlattened.Clear();
foreach (var data in allItemDatasFlattened)
{
var item = root.GetComponentsInChildren<TreeListItem>().FirstOrDefault(x => x.Data == data);
if(item != null)
{
allItemFlattened.Add(item);
}
}
}
@@ -366,31 +463,31 @@ namespace UVC.UI.List.Tree
/// 동작:
/// 1. 현재 data를 allItemsFlattened에 추가
/// 2. data의 모든 자식 Children을 순회
/// 3. 각 자식에 대해 AddItemToFlattened() 재귀 호출
/// 3. 각 자식에 대해 AddItemDataToFlattened() 재귀 호출
///
/// 예시 - 트리 구조를 평탄화하는 과정:
///
/// 트리: 재귀 호출 흐름: 결과:
/// 폴더1 AddItemToFlattened(폴더1) [폴더1,
/// ├─ 파일1 → AddItemToFlattened(파일1) 파일1,
/// ├─ 파일2 → AddItemToFlattened(파일2) 파일2,
/// └─ 폴더2 → AddItemToFlattened(폴더2) 폴더2,
/// └─ 파일3 → AddItemToFlattened(파일3) 파일3]
/// 폴더1 AddItemDataToFlattened(폴더1) [폴더1,
/// ├─ 파일1 → AddItemDataToFlattened(파일1) 파일1,
/// ├─ 파일2 → AddItemDataToFlattened(파일2) 파일2,
/// └─ 폴더2 → AddItemDataToFlattened(폴더2) 폴더2,
/// └─ 파일3 → AddItemDataToFlattened(파일3) 파일3]
///
/// 매개변수:
/// - data: 현재 처리할 아이템
/// </summary>
private void AddItemToFlattened(TreeListItemData data)
private void AddItemDataToFlattened(TreeListItemData data)
{
// 현재 아이템을 평탄화 리스트에 추가
allItemsFlattened.Add(data);
allItemDatasFlattened.Add(data);
// 현재 아이템의 모든 자식을 순회
foreach (var child in data.Children)
{
// 각 자식에 대해 재귀 호출
// = 자식의 자식들도 모두 추가됨
AddItemToFlattened(child);
AddItemDataToFlattened(child);
}
}
@@ -516,10 +613,10 @@ namespace UVC.UI.List.Tree
private void SelectRange(TreeListItemData startItem, TreeListItemData endItem)
{
// 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기
int startIndex = allItemsFlattened.IndexOf(startItem);
int startIndex = allItemDatasFlattened.IndexOf(startItem);
// 평탄화 리스트에서 끝 아이템의 위치(인덱스) 찾기
int endIndex = allItemsFlattened.IndexOf(endItem);
int endIndex = allItemDatasFlattened.IndexOf(endItem);
// 두 아이템 모두 리스트에 없으면 종료
if (startIndex == -1 || endIndex == -1)
@@ -537,7 +634,7 @@ namespace UVC.UI.List.Tree
// i = minIndex부터 i = maxIndex까지 (포함)
for (int i = minIndex; i <= maxIndex; i++)
{
SelectItem(allItemsFlattened[i]);
SelectItem(allItemDatasFlattened[i]);
}
}
@@ -722,7 +819,7 @@ namespace UVC.UI.List.Tree
private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem)
{
// 평탄화 리스트 업데이트
UpdateFlattenedItemList();
UpdateFlattenedItemDataList();
// 필요시 UI 재구성 (예: 자식 컨테이너 위치 변경 등)
Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'");

View File

@@ -34,22 +34,22 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 드래그 시작 시 발생하는 이벤트입니다.
/// </summary>
public event Action<TreeListItemData>? OnDragStarted;
public Action<TreeListItemData>? OnDragStarted;
/// <summary>
/// 드래그 진행 중 발생하는 이벤트입니다.
/// </summary>
public event Action<TreeListItemData, TreeListItemData?>? OnDragEntered;
public Action<TreeListItemData, TreeListItemData?>? OnDragEntered;
/// <summary>
/// 드래그 종료 시 발생하는 이벤트입니다.
/// </summary>
public event Action<TreeListItemData>? OnDragEnded;
public Action<TreeListItemData>? OnDragEnded;
/// <summary>
/// 드롭 완료 시 발생하는 이벤트입니다.
/// </summary>
public event Action<TreeListItemData, TreeListItemData?>? OnDropped;
public Action<TreeListItemData, TreeListItemData?>? OnDropped;
/// <summary>
/// 드래그를 시작합니다.
@@ -140,44 +140,21 @@ namespace UVC.UI.List.Tree
/// <returns>조상-후손 관계이면 true</returns>
public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant)
{
var current = potentialDescendant;
var current = potentialDescendant.Parent;
while (current != null)
{
// 재귀적으로 부모를 찾는 로직
// TreeListItemData는 부모 참조가 없으므로, 전체 리스트에서 찾아야 함
// 여기서는 간단한 버전으로, 자식 리스트를 통해 확인
var parent = FindParent(current, potentialAncestor);
if (parent == null)
{
break;
}
if (parent == potentialAncestor)
if (current == potentialAncestor)
{
return true;
}
current = parent;
current = current.Parent;
}
return false;
}
/// <summary>
/// 주어진 아이템의 부모를 찾습니다.
/// </summary>
/// <param name="child">자식 아이템</param>
/// <param name="searchLimit">검색 제한 (이 아이템이 나타나면 멈춤)</param>
/// <returns>부모 아이템 (없으면 null)</returns>
private static TreeListItemData? FindParent(TreeListItemData child, TreeListItemData searchLimit)
{
// 이 구현은 전체 트리를 순회하므로 성능이 낮음
// 실제 구현에서는 TreeListItemData에 부모 참조를 추가하는 것이 나음
// 여기서는 시연 목적으로 간단하게 구현
return null;
}
/// <summary>
/// 모든 드래그 & 드롭 상태를 리셋합니다.

View File

@@ -1,7 +1,9 @@
#nullable enable
using Cysharp.Threading.Tasks;
using DG.Tweening;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
@@ -46,7 +48,7 @@ namespace UVC.UI.List.Tree
/// (private이지만 Unity가 특별히 접근 가능)
/// </summary>
[SerializeField]
protected TreeList control;
protected TreeList treeList;
/// <summary>
/// 이 아이템의 이름을 표시하는 텍스트 UI입니다.
@@ -224,7 +226,7 @@ namespace UVC.UI.List.Tree
///
/// 매개변수:
/// - data: 표시할 데이터
/// - control: 부모 TreeList (선택 관리, 클릭 처리 등)
/// - treeList: 부모 TreeList (선택 관리, 클릭 처리 등)
///
/// 사용 예:
/// var item = Instantiate(prefab);
@@ -233,40 +235,29 @@ namespace UVC.UI.List.Tree
public void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
{
// 1. 기본 정보 할당
this.control = control;
this.treeList = control;
this.data = data;
gameObject.name = "TreeListItem_" + data.Name;
// 2. 아이템 이름을 UI에 표시
valueText.text = data.Name;
// 3. 자식 아이템들을 UI로 생성
Debug.Log("Creating Children for " + data.Name + ", " + data.Children.Count);
// 자식이 없는 경우
if (data.Children.Count == 0)
{
// 펼침 버튼 숨기기 (자식이 없으니까 펼칠 게 없음)
childExpand.gameObject.SetActive(false);
// 자식 컨테이너도 숨기기
childContainer.SetActive(false);
}
// 자식이 있는 경우
else
{
if (data.Children.Count > 0)
{
// 각 자식 데이터에 대해 UI 생성
foreach (var childData in data.Children)
{
CreateItem(childData); // 재귀적으로 트리 구조 생성
}
// 펼침 버튼과 컨테이너 표시
childExpand.gameObject.SetActive(true);
childContainer.SetActive(true);
// 화살표 방향 설정 (초기에는 펼쳐짐)
SetExpand();
}
// 화살표 방향 설정 (초기에는 펼쳐짐)
SetExpand();
// 4. 데이터 변경 감지 구독
// 데이터의 이름, 자식 목록 등이 변경되면 OnDataChanged 호출
@@ -323,118 +314,93 @@ namespace UVC.UI.List.Tree
/// 데이터가 변경되면 UI가 자동으로 업데이트되는 패턴입니다.
/// 이렇게 하면 데이터와 UI를 동기화 유지하기 쉽습니다.
/// </summary>
private void OnDataChanged(TreeListItemData changedData)
private void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
{
if (data == null) return;
// 이름이 변경된 경우
if (valueText.text != data.Name)
if (changedType == ChangedType.Expanded)
{
valueText.text = data.Name;
childContainer.SetActive(data.IsExpanded);
// 펼침/접힘 상태 변경 처리
SetExpand();
return;
}
// 자식 목록이 변경된 경우 (추가/제거/순서 변경)
// 1. 현재 UI의 모든 자식 TreeListItem을 수집
var currentUIChildren = new List<TreeListItem>();
for (int i = 0; i < childRoot.childCount; i++)
{
var childItem = childRoot.GetChild(i).GetComponent<TreeListItem>();
if (childItem != null && childItem.Data != null)
if (changedType == ChangedType.Name)
{
// 이름이 변경된 경우
if (valueText.text != data.Name)
{
currentUIChildren.Add(childItem);
valueText.text = data.Name;
}
return;
}
// 2. 데이터와 UI의 자식 개수가 다르거나 순서가 다르면 동기화
bool needsReorder = false;
// 2-1. 기본적인 개수 확인
if (currentUIChildren.Count != data.Children.Count)
if (changedType == ChangedType.ResetChildren)
{
needsReorder = true;
}
// 2-2. 개수가 같으면 순서 확인
else if (currentUIChildren.Count > 0)
{
for (int i = 0; i < data.Children.Count; i++)
// 전체 리셋 처리
// 자식 모두 삭제 후 재생성
for (int i = childRoot.childCount - 1; i >= 0; i--)
{
// UI 아이템의 데이터와 데이터 리스트의 순서가 다르면 재정렬 필요
if (currentUIChildren[i].Data != data.Children[i])
var childItem = childRoot.GetChild(i).GetComponent<TreeListItem>();
if (childItem != null)
{
needsReorder = true;
break;
childItem.Delete(true);
}
}
}
// 3. 재정렬이 필요하면 전체 재구성
if (needsReorder)
{
// 데이터에는 있지만 UI에는 없는 자식 추가
var childrenToAdd = new List<TreeListItemData>();
foreach (var childData in data.Children)
{
bool uiChildExists = currentUIChildren.Any(ui => ui.Data == childData);
if (!uiChildExists)
{
childrenToAdd.Add(childData);
}
}
// UI에는 있지만 데이터에는 없는 자식 제거
var childrenToDestroy = new List<TreeListItem>();
foreach (var uiChild in currentUIChildren)
{
if (uiChild.Data != null && !data.Children.Contains(uiChild.Data))
{
childrenToDestroy.Add(uiChild);
}
}
// 수집한 자식들 삭제
foreach (var child in childrenToDestroy)
{
child.Delete();
}
// 새로운 자식 추가
foreach (var childData in childrenToAdd)
{
CreateItem(childData);
}
// 4. 자식들을 데이터 순서대로 재정렬
// 현재 UI 자식들을 다시 수집 (추가/삭제 후이므로)
var updatedUIChildren = new List<TreeListItem>();
for (int i = 0; i < childRoot.childCount; i++)
treeList.UpdateFlattenedItemDataList();
}
else if (changedType == ChangedType.AddChild)
{
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null)
{
var childItem = childRoot.GetChild(i).GetComponent<TreeListItem>();
if (childItem != null && childItem.Data != null)
{
updatedUIChildren.Add(childItem);
}
item.transform.SetParent(childRoot);
item.SetExpand();
}
else
{
CreateItem(changedData);
}
UniTask.DelayFrame(1).ContinueWith(() =>
{
if (item != null) item.transform.SetAsLastSibling();
treeList.UpdateFlattenedItemDataList();
});
}
else if (changedType == ChangedType.AddAtChild)
{
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null)
{
item.transform.SetParent(childRoot);
item.SetExpand();
}
else
{
item = CreateItem(changedData);
}
// 데이터 순서에 맞게 UI 자식들을 정렬
for (int i = 0; i < data.Children.Count; i++)
UniTask.DelayFrame(1).ContinueWith(() =>
{
var targetData = data.Children[i];
// 현재 위치의 UI 자식이 목표 데이터와 다르면 이동
if (updatedUIChildren[i].Data != targetData)
item.transform.SetSiblingIndex(index);
treeList.UpdateFlattenedItemDataList();
});
}
else if (changedType == ChangedType.RemoveChild)
{
var childItem = childRoot.GetChild(index).GetComponent<TreeListItem>();
if (childItem != null)
{
childItem.Delete(false);
UniTask.DelayFrame(1).ContinueWith(() =>
{
// 목표 데이터에 해당하는 UI 자식을 찾기
var targetUIChild = updatedUIChildren.FirstOrDefault(ui => ui.Data == targetData);
if (targetUIChild != null)
{
// 목표 위치로 이동 (SetSiblingIndex: 형제 중 순서 변경)
targetUIChild.transform.SetSiblingIndex(i);
// 로컬 리스트에서도 위치 업데이트
updatedUIChildren.Remove(targetUIChild);
updatedUIChildren.Insert(i, targetUIChild);
}
}
treeList.UpdateFlattenedItemDataList();
});
}
}
@@ -655,7 +621,7 @@ namespace UVC.UI.List.Tree
// 4. 부모 TreeList에 클릭 정보 전달
// TreeList는 이 정보를 받아서 선택 로직을 처리합니다.
// (단일 선택 / 다중 선택 / 범위 선택 등)
control.OnItemClicked(data, ctrlPressed, shiftPressed);
treeList.OnItemClicked(data, ctrlPressed, shiftPressed);
}
#endregion
@@ -690,12 +656,6 @@ namespace UVC.UI.List.Tree
// ! 연산자: 반대로 변경 (true → false, false → true)
data!.IsExpanded = !data.IsExpanded;
// 3. 자식 컨테이너 표시/숨김
// IsExpanded가 true면 표시, false면 숨김
childContainer.SetActive(data.IsExpanded);
// 4. 0.3초에 걸쳐 펼침/접힘 애니메이션 실행
SetExpand(0.3f);
}
/// <summary>
@@ -719,34 +679,23 @@ namespace UVC.UI.List.Tree
/// 애니메이션이 완료되면 호출되는 콜백입니다.
/// 람다 식(=>)으로 익명 함수를 정의합니다.
/// </summary>
private void SetExpand(float duration = 0.0f)
internal void SetExpand(float duration = 0.0f)
{
// 1. 자식이 있는지 확인해서 펼침 버튼 표시 여부 결정
childExpand.gameObject.SetActive(childRoot.childCount > 0);
// 2. 자식이 있는 경우에만 애니메이션 실행
if (childRoot.childCount > 0)
{
// UI 상태와 데이터 동기화
if (data != null) data.IsExpanded = childContainer.activeSelf == true;
// 3. 화살표 회전 애니메이션 실행
// DORotate(목표 각도, 지속 시간)
// IsExpanded가 true면 0도 (▼), false면 90도 (▶)
childExpand.transform.DOKill();
childExpand.transform.DORotate(new Vector3(0, 0, data.IsExpanded ? 0 : 90), duration)
.OnComplete(() =>
{
// 4. 애니메이션이 완료되면 플래그 리셋
// 이제 다시 ToggleChild() 호출 가능
isAnimating = false;
});
}
else
{
// 자식이 없으면 IsExpanded는 항상 false
if (data != null) data.IsExpanded = false;
}
// 펼침 버튼
childExpand.gameObject.SetActive(data!.Children.Count > 0);
childContainer.SetActive(data!.Children.Count > 0 && data!.IsExpanded);
// DORotate(목표 각도, 지속 시간)
// IsExpanded가 true면 0도 (▼), false면 90도 (▶)
childExpand.transform.DOKill();
childExpand.transform.DORotate(new Vector3(0, 0, data!.IsExpanded ? 0 : 90), duration)
.OnComplete(() =>
{
// 4. 애니메이션이 완료되면 플래그 리셋
// 이제 다시 ToggleChild() 호출 가능
isAnimating = false;
});
}
#endregion
@@ -794,7 +743,7 @@ namespace UVC.UI.List.Tree
/// - AddChild()에서 새 자식을 추가할 때
///
/// 동작:
/// 1. control.ItemPrefab를 childRoot 아래에 인스턴스화
/// 1. treeList.ItemPrefab를 childRoot 아래에 인스턴스화
/// 2. 생성된 item의 Init() 메서드 호출
/// 3. 생성된 item 반환
///
@@ -816,15 +765,15 @@ namespace UVC.UI.List.Tree
{
// 1. 프리팹을 복제해서 새로운 TreeListItem 생성
// Instantiate<T>(원본, 부모, 옵션)
// control.ItemPrefab: UI 아이템 템플릿
// treeList.ItemPrefab: UI 아이템 템플릿
// childRoot: 새 아이템의 부모 Transform
TreeListItem item = GameObject.Instantiate<TreeListItem>(
control.ItemPrefab, // 복제할 프리팹
treeList.ItemPrefab, // 복제할 프리팹
childRoot // 부모로 배치할 위치
);
// 2. 생성된 아이템 초기화
item.Init(data, control, control.DragDropManager);
item.Init(data, treeList, treeList.DragDropManager);
// 3. 생성된 아이템 반환
return item;
@@ -856,7 +805,7 @@ namespace UVC.UI.List.Tree
/// 게임이 계속 실행되면서 메모리 사용량이 증가해서
/// 결국 게임이 느려지거나 충돌할 수 있습니다.
/// </summary>
public void Delete()
public void Delete(bool deleteData = false)
{
// 1. 데이터 변경 이벤트 구독 해제
if (data != null)
@@ -865,6 +814,8 @@ namespace UVC.UI.List.Tree
data.OnDataChanged -= OnDataChanged;
data.OnSelectionChanged -= OnSelectionChanged;
data.Parent = null;
if(deleteData) data.Dispose();
data = null;
}
// 2. 버튼 클릭 이벤트 구독 해제
@@ -908,6 +859,9 @@ namespace UVC.UI.List.Tree
{
data.OnDataChanged -= OnDataChanged;
data.OnSelectionChanged -= OnSelectionChanged;
data.Parent = null;
data.Dispose();
data = null;
}
// 2. 버튼 클릭 이벤트 구독 해제

View File

@@ -17,7 +17,7 @@ namespace UVC.UI.List.Tree
///
/// 이 클래스는 InfiniteScrollData를 상속하여 UI 스크롤 리스트와 연동됩니다.
/// </summary>
public class TreeListItemData
public class TreeListItemData: IDisposable
{
#region (Events)
@@ -30,7 +30,7 @@ namespace UVC.UI.List.Tree
/// 사용 예:
/// treeItem.OnDataChanged += (data) => Debug.Log("데이터 변경됨!");
/// </summary>
public Action<TreeListItemData>? OnDataChanged;
public Action<ChangedType, TreeListItemData, int>? OnDataChanged;
/// <summary>
/// 선택 상태가 변경되었을 때 발생하는 이벤트입니다.
@@ -136,7 +136,7 @@ namespace UVC.UI.List.Tree
if (_name != value)
{
_name = value;
NotifyDataChanged(); // UI에 변경을 알림
NotifyDataChanged(ChangedType.Name); // UI에 변경을 알림
}
}
}
@@ -158,7 +158,7 @@ namespace UVC.UI.List.Tree
if (_option != value)
{
_option = value;
NotifyDataChanged();
NotifyDataChanged(ChangedType.Option);
}
}
}
@@ -184,7 +184,7 @@ namespace UVC.UI.List.Tree
if (_isExpanded != value)
{
_isExpanded = value;
NotifyDataChanged(); // 트리 구조 UI 갱신
NotifyDataChanged(ChangedType.Expanded); // 트리 구조 UI 갱신
}
}
}
@@ -261,7 +261,7 @@ namespace UVC.UI.List.Tree
set
{
_children = value ?? new List<TreeListItemData>();
NotifyDataChanged();
NotifyDataChanged(ChangedType.ResetChildren);
}
}
@@ -344,11 +344,18 @@ namespace UVC.UI.List.Tree
{
child._parent = this;
_children.Add(child);
NotifyDataChanged(); // UI에 트리 구조 변경 알림
NotifyDataChanged(ChangedType.AddChild, child); // UI에 트리 구조 변경 알림
}
public void AddChildAt(TreeListItemData child, int index)
{
child._parent = this;
_children.Insert(index, child);
NotifyDataChanged(ChangedType.AddAtChild, child, index); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 이 아이템에서 지정된 자식 아이템을 제거합니다.
/// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하지만 아이템 자체는 삭제하지 않습니다.
///
/// 동작:
/// 1. 자식을 _children 리스트에서 제거
@@ -371,8 +378,19 @@ namespace UVC.UI.List.Tree
public void RemoveChild(TreeListItemData child)
{
child._parent = null;
int index = _children.IndexOf(child);
_children.Remove(child);
NotifyDataChanged(); // UI에 트리 구조 변경 알림
NotifyDataChanged(ChangedType.RemoveChild, child, index); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하고 아이템 자체도 삭제합니다.
/// </summary>
/// <param name="child"></param>
public void DeleteChild(TreeListItemData child)
{
RemoveChild(child);
child.Dispose();
}
/// <summary>
@@ -401,9 +419,10 @@ namespace UVC.UI.List.Tree
foreach (var child in _children)
{
child._parent = null;
child.Dispose();
}
_children.Clear();
NotifyDataChanged(); // UI에 트리 구조 변경 알림
NotifyDataChanged(ChangedType.ResetChildren); // UI에 트리 구조 변경 알림
}
#endregion
@@ -424,11 +443,11 @@ namespace UVC.UI.List.Tree
/// 왜 protected인가?
/// 이 클래스를 상속받은 자식 클래스에서도 호출할 수 있도록 하기 위함입니다.
/// </summary>
internal void NotifyDataChanged()
internal void NotifyDataChanged(ChangedType changedType, TreeListItemData? target = null, int index = -1)
{
// OnDataChanged가 등록되어 있으면 실행
// ?. 연산자: null이면 실행하지 않음 (null reference exception 방지)
OnDataChanged?.Invoke(this);
OnDataChanged?.Invoke(changedType, (target == null ? this : target), index);
}
#endregion
@@ -555,6 +574,32 @@ namespace UVC.UI.List.Tree
return false;
}
public void Dispose()
{
if(OnDataChanged != null) OnDataChanged = null;
if(OnSelectionChanged != null) OnSelectionChanged = null;
if(OnClickAction != null) OnClickAction = null;
if(Children != null)
{
//foreach(var child in Children)
//{
// child.Dispose();
//}
Children.Clear();
}
}
#endregion
}
public enum ChangedType
{
Name,
Option,
Expanded,
ResetChildren,
AddChild,
AddAtChild,
RemoveChild
}
}

View File

@@ -183,11 +183,6 @@ namespace UVC.UI.List.Tree
// 드롭 위치 표시 업데이트
UpdateDropIndicator(targetItem);
if (targetItem != null)
{
Debug.Log($"[OnDrag] 드래그 중: {targetItem.Data?.Name ?? "Unknown"} 위에 있음");
}
}
/// <summary>
@@ -535,7 +530,7 @@ namespace UVC.UI.List.Tree
{
Debug.Log($"[HandleDropSuccess] 루트로 이동");
MoveToRoot(draggedData);
treeList.UpdateFlattenedItemList();
treeList.UpdateFlattenedItemDataList();
return;
}
@@ -568,7 +563,7 @@ namespace UVC.UI.List.Tree
break;
}
treeList.UpdateFlattenedItemList();
treeList.UpdateFlattenedItemDataList();
}
/// <summary>
@@ -584,8 +579,8 @@ namespace UVC.UI.List.Tree
// 하위 1/3: 아래
var height = targetRect.rect.height;
var thresholdUpper = height * 0.2f;
var thresholdLower = height * 0.8f;
var thresholdUpper = height * 0.3f;
var thresholdLower = height * 0.7f;
// 월드 좌표에서 로컬 좌표로 변환
RectTransformUtility.ScreenPointToLocalPointInRectangle(
@@ -597,7 +592,9 @@ namespace UVC.UI.List.Tree
// 로컬 Y 좌표로 판단 (RectTransform의 피벗이 중앙이면 -height/2 ~ height/2)
float relativeY = localMousePos.y;
if (relativeY > thresholdUpper)
//Debug.Log($"GetDropPosition height:{height}, relativeY:{relativeY}, thresholdUpper:{thresholdUpper}, thresholdLower:{thresholdLower}");
if (relativeY > -thresholdUpper)
{
return DropPosition.Above;
}
@@ -617,7 +614,7 @@ namespace UVC.UI.List.Tree
private void MoveToRoot(TreeListItemData draggedData)
{
// 기존 부모에서 제거
RemoveFromParent(draggedData);
//RemoveFromParent(draggedData);
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트로 이동");
// ✅ 루트 레벨에 추가
@@ -689,7 +686,7 @@ namespace UVC.UI.List.Tree
/// </summary>
private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData)
{
RemoveFromParent(draggedData);
//RemoveFromParent(draggedData);
targetData.AddChild(draggedData);
Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동");
}
@@ -699,16 +696,15 @@ namespace UVC.UI.List.Tree
/// </summary>
private void MoveBefore(TreeListItemData draggedData, TreeListItemData targetData)
{
var parentData = FindParentOfItem(draggedData);
RemoveFromParent(draggedData);
var parentData = targetData.Parent;// FindParentOfItem(targetData);
//RemoveFromParent(draggedData);
if (parentData != null)
{
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
parentData.Children.Insert(targetIndex, draggedData);
parentData.NotifyDataChanged();
parentData.AddChildAt(draggedData, targetIndex); // 자식으로 추가
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 이동");
}
}
@@ -734,16 +730,15 @@ namespace UVC.UI.List.Tree
/// </summary>
private void MoveAfter(TreeListItemData draggedData, TreeListItemData targetData)
{
var parentData = FindParentOfItem(draggedData);
RemoveFromParent(draggedData);
var parentData = targetData.Parent;
//RemoveFromParent(draggedData);
if (parentData != null)
{
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
parentData.Children.Insert(targetIndex + 1, draggedData);
parentData.NotifyDataChanged();
parentData.AddChildAt(draggedData, targetIndex + 1);
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 이동");
}
}
@@ -770,18 +765,17 @@ namespace UVC.UI.List.Tree
private void RemoveFromParent(TreeListItemData item)
{
var parent = FindParentOfItem(item);
Debug.Log($"[RemoveFromParent] {item.Name}을(를) 부모 {parent == null}에서 제거");
if (parent != null)
Debug.Log($"[RemoveFromParent] {item.Name}을(를) 부모 {item.Parent == null}에서 제거");
if (item.Parent != null)
{
parent.RemoveChild(item);
item.Parent.RemoveChild(item);
}
else
{
// 루트 레벨에서 제거
treeList?.RemoveItem(item);
}
treeList?.UpdateFlattenedItemList();
treeList?.UpdateFlattenedItemDataList();
}
/// <summary>