버그 수정 중. 이동 후 남겨지는 경우가 있음

This commit is contained in:
logonkhi
2025-10-31 19:55:14 +09:00
parent 09a620ff71
commit 02ed8a01a0
10 changed files with 1079 additions and 1345 deletions

View File

@@ -901,6 +901,7 @@ GameObject:
- component: {fileID: 6986221832819843173}
- component: {fileID: 1154093525567517578}
- component: {fileID: 2733383086465723583}
- component: {fileID: 1132190849957045457}
m_Layer: 5
m_Name: Root
m_TagString: Untagged
@@ -967,6 +968,21 @@ MonoBehaviour:
m_EditorClassIdentifier:
m_HorizontalFit: 2
m_VerticalFit: 2
--- !u!114 &1132190849957045457
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 6690344807426313734}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: de4afb3a0d93f05448f2fc60683275c0, type: 3}
m_Name:
m_EditorClassIdentifier:
margin: {x: 0, y: 0}
target:
- {fileID: 2240268413977263779}
--- !u!1 &6757006638454796643
GameObject:
m_ObjectHideFlags: 0

View File

@@ -13,7 +13,7 @@ public class HierarchySample : MonoBehaviour
{
TreeListItemData itemData = new TreeListItemData("Item " + i);
int len = 3;// Random.Range(1, 5);
int len = i < 5 ? 0 : 2;// Random.Range(1, 5);
Debug.Log("len: " + len);
for (int j = 0; j < len; j++)
{
@@ -22,6 +22,10 @@ public class HierarchySample : MonoBehaviour
for (int k = 0; k < childLen; k++)
{
itemData.Children[j].AddChild(new TreeListItemData("Item " + i + "." + j + "." + k));
for (int l = 0; l < 2; l++)
{
itemData.Children[j].Children[k].AddChild(new TreeListItemData("Item " + i + "." + j + "." + k + "." + l));
}
}
}
hierarchyWindow.AddItem(itemData);

View File

@@ -933,6 +933,10 @@ PrefabInstance:
serializedVersion: 3
m_TransformParent: {fileID: 651880472}
m_Modifications:
- target: {fileID: 1132190849957045457, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: EnableWidth
value: 0
objectReference: {fileID: 0}
- target: {fileID: 1697446796394887609, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_AnchorMax.x
value: 0
@@ -941,10 +945,22 @@ PrefabInstance:
propertyPath: m_AnchorMax.y
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_SizeDelta.x
value: 0
objectReference: {fileID: 0}
- target: {fileID: 2240268413977263779, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_SizeDelta.y
value: 2
objectReference: {fileID: 0}
- target: {fileID: 2651864206743669424, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_Name
value: TreeList
objectReference: {fileID: 0}
- target: {fileID: 5353790932593107132, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_Size
value: 0.9375
objectReference: {fileID: 0}
- target: {fileID: 5613474474329381979, guid: a030935af8c62d748b6fe3d01bd4851f, type: 3}
propertyPath: m_AnchorMax.x
value: 0

View File

@@ -12,7 +12,7 @@ namespace Gpm.Ui
/// <b>UI 계층 구조 예시:</b>
/// <code>
/// - Background (RectTransform)
/// - Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter, ContentSizeSetter)
/// - Content (RectTransform, VerticalLayoutGroup, ContentSizeFitter, ContentSizeSetter)
/// - Item1 (Image)
/// - Item2 (Image)
/// - ...
@@ -45,16 +45,28 @@ namespace Gpm.Ui
}
}
[Tooltip("넓이에 크기 조절 적용")]
[SerializeField]
public bool EnableWidth = true;
[Tooltip("높이에 크기 조절 적용")]
[SerializeField]
public bool EnableHeight = true;
/// <summary>
/// 크기를 조절할 때 추가할 여백(margin)입니다.
/// x, y 값을 설정하여 target의 너비와 높이에 각각 추가적인 공간을 줄 수 있습니다.
/// </summary>
[Tooltip("크기를 조절할 때 추가할 여백(margin)입니다.")]
[SerializeField]
public Vector2 margin;
/// <summary>
/// 이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.
/// 인스펙터 창에서 크기를 동기화할 UI 요소들을 여기에 할당합니다.
/// </summary>
[Tooltip("이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.")]
[SerializeField]
public RectTransform[] target;
/// <summary>
@@ -76,12 +88,13 @@ namespace Gpm.Ui
// 현재 RectTransform의 크기에 margin 값을 더하여 최종 크기를 계산합니다.
Vector2 sizeDelta = new Vector2(rectTransform.sizeDelta.x + margin.x, rectTransform.sizeDelta.y + margin.y);
if (target != null)
{
// target 배열의 모든 RectTransform에 대해 계산된 크기를 적용합니다.
for (int i = 0; i < target.Length; i++)
{
if (!EnableWidth) sizeDelta.x = target[i].sizeDelta.x;
if (!EnableHeight) sizeDelta.y = target[i].sizeDelta.y;
target[i].sizeDelta = sizeDelta;
// target의 레이아웃을 갱신하도록 표시하여 UI가 올바르게 다시 그려지도록 합니다.
LayoutRebuilder.MarkLayoutForRebuild(target[i]);

View File

@@ -70,7 +70,6 @@ namespace Gpm.Ui
{
return;
}
if (force == true)
{
// 즉시 레이아웃을 강제로 다시 계산합니다.

File diff suppressed because it is too large Load Diff

View File

@@ -6,23 +6,27 @@ using UnityEngine;
namespace UVC.UI.List.Tree
{
/// <summary>
/// 트리 리스트의 드래그 & 드롭 기능을 관리하는 클래스입니다.
/// 트리 리스트의 드래그 & 드롭 상호작용을 상태와 이벤트로 중재하는 관리자입니다.
///
/// 역할:
/// 1. 드래그 시작/진행/종료 상태 관리
/// 2. 유효한 드롭 대상 판단 (순환 참조 방지)
/// 3. 아이템 위치 변경 (형제 아이템 간 순서 변경)
/// 4. 아이템 계층 구조 변경 (부모-자식 관계 수정)
/// - 드래그 시작/진행/종료 상태 관리(상태 머신)
/// - 드롭 유효성 검사(자기 자신/조상에게 드롭 금지로 순환 참조 방지)
/// - 구독자에게 이벤트로 진행 상황 전달
///
/// 기능:
/// - 드래그할 아이템과 드롭 대상을 추적
/// - 유효성 검사 (자기 자신에게 드롭 금지, 순환 참조 방지)
/// - 드롭 완료 후 데이터 동기화
/// 책임 경계:
/// - 이 매니저는 데이터를 직접 수정(이동/부모 변경)하지 않습니다.
/// - 실제 아이템 재배치/재귀 로직은 이벤트 구독자(예: 리스트/뷰 모델)에서 수행해야 합니다.
///
/// 이벤트 흐름:
/// - StartDrag → OnDragStarted(once)
/// - OnDragOver → OnDragEntered(repeat, hover 대상에 따라 여러 번)
/// - TryDrop 유효 시 → OnDropped → EndDrag → OnDragEnded
/// - TryDrop 무효/취소 시 → EndDrag → OnDragEnded
/// </summary>
public class TreeListDragDropManager
{
/// <summary>
/// 드래그 중인 아이템의 데이터입니다.
/// 현재 드래그 중인 아이템입니다(드래그 중이 아니면 null).
/// </summary>
public TreeListItemData? DraggedItem { get; private set; }
@@ -32,29 +36,45 @@ namespace UVC.UI.List.Tree
public bool IsDragging { get; private set; }
/// <summary>
/// 드래그 시작 시 발생하는 이벤트입니다.
/// 드래그 시작 시 1회 발생하는 이벤트입니다.
/// </summary>
/// <remarks>
/// 핸들러 시그니처: (TreeListItemData dragged)
/// </remarks>
public Action<TreeListItemData>? OnDragStarted;
/// <summary>
/// 드래그 진행 중 발생하는 이벤트입니다.
/// 드래그 진행 중 마우스가 특정 아이템 위에 있을 때 반복적으로 발생하는 이벤트입니다.
/// 빈 공간 위라면 <c>targetItem</c>은 null이 될 수 있습니다.
/// </summary>
/// <remarks>
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? targetItem)
/// 이벤트 발생 빈도가 높으므로, 처리 로직은 가볍게 유지하세요.
/// </remarks>
public Action<TreeListItemData, TreeListItemData?>? OnDragEntered;
/// <summary>
/// 드래그 종료 발생하는 이벤트입니다.
/// 드래그 종료될 때 발생하는 이벤트입니다(드롭 성공/실패/취소 포함).
/// </summary>
/// <remarks>
/// 핸들러 시그니처: (TreeListItemData dragged)<br/>
/// 드롭 성공 시에는 OnDropped 이후에 호출됩니다.
/// </remarks>
public Action<TreeListItemData>? OnDragEnded;
/// <summary>
/// 드롭 완료 시 발생하는 이벤트입니다.
/// 유효성 검사를 통과한 드롭이 확정될 때 발생하는 이벤트입니다.
/// </summary>
/// <remarks>
/// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? target)<br/>
/// 이 이벤트에서 실제 데이터 구조 변경(이동/부모 변경/정렬)을 수행하세요.
/// </remarks>
public Action<TreeListItemData, TreeListItemData?>? OnDropped;
/// <summary>
/// 드래그를 시작합니다.
/// 드래그를 시작합니다. 이미 드래그 중이면 무시됩니다.
/// </summary>
/// <param name="draggedItem">드래그할 아이템</param>
/// <param name="draggedItem">드래그할 아이템.</param>
public void StartDrag(TreeListItemData draggedItem)
{
if (IsDragging)
@@ -69,9 +89,10 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드래그 중 마우스가 다른 아이템 위에 있을 때 호출됩니다.
/// 드래그 중 마우스가 다른 아이템(또는 빈 영역) 위에 있을 때 호출됩니다.
/// 상태를 변경하지 않고, 단순히 현재 hover 대상을 이벤트로 통지합니다.
/// </summary>
/// <param name="targetItem">현재 마우스 위에 있는 아이템</param>
/// <param name="targetItem">현재 마우스 아래의 아이템. 빈 공간이면 null.</param>
public void OnDragOver(TreeListItemData? targetItem)
{
if (!IsDragging || DraggedItem == null)
@@ -83,7 +104,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드래그를 종료합니다.
/// 드래그를 종료합니다. 드래그 중이 아니면 아무 동작도 하지 않습니다.
/// </summary>
public void EndDrag()
{
@@ -99,11 +120,19 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드래그된 아이템을 대상 아이템에 드롭합니다.
/// 드래그된 아이템을 대상 아이템에 드롭 시도합니다.
/// 유효성 검사(자기 자신/조상에게 드롭 금지)를 통과한 경우에만 OnDropped를 발생시킵니다.
/// </summary>
/// <param name="targetItem">드롭 대상 아이템 (null이면 루트 레벨)</param>
/// <param name="insertIndex">대상 부모 내에서의 삽입 위치 (-1이면 끝에 추가)</param>
/// <returns>드롭 성공 여부</returns>
/// <param name="targetItem">드롭 대상 아이템. 루트 레벨로 드롭하려면 null.</param>
/// <param name="insertIndex">
/// 대상 부모 내 삽입 위치. -1이면 끝에 추가 의도.
/// 현재 구현에서는 이 값이 내부에서 사용되지 않으며, 필요 시 이벤트 모델 확장이 필요합니다.
/// </param>
/// <returns>드롭을 성공적으로 수락하여 이벤트를 발생시켰으면 true, 그 외는 false.</returns>
/// <remarks>
/// 성공 시 순서: OnDropped(once) → EndDrag → OnDragEnded.<br/>
/// 실패/무효 시: EndDrag → OnDragEnded.
/// </remarks>
public bool TryDrop(TreeListItemData? targetItem, int insertIndex = -1)
{
if (!IsDragging || DraggedItem == null)
@@ -125,6 +154,7 @@ namespace UVC.UI.List.Tree
return false;
}
// 주의: insertIndex는 현재 이벤트로 전달되지 않습니다(모델 확장 필요).
OnDropped?.Invoke(DraggedItem, targetItem);
EndDrag();
@@ -133,11 +163,13 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 첫 번째 아이템이 두 번째 아이템의 조상인지 확인합니다.
/// 순환 참조를 방지하기 위해 사용됩니다.
/// </summary>
/// <param name="potentialAncestor">조상일 가능성이 있는 아이템</param>
/// <param name="potentialDescendant">후손일 가능성이 있는 아이템</param>
/// <returns>조상-후손 관계이면 true</returns>
/// <param name="potentialAncestor">조상일 가능성이 있는 아이템.</param>
/// <param name="potentialDescendant">후손일 가능성이 있는 아이템.</param>
/// <returns>조상-후손 관계이면 true, 아니면 false.</returns>
/// <remarks>
/// 상향 탐색으로 O(h) 시간 복잡도입니다(h: 트리 높이).
/// </remarks>
public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant)
{
var current = potentialDescendant.Parent;
@@ -157,7 +189,7 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 모든 드래그 & 드롭 상태를 리셋합니다.
/// 드래그 & 드롭 상태를 초기화합니다.
/// </summary>
public void Reset()
{

View File

@@ -1,9 +1,7 @@
#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;
@@ -13,130 +11,63 @@ namespace UVC.UI.List.Tree
/// 트리 리스트의 개별 아이템을 표시하고 관리하는 UI 클래스입니다.
///
/// 역할:
/// 1. 아이템 이름을 화면에 표시
/// 2. 자식 아이템 펼침/접힘 기능 관리
/// 3. 아이템 선택/선택 해제 표시
/// 4. 사용자 입력(클릭, 키) 처리
/// 5. 데이터 변경 감지 및 UI 업데이트
/// - 아이템 이름 표시
/// - 자식 아이템 펼침/접힘 관리
/// - 선택 상태 UI 표시 및 입력 처리
/// - 데이터 변경 감지 후 UI 동기화
/// - 드래그 & 드롭 핸들러 연결
///
/// 구:
/// 📦 TreeListItem (이 클래스)
/// ├─ 📝 valueText: 아이템 이름 표시 (TextMeshPro)
/// ├─ 🔘 childExpand: 펼침/접힘 버튼
/// ├─ 📦 childContainer: 자식 아이템들을 담는 컨테이너
/// ├─ 🎨 selectedBg: 선택됨 표시 배경
/// 🔘 itemButton: 클릭 감지 버튼
///
/// MonoBehaviour란?
/// Unity의 모든 GameObject가 가져야 할 기본 클래스입니다.
/// Update, OnDestroy 같은 Unity의 생명주기 메서드를 사용할 수 있습니다.
/// 구:
/// "TreeListItem"
/// ├─ "valueText": 이름 텍스트
/// ├─ "childExpand": 펼침/접힘 버튼
/// ├─ "childContainer": 자식 컨테이너
/// ├─ "childRoot": 자식 배치 부모
/// "selectedBg": 선택 배경
/// └─ "itemButton": 아이템 클릭 버튼
/// </summary>
public class TreeListItem : MonoBehaviour
{
#region UI (UI Component References)
/// <summary>
/// 이 아이템을 관리하는 부모 TreeList입니다.
///
/// 용도:
/// - 선택 상태 변경 시 TreeList에 알림
/// - 새 자식 아이템 생성 시 프리팹 가져오기
/// - 키보드 입력(Ctrl, Shift) 상태 전달
///
/// [SerializeField]란?
/// Inspector에서 이 값을 직접 할당할 수 있게 해줍니다.
/// (private이지만 Unity가 특별히 접근 가능)
/// 이 아이템을 관리하는 상위 컨트롤입니다. (선택/평탄화/프리팹 액세스 등에 사용)
/// </summary>
[SerializeField]
protected TreeList treeList;
/// <summary>
/// 아이템 이름을 표시하는 텍스트 UI입니다.
///
/// TMPro.TextMeshProUGUI란?
/// TextMeshPro는 Unity의 고급 텍스트 시스템입니다.
/// 일반 Text보다 더 예쁘고 빠릅니다.
///
/// 사용:
/// valueText.text = "새로운 이름"; // 화면에 표시되는 텍스트 변경
/// 아이템 이름 텍스트(UI).
/// </summary>
[SerializeField]
protected TMPro.TextMeshProUGUI valueText;
/// <summary>
/// 트리의 자식 펼침/접힘하는 화살표 버튼입니다.
///
/// 특징:
/// - 자식이 없으면 숨겨집니다
/// - 자식이 펼쳐지면 ▼ 모양
/// - 자식이 접혀있으면 ▶ 모양
/// - 클릭 시 ToggleChild() 메서드 호출
///
/// 회전 애니메이션:
/// DORotate()를 사용해 부드럽게 회전합니다.
/// 자식 펼침/접힘 버튼. onClick에서 ToggleChild를 호출합니다.
/// </summary>
[SerializeField]
protected Button childExpand;
/// <summary>
/// 모든 자식 아이템들을 는 컨테이너 GameObject입니다.
///
/// 역할:
/// - 자식 아이템들을 묶음으로 보관
/// - 펼침/접힘 시 이 컨테이너 전체를 표시/숨김
/// - SetActive(true/false)로 온/오프 제어
///
/// GameObject란?
/// Unity의 모든 객체(씬의 모든 것)의 기본 단위입니다.
/// 게임 오브젝트는 여러 컴포넌트를 가질 수 있습니다.
/// 자식 아이템들을 포함하는 컨테이너 GameObject.
/// </summary>
[SerializeField]
protected GameObject childContainer;
/// <summary>
/// 자식 아이템들이 실제로 배치되는 부모 Transform입니다.
///
/// RectTransform란?
/// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다.
/// Canvas 아래의 모든 UI 요소가 RectTransform을 가집니다.
///
/// 용도:
/// - childRoot.childCount: 현재 몇 개의 자식이 있는지 확인
/// - new TreeListItem을 Instantiate할 때 부모로 지정
///
/// 구조 예:
/// childRoot (이것)
/// ├─ 자식1 (TreeListItem)
/// ├─ 자식2 (TreeListItem)
/// └─ 자식3 (TreeListItem)
/// 자식 TreeListItem이 배치될 부모 RectTransform.
/// </summary>
[SerializeField]
protected RectTransform childRoot;
/// <summary>
/// 아이템이 선택되었을 때 배경으로 표시는 이미지입니다.
///
/// 예: 파일 탐색기에서 파일을 선택했을 때 파일 이름 뒤의 파란 배경
///
/// 동작:
/// - IsSelected가 true면 selectedBg.gameObject.SetActive(true)
/// - IsSelected가 false면 selectedBg.gameObject.SetActive(false)
/// - 모든 레벨의 선택된 아이템의 selectedBg 왼쪽이 정렬됨
/// 선택 상태를 표시배경 이미지.
/// </summary>
[SerializeField]
protected Image selectedBg;
/// <summary>
/// 아이템 전체 클릭 가능하게 하는 버튼 컴포넌트입니다.
///
/// Button이란?
/// 사용자가 클릭하면 onClick 이벤트를 발생시킵니다.
/// 이 경우 OnItemClicked() 메서드가 호출됩니다.
///
/// onClick.AddListener는?
/// 버튼이 클릭되면 리스너 함수를 호출하라는 뜻입니다.
/// 예: itemButton.onClick.AddListener(OnItemClicked);
/// → itemButton을 클릭하면 OnItemClicked() 실행
/// 아이템 전체 클릭 버튼. onClick에서 OnItemClicked를 호출합니다.
/// </summary>
[SerializeField]
protected Button itemButton;
@@ -146,62 +77,17 @@ namespace UVC.UI.List.Tree
#region (Data Fields)
/// <summary>
/// 이 UI 아이템이 표시하는 데이터 객체입니다.
///
/// 구조:
/// TreeListItem (UI - 화면에 보이는 것)
/// ↓
/// TreeListItemData (데이터 - 실제 정보)
///
/// 관계:
/// - TreeListItemData에 이름, 자식 목록, 선택 상태 등이 저장됨
/// - TreeListItem은 이 데이터를 화면에 표시하고 상호작용 처리
///
/// 예:
/// data.Name = "폴더"
/// → valueText.text = "폴더"로 화면에 표시
///
/// data.IsSelected = true
/// → selectedBg가 활성화되어 선택 표시
///
/// ? 연산자 (nullable):
/// data는 null일 수 있습니다.
/// 따라서 사용 전에 null 체크를 해야 합니다.
/// if (data == null) return;
/// 이 UI 표시하는 데이터(Nullable). UI는 이 데이터의 변경 이벤트에 반응합니다.
/// </summary>
protected TreeListItemData? data;
/// <summary>
/// data 속성을 읽기 전용으로 공개합니다.
///
/// 문법:
/// public TreeListItemData? Data => data;
/// = getter 전용 프로퍼티
/// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음
///
/// 사용 예:
/// if (treeItem.Data != null)
/// {
/// Debug.Log(treeItem.Data.Name); // 읽기만 가능
/// }
/// 표시 데이터의 읽기 전용 접근자.
/// </summary>
public TreeListItemData? Data => data;
/// <summary>
/// 펼침/접힘 애니메이션 진행 중인지를 나타내는 플래그입니다.
///
/// 용도: 애니메이션이 끝나기 전에 다시 클릭하는 것을 방지합니다.
///
/// 시나리오:
/// 1. 사용자가 화살표 버튼 클릭
/// 2. isAnimating = true (애니메이션 시작)
/// 3. 0.3초 동안 화살표가 회전
/// 4. 애니메이션 완료 → isAnimating = false
///
/// 중간에 다시 클릭해도:
/// if (isAnimating) return; ← 여기서 무시됨
///
/// 이렇게 하는 이유: 애니메이션이 겹치면 버그가 발생할 수 있음
/// 펼침/접힘 애니메이션 진행 여부(중복 입력 방지).
/// </summary>
protected bool isAnimating = false;
@@ -210,27 +96,20 @@ namespace UVC.UI.List.Tree
#region (Initialization)
/// <summary>
/// TreeListItem을 초기화합니다.
/// TreeListItem을 초기화합니다.
///
/// 호출 시점:
/// - 새로운 TreeListItem이 생성될 때
/// - Instantiate 직후 에 Init()이 호출됨
///
/// 초기화 과정:
/// 1. UI 요소 설정 (이름 표시, 이벤트 연결)
/// 2. 자식이 있으면 자식 UI 생성
/// 3. 데이터 변경 이벤트 구독
/// 4. 버튼 클릭 이벤트 구독
/// 5. 선택 상태 UI 업데이트
/// 6. 선택된 배경 위치 정렬
/// 절차:
/// 1) 데이터/컨트롤 연결 및 이름 표시
/// 2) 자식 UI 생성
/// 3) 데이터/선택 변경 이벤트 구독
/// 4) 버튼(onClick) 이벤트 연결
/// 5) 선택 UI 반영 및 선택 배경 정렬
/// 6) 드래그 핸들러 연결
///
/// 매개변수:
/// - data: 표시할 데이터
/// - treeList: 부모 TreeList (선택 관리, 클릭 처리 등)
///
/// 사용 예:
/// var item = Instantiate(prefab);
/// item.Init(treeData, treeList);
/// - data: 표시할 데이터
/// - control: 상위 TreeList
/// - dragDropManager: 드래그 & 드롭 매니저
/// </summary>
public void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
{
@@ -248,13 +127,13 @@ namespace UVC.UI.List.Tree
if (data.Children.Count > 0)
{
{
// 각 자식 데이터에 대해 UI 생성
foreach (var childData in data.Children)
{
CreateItem(childData); // 재귀적으로 트리 구조 생성
}
}
// 화살표 방향 설정 (초기에는 펼쳐짐)
SetExpand();
@@ -299,20 +178,12 @@ namespace UVC.UI.List.Tree
#region (Data Change Handlers)
/// <summary>
/// 데이터 변경되었을 때 호출되는 메서드입니다.
/// 데이터 변경에 반응하여 UI를 갱신합니다.
///
/// 호출되는 경우:
/// 1. data.Name이 변경됨
/// 2. data.Children이 추가/제거됨
/// 3. data.IsExpanded가 변경됨
///
/// 역할:
/// - 화면에 표시된 내용과 데이터를 동기화
/// - 필요한 UI 컴포넌트 업데이트
///
/// 데이터 바인딩이란?
/// 데이터가 변경되면 UI가 자동으로 업데이트되는 패턴입니다.
/// 이렇게 하면 데이터와 UI를 동기화 유지하기 쉽습니다.
/// 매개변수:
/// - changedType: 변경 종류(이름/확장/자식 추가 등)
/// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효)
/// - index: 삽입/이동 시 기준 인덱스(해당되는 경우)
/// </summary>
private void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
{
@@ -328,7 +199,7 @@ namespace UVC.UI.List.Tree
if (changedType == ChangedType.Name)
{
{
// 이름이 변경된 경우
if (valueText.text != data.Name)
{
@@ -356,34 +227,16 @@ namespace UVC.UI.List.Tree
}
else if (changedType == ChangedType.AddChild)
{
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null)
{
item.transform.SetParent(childRoot);
item.SetExpand();
}
else
{
CreateItem(changedData);
}
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);
}
TreeListItem item = CreateItem(changedData);
UniTask.DelayFrame(1).ContinueWith(() =>
{
@@ -391,28 +244,68 @@ namespace UVC.UI.List.Tree
treeList.UpdateFlattenedItemDataList();
});
}
else if (changedType == ChangedType.RemoveChild)
{
var childItem = childRoot.GetChild(index).GetComponent<TreeListItem>();
if (childItem != null)
else if (changedType == ChangedType.AddCloneChild)
{
//데이터 복사
TreeListItemData clone = changedData.Clone();
//changedData 부모에게 알림 - UI 갱신 용
if (changedData.Parent != null)
{
childItem.Delete(false);
UniTask.DelayFrame(1).ContinueWith(() =>
{
treeList.UpdateFlattenedItemDataList();
});
changedData.Parent.RemoveChild(changedData);
}
else
{
treeList.RemoveItem(changedData);
}
// TreeListItem 제거
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null) item.Delete(true);
data.AddChild(clone);
}
else if (changedType == ChangedType.AddCloneAtChild)
{
//데이터 복사
TreeListItemData clone = changedData.Clone();
//changedData 부모에게 알림
if (changedData.Parent != null)
{
changedData.Parent.RemoveChild(changedData);
}
else
{
treeList.RemoveItem(changedData);
}
// TreeListItem 제거
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null) item.Delete(true);
data.AddChildAt(clone, index);
}
else if (changedType == ChangedType.SwapChild)
{
UniTask.DelayFrame(1).ContinueWith(() =>
{
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
if (item != null) item.transform.SetSiblingIndex(index);
treeList.UpdateFlattenedItemDataList();
});
}
else if (changedType == ChangedType.RemoveChild)
{
//따로 할것 없음 - 펼침 버튼 갱신 용
Debug.Log($"RemoveChild 처리 완료 {changedData.Name}, {index}");
}
// 5. 펼침 버튼 표시 여부 결정
childExpand.gameObject.SetActive(data.Children.Count > 0);
// 확장 상태 변경 않함
//if (data.Children.Count > 0 && !childContainer.activeSelf)
//{
// childContainer.SetActive(true);
// SetExpand();
//}
UniTask.DelayFrame(1).ContinueWith(() =>
{
// 5. 펼침 버튼 표시 여부 결정
childExpand.gameObject.SetActive(data.Children.Count > 0);
});
}
#endregion
@@ -420,17 +313,11 @@ namespace UVC.UI.List.Tree
#region (Selection Management)
/// <summary>
/// 아이템의 선택 상태 변경되었을 때 호출됩니다.
///
/// 호출 시기:
/// - data.IsSelected = true/false 일 때
///
/// 동작:
/// - UpdateSelectionUI() 호출해서 화면 업데이트
/// 선택 상태 변경에 반응하여 UI를 갱신합니다.
///
/// 매개변수:
/// - changedData: 변경된 데이터 (이 경우 항상 this.data와 같음)
/// - isSelected: 새로운 선택 상태 (true = 선택됨, false = 해제됨)
/// - changedData: 변경된 데이터(자기 자신)
/// - isSelected: 선택 여부
/// </summary>
private void OnSelectionChanged(TreeListItemData changedData, bool isSelected)
{
@@ -439,18 +326,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 선택 상태에 따라 UI를 업데이트합니다.
///
/// 동작:
/// - data.IsSelected = true → selectedBg 표시 (배경 보이기)
/// - data.IsSelected = false → selectedBg 숨김 (배경 숨기기)
///
/// 예시:
/// data.IsSelected = true
/// ↓
/// selectedBg.gameObject.SetActive(true)
/// ↓
/// 화면에 파란 배경이 나타남
/// 선택 상태에 따라 배경 표시/숨김을 갱신합니다.
/// </summary>
private void UpdateSelectionUI()
{
@@ -464,25 +340,7 @@ namespace UVC.UI.List.Tree
#region (Position Alignment)
/// <summary>
/// 모든 선택 배경(selectedBg)의 왼쪽 위치를 정렬합니다.
///
/// 목표:
/// 📌 폴더1
/// 📌├─ 파일1
/// 📌├─ 파일2
/// 📌└─ 폴더2
/// 📌├─ 파일3
///
/// 모든 선택 배경의 왼쪽이 📌 위치에서 시작하도록 합니다.
///
/// 이렇게 하는 이유:
/// - 깔끔한 UI 표현
/// - 다중 레벨 트리에서 시각적 일관성 유지
///
/// 좌표 변환:
/// - 월드 좌표(World Coordinate): 게임 전체에서의 절대 위치
/// - 로컬 좌표(Local Coordinate): 부모 기준 상대 위치
/// - TransformPoint: 로컬 좌표를 월드 좌표로 변환
/// 모든 레벨에서 선택 배경의 왼쪽 정렬을 루트와 맞춥니다.
/// </summary>
private void AlignSelectedBgToRoot()
{
@@ -523,25 +381,10 @@ namespace UVC.UI.List.Tree
#region (Hierarchy Navigation)
/// <summary>
/// 최상위 부모(루트)의 TreeListItem을 찾습니다.
/// 현재 아이템의 루트 TreeListItem을 반환합니다.
///
/// 트리 구조:
/// TreeListItem1 (루트)
/// ├─ TreeListItem2 (부모)
/// │ └─ TreeListItem3 (자식 ← 이 메서드를 호출하면)
/// └─ TreeListItem4
///
/// 반환값: TreeListItem1
///
/// 동작:
/// 1. 현재 객체의 부모 확인
/// 2. 부모가 TreeListItem을 가지는지 확인
/// 3. 가지면 그 부모의 GetRootTreeListItem() 재귀 호출
/// 4. 루트에 도달할 때까지 반복
///
/// 재귀(Recursion)란?
/// 함수가 자기 자신을 호출하는 것입니다.
/// 계층 구조를 탐색하는 데 효과적입니다.
/// 반환:
/// - 루트 TreeListItem, 없으면 null
/// </summary>
private TreeListItem? GetRootTreeListItem()
{
@@ -580,35 +423,18 @@ namespace UVC.UI.List.Tree
#region (Input Handling)
/// <summary>
/// 아이템 클릭했을 때 호출됩니다.
/// 아이템 클릭 호출됩니다.
///
/// 호출 시점:
/// - itemButton.onClick 이벤트 발생 시
/// = 사용자가 이 아이템을 마우스로 클릭했을 때
///
/// 처리 순서:
/// 1. 데이터의 OnClickAction 실행 (있으면)
/// 2. Ctrl, Shift 키 상태 감지
/// 3. TreeList에 클릭 정보 전달 (다중 선택 로직)
///
/// 키 입력:
/// - Ctrl 클릭: 현재 선택을 유지하면서 이 아이템 토글 (다중 선택)
/// - Shift 클릭: 마지막 선택부터 이 아이템까지 범위 선택
/// - 일반 클릭: 이 아이템만 선택
///
/// 파일 탐색기와 동일한 동작:
/// Windows 탐색기를 생각하면 쉽습니다.
/// - 클릭: 한 파일만 선택
/// - Ctrl+클릭: 여러 파일 선택 (기존 선택 유지)
/// - Shift+클릭: 범위 선택
/// 동작:
/// - 등록된 OnClickAction 실행(있으면)
/// - Ctrl/Shift 상태를 읽어 TreeList에 전달
/// (일반/토글/범위 선택)
/// </summary>
private void OnItemClicked()
{
if (data == null) return;
// 1. 데이터에 등록된 클릭 액션 실행 (있으면)
// ?. 연산자: null이면 실행 안 함
// 예: 펼침/접힘 버튼 클릭 시 자동으로 호출되는 액션
data.OnClickAction?.Invoke(data);
// 2. Ctrl 키 상태 감지
@@ -629,20 +455,8 @@ namespace UVC.UI.List.Tree
#region / (Expand/Collapse)
/// <summary>
/// 자식 아이템의 펼침/접힘을 토글합니다. (펼쳐있으면 접고, 접혀있으면 펼침)
///
/// 호출 시점:
/// - childExpand 버튼을 클릭했을 때
///
/// 동작:
/// 1. 애니메이션이 진행 중이면 무시 (중복 클릭 방지)
/// 2. data.IsExpanded 토글 (true ↔ false)
/// 3. childContainer 활성화/비활성화
/// 4. 0.3초에 걸쳐 화살표 회전 애니메이션 실행
///
/// UI 피드백:
/// 애니메이션이 있으면 사용자가 반응을 확인할 수 있습니다.
/// 즉시 완료되는 것보다 더 자연스럽고 좋은 경험입니다.
/// 자식의 펼침/접힘을 토글합니다.
/// (중복 입력은 애니메이션 종료까지 무시)
/// </summary>
public void ToggleChild()
{
@@ -659,26 +473,9 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 펼침/접힘 화살표 회전 애니메이션을 실행합니다.
///
/// 파라미터:
/// - duration: 애니메이션 지속 시간 (초)
/// 기본값 0.0 = 즉시 완료 (애니메이션 없음)
/// 0.3 = 0.3초에 걸쳐 회전
///
/// 동작:
/// - 펼쳐짐 (IsExpanded=true): 화살표를 0도로 회전 (▼ 모양)
/// - 접혀짐 (IsExpanded=false): 화살표를 90도로 회전 (▶ 모양)
/// - 애니메이션 완료 후 isAnimating 플래그 리셋
///
/// DORotate란? (Tweening 라이브러리)
/// 부드럽게 회전하는 애니메이션을 쉽게 만들어줍니다.
/// duration 시간에 걸쳐 지정된 각도까지 회전합니다.
///
/// OnComplete란?
/// 애니메이션이 완료되면 호출되는 콜백입니다.
/// 람다 식(=>)으로 익명 함수를 정의합니다.
/// 펼침/접힘 화살표 회전과 컨테이너 표시를 갱신합니다.
/// </summary>
/// <param name="duration">회전 애니메이션 시간(초). 0이면 즉시 적용.</param>
internal void SetExpand(float duration = 0.0f)
{
@@ -695,6 +492,7 @@ namespace UVC.UI.List.Tree
// 4. 애니메이션이 완료되면 플래그 리셋
// 이제 다시 ToggleChild() 호출 가능
isAnimating = false;
LayoutRebuilder.ForceRebuildLayoutImmediate(transform as RectTransform);
});
}
@@ -703,24 +501,9 @@ namespace UVC.UI.List.Tree
#region (Child Management)
/// <summary>
/// 새로운 자식 아이템을 추가합니다.
///
/// 호출 시점:
/// - 프로그램이 런타임에 새 자식을 추가할 때
/// - 예: 폴더에 파일을 추가했을 때
///
/// 동작:
/// 1. 새 자식 UI 생성 (CreateItem)
/// 2. 자식 컨테이너 활성화
/// 3. 펼침 애니메이션 (자동으로 펼침)
///
/// 매개변수:
/// - data: 추가할 자식 데이터
///
/// 사용 예:
/// treeItem.AddChild(newChildData);
/// // → 새 자식이 자동으로 UI에 추가되고 펼쳐짐
/// 새로운 자식 아이템을 추가하고 UI를 갱신합니다.
/// </summary>
/// <param name="data">추가할 자식 데이터</param>
public void AddChild(TreeListItemData data)
{
// 1. 새 자식 UI 생성
@@ -735,32 +518,10 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 자식 데이터를 받아 UI TreeListItem으로 생성합니다.
///
/// 호출 시점:
/// - Init()에서 기존 자식들을 UI로 생성할 때
/// - OnDataChanged()에서 추가된 자식을 UI로 생성할 때
/// - AddChild()에서 새 자식을 추가할 때
///
/// 동작:
/// 1. treeList.ItemPrefab를 childRoot 아래에 인스턴스화
/// 2. 생성된 item의 Init() 메서드 호출
/// 3. 생성된 item 반환
///
/// Instantiate란?
/// 프리팹(템플릿)을 복제해서 새로운 객체를 만드는 함수입니다.
/// Instantiate(프리팹, 부모, 복제)
///
/// 매개변수:
/// - data: 생성할 아이템의 데이터
///
/// 반환값:
/// - 생성된 TreeListItem 컴포넌트
///
/// 재귀 구조:
/// CreateItem은 계속 자식을 생성하므로
/// 깊이 있는 트리 구조를 만들 수 있습니다.
/// 자식 데이터를 받아 UI TreeListItem 생성합니다.
/// </summary>
/// <param name="data">생성할 아이템 데이터</param>
/// <returns>생성된 TreeListItem</returns>
protected TreeListItem CreateItem(TreeListItemData data)
{
// 1. 프리팹을 복제해서 새로운 TreeListItem 생성
@@ -784,27 +545,9 @@ namespace UVC.UI.List.Tree
#region (Deletion)
/// <summary>
/// 이 TreeListItem과 관련된 모든 리소스를 정리하고 삭제합니다.
///
/// 호출 시점:
/// - 트리에서 아이템을 제거하고 싶을 때
/// - 프로그램이 명시적으로 아이템을 삭제할 때
///
/// 정리 작업:
/// 1. 데이터 변경 이벤트 구독 해제
/// 2. 버튼 클릭 이벤트 구독 해제
/// 3. GameObject 삭제
///
/// 왜 구독을 해제하나?
/// - 이벤트를 구독하는 것은 메모리 연결을 만듭니다.
/// - 해제하지 않으면 메모리 누수가 발생할 수 있습니다.
/// - 따라서 삭제 전에 반드시 해제해야 합니다.
///
/// 메모리 누수란?
/// 불필요한 메모리가 해제되지 않고 계속 사용되는 문제입니다.
/// 게임이 계속 실행되면서 메모리 사용량이 증가해서
/// 결국 게임이 느려지거나 충돌할 수 있습니다.
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 삭제합니다.
/// </summary>
/// <param name="deleteData">데이터도 삭제할지 여부</param>
public void Delete(bool deleteData = false)
{
// 1. 데이터 변경 이벤트 구독 해제
@@ -813,8 +556,7 @@ namespace UVC.UI.List.Tree
// -= 연산자: 이벤트에서 리스너 제거
data.OnDataChanged -= OnDataChanged;
data.OnSelectionChanged -= OnSelectionChanged;
data.Parent = null;
if(deleteData) data.Dispose();
data.Dispose();
data = null;
}
@@ -835,22 +577,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// GameObject가 파괴될 때 자동으로 호출되는 Unity 메서드입니다.
///
/// 호출 시점:
/// - Destroy(gameObject) 호출 후 실제 삭제되기 직전
/// - 게임이 종료될 때
/// - 씬이 언로드될 때
///
/// 용도:
/// - 정리 작업 (Clean-up)
/// - 리소스 해제
/// - 이벤트 구독 해제
///
/// Delete()와의 차이:
/// - Delete(): 명시적으로 호출하는 메서드
/// - OnDestroy(): Unity에서 자동으로 호출하는 메서드
/// - 둘 다 같은 정리 작업을 합니다 (중복 방지)
/// Unity 파괴 시점에 정리합니다. (중복 정리 방지)
/// </summary>
private void OnDestroy()
{
@@ -859,7 +586,6 @@ namespace UVC.UI.List.Tree
{
data.OnDataChanged -= OnDataChanged;
data.OnSelectionChanged -= OnSelectionChanged;
data.Parent = null;
data.Dispose();
data = null;
}

View File

@@ -6,106 +6,74 @@ using System.Collections.Generic;
namespace UVC.UI.List.Tree
{
/// <summary>
/// 트리 구조 리스트에서 각 아이템이 갖는 데이터 클래스입니다.
/// 트리 리스트 아이템의 데이터 모델.
///
/// 트리 구조란? 폴더-파일처럼 상위(부모)와 하위(자식) 관계를 가진 계층 구조입니다.
/// 예: 📁 폴더
/// ├─ 📄 파일1
/// ├─ 📄 파일2
/// └─ 📁 하위폴더
/// └─ 📄 파일3
/// 목적:
/// - 이름/옵션/선택/펼침 상태 및 계층(부모-자식) 보유
/// - 상태 변경 시 UI에 알릴 이벤트 제공(데이터 변경/선택 변경)
/// - 자식 추가/삽입/복제/재배치/초기화 등 계층 편집 기능
///
/// 이 클래스는 InfiniteScrollData를 상속하여 UI 스크롤 리스트와 연동됩니다.
/// 이벤트 설계:
/// - OnDataChanged(ChangedType type, TreeListItemData changed, int index):
/// 데이터/구조 변경 통지. changed는 변동 주체(보통 this 또는 대상 자식),
/// index는 삽입/이동 시 사용(해당 없으면 -1).
/// - OnSelectionChanged(TreeListItemData data, bool isSelected):
/// 선택 상태 변경 통지(선택 UI/로직 처리용).
///
/// 동등성/해시:
/// - 이름(Name) 기반 비교/해시 코드. 이름이 같으면 같은 개체로 간주(대소문자 구분).
/// - 해시/Equals가 이름에 의존하므로 Name 변경 시 컬렉션 키로 사용 중이면 주의.
///
/// 주의사항:
/// - ClearChildren은 각 자식에 Dispose를 호출하여 이벤트/내부 상태를 정리한 뒤
/// 리스트를 비웁니다(연결만 끊는 수준을 넘어 자식도 정리됨).
/// - AddClone계열은 전달된 원본 child를 Dispose한 뒤 복제본을 추가합니다(파괴적).
/// </summary>
public class TreeListItemData: IDisposable
{
#region (Events)
/// <summary>
/// 데이터가 변경되었을 때 발생하는 이벤트입니다.
///
/// 용도: 이 데이터의 속성(Name, Option 등)이 변경되면
/// UI에 자동으로 반영되도록 통지합니다.
///
/// 사용 예:
/// treeItem.OnDataChanged += (data) => Debug.Log("데이터 변경됨!");
/// 데이터/구조 변경 통지 이벤트.
/// 시그니처: (ChangedType type, TreeListItemData changed, int index)
/// - type: 변경 종류
/// - changed: 변경 대상(보통 this 또는 특정 자식)
/// - index: 위치 관련 변경 시 사용(해당 없으면 -1)
/// </summary>
public Action<ChangedType, TreeListItemData, int>? OnDataChanged;
/// <summary>
/// 선택 상태 변경되었을 때 발생하는 이벤트입니다.
///
/// 용도: 사용자가 이 아이템을 클릭해서 선택 또는 선택 해제했을 때
/// 다른 시스템(예: 옵션 창)에 알려줍니다.
///
/// 매개변수:
/// - TreeListItemData: 변경된 아이템 자신
/// - bool: true면 선택됨, false면 선택 해제됨
///
/// 사용 예:
/// treeItem.OnSelectionChanged += (data, isSelected) => {
/// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}");
/// };
/// 선택 상태 변경 통지 이벤트.
/// 시그니처: (TreeListItemData data, bool isSelected)
/// </summary>
public Action<TreeListItemData, bool>? OnSelectionChanged;
/// <summary>
/// 아이템 클릭 시 실행할 사용자 정의 동작.
/// (예: 속성 패널 열기, 포커스 이동 등. 확장/축소와는 별개)
/// </summary>
public Action<TreeListItemData>? OnClickAction;
#endregion
#region (Private Fields)
/// <summary>
/// 아이템의 이름을 저장하는 비공개 필드입니다.
///
/// '_' 접두사를 붙인 이유:
/// 실제 데이터는 여기 저장하고, public 프로퍼티(Name)를 통해
/// 접근을 제어합니다. (캡슐화)
///
/// 예: _name = "폴더", Name 프로퍼티로 접근
/// </summary>
/// <summary>아이템 이름.</summary>
private string _name = string.Empty;
/// <summary>
/// 아이템의 추가 옵션 정보를 저장합니다.
///
/// 용도: 읽기 전용, 숨김 속성 등의 추가 설정 정보
/// 예: "readonly", "hidden", "locked" 등
/// </summary>
/// <summary>추가 옵션 문자열.</summary>
private string _option = string.Empty;
/// <summary>
/// 이 아이템의 자식들이 펼쳐져 있는지 여부를 나타냅니다.
///
/// true: 자식들이 표시됨 (▼ 펼침 상태)
/// false: 자식들이 숨겨짐 (▶ 접혀있는 상태)
///
/// 자식이 없으면 이 값은 의미가 없습니다.
/// </summary>
/// <summary>자식 펼침 여부.</summary>
private bool _isExpanded = false;
/// <summary>
/// 현재 아이템이 사용자에게 선택되어 있는지를 나타냅니다.
///
/// true: 선택됨 (보통 배경색이 다르게 표시)
/// false: 선택 안 됨 (기본 상태)
///
/// 예: 파일 탐색기에서 파일을 클릭했을 때 그 파일의 _isSelected = true
/// </summary>
/// <summary>선택 여부.</summary>
private bool _isSelected = false;
/// <summary>부모</summary>
private TreeListItemData? _parent;
/// <summary>
/// 이 아이템의 하위 아이템들을 모두 저장하는 리스트입니다.
///
/// 트리 구조 예:
/// 부모 (이 객체)
/// ├─ 자식1
/// ├─ 자식2
/// └─ 자식3
///
/// _children 리스트에 [자식1, 자식2, 자식3]이 저장됩니다.
/// 자식이 없으면 빈 리스트입니다.
/// </summary>
/// <summary>자식 리스트.</summary>
private List<TreeListItemData> _children = new List<TreeListItemData>();
#endregion
@@ -113,20 +81,8 @@ namespace UVC.UI.List.Tree
#region (Public Properties)
/// <summary>
/// 아이템 이름을 가져오거나 설정합니다.
///
/// 동작:
/// - 가져올 때(get): _name의 값을 반환합니다.
/// - 설정할 때(set):
/// 1. 기존 값과 비교해서 정말 달라졌는지 확인
/// 2. 다르면 새 값으로 변경
/// 3. OnDataChanged 이벤트를 발생시켜 UI에 알림
///
/// 이렇게 하는 이유: 같은 값으로 변경되는 불필요한 갱신을 피합니다.
///
/// 사용 예:
/// treeItem.Name = "새로운 이름"; // 자동으로 UI 업데이트
/// string currentName = treeItem.Name; // 이름 읽기
/// 아이템 이름.
/// 변경 시 OnDataChanged(Name) 발생.
/// </summary>
public string Name
{
@@ -142,13 +98,8 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 아이템의 추가 옵션을 가져오거나 설정합니다.
///
/// 동작: Name 프로퍼티와 동일하게 작동합니다.
/// 변경될 때마다 OnDataChanged 이벤트를 발생시킵니다.
///
/// 사용 예:
/// treeItem.Option = "readonly"; // 읽기 전용으로 설정
/// 옵션 문자열.
/// 변경 시 OnDataChanged(Option) 발생.
/// </summary>
public string Option
{
@@ -164,17 +115,8 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 이 아이템의 자식들이 펼쳐져 있는지 여부를 가져오거나 설정합니다.
///
/// internal 접근제한자 이유:
/// 이것은 UI 시스템에서만 관리해야 하므로 외부에서 직접 접근할 수 없습니다.
/// (같은 어셈블리 내부에서만 접근 가능)
///
/// true: 자식들이 표시됨 (트리 펼침)
/// false: 자식들이 숨겨짐 (트리 접힘)
///
/// 사용 예: (UI 시스템에서만)
/// treeItem.IsExpanded = true; // 자식들을 표시
/// 펼침 상태(같은 어셈블리 내 전용).
/// 변경 시 OnDataChanged(Expanded) 발생.
/// </summary>
internal bool IsExpanded
{
@@ -190,16 +132,8 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 이 아이템이 선택되어 있는지 여부를 가져오거나 설정합니다.
///
/// 중요한 차이점: 다른 프로퍼티는 OnDataChanged를 호출하지만,
/// 이것은 OnSelectionChanged를 호출합니다.
/// 왜? 선택 상태는 UI 갱신이 아니라
/// 선택 이벤트 처리가 필요하기 때문입니다.
///
/// 사용 예:
/// treeItem.IsSelected = true; // 아이템 선택
/// if (treeItem.IsSelected) { ... } // 선택 여부 확인
/// 선택 상태.
/// 변경 시 OnSelectionChanged 발생.
/// </summary>
public bool IsSelected
{
@@ -216,21 +150,9 @@ namespace UVC.UI.List.Tree
}
}
/// <summary>
/// 사용자가 확장/축소 버튼을 클릭했을 때 호출될 함수입니다.
///
/// 용도: 트리의 화살표(▼/▶) 버튼을 클릭했을 때
/// IsExpanded 상태를 변경하는 로직을 실행합니다.
///
/// 누가 등록하나? UI 시스템 (TreeListItem 클래스)
///
/// 사용 예:
/// treeItem.OnClickAction = (data) => {
/// data.IsExpanded = !data.IsExpanded; // 펼침/접힘 토글
/// };
/// </summary>
public Action<TreeListItemData>? OnClickAction;
/// <summary>부모 데이터(내부 전용).</summary>
internal TreeListItemData? Parent
{
get => _parent;
@@ -238,22 +160,8 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 이 아이템의 모든 자식 아이템들을 가져오거나 설정합니다.
///
/// internal 접근제한자 이유:
/// 자식 리스트는 AddChild, RemoveChild, ClearChildren 메서드로만
/// 수정되어야 데이터 일관성이 보장됩니다.
///
/// 동작:
/// - 가져올 때(get): _children 리스트 반환
/// - 설정할 때(set):
/// 1. null이면 빈 리스트로 설정 (null 방지)
/// 2. OnDataChanged 이벤트 발생
///
/// ?? 연산자 설명:
/// childrenItemData ?? new List<>()
/// = childrenItemData가 null이면 빈 리스트 사용
/// null이 아니면 childrenItemData 사용
/// 자식 컬렉션(내부 전용).
/// set 시 null을 빈 리스트로 대체하고 OnDataChanged(ResetChildren) 발생.
/// </summary>
internal List<TreeListItemData> Children
{
@@ -270,47 +178,23 @@ namespace UVC.UI.List.Tree
#region (Constructors)
/// <summary>
/// 빈 TreeListItemData를 생성합니다.
///
/// 초기값:
/// - Name: 빈 문자열
/// - Option: 빈 문자열
/// - IsExpanded: false (접혀있음)
/// - Children: 빈 리스트 (자식 없음)
///
/// 사용 예:
/// var item = new TreeListItemData();
/// item.Name = "새 폴더";
/// 기본 생성자.
/// 초기값: Name="", Option="", IsExpanded=false, IsSelected=false, Children=[]
/// </summary>
public TreeListItemData()
{
_name = string.Empty;
_option = string.Empty;
_isExpanded = false;
_isSelected = false;
_children = new List<TreeListItemData>();
}
/// <summary>
/// 이름과 선택적으로 자식 목록을 지정하여 TreeListItemData를 생성합니다.
///
/// 매개변수:
/// - generalName: 아이템의 이름 (필수)
/// - childrenItemData: 초기 자식 목록 (선택사항, null 가능)
///
/// 초기값:
/// - Name: generalName
/// - Option: 빈 문자열
/// - IsExpanded: false
/// - Children: childrenItemData (null이면 빈 리스트)
///
/// 사용 예:
/// // 간단한 아이템 생성
/// var item1 = new TreeListItemData("폴더");
///
/// // 자식을 포함해서 생성
/// var children = new List<TreeListItemData> { item1 };
/// var parent = new TreeListItemData("부모 폴더", children);
/// 이름과 초기 자식으로 생성합니다.
/// </summary>
/// <param name="generalName">아이템 이름(필수).</param>
/// <param name="childrenItemData">초기 자식 목록(null 허용).</param>
public TreeListItemData(string generalName, List<TreeListItemData>? childrenItemData = null)
{
_name = generalName;
@@ -324,29 +208,21 @@ namespace UVC.UI.List.Tree
#region (Child Management Methods)
/// <summary>
/// 이 아이템에 자식 아이템을 추가합니다.
///
/// 동작:
/// 1. 자식을 _children 리스트에 추가
/// 2. OnDataChanged 이벤트 발생 (UI 트리 구조 갱신)
///
/// 사용 예:
/// parent.AddChild(child); // 부모에 자식 추가
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 ├─ 자식1
/// └─ 자식2 ├─ 자식2
/// └─ 새자식
/// 자식을 끝에 추가합니다.
/// </summary>
/// <param name="child">추가할 자식.</param>
public void AddChild(TreeListItemData child)
{
child._parent = this;
child.Parent = this;
_children.Add(child);
NotifyDataChanged(ChangedType.AddChild, child); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 자식을 지정 인덱스에 삽입합니다.
/// </summary>
/// <param name="child">삽입할 자식.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddChildAt(TreeListItemData child, int index)
{
child._parent = this;
@@ -355,64 +231,51 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하지만 아이템 자체는 삭제하지 않습니다.
///
/// 동작:
/// 1. 자식을 _children 리스트에서 제거
/// 2. OnDataChanged 이벤트 발생
///
/// 주의: 같은 이름의 첫 번째 자식만 제거됩니다.
/// (TreeListItemData의 == 연산자가 Name으로 비교하기 때문)
///
/// 사용 예:
/// parent.RemoveChild(child); // 부모에서 자식 제거
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 ├─ 자식1
/// ├─ 자식2
/// └─ 자식3 └─ 자식3
/// (자식2 제거)
/// 전달된 자식을 복제하여 추가합니다.
/// 주의: 원본 child는 Dispose됩니다(파괴적).
/// </summary>
/// <param name="child">복제 및 대체할 원본 자식.</param>
public void AddCloneChild(TreeListItemData child)
{
NotifyDataChanged(ChangedType.AddCloneChild, child); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 전달된 자식을 복제하여 지정 인덱스에 삽입합니다.
/// 주의: 원본 child는 Dispose됩니다(파괴적).
/// </summary>
/// <param name="child">복제 및 대체할 원본 자식.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void AddCloneAtChild(TreeListItemData child, int index)
{
NotifyDataChanged(ChangedType.AddCloneAtChild, child, index); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 지정 자식을 지정 인덱스 위치에 삽입합니다.
/// 주의: 기존 위치에서 제거하지 않으므로 "이동"이 아닌 "삽입" 동작입니다.
/// (호출 측에서 기존 위치 정리는 별도로 해야 합니다)
/// </summary>
/// <param name="child">삽입할 기존 자식 참조.</param>
/// <param name="index">삽입 인덱스(0 기반).</param>
public void SwapChild(TreeListItemData child, int index)
{
_children.Insert(index, child);
NotifyDataChanged(ChangedType.SwapChild, child, index); // UI에 트리 구조 변경 알림
}
public void RemoveChild(TreeListItemData child)
{
child._parent = null;
int index = _children.IndexOf(child);
_children.Remove(child);
NotifyDataChanged(ChangedType.RemoveChild, child, index); // UI에 트리 구조 변경 알림
if (_children.Remove(child))
{
child._parent = null;
NotifyDataChanged(ChangedType.RemoveChild, child); // UI에 트리 구조 변경 알림
}
}
/// <summary>
/// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하고 아이템 자체도 삭제합니다.
/// </summary>
/// <param name="child"></param>
public void DeleteChild(TreeListItemData child)
{
RemoveChild(child);
child.Dispose();
}
/// <summary>
/// 이 아이템의 모든 자식을 한 번에 제거합니다.
///
/// 동작:
/// 1. _children 리스트를 완전히 비움
/// 2. OnDataChanged 이벤트 발생
///
/// 주의: 자식들이 메모리에서 삭제되는 것은 아니고,
/// 이 아이템과의 연결만 끊어집니다.
/// (C#의 가비지 컬렉션이 필요 없으면 나중에 정리)
///
/// 사용 예:
/// parent.ClearChildren(); // 모든 자식 제거
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 (모든 자식 제거됨)
/// ├─ 자식2
/// └─ 자식3
/// 모든 자식을 제거하고 정리합니다.
/// 각 자식에 대해 Parent=null 및 Dispose()를 호출한 뒤 리스트를 비웁니다.
/// </summary>
public void ClearChildren()
{
@@ -430,19 +293,11 @@ namespace UVC.UI.List.Tree
#region (Internal Methods)
/// <summary>
/// 데이터가 변경되었음을 UI 시스템에 알립니다.
///
/// 동작: OnDataChanged 이벤트를 발생시킵니다.
/// 이를 통해 UI는 자동으로 이 아이템의 정보를 갱신합니다.
///
/// 호출되는 시점:
/// - Name이나 Option이 변경될 때
/// - 자식이 추가/제거될 때
/// - IsExpanded 상태가 변경될 때
///
/// 왜 protected인가?
/// 이 클래스를 상속받은 자식 클래스에서도 호출할 수 있도록 하기 위함입니다.
/// 변경을 구독자에게 알립니다.
/// </summary>
/// <param name="changedType">변경 종류.</param>
/// <param name="target">변경 대상(null이면 this).</param>
/// <param name="index">위치 관련 인덱스(없으면 -1).</param>
internal void NotifyDataChanged(ChangedType changedType, TreeListItemData? target = null, int index = -1)
{
// OnDataChanged가 등록되어 있으면 실행
@@ -455,28 +310,7 @@ namespace UVC.UI.List.Tree
#region (Comparison Operators)
/// <summary>
/// 두 TreeListItemData 객체가 같은지 비교합니다. (== 연산자)
///
/// 비교 기준: Name (아이템의 이름)
/// 즉, 이름이 같으면 같은 아이템으로 간주합니다.
///
/// 비교 로직:
/// 1. 같은 객체인가? (메모리 주소가 같음) → true
/// 2. 둘 다 null이거나 하나가 null? → false (둘 다 null이면 true인데, 1번에서 처리)
/// 3. Name이 같은가? → true/false
///
/// 사용 예:
/// var item1 = new TreeListItemData("파일");
/// var item2 = new TreeListItemData("파일");
/// var item3 = new TreeListItemData("폴더");
///
/// item1 == item2 // true (Name이 "파일"로 같음)
/// item1 == item3 // false (Name이 다름)
/// item1 == null // false
///
/// 주의: 같은 이름이면 같은 아이템으로 취급되므로,
/// 실제로 다른 객체임에도 true가 될 수 있습니다.
/// 이는 의도된 설계입니다.
/// 이름(대소문자 구분, Ordinal) 기반 동등 비교.
/// </summary>
public static bool operator ==(TreeListItemData? left, TreeListItemData? right)
{
@@ -497,12 +331,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 두 TreeListItemData 객체가 다른지 비교합니다. (!= 연산자)
///
/// 동작: == 연산자의 결과를 반대(!)로 반환합니다.
///
/// 사용 예:
/// if (item1 != item2) { ... } // 다른 아이템이면 실행
/// 이름 기반 비동등 비교.
/// </summary>
public static bool operator !=(TreeListItemData? left, TreeListItemData? right)
{
@@ -514,23 +343,7 @@ namespace UVC.UI.List.Tree
#region (Object Methods)
/// <summary>
/// 이 객체의 고유한 해시 코드 반환합니다.
///
/// 용도: 이 객체를 Dictionary나 HashSet 같은 컬렉션에 저장할 때 사용합니다.
///
/// 해시 코드란? 객체를 빠르게 비교/검색하기 위한 고유 숫자입니다.
/// 같은 내용이면 같은 해시 코드를 반환해야 합니다.
///
/// 우리의 기준:
/// Name의 해시 코드 = 이 객체의 해시 코드
/// 왜? == 연산자에서 Name으로 비교하기 때문입니다.
///
/// 사용 예:
/// int hash = item.GetHashCode();
///
/// // Dictionary에 저장
/// Dictionary<TreeListItemData, string> dict = new();
/// dict[item] = "값"; // 내부적으로 GetHashCode() 사용
/// 이름 기반 해시 코드 반환.
/// </summary>
public override int GetHashCode()
{
@@ -538,29 +351,9 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 이 객체가 다른 객체와 같은지 비교합니다. (Equals 메서드)
///
/// 용도: 모든 C# 객체는 Equals 메서드를 가집니다.
/// 이 메서드를 오버라이드하여 우리의 비교 로직을 정의합니다.
///
/// 동작:
/// 1. 다른 객체가 TreeListItemData 타입인지 확인
/// 2. 맞으면 == 연산자로 비교 (Name으로 비교)
/// 3. 아니면 false 반환
///
/// 사용 예:
/// var item1 = new TreeListItemData("파일");
/// var item2 = new TreeListItemData("파일");
///
/// item1.Equals(item2) // true
/// item1.Equals("파일") // false (문자열은 다른 타입)
/// item1 == item2 // true (== 연산자와 동일)
///
/// GetHashCode()와 Equals의 관계:
/// - 같은 객체면 같은 해시 코드를 가져야 함
/// - 우리의 경우: Name이 같으면 Equals는 true, 해시 코드도 같음
/// - 이는 일관성 있게 설계되어 있습니다.
/// 이름 기반 동등성 비교(== 사용).
/// </summary>
/// <param name="obj">비교 대상.</param>
public override bool Equals(object? obj)
{
// obj가 TreeListItemData 타입인지 확인
@@ -574,24 +367,49 @@ namespace UVC.UI.List.Tree
return false;
}
/// <summary>
/// 이벤트/참조/자식 컬렉션을 정리합니다.
/// - Parent=null, 이벤트 핸들러 해제, Children.Clear()
/// - 하위 항목을 재귀적으로 Dispose하지는 않습니다
/// (재귀 정리는 ClearChildren에서 수행).
/// </summary>
public void Dispose()
{
if(OnDataChanged != null) OnDataChanged = null;
if (_parent != null && _parent.Children.Contains(this)) _parent.Children.Remove(this);
_parent = null;
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();
}
}
/// <summary>
/// 깊은 복제본을 생성합니다(자식까지 재귀 복제).
/// </summary>
/// <returns>복제된 새 인스턴스.</returns>
public TreeListItemData Clone()
{
TreeListItemData clone = new TreeListItemData();
clone.Name = this.Name;
clone.Option = this.Option;
clone.IsExpanded = this.IsExpanded;
clone.IsSelected = this.IsSelected;
foreach (var child in this.Children)
{
clone.AddChild(child.Clone());
}
return clone;
}
#endregion
}
/// <summary>
/// 데이터 변경 종류 열거형.
/// </summary>
public enum ChangedType
{
Name,
@@ -599,7 +417,10 @@ namespace UVC.UI.List.Tree
Expanded,
ResetChildren,
AddChild,
RemoveChild,
AddAtChild,
RemoveChild
AddCloneChild,
AddCloneAtChild,
SwapChild,
}
}

View File

@@ -9,74 +9,57 @@ namespace UVC.UI.List.Tree
/// TreeListItem의 드래그 & 드롭 입력을 처리하는 컴포넌트입니다.
///
/// 역할:
/// 1. 마우스 입력 감지 (클릭, 드래그)
/// 2. 드래그 시각 피드백 (알파 변경, 오프셋 이동)
/// 3. 드롭 대상 판단 (마우스 위치 기반)
/// 4. 드래그 매니저 이벤트 전달
/// 1) 마우스 입력 처리: 드래그 시작/진행/종료
/// 2) 드래그 시각 피드백: 알파 변경, 드롭 위치 표시기 갱신
/// 3) 마우스 위치 기반 드롭 대상 탐색 및 드롭 위치 판정
/// 4) 드래그 매니저(TreeListDragDropManager)와 이벤트 연동
///
/// 구조:
/// - PointerDown: 마우스 클릭 감지 → 드래그 시작 준비
/// - Drag: 마우스 이동 중 → 드래그 진행, 시각 피드백
/// - PointerUp: 마우스 해제 → 드롭 처리
/// 이벤트 흐름(IBeginDragHandler/IDragHandler/IEndDragHandler):
/// - OnBeginDrag: 드래그 시작 준비(안전성 검증, 오프셋 계산)
/// - OnDrag: 최초 드래그 프레임에 StartDrag, DropIndicator 생성/갱신
/// - OnEndDrag: 알파/레이캐스트 복구, DropIndicator 숨김, TryDrop 수행
///
/// 시각 피드백:
/// - CanvasGroup.alpha를 일시적으로 낮춰 드래그 중임을 표시
/// - DropIndicator(얇은 선 또는 블록)로 삽입 위치/자식 투입 위치를 표시
/// </summary>
public class TreeListItemDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
/// <summary>
/// 부모 TreeListItem입니다.
/// </summary>
/// <summary>부모 TreeListItem.</summary>
private TreeListItem? treeListItem;
/// <summary>
/// 부모 TreeList입니다.
/// </summary>
/// <summary>부모 TreeList.</summary>
private TreeList? treeList;
/// <summary>
/// 드래그 & 드롭 매니저입니다.
/// </summary>
/// <summary>드래그 & 드롭 매니저.</summary>
private TreeListDragDropManager? dragDropManager;
private RectTransform? rectTransform;
/// <summary>
/// 드래그 중 시각 피드백을 위한 CanvasGroup입니다.
/// </summary>
/// <summary>드래그 시각 피드백용 CanvasGroup.</summary>
private CanvasGroup? canvasGroup;
/// <summary>
/// 드래그 시작 시 원본 알파값입니다.
/// </summary>
/// <summary>드래그 시작 전 원본 알파값.</summary>
private float originalAlpha = 1f;
private Vector2 dragOffset = Vector2.zero;
/// <summary>
/// 드래그 중 적용할 알파값입니다.
/// </summary>
/// <summary>드래그 중 적용할 알파값.</summary>
[SerializeField]
private float dragAlpha = 0.5f;
/// <summary>
/// 드래그 활성화 여부입니다.
/// </summary>
/// <summary>드래그 활성화 여부.</summary>
[SerializeField]
private bool enableDragDrop = true;
/// <summary>
/// 드롭 위치 표시 막대 프리팹입니다.
/// </summary>
/// <summary>드롭 위치 표시 라인/블록.</summary>
private Image? dropIndicator;
private RectTransform? dropIndicatorRect;
/// <summary>
/// 드롭 표시기의 부모 (Content 또는 EntryRoot)
/// </summary>
/// <summary>드롭 표시기의 부모(RectTransform).</summary>
private RectTransform? dropIndicatorParent;
/// <summary>
/// 드래그 시작 시 아이템의 원본 부모입니다.
/// 드래그 후 원래 위치로 복구할 때 사용합니다.
/// </summary>
/// <summary>드래그 시작 시의 원본 부모/순서.</summary>
private Transform? originalParent;
private int originalSiblingIndex;
@@ -95,6 +78,12 @@ namespace UVC.UI.List.Tree
originalAlpha = canvasGroup.alpha;
}
/// <summary>
/// 외부에서 드래그 매니저 및 상위 컨텍스트를 주입합니다.
/// </summary>
/// <param name="item">이 핸들러가 속한 아이템.</param>
/// <param name="list">아이템 컨테이너(TreeList).</param>
/// <param name="manager">드래그 & 드롭 매니저.</param>
public void SetDragDropManager(TreeListItem item, TreeList list, TreeListDragDropManager manager)
{
treeListItem = item;
@@ -105,9 +94,10 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler)
/// OnPointerDown 이후 마우스가 약간 움직이면 자동으로 호출됩니다.
/// 드래그가 시작될 때 호출됩니다.
/// OnPointerDown 이후 마우스가 임계치 이상 이동하면 자동 호출됩니다.
/// </summary>
/// <param name="eventData">포인터 이벤트 데이터.</param>
public void OnBeginDrag(PointerEventData eventData)
{
Debug.Log($"[OnPointerDown]");
@@ -122,9 +112,9 @@ namespace UVC.UI.List.Tree
return;
}
// 드래그 시작 준비: 마우스 위치와 아이템 위치의 오프셋 계산
// 마우스 위치의 로컬 좌표 저장(필요 시 Y 오프셋 계산에 사용)
RectTransformUtility.ScreenPointToLocalPointInRectangle(
treeListRootParent,//rectTransform,
treeListRootParent,
eventData.position,
null,
out var localPoint);
@@ -135,8 +125,10 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드래그 중에 마우스가 이동할 때 호출됩니다. (IDragHandler)
/// 드래그 중 프레임마다 호출됩니다.
/// 최초 1프레임에 드래그 상태 진입 및 표시기 생성, 이후 hover 대상/표시기 갱신을 수행합니다.
/// </summary>
/// <param name="eventData">포인터 이벤트 데이터.</param>
public void OnDrag(PointerEventData eventData)
{
if (!enableDragDrop || dragDropManager == null || treeListItem?.Data == null || treeList == null || rectTransform == null)
@@ -152,16 +144,16 @@ namespace UVC.UI.List.Tree
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 중");
// 드래그 시작 처리 (첫 드래그 프레임)
// 최초 드래그 프레임: 드래그 상태 진입 및 시각 피드백/표시기 준비
if (!dragDropManager.IsDragging)
{
dragDropManager.StartDrag(treeListItem.Data);
// 원본 부모와 위치 저장
// 원본 부모/순서 저장
originalParent = rectTransform.parent;
originalSiblingIndex = rectTransform.GetSiblingIndex();
// 드래그 중 시각 피드백
// 시각 피드백(투명도/레이캐스트)
if (canvasGroup != null)
{
canvasGroup.alpha = dragAlpha;
@@ -174,21 +166,22 @@ namespace UVC.UI.List.Tree
Debug.Log($"[OnDrag] {treeListItem.Data.Name} 드래그 시작");
}
// 아이템이 마우스를 따라다니도록 위치 업데이트
// 필요 시 실제 UI를 마우스를 따라 이동시키려면 아래 호출을 활성화
//UpdateDragPosition(eventData);
// 마우스 위의 드롭 대상 찾기
var targetItem = GetItemAtMousePosition(eventData.position);
dragDropManager.OnDragOver(targetItem?.Data);
// 드롭 위치 표시 업데이트
// 드롭 위치 표시 갱신(위/아래/자식)
UpdateDropIndicator(targetItem);
}
/// <summary>
/// 드래그가 종료될 때 호출됩니다. (IEndDragHandler)
/// 마우스 버튼을 놓으면 자동으로 호출됩니다.
/// 드래그가 종료될 때 호출됩니다(마우스 버튼 해제).
/// 상태/시각 피드백 복구 후 TryDrop을 수행합니다.
/// </summary>
/// <param name="eventData">포인터 이벤트 데이터.</param>
public void OnEndDrag(PointerEventData eventData)
{
Debug.Log($"[OnPointerUp]");
@@ -206,7 +199,7 @@ namespace UVC.UI.List.Tree
canvasGroup.blocksRaycasts = true;
}
// 원본 부모 복구 (드래그 중 이동했던 위치 복구)
// 원본 부모/순서 복구(드래그 중 시각 이동을 되돌림)
if (originalParent != null)
{
rectTransform?.SetParent(originalParent);
@@ -230,17 +223,13 @@ namespace UVC.UI.List.Tree
var targetItem = GetItemAtMousePosition(eventData.position);
// 드롭 시도
// 드롭 시도 및 성공 시 데이터 동기화
if (treeListItem?.Data != null)
{
var result = dragDropManager.TryDrop(targetItem?.Data);
Debug.Log($"[OnPointerUp] 드롭 결과: {(result ? "" : "")}");
if (result)
{
// 드롭 성공 → 데이터 동기화
HandleDropSuccess(treeListItem.Data, targetItem);
}
if (result) HandleDropSuccess(treeListItem.Data, targetItem);
}
dragDropManager.EndDrag();
@@ -248,8 +237,9 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 드래그 중 아이템이 마우스를 따라다니도록 위치를 업데이트합니다.
/// Y축만 이동 (X축 고정)
/// Y축만 이동(X축 고정).
/// </summary>
/// <param name="eventData">포인터 이벤트 데이터.</param>
private void UpdateDragPosition(PointerEventData eventData)
{
if (rectTransform == null || treeList == null)
@@ -281,7 +271,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드롭 위치 표시 막대를 생성합니다.
/// 드롭 위치 표시기(DropIndicator)를 생성하거나 재사용합니다.
/// </summary>
private void CreateDropIndicator()
{
@@ -340,9 +330,11 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드롭 위치 표시 막대를 업데이트합니다.
/// VerticalLayoutGroup 환경에서도 정확하게 위치를 계산합니다.
/// 드롭 위치 표시기를 갱신합니다.
/// 복잡도: 대상 RectTransform의 월드 코너를 부모 기준 로컬 좌표로 환산하여,
/// 위/아래/자식(블록) 위치를 정확히 계산합니다.
/// </summary>
/// <param name="targetItem">현재 마우스 아래의 대상 아이템(null이면 숨김).</param>
private void UpdateDropIndicator(TreeListItem? targetItem)
{
if (dropIndicator == null || dropIndicatorRect == null || dropIndicatorParent == null)
@@ -437,7 +429,7 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드롭 위치 표시 막대를 숨깁니다.
/// 드롭 위치 표시를 숨깁니다.
/// </summary>
private void HideDropIndicator()
{
@@ -448,9 +440,9 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// treeListRootParent의 자식 중에서 "DropIndicator"라는 이름의 GameObject를 찾습니다.
/// treeListRootParent의 직접 자식 중 "DropIndicator" GameObject를 찾습니다.
/// </summary>
/// <returns>찾은 GameObject (없으면 null)</returns>
/// <returns>찾은 GameObject. 없으면 null.</returns>
private GameObject? FindDropIndicatorInRoot()
{
if (treeListRootParent == null)
@@ -477,10 +469,10 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 주어진 스크린 좌표에 있는 TreeListItem을 찾습니다.
/// 스크린 좌표에 위치한 TreeListItem을 찾습니다.
/// </summary>
/// <param name="screenPosition">스크린 좌표</param>
/// <returns>찾은 TreeListItem (없으면 null)</returns>
/// <param name="screenPosition">스크린 좌표.</param>
/// <returns>찾은 TreeListItem. 없으면 null.</returns>
private TreeListItem? GetItemAtMousePosition(Vector2 screenPosition)
{
if (treeList == null)
@@ -514,10 +506,11 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 드롭 성공 후 데이터를 동기화합니다.
/// 드롭 성공 후 데이터 구조를 갱신합니다.
/// 루트 드롭/형제 간 이동/자식으로 이동 등을 처리합니다.
/// </summary>
/// <param name="draggedData">드래그된 아이템</param>
/// <param name="targetItem">드롭 대상 UI 아이템</param>
/// <param name="draggedData">드래그한 데이터.</param>
/// <param name="targetItem">드롭 대상 UI 아이템(null: 루트로 드롭).</param>
private void HandleDropSuccess(TreeListItemData? draggedData, TreeListItem? targetItem)
{
if (draggedData == null || treeList == null)
@@ -567,10 +560,11 @@ namespace UVC.UI.List.Tree
}
/// <summary>
/// 드롭 위치를 판합니다.
/// 대상 RectTransform 내 마우스 Y 위치로 위/아래/자식 드롭 위치를 판합니다.
/// 상/하 30% → 위/아래, 중간 40% → 자식.
/// </summary>
/// <param name="targetRect">대상 아이템 RectTransform</param>
/// <returns>드롭 위치</returns>
/// <param name="targetRect">대상 아이템 RectTransform.</param>
/// <returns>드롭 위치.</returns>
private DropPosition GetDropPosition(RectTransform targetRect)
{
// 목표: 마우스 Y 위치를 기반으로 위/아래/안쪽 판단
@@ -610,18 +604,13 @@ namespace UVC.UI.List.Tree
/// <summary>
/// 아이템을 루트 레벨로 이동합니다.
/// 이미 루트면 순서만 변경합니다.
/// </summary>
/// <param name="draggedData">드래그된 데이터.</param>
private void MoveToRoot(TreeListItemData draggedData)
{
// 기존 부모에서 제거
//RemoveFromParent(draggedData);
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트로 이동");
// ✅ 루트 레벨에 추가
// TreeList의 Root는 직접 자식들을 포함하는 컨테이너
// Root의 자식 TreeListItem들이 실제 루트 레벨 아이템
// 데이터 구조에서 루트 아이템을 찾기 위해 모든 루트 아이템들을 순회
//
if (treeList == null)
{
return;
@@ -633,174 +622,173 @@ namespace UVC.UI.List.Tree
return;
}
// 1. 현재 마우스 위치(드래그 대상)를 기반으로 루트 아이템의 인덱스 계산
// 2. AddItemAt()을 호출하여 정확한 위치에 아이템 추가
// 루트 아이템 중 마우스 위의 아이템 찾기
var targetItem = GetItemAtMousePosition(Input.mousePosition);
if (targetItem != null && targetItem.Data != null)
if (draggedData.Parent == null)
{
// 드롭 위치 판단
var dropPosition = GetDropPosition(targetItem.GetComponent<RectTransform>());
var targetData = targetItem.Data;
// 대상 아이템이 루트 레벨의 아이템인지 확인
if (targetData.Parent == null)
{
// 루트 레벨 아이템이라면 해당 위치에 삽입
int targetIndex = treeList.Items.IndexOf(targetData);
if (targetIndex >= 0)
{
// Above: 대상 아이템 앞에 삽입
if (dropPosition == DropPosition.Above)
{
treeList.AddItemAt(draggedData, targetIndex);
}
// Below: 대상 아이템 뒤에 삽입
else if (dropPosition == DropPosition.Below)
{
treeList.AddItemAt(draggedData, targetIndex + 1);
}
else
{
// InsideAsChild: 루트 레벨에서는 아래에 추가
treeList.AddItemAt(draggedData, targetIndex + 1);
}
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트 레벨의 인덱스 {targetIndex}에 추가");
return;
}
}
// 이미 루트 레벨인 경우 순서만 변경
treeList.Items.Remove(draggedData);
treeList.SwapItem(draggedData, treeList.Items.Count);
}
else
{
// 루트 레벨에 아이템이 없거나 유효한 위치를 찾지 못한 경우 끝에 추가
treeList.AddCloneItem(draggedData);
}
// 루트 레벨에 아이템이 없거나 유효한 위치를 찾지 못한 경우 끝에 추가
treeList.AddItem(draggedData);
Debug.Log($"[MoveToRoot] {draggedData.Name}을(를) 루트 레벨의 끝에 추가");
}
/// <summary>
/// 아이템을 다른 아이템의 자식으로 이동합니다.
/// 아이템을 대상의 자식으로 이동합니다.
/// </summary>
/// <param name="draggedData">이동할 데이터.</param>
/// <param name="targetData">대상 데이터.</param>
private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData)
{
//RemoveFromParent(draggedData);
targetData.AddChild(draggedData);
targetData.AddCloneChild(draggedData);
Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동");
}
/// <summary>
/// 아이템을 다른 아이템 앞으로 이동합니다.
/// 아이템을 대상 앞(위)으로 이동합니다.
/// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다.
/// </summary>
/// <param name="draggedData">이동할 데이터.</param>
/// <param name="targetData">기준 데이터.</param>
private void MoveBefore(TreeListItemData draggedData, TreeListItemData targetData)
{
var parentData = targetData.Parent;// FindParentOfItem(targetData);
//RemoveFromParent(draggedData);
if (parentData != null)
//부모가 같은 경우 순서만 변경
if (draggedData.Parent == targetData.Parent)
{
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
if (draggedData.Parent != null)
{
parentData.AddChildAt(draggedData, targetIndex); // 자식으로 추가
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 이동");
var draggedIndex = draggedData.Parent.Children.IndexOf(draggedData);
draggedData.Parent.Children.Remove(draggedData);
var targetIndex = draggedData.Parent.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
draggedData.Parent.SwapChild(draggedData, targetIndex);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경");
}
}
else
{
var treeListItems = treeList?.Items;
if (treeListItems != null)
{
treeListItems.Remove(draggedData);
var targetIndex = treeListItems.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.SwapItem(draggedData, targetIndex);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경(루트레벨)");
}
}
}
}
else
{
// 루트 레벨인 경우
var treeListItems = treeList?.Items;
if (treeListItems != null)
var parentData = targetData.Parent;// FindParentOfItem(targetData);
//RemoveFromParent(draggedData);
if (parentData != null)
{
var targetIndex = treeListItems.IndexOf(targetData);
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.AddItemAt(draggedData, targetIndex);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 이동 (루트 레벨)");
parentData.AddCloneAtChild(draggedData, targetIndex); // 자식으로 추가
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 이동");
}
}
else
{
// 루트 레벨인 경우
var treeListItems = treeList?.Items;
if (treeListItems != null)
{
var targetIndex = treeListItems.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.AddCloneItemAt(draggedData, targetIndex);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 이동 (루트 레벨)");
}
}
}
}
}
/// <summary>
/// 아이템을 다른 아이템 뒤로 이동합니다.
/// 아이템을 대상 뒤(아래)로 이동합니다.
/// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다.
/// </summary>
/// <param name="draggedData">이동할 데이터.</param>
/// <param name="targetData">기준 데이터.</param>
private void MoveAfter(TreeListItemData draggedData, TreeListItemData targetData)
{
var parentData = targetData.Parent;
//RemoveFromParent(draggedData);
if (parentData != null)
//부모가 같은 경우 순서만 변경
if (draggedData.Parent == targetData.Parent)
{
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
if (draggedData.Parent != null)
{
parentData.AddChildAt(draggedData, targetIndex + 1);
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 이동");
var draggedIndex = draggedData.Parent.Children.IndexOf(draggedData);
draggedData.Parent.Children.Remove(draggedData);
var targetIndex = draggedData.Parent.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
draggedData.Parent.SwapChild(draggedData, targetIndex + 1);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경");
}
}
else
{
var treeListItems = treeList?.Items;
if (treeListItems != null)
{
treeListItems.Remove(draggedData);
var targetIndex = treeListItems.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.SwapItem(draggedData, targetIndex + 1);
Debug.Log($"[MoveBefore] {draggedData.Name}을(를) {targetData.Name} 앞으로 {targetIndex} 순서 변경(루트레벨)");
}
}
}
}
else
{
// 루트 레벨인 경우
var treeListItems = treeList?.Items;
if (treeListItems != null)
var parentData = targetData.Parent;
if (parentData != null)
{
var targetIndex = treeListItems.IndexOf(targetData);
var targetIndex = parentData.Children.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.AddItemAt(draggedData, targetIndex + 1);
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 이동 (루트 레벨)");
parentData.AddCloneAtChild(draggedData, targetIndex + 1);
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 {targetIndex} 이동");
}
}
else
{
// 루트 레벨인 경우
var treeListItems = treeList?.Items;
if (treeListItems != null)
{
var targetIndex = treeListItems.IndexOf(targetData);
if (targetIndex >= 0)
{
treeList?.AddCloneItemAt(draggedData, targetIndex + 1);
Debug.Log($"[MoveAfter] {draggedData.Name}을(를) {targetData.Name} 뒤로 {targetIndex} 이동 (루트 레벨)");
}
}
}
}
}
/// <summary>
/// 아이템을 현재 부모에서 제거합니다.
/// </summary>
private void RemoveFromParent(TreeListItemData item)
{
Debug.Log($"[RemoveFromParent] {item.Name}을(를) 부모 {item.Parent == null}에서 제거");
if (item.Parent != null)
{
item.Parent.RemoveChild(item);
}
else
{
// 루트 레벨에서 제거
treeList?.RemoveItem(item);
}
treeList?.UpdateFlattenedItemDataList();
}
/// <summary>
/// 주어진 아이템의 부모를 찾습니다.
/// </summary>
private TreeListItemData? FindParentOfItem(TreeListItemData item)
{
if (treeList == null)
{
return null;
}
foreach (TreeListItemData data in treeList!.AllItemsFlattened)
{
if (data == item)
{
return data.Parent;
}
}
return null;
}
/// <summary>
/// 드롭 위치를 나타내는 열거형입니다.
/// 드롭 위치를 나타냅니다.
/// </summary>
private enum DropPosition
{