From 02ed8a01a000f8f141c354d9ef54bbebf7bc33b2 Mon Sep 17 00:00:00 2001 From: logonkhi Date: Fri, 31 Oct 2025 19:55:14 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=A4=91.=20=EC=9D=B4=EB=8F=99=20=ED=9B=84=20=EB=82=A8?= =?UTF-8?q?=EA=B2=A8=EC=A7=80=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EA=B0=80=20?= =?UTF-8?q?=EC=9E=88=EC=9D=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Resources/Prefabs/UI/List/TreeList.prefab | 16 + Assets/Scenes/Sample/HierarchySample.cs | 6 +- Assets/Scenes/Sample/HierarchySample.unity | 16 + Assets/Scripts/NHN/ContentSizeSetter.cs | 17 +- Assets/Scripts/NHN/LayoutUpdater.cs | 1 - Assets/Scripts/UVC/UI/List/Tree/TreeList.cs | 895 ++++++++++-------- .../UI/List/Tree/TreeListDragDropManager.cs | 88 +- .../Scripts/UVC/UI/List/Tree/TreeListItem.cs | 534 +++-------- .../UVC/UI/List/Tree/TreeListItemData.cs | 473 +++------ .../UI/List/Tree/TreeListItemDragHandler.cs | 378 ++++---- 10 files changed, 1079 insertions(+), 1345 deletions(-) diff --git a/Assets/Resources/Prefabs/UI/List/TreeList.prefab b/Assets/Resources/Prefabs/UI/List/TreeList.prefab index e1cca496..ba1a3ab1 100644 --- a/Assets/Resources/Prefabs/UI/List/TreeList.prefab +++ b/Assets/Resources/Prefabs/UI/List/TreeList.prefab @@ -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 diff --git a/Assets/Scenes/Sample/HierarchySample.cs b/Assets/Scenes/Sample/HierarchySample.cs index 47f7f8f2..4360f85c 100644 --- a/Assets/Scenes/Sample/HierarchySample.cs +++ b/Assets/Scenes/Sample/HierarchySample.cs @@ -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); diff --git a/Assets/Scenes/Sample/HierarchySample.unity b/Assets/Scenes/Sample/HierarchySample.unity index bdad2722..4ed82ae5 100644 --- a/Assets/Scenes/Sample/HierarchySample.unity +++ b/Assets/Scenes/Sample/HierarchySample.unity @@ -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 diff --git a/Assets/Scripts/NHN/ContentSizeSetter.cs b/Assets/Scripts/NHN/ContentSizeSetter.cs index b535ebe3..38eccc08 100644 --- a/Assets/Scripts/NHN/ContentSizeSetter.cs +++ b/Assets/Scripts/NHN/ContentSizeSetter.cs @@ -12,7 +12,7 @@ namespace Gpm.Ui /// UI 계층 구조 예시: /// /// - 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; + /// /// 크기를 조절할 때 추가할 여백(margin)입니다. /// x, y 값을 설정하여 target의 너비와 높이에 각각 추가적인 공간을 줄 수 있습니다. /// + [Tooltip("크기를 조절할 때 추가할 여백(margin)입니다.")] + [SerializeField] public Vector2 margin; /// /// 이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다. /// 인스펙터 창에서 크기를 동기화할 UI 요소들을 여기에 할당합니다. /// + [Tooltip("이 컴포넌트의 크기에 맞춰 함께 크기가 조절될 RectTransform(들)의 배열입니다.")] + [SerializeField] public RectTransform[] target; /// @@ -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]); diff --git a/Assets/Scripts/NHN/LayoutUpdater.cs b/Assets/Scripts/NHN/LayoutUpdater.cs index 030e4bae..5d8c7cb7 100644 --- a/Assets/Scripts/NHN/LayoutUpdater.cs +++ b/Assets/Scripts/NHN/LayoutUpdater.cs @@ -70,7 +70,6 @@ namespace Gpm.Ui { return; } - if (force == true) { // 즉시 레이아웃을 강제로 다시 계산합니다. diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs index b63612d8..8817ed66 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeList.cs @@ -4,218 +4,127 @@ using System; using System.Collections.Generic; using System.Linq; using UnityEngine; +using UVC.UI.Modal.DatePicker; namespace UVC.UI.List.Tree { /// /// 트리 구조의 리스트를 관리하고 제어하는 클래스입니다. /// - /// 역할: - /// 1. 아이템 추가/제거 관리 - /// 2. 선택 상태 관리 (단일, 다중 선택) - /// 3. 키 입력(Ctrl, Shift)에 따른 선택 로직 처리 - /// 4. 선택 상태 변경 이벤트 발생 + /// 주요 기능: + /// - 계층 구조 아이템의 추가/제거 관리 + /// - 단일 및 다중 선택 지원 (파일 탐색기와 동일) + /// - 키보드 입력(Delete, 화살표, Ctrl, Shift) 처리 + /// - 드래그 & 드롭 기능 지원 + /// - 선택 상태 변경 이벤트 제공 /// - /// 파일 탐색기와 비슷한 동작: + /// 선택 동작: /// - 클릭: 한 항목 선택 /// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지) /// - Shift+클릭: 범위 선택 (시작~끝) - /// - /// MonoBehaviour란? - /// Unity의 모든 게임 로직이 상속받는 기본 클래스입니다. - /// Inspector에서 설정할 수 있고, Update 같은 생명주기 함수를 사용할 수 있습니다. + /// - 화살표: 선택된 아이템 이동 + /// - Delete: 선택된 아이템 삭제 /// public class TreeList : MonoBehaviour { /// - /// UI 아이템(TreeListItem)의 프리팹(템플릿)입니다. - /// - /// 프리팹이란? - /// 미리 만들어둔 UI 템플릿입니다. - /// 이것을 복제(Instantiate)해서 새로운 아이템을 여러 개 만들 수 있습니다. - /// - /// [SerializeField]란? - /// 이 필드를 Inspector(게임 편집기)에서 직접 설정할 수 있게 해줍니다. - /// (private이지만 Unity에서만 접근 가능) - /// - /// 사용 흐름: - /// 1. Inspector에서 프리팹을 이 필드에 드래그&드롭 - /// 2. AddItem()이 호출되면 이 프리팹을 복제해서 새 아이템 생성 + /// UI 아이템 프리팹 (복제할 템플릿) /// [SerializeField] protected TreeListItem itemPrefab; /// - /// itemPrefab을 읽기만 가능하게 공개합니다. - /// - /// 문법: public TreeListItem ItemPrefab => itemPrefab; - /// = 프로퍼티 (getter 전용) - /// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음 + /// itemPrefab 프로퍼티 (읽기 전용) /// public TreeListItem ItemPrefab => itemPrefab; /// - /// 모든 아이템을 담는 부모 컨테이너입니다. - /// - /// RectTransform이란? - /// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다. - /// Canvas(캔버스) 아래의 모든 UI 요소가 이것을 가집니다. - /// - /// 역할: - /// - AddItem()에서 새 아이템의 부모로 지정 - /// - UpdateFlattenedItemDataList()에서 모든 자식을 순회 - /// - /// 구조 예: - /// root (이것) - /// ├─ 아이템1 - /// ├─ 아이템2 - /// └─ 아이템3 + /// 모든 아이템을 담는 부모 컨테이너 (RectTransform) /// [SerializeField] protected RectTransform root; + /// + /// root 프로퍼티 (읽기 전용) + /// public RectTransform Root => root; /// - /// 여러 개 선택이 가능한지 여부를 나타냅니다. - /// - /// true: 여러 개 선택 가능 (Ctrl+클릭, Shift+클릭 작동) - /// false: 한 개만 선택 가능 (최신 선택이 기존 선택을 덮어씀) - /// - /// 예시: - /// - true인 경우: 파일 탐색기 (여러 파일 선택 가능) - /// - false인 경우: 라디오 버튼 (하나만 선택) - /// - /// SetAllowMultipleSelection() 메서드로 런타임에 변경 가능합니다. + /// 다중 선택 허용 여부 + /// (true: 여러 개 선택 가능, false: 한 개만 선택 가능) /// [SerializeField] protected bool allowMultipleSelection = true; /// - /// 드래그 & 드롭 기능 활성화 여부입니다. + /// 드래그 & 드롭 기능 활성화 여부 /// [SerializeField] protected bool enableDragDrop = true; /// - /// 현재 선택된 아이템들을 저장하는 리스트입니다. - /// - /// 역할: - /// - SelectItem(): 선택된 아이템 추가 - /// - DeselectItem(): 선택된 아이템 제거 - /// - ClearSelection(): 모두 해제 - /// - SelectedItems 프로퍼티: 외부에 읽기 전용으로 제공 - /// - /// 예: - /// 사용자가 3개 파일을 선택하면 이 리스트에 [파일1, 파일2, 파일3]이 저장됩니다. - /// - /// List란? - /// 배열처럼 여러 개 항목을 저장하는 컨테이너입니다. - /// 크기가 자동으로 조정되므로 개수를 미리 정할 필요가 없습니다. + /// 현재 선택된 아이템 리스트 /// protected List selectedItems = new List(); /// - /// 사용자가 마지막으로 선택한 아이템입니다. - /// - /// 용도: Shift+클릭으로 범위 선택할 때 시작점으로 사용 - /// - /// 시나리오: - /// 1. 파일1 클릭 → lastSelectedItem = 파일1 - /// 2. Shift+파일5 클릭 → 파일1부터 파일5까지 모두 선택 - /// - /// ? 연산자 (nullable)란? - /// 이 변수는 null(값 없음)일 수 있습니다. - /// 예: lastSelectedItem = null (아직 선택한 게 없음) - /// - /// null 체크: - /// if (lastSelectedItem != null) { ... } - /// = 값이 있는지 확인 후에 사용 + /// 마지막으로 선택한 아이템 (Shift+클릭 범위 선택의 시작점) /// protected TreeListItemData? lastSelectedItem = null; /// - /// 모든 아이템을 1차원 리스트로 변환한 것입니다. (평탄화) - /// - /// "평탄화"란? - /// 원본 트리 구조: - /// 폴더1 - /// ├─ 파일1 - /// └─ 폴더2 - /// └─ 파일2 - /// - /// 평탄화된 리스트: - /// [폴더1, 파일1, 폴더2, 파일2] (계층 무시, 선형으로 배열) - /// - /// 용도: - /// - SelectRange()에서 두 아이템 사이의 인덱스를 빠르게 찾기 - /// - Shift+클릭 범위 선택 구현 - /// - /// UpdateFlattenedItemDataList() 메서드로 항상 최신 상태로 유지됩니다. + /// 모든 아이템을 1차원으로 평탄화한 리스트 (범위 선택용) /// protected List allItemDatasFlattened = new List(); + /// + /// 평탄화된 아이템 데이터 리스트 (읽기 전용) + /// public List AllItemsFlattened => allItemDatasFlattened; + /// + /// 루트 레벨 아이템 리스트 + /// protected List items = new List(); + /// + /// 루트 레벨 아이템 리스트 (읽기 전용) + /// public List Items => items; + /// + /// 평탄화된 UI 아이템(TreeListItem) 리스트 + /// protected List allItemFlattened = new List(); + /// + /// 평탄화된 UI 아이템 리스트 (읽기 전용) + /// public List AllItemFlattened => allItemFlattened; /// - /// 드래그 & 드롭 매니저입니다. + /// 드래그 & 드롭 매니저 /// protected TreeListDragDropManager dragDropManager = new TreeListDragDropManager(); + /// + /// 드래그 & 드롭 매니저 (읽기 전용) + /// public TreeListDragDropManager DragDropManager => dragDropManager; /// - /// 아이템의 선택 상태가 변경되었을 때 발생하는 이벤트입니다. - /// - /// 이벤트란? - /// 무언가 일어났을 때 다른 코드에 알려주는 메커니즘입니다. - /// 구독자(listener)가 등록되어 있으면 자동으로 호출됩니다. - /// - /// 문법: Action? - /// = TreeListItemData 1개, bool 1개를 매개변수로 받는 함수들을 등록 - /// - /// 호출되는 경우: - /// - SelectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, true); - /// - DeselectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, false); - /// - /// 사용 예: - /// treeList.OnItemSelectionChanged += (data, isSelected) => { - /// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}"); - /// }; + /// 아이템 선택 상태 변경 이벤트 (data, isSelected) /// public Action? OnItemSelectionChanged; /// - /// 현재 선택된 아이템 목록을 반환합니다. (읽기 전용) - /// - /// 문법: public IReadOnlyList SelectedItems => ... - /// = 프로퍼티 (getter 전용) - /// = 외부에서 읽을 수 있지만 변경할 수 없음 - /// - /// AsReadOnly()란? - /// 리스트를 읽기 전용으로 변환합니다. - /// 따라서 외부에서 SelectedItems.AddChild()같은 수정이 불가능합니다. - /// - /// 왜 이렇게 하나? - /// 클래스 내부에서만 selectedItems를 제어하고, - /// 외부는 결과만 볼 수 있게 하기 위함입니다. (데이터 무결성) - /// - /// 사용 예: - /// foreach (var item in treeList.SelectedItems) - /// { - /// Debug.Log(item.Name); // 읽기만 가능 - /// } + /// 현재 선택된 아이템 목록 (읽기 전용) /// public IReadOnlyList SelectedItems => selectedItems.AsReadOnly(); + /// + /// root 아래의 모든 TreeListItem 컴포넌트 + /// public List TreeLists => root.GetComponentsInChildren().ToList(); private void Awake() @@ -234,7 +143,7 @@ namespace UVC.UI.List.Tree dragDropManager.OnDropped -= HandleItemDropped; } - if(OnItemSelectionChanged != null) OnItemSelectionChanged = null; + if (OnItemSelectionChanged != null) OnItemSelectionChanged = null; } private void Update() @@ -244,23 +153,40 @@ namespace UVC.UI.List.Tree { HandleDeleteKeyPressed(); } + + // 위/아래 화살표 키로 선택 아이템 변경 + if (Input.GetKeyDown(KeyCode.UpArrow)) + { + HandleUpArrowKeyPressed(); + } + + if (Input.GetKeyDown(KeyCode.DownArrow)) + { + HandleDownArrowKeyPressed(); + } + + // 좌/우 키로 선택된 아이템 접힘/펼침접힘 + if (Input.GetKeyDown(KeyCode.RightArrow)) + { + HandleRightArrowKeyPressed(); + } + + if (Input.GetKeyDown(KeyCode.LeftArrow)) + { + HandleLeftArrowKeyPressed(); + } } /// - /// Delete 키가 눌렸을 때 선택된 아이템들을 삭제합니다. + /// Delete 키 입력 시 선택된 모든 아이템 삭제 /// /// 동작: - /// 1. 현재 선택된 아이템 목록을 복사 (선택 상태 변경 중 문제 방지) - /// 2. 복사본을 역순으로 순회 (뒤에서 앞으로) - /// 3. 각 아이템을 RemoveItem()으로 삭제 + /// 1. 선택된 아이템 목록 복사 + /// 2. 각 아이템에 대해 DeleteItem() 호출 + /// 3. 평탄화 리스트 업데이트 /// - /// 역순 삭제 이유: - /// - 앞에서부터 삭제하면 인덱스가 변경되어 뒤의 아이템을 놓칠 수 있음 - /// - 뒤에서부터 삭제하면 앞의 인덱스는 영향을 받지 않음 - /// - /// ToList()로 복사하는 이유: - /// - RemoveItem() 호출 중 selectedItems가 수정될 수 있음 - /// - 복사본을 순회하면 안전하게 모든 아이템을 삭제 가능 + /// 주의: 반복 중 리스트 수정으로 인한 문제 방지를 위해 + /// ToList()로 복사 후 순회합니다. /// private void HandleDeleteKeyPressed() { @@ -281,101 +207,403 @@ namespace UVC.UI.List.Tree { DeleteItem(item); } + + UpdateFlattenedItemDataList(); } /// - /// 새로운 아이템을 트리 리스트에 추가합니다. + /// 펼쳐진 아이템들만 포함하는 가시 리스트 생성 /// - /// 호출 시점: - /// - 프로그램이 새 데이터를 UI에 표시하고 싶을 때 + /// 동작: + /// 1. 루트 레벨 아이템부터 시작 + /// 2. IsExpanded=true인 아이템의 자식들만 포함 + /// 3. 재귀적으로 모든 가시 아이템 수집 /// - /// 처리 순서: - /// 1. itemPrefab을 복제해서 새 UI 객체 생성 (Instantiate) - /// 2. 복제된 객체를 root(부모 컨테이너) 아래에 배치 - /// 3. 복제된 객체의 Init() 메서드 호출해서 초기화 - /// 4. 평탄화 리스트 업데이트 (범위 선택 시 사용) - /// - /// 매개변수: - /// - data: 표시할 데이터 (이름, 자식 등의 정보 포함) - /// - /// 사용 예: - /// var newItem = new TreeListItemData("새 폴더"); - /// treeList.AddItem(newItem); // 화면에 표시됨 - /// - /// Instantiate란? - /// 프리팹(템플릿)을 복제해서 새로운 객체를 만듭니다. - /// 게임의 모든 객체는 이 방식으로 생성됩니다. + /// 용도: 화살표 키 네비게이션 시 보이는 아이템만 선택 가능 /// - public void AddItem(TreeListItemData data) + private List GetVisibleFlattenedItems() { - data.Parent = null; - //data에 해당하는 TreeListItem 찾기 - TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); - if (item != null) + List visibleItems = new List(); + + // 루트 레벨 아이템들부터 시작 + foreach (var rootItem in items) { - item.transform.SetParent(root); - item.SetExpand(); - - UniTask.DelayFrame(1).ContinueWith(() => - { - //gameObject 순서 조절 - item.transform.SetAsLastSibling(); - - // 범위 선택에 필요한 평탄화 리스트 업데이트 - UpdateFlattenedItemDataList(); - }); + AddVisibleItemsRecursive(rootItem, visibleItems); } - else + + return visibleItems; + } + + /// + /// 가시 아이템을 재귀적으로 추가 + /// + /// 동작: + /// 1. 현재 아이템을 리스트에 추가 + /// 2. IsExpanded=true이면 모든 자식 재귀 처리 + /// 3. IsExpanded=false이면 자식 무시 (접혀있음) + /// + /// 현재 처리할 아이템 + /// 수집 결과 리스트 + private void AddVisibleItemsRecursive(TreeListItemData item, List visibleItems) + { + // 현재 아이템을 리스트에 추가 + visibleItems.Add(item); + + // IsExpanded=true인 경우에만 자식들 처리 + if (item.IsExpanded) { - // Instantiate(템플릿, 부모 Transform) - // = 템플릿을 복제하고 부모의 자식으로 설정 - item = GameObject.Instantiate(ItemPrefab, root); - - // 생성된 아이템 초기화 - // 데이터를 UI에 바인딩하고 이벤트 리스너 등록 - item.Init(data, this, dragDropManager); - items.Add(data); - - // 범위 선택에 필요한 평탄화 리스트 업데이트 - UpdateFlattenedItemDataList(); + foreach (var child in item.Children) + { + AddVisibleItemsRecursive(child, visibleItems); + } } } + /// + /// 위쪽 화살표 키 처리: 선택 아이템을 위로 이동 + /// + /// 동작: + /// 1. 정확히 1개 아이템만 선택되어 있는지 확인 + /// 2. 가시 리스트에서 현재 아이템의 위치 찾기 + /// 3. 이전 아이템이 있으면 선택 변경 + /// + /// 제약: 단일 선택 상태에서만 작동 + /// + private void HandleUpArrowKeyPressed() + { + // 정확히 하나의 아이템만 선택되어 있어야 함 + if (selectedItems.Count != 1) + { + return; + } + + TreeListItemData currentSelected = selectedItems[0]; + + // 펼쳐진 아이템들만 포함하는 임시 리스트 생성 + List visibleItems = GetVisibleFlattenedItems(); + + // 임시 리스트에서 현재 선택된 아이템의 인덱스 찾기 + int currentIndex = visibleItems.IndexOf(currentSelected); + + // 아이템을 찾을 수 없으면 종료 + if (currentIndex == -1) + { + return; + } + + // 이전 아이템이 있는지 확인 (0보다 크면 이전 아이템이 있음) + if (currentIndex > 0) + { + int previousIndex = currentIndex - 1; + TreeListItemData previousItem = visibleItems[previousIndex]; + + // 현재 아이템 선택 해제 + DeselectItem(currentSelected); + + // 이전 아이템 선택 + SelectItem(previousItem); + lastSelectedItem = previousItem; + + // 디버그 로그 + Debug.Log($"Up arrow: Selected '{previousItem.Name}' (visible index {previousIndex})"); + } + } + + /// + /// 아래쪽 화살표 키 처리: 선택 아이템을 아래로 이동 + /// + /// 동작: + /// 1. 가시 리스트 생성 + /// 2. 선택 없음: 첫 번째 아이템 선택 + /// 3. 단일 선택: 다음 아이템으로 이동 + /// 4. 다중 선택: 무시 + /// + /// 제약: 단일 선택 상태에서만 작동 + /// + private void HandleDownArrowKeyPressed() + { + // 펼쳐진 아이템들만 포함하는 임시 리스트 생성 + List visibleItems = GetVisibleFlattenedItems(); + + // 선택된 아이템이 없는 경우 + if (selectedItems.Count == 0) + { + // 펼쳐진 아이템 리스트에 아이템이 있으면 첫 번째 아이템 선택 + if (visibleItems.Count > 0) + { + TreeListItemData firstItem = visibleItems[0]; + SelectItem(firstItem); + lastSelectedItem = firstItem; + + // 디버그 로그 + Debug.Log($"Down arrow (no selection): Selected first visible item '{firstItem.Name}'"); + } + return; + } + + // 정확히 하나의 아이템만 선택되어 있어야 함 + if (selectedItems.Count != 1) + { + return; + } + + TreeListItemData currentSelected = selectedItems[0]; + + // 임시 리스트에서 현재 선택된 아이템의 인덱스 찾기 + int currentIndex = visibleItems.IndexOf(currentSelected); + + // 아이템을 찾을 수 없으면 종료 + if (currentIndex == -1) + { + return; + } + + // 다음 아이템이 있는지 확인 + if (currentIndex < visibleItems.Count - 1) + { + int nextIndex = currentIndex + 1; + TreeListItemData nextItem = visibleItems[nextIndex]; + + // 현재 아이템 선택 해제 + DeselectItem(currentSelected); + + // 다음 아이템 선택 + SelectItem(nextItem); + lastSelectedItem = nextItem; + + // 디버그 로그 + Debug.Log($"Down arrow: Selected '{nextItem.Name}' (visible index {nextIndex})"); + } + } + + /// + /// 우측 화살표 키 처리: 선택된 아이템 펼치기 + /// + /// 동작: + /// 1. 정확히 1개 아이템만 선택되어 있는지 확인 + /// 2. IsExpanded를 true로 설정 + /// 3. 평탄화 리스트 업데이트 + /// + /// 제약: 이미 펼쳐진 아이템은 동작 없음 + /// + private void HandleRightArrowKeyPressed() + { + // 선택된 아이템이 없으면 아무것도 하지 않음 + if (selectedItems.Count == 0) + { + return; + } + + // 정확히 하나의 아이템만 선택되어 있어야 함 + // (다중 선택 상태에서는 작동하지 않음) + if (selectedItems.Count != 1) + { + return; + } + + TreeListItemData selectedItem = selectedItems[0]; + + if (selectedItem.IsExpanded) return; + + // IsExpanded 상태를 반대로 토글 + selectedItem.IsExpanded = true; + + // 디버그 로그 + Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}"); + + // 펼침/접힘 상태 변경을 UI에 반영하기 위해 평탄화 리스트 업데이트 + UpdateFlattenedItemDataList(); + } + + /// + /// 좌측 화살표 키 처리: 선택된 아이템 접기 + /// + /// 동작: + /// 1. 정확히 1개 아이템만 선택되어 있는지 확인 + /// 2. IsExpanded를 false로 설정 + /// 3. 평탄화 리스트 업데이트 + /// + /// 제약: 이미 접혀진 아이템은 동작 없음 + /// + private void HandleLeftArrowKeyPressed() + { + // 선택된 아이템이 없으면 아무것도 하지 않음 + if (selectedItems.Count == 0) + { + return; + } + + // 정확히 하나의 아이템만 선택되어 있어야 함 + // (다중 선택 상태에서는 작동하지 않음) + if (selectedItems.Count != 1) + { + return; + } + + TreeListItemData selectedItem = selectedItems[0]; + + if (!selectedItem.IsExpanded) return; + + // IsExpanded 상태를 반대로 토글 + selectedItem.IsExpanded = false; + + // 디버그 로그 + Debug.Log($"Return key: Toggled '{selectedItem.Name}' IsExpanded to {selectedItem.IsExpanded}"); + + // 펼침/접힘 상태 변경을 UI에 반영하기 위해 평탄화 리스트 업데이트 + UpdateFlattenedItemDataList(); + } + + /// + /// 새 아이템 추가 + /// + /// 동작: + /// 1. 아이템 프리팹 인스턴스화 + /// 2. UI 초기화 (Init 호출) + /// 3. 데이터 리스트에 추가 + /// 4. 평탄화 리스트 업데이트 + /// + /// 매개변수: + /// - data: 추가할 아이템 데이터 + /// + /// 추가할 아이템 데이터 + public void AddItem(TreeListItemData data) + { + data.Parent = null; + // Instantiate(템플릿, 부모 Transform) + // = 템플릿을 복제하고 부모의 자식으로 설정 + TreeListItem item = GameObject.Instantiate(ItemPrefab, root); + + // 생성된 아이템 초기화 + // 데이터를 UI에 바인딩하고 이벤트 리스너 등록 + item.Init(data, this, dragDropManager); + items.Add(data); + + // 범위 선택에 필요한 평탄화 리스트 업데이트 + UpdateFlattenedItemDataList(); + + } + + /// + /// 지정 인덱스에 아이템 추가 + /// + /// 동작: + /// 1. 아이템 프리팹 인스턴스화 + /// 2. 지정 위치에 데이터 삽입 + /// 3. UI 순서 조정 + /// 4. 평탄화 리스트 업데이트 + /// + /// 추가할 아이템 데이터 + /// 삽입 위치 public void AddItemAt(TreeListItemData data, int index) { data.Parent = null; - //data에 해당하는 TreeListItem 찾기 - TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); - if (item != null) - { - item.transform.SetParent(root); - item.SetExpand(); - } - else - { - item = GameObject.Instantiate(ItemPrefab, root); - item.Init(data, this, dragDropManager); - items.Insert(index, data); - } + + TreeListItem item = GameObject.Instantiate(ItemPrefab, root); + item.Init(data, this, dragDropManager); + items.Insert(index, data); UniTask.DelayFrame(1).ContinueWith(() => { //gameObject 순서 조절 item.transform.SetSiblingIndex(index); - + // 범위 선택에 필요한 평탄화 리스트 업데이트 UpdateFlattenedItemDataList(); }); } /// - /// 해당 항목을 트리 리스트에서 제거합니다. date 자체를 삭제하지 않습니다. + /// 아이템 복제 추가 /// - /// + /// 복제할 원본 아이템 데이터 + public void AddCloneItem(TreeListItemData data) + { + TreeListItemData clone = data.Clone(); + + //changedData 부모에게 알림 + if (data.Parent != null) + { + data.Parent.RemoveChild(data); + } + else + { + RemoveItem(data); + } + + //data에 해당하는 TreeListItem 찾기 + TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); + if (item != null) item.Delete(true); + + AddItem(clone); + + } + + /// + /// 아이템 복제 후 지정 인덱스에 추가 + /// + /// 복제할 원본 아이템 데이터 + /// 삽입 위치 + public void AddCloneItemAt(TreeListItemData data, int index) + { + TreeListItemData clone = data.Clone(); + + //changedData 부모에게 알림 + if (data.Parent != null) + { + data.Parent.RemoveChild(data); + } + else + { + RemoveItem(data); + } + + //data에 해당하는 TreeListItem 찾기 + TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); + if (item != null) item.Delete(true); + + AddItemAt(clone, index); + } + + /// + /// 아이템을 지정 인덱스로 이동 + /// + /// 이동할 아이템 데이터 + /// 목표 위치 + public void SwapItem(TreeListItemData data, int index) + { + TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); + if (item != null) items.Insert(index, data); + UniTask.DelayFrame(1).ContinueWith(() => + { + //gameObject 순서 조절 + if (item != null) item.transform.SetSiblingIndex(index); + + // 범위 선택에 필요한 평탄화 리스트 업데이트 + UpdateFlattenedItemDataList(); + }); + } + public void RemoveItem(TreeListItemData data) + { + if (items.Contains(data)) items.Remove(data); + UpdateFlattenedItemDataList(); + } + + + /// + /// 아이템 삭제 + /// + /// 동작: + /// 1. 데이터 리스트에서 제거 + /// 2. 선택 리스트에서 제거 + /// 3. UI 컴포넌트 삭제 + /// 4. 데이터 메모리 해제 + /// + /// 삭제할 아이템 데이터 + public void DeleteItem(TreeListItemData data) { items.Remove(data); - // selectedItems에서도 제거 if (selectedItems.Contains(data)) @@ -386,48 +614,19 @@ namespace UVC.UI.List.Tree TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); if (item != null) { - item.Delete(); + item.Delete(true); } - - // 범위 선택에 필요한 평탄화 리스트 업데이트 - UpdateFlattenedItemDataList(); } /// - /// 해당 항목을 트리 리스트에서 제거며 date 자체도 삭제합니다. - /// - /// - public void DeleteItem(TreeListItemData data) - { - RemoveItem(data); - data.Dispose(); - } - - /// - /// 모든 아이템을 평탄화된 1차원 리스트로 재구성합니다. + /// 모든 아이템을 1차원 리스트로 평탄화 /// /// 동작: - /// 1. 기존 평탄화 리스트를 비움 (Clear) - /// 2. root의 모든 직접 자식을 순회 (foreach) - /// 3. 각 자식의 TreeListItem 컴포넌트 획득 (GetComponent) - /// 4. AddItemDataToFlattened() 호출해서 자식과 손자까지 재귀 추가 + /// 1. 평탄화 리스트 초기화 + /// 2. 루트 아이템부터 재귀적으로 추가 + /// 3. 각 데이터에 대응하는 UI 컴포넌트 수집 /// - /// 호출되는 시점: - /// - AddItem() 실행 후 (새 아이템 추가됨) - /// - OnDataChanged 이벤트 발생 시 (자식 구조 변경됨) - /// - /// Transform이란? - /// 게임 오브젝트의 위치, 회전, 크기 정보를 가진 컴포넌트입니다. - /// 계층 구조(부모-자식 관계)의 중심입니다. - /// root.childCount: 몇 개의 자식이 있는지 - /// for (Transform child in root): 모든 자식을 순회 - /// - /// GetComponent() 란? - /// 게임 오브젝트에 붙어있는 특정 컴포넌트를 찾아서 반환합니다. - /// 찾지 못하면 null 반환 - /// - /// 예: child.GetComponent() - /// = child 게임 오브젝트에서 TreeListItem 컴포넌트 찾기 + /// 용도: 범위 선택 및 화살표 네비게이션 /// internal void UpdateFlattenedItemDataList() { @@ -437,7 +636,7 @@ namespace UVC.UI.List.Tree // root의 모든 직접 자식을 순회 // (손자, 증손자는 재귀로 처리됨) foreach (TreeListItemData itemData in items) - { + { // 이 아이템과 자식들을 재귀적으로 평탄화 리스트에 추가 AddItemDataToFlattened(itemData); } @@ -446,7 +645,7 @@ namespace UVC.UI.List.Tree foreach (var data in allItemDatasFlattened) { var item = root.GetComponentsInChildren().FirstOrDefault(x => x.Data == data); - if(item != null) + if (item != null) { allItemFlattened.Add(item); } @@ -454,29 +653,16 @@ namespace UVC.UI.List.Tree } /// - /// 아이템과 그 모든 자식 아이템들을 평탄화 리스트에 재귀적으로 추가합니다. - /// - /// "재귀"란? - /// 함수가 자기 자신을 호출하는 것입니다. - /// 계층 구조(깊이가 정해지지 않은)를 탐색하는 데 유용합니다. + /// 아이템과 자식들을 재귀적으로 평탄화 /// /// 동작: - /// 1. 현재 data를 allItemsFlattened에 추가 - /// 2. data의 모든 자식 Children을 순회 - /// 3. 각 자식에 대해 AddItemDataToFlattened() 재귀 호출 + /// 1. 현재 아이템 추가 + /// 2. 모든 자식에 대해 재귀 호출 /// - /// 예시 - 트리 구조를 평탄화하는 과정: - /// - /// 트리: 재귀 호출 흐름: 결과: - /// 폴더1 AddItemDataToFlattened(폴더1) [폴더1, - /// ├─ 파일1 → AddItemDataToFlattened(파일1) 파일1, - /// ├─ 파일2 → AddItemDataToFlattened(파일2) 파일2, - /// └─ 폴더2 → AddItemDataToFlattened(폴더2) 폴더2, - /// └─ 파일3 → AddItemDataToFlattened(파일3) 파일3] - /// - /// 매개변수: - /// - data: 현재 처리할 아이템 + /// 트리 구조를 1차원 배열로 변환하여 + /// 빠른 인덱스 검색 및 범위 선택을 가능하게 합니다. /// + /// 현재 처리할 아이템 private void AddItemDataToFlattened(TreeListItemData data) { // 현재 아이템을 평탄화 리스트에 추가 @@ -492,56 +678,43 @@ namespace UVC.UI.List.Tree } /// - /// 아이템이 클릭되었을 때 호출됩니다. + /// 아이템 클릭 처리 /// - /// 역할: - /// - 사용자가 어떤 키를 누르고 있는지 확인 - /// - 그에 맞는 선택 로직 실행 - /// - 마지막 선택 아이템 기억 - /// - /// 파일 탐색기와 동일한 동작: - /// - 일반 클릭: 한 항목만 선택 - /// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지) - /// - Shift+클릭: 범위 선택 (마지막 선택~현재 선택) + /// 동작: + /// 1. 키 입력 상태 확인 (Ctrl, Shift) + /// 2. 선택 모드에 따라 로직 실행: + /// - 일반 클릭: 이 아이템만 선택 + /// - Ctrl+클릭: 토글 (기존 선택 유지) + /// - Shift+클릭: 범위 선택 + /// 3. 마지막 선택 아이템 기록 /// /// 매개변수: - /// - data: 클릭된 아이템의 데이터 - /// - ctrlPressed: Ctrl 키가 눌렸는지 여부 - /// - shiftPressed: Shift 키가 눌렸는지 여부 + /// - data: 클릭한 아이템 + /// - ctrlPressed: Ctrl 키 입력 여부 + /// - shiftPressed: Shift 키 입력 여부 /// + /// 클릭한 아이템 + /// Ctrl 키 입력 여부 + /// Shift 키 입력 여부 public void OnItemClicked(TreeListItemData data, bool ctrlPressed, bool shiftPressed) { // 디버그 로그: 클릭 정보를 콘솔에 출력 (개발 중 확인용) Debug.Log($"OnItemClicked {data.Name}, ctrlPressed:{ctrlPressed}, shiftPressed:{shiftPressed}, lastSelectedItem:{lastSelectedItem}"); - // ============================================================ - // 경우 1: 다중 선택이 비활성화된 경우 - // ============================================================ + // 다중 선택 비활성화 if (!allowMultipleSelection) { - // 한 개만 선택 가능하므로 단순히 토글 - // (선택 상태를 반대로) ToggleItemSelection(data); return; } - // ============================================================ - // 경우 2: Ctrl 키를 누르고 클릭 - // ============================================================ + // Ctrl+클릭: 토글 if (ctrlPressed) { - // 기존 선택을 유지하면서 현재 아이템을 토글 - // 이미 선택됨 → 해제 - // 선택 안 됨 → 선택 ToggleItemSelection(data); - - // 이 아이템을 "마지막 선택"으로 기억 - // (다음 Shift+클릭의 시작점) lastSelectedItem = data; } - // ============================================================ - // 경우 3: Shift 키를 누르고 클릭 - // ============================================================ + // Shift+클릭: 범위 선택 else if (shiftPressed) { if (lastSelectedItem != null) @@ -559,9 +732,7 @@ namespace UVC.UI.List.Tree // 이 아이템을 "마지막 선택"으로 기억 lastSelectedItem = data; } - // ============================================================ - // 경우 4: 아무 키도 누르지 않고 클릭 (일반 클릭) - // ============================================================ + // 일반 클릭: 단일 선택 else { if (data.IsSelected) @@ -581,35 +752,19 @@ namespace UVC.UI.List.Tree } /// - /// 두 아이템 사이의 모든 아이템을 선택합니다. (범위 선택) - /// - /// 시나리오: - /// 평탄화 리스트: [파일1, 파일2, 파일3, 파일4, 파일5] - /// 파일2를 선택한 후 Shift+파일5 클릭 - /// → SelectRange(파일2, 파일5) - /// → 파일2, 파일3, 파일4, 파일5 모두 선택됨 + /// 두 아이템 사이의 범위 선택 /// /// 동작: - /// 1. 평탄화 리스트에서 두 아이템의 인덱스(위치) 찾기 - /// 2. 시작과 끝 인덱스를 정렬 (둘 중 작은 값, 큰 값) - /// 3. 그 범위의 모든 아이템 선택 - /// - /// IndexOf란? - /// 리스트에서 특정 항목의 위치(인덱스)를 찾습니다. - /// 찾으면 위치 반환 (0부터 시작), 못 찾으면 -1 반환 - /// - /// 예: list = [A, B, C, D] - /// list.IndexOf(C) → 2 (3번째이지만 0부터 시작하므로 2) - /// - /// Mathf.Min/Max란? - /// 두 숫자 중 작은/큰 값을 반환합니다. - /// Mathf.Min(5, 2) → 2 - /// Mathf.Max(5, 2) → 5 + /// 1. 평탄화 리스트에서 두 아이템의 인덱스 찾기 + /// 2. 범위 정렬 (시작 ≤ 끝) + /// 3. 범위 내 모든 아이템 선택 /// /// 매개변수: - /// - startItem: 범위의 시작점 (사용자가 먼저 선택한 아이템) - /// - endItem: 범위의 끝점 (Shift+클릭한 아이템) + /// - startItem: 범위 시작 아이템 + /// - endItem: 범위 끝 아이템 /// + /// 범위 시작 아이템 + /// 범위 끝 아이템 private void SelectRange(TreeListItemData startItem, TreeListItemData endItem) { // 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기 @@ -639,20 +794,13 @@ namespace UVC.UI.List.Tree } /// - /// 아이템의 선택 상태를 반대로 바꿉니다. (토글) + /// 아이템 선택 상태 토글 /// /// 동작: - /// - 선택됨 → 선택 해제 - /// - 선택 안 됨 → 선택 - /// - /// 사용 예: - /// treeItem.IsSelected = false - /// treeList.ToggleItemSelection(treeItem) - /// // 이후 treeItem.IsSelected = true - /// - /// 매개변수: - /// - data: 토글할 아이템 + /// - 선택됨 → 해제 + /// - 미선택 → 선택 /// + /// 토글할 아이템 public void ToggleItemSelection(TreeListItemData data) { if (data.IsSelected) @@ -668,22 +816,16 @@ namespace UVC.UI.List.Tree } /// - /// 아이템을 선택합니다. + /// 아이템 선택 /// /// 동작: - /// 1. 이미 선택되어 있으면 아무것도 안 함 (중복 선택 방지) - /// 2. 다중 선택이 비활성화되어 있으면 기존 선택 모두 해제 - /// 3. 현재 아이템 선택 표시 - /// 4. selectedItems 리스트에 추가 - /// 5. OnItemSelectionChanged 이벤트 발생 - /// - /// 매개변수: - /// - data: 선택할 아이템 - /// - /// 이벤트란? - /// SelectItem이 호출되면 이 이벤트를 구독한 다른 코드들이 - /// 자동으로 실행됩니다. (옵션 창 업데이트 등) + /// 1. 중복 선택 방지 + /// 2. 다중 선택 비활성화 시 기존 선택 해제 + /// 3. 아이템 선택 표시 + /// 4. 선택 리스트에 추가 + /// 5. 선택 변경 이벤트 발생 /// + /// 선택할 아이템 public void SelectItem(TreeListItemData data) { // 이미 선택되어 있으면 중복 선택 방지 @@ -716,17 +858,15 @@ namespace UVC.UI.List.Tree } /// - /// 아이템의 선택을 해제합니다. + /// 아이템 선택 해제 /// /// 동작: - /// 1. 선택되어 있지 않으면 아무것도 안 함 (중복 해제 방지) - /// 2. 아이템의 선택 상태를 false로 설정 - /// 3. selectedItems 리스트에서 제거 - /// 4. OnItemSelectionChanged 이벤트 발생 - /// - /// 매개변수: - /// - data: 선택 해제할 아이템 + /// 1. 중복 해제 방지 + /// 2. 아이템 선택 상태 해제 + /// 3. 선택 리스트에서 제거 + /// 4. 선택 변경 이벤트 발생 /// + /// 선택 해제할 아이템 public void DeselectItem(TreeListItemData data) { // 이미 선택 해제되어 있으면 아무것도 안 함 @@ -746,21 +886,11 @@ namespace UVC.UI.List.Tree } /// - /// 모든 선택 상태를 한 번에 해제합니다. + /// 모든 아이템 선택 해제 /// /// 동작: - /// 1. selectedItems를 복사 (ToList()) - /// 왜? 반복 중에 리스트를 수정하면 버그가 발생할 수 있으므로 - /// 2. 복사본을 순회하면서 각각 DeselectItem() 호출 - /// - /// 사용 예: - /// treeList.ClearSelection(); - /// // 이후 selectedItems는 비어있음 - /// - /// 결과: - /// - 모든 아이템의 IsSelected = false - /// - selectedItems 리스트 비어있음 - /// - 각 아이템마다 OnItemSelectionChanged 이벤트 발생 + /// 1. 선택 리스트 복사 (반복 중 수정 방지) + /// 2. 각 아이템에 대해 DeselectItem() 호출 /// public void ClearSelection() { @@ -774,26 +904,13 @@ namespace UVC.UI.List.Tree } /// - /// 다중 선택 가능 여부를 설정합니다. + /// 다중 선택 활성화/비활성화 설정 /// /// 동작: /// 1. allowMultipleSelection 값 변경 - /// 2. 만약 false로 변경하면서 여러 개가 선택되어 있으면: - /// - 첫 번째 아이템만 유지 - /// - 나머지 모두 해제 - /// - /// 시나리오: - /// 현재: 3개 선택된 상태 (파일1, 파일2, 파일3) - /// SetAllowMultipleSelection(false) 호출 - /// 결과: 파일1만 선택, 파일2와 파일3은 해제 - /// - /// 매개변수: - /// - allow: true면 다중 선택 가능, false면 단일 선택만 가능 - /// - /// 사용 예: - /// // 라디오 버튼 같은 단일 선택 모드로 변경 - /// treeList.SetAllowMultipleSelection(false); + /// 2. false로 변경 시 여러 선택이 있으면 첫 번째만 유지 /// + /// true: 다중 선택 허용, false: 단일 선택만 허용 public void SetAllowMultipleSelection(bool allow) { // 다중 선택 가능 여부 설정 @@ -814,8 +931,10 @@ namespace UVC.UI.List.Tree } /// - /// 드롭 완료 후 UI를 업데이트합니다. + /// 드롭 완료 후 UI 업데이트 /// + /// 드래그된 아이템 + /// 드롭 대상 아이템 (null: 루트) private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem) { // 평탄화 리스트 업데이트 diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs index f2fe99cf..923bd419 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListDragDropManager.cs @@ -6,23 +6,27 @@ using UnityEngine; namespace UVC.UI.List.Tree { /// - /// 트리 리스트의 드래그 & 드롭 기능을 관리하는 클래스입니다. + /// 트리 리스트의 드래그 & 드롭 상호작용을 상태와 이벤트로 중재하는 관리자입니다. /// /// 역할: - /// 1. 드래그 시작/진행/종료 상태 관리 - /// 2. 유효한 드롭 대상 판단 (순환 참조 방지) - /// 3. 아이템 위치 변경 (형제 아이템 간 순서 변경) - /// 4. 아이템 계층 구조 변경 (부모-자식 관계 수정) + /// - 드래그 시작/진행/종료 상태 관리(상태 머신) + /// - 드롭 유효성 검사(자기 자신/조상에게 드롭 금지로 순환 참조 방지) + /// - 구독자에게 이벤트로 진행 상황 전달 /// - /// 기능: - /// - 드래그할 아이템과 드롭 대상을 추적 - /// - 유효성 검사 (자기 자신에게 드롭 금지, 순환 참조 방지) - /// - 드롭 완료 후 데이터 동기화 + /// 책임 경계: + /// - 이 매니저는 데이터를 직접 수정(이동/부모 변경)하지 않습니다. + /// - 실제 아이템 재배치/재귀 로직은 이벤트 구독자(예: 리스트/뷰 모델)에서 수행해야 합니다. + /// + /// 이벤트 흐름: + /// - StartDrag → OnDragStarted(once) + /// - OnDragOver → OnDragEntered(repeat, hover 대상에 따라 여러 번) + /// - TryDrop 유효 시 → OnDropped → EndDrag → OnDragEnded + /// - TryDrop 무효/취소 시 → EndDrag → OnDragEnded /// public class TreeListDragDropManager { /// - /// 드래그 중인 아이템의 데이터입니다. + /// 현재 드래그 중인 아이템입니다(드래그 중이 아니면 null). /// public TreeListItemData? DraggedItem { get; private set; } @@ -32,29 +36,45 @@ namespace UVC.UI.List.Tree public bool IsDragging { get; private set; } /// - /// 드래그 시작 시 발생하는 이벤트입니다. + /// 드래그 시작 시 1회 발생하는 이벤트입니다. /// + /// + /// 핸들러 시그니처: (TreeListItemData dragged) + /// public Action? OnDragStarted; /// - /// 드래그 진행 중 발생하는 이벤트입니다. + /// 드래그 진행 중 마우스가 특정 아이템 위에 있을 때 반복적으로 발생하는 이벤트입니다. + /// 빈 공간 위라면 targetItem은 null이 될 수 있습니다. /// + /// + /// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? targetItem) + /// 이벤트 발생 빈도가 높으므로, 처리 로직은 가볍게 유지하세요. + /// public Action? OnDragEntered; /// - /// 드래그 종료 시 발생하는 이벤트입니다. + /// 드래그가 종료될 때 발생하는 이벤트입니다(드롭 성공/실패/취소 포함). /// + /// + /// 핸들러 시그니처: (TreeListItemData dragged)
+ /// 드롭 성공 시에는 OnDropped 이후에 호출됩니다. + ///
public Action? OnDragEnded; /// - /// 드롭 완료 시 발생하는 이벤트입니다. + /// 유효성 검사를 통과한 드롭이 확정될 때 발생하는 이벤트입니다. /// + /// + /// 핸들러 시그니처: (TreeListItemData dragged, TreeListItemData? target)
+ /// 이 이벤트에서 실제 데이터 구조 변경(이동/부모 변경/정렬)을 수행하세요. + ///
public Action? OnDropped; /// - /// 드래그를 시작합니다. + /// 드래그를 시작합니다. 이미 드래그 중이면 무시됩니다. /// - /// 드래그할 아이템 + /// 드래그할 아이템. public void StartDrag(TreeListItemData draggedItem) { if (IsDragging) @@ -69,9 +89,10 @@ namespace UVC.UI.List.Tree } /// - /// 드래그 중에 마우스가 다른 아이템 위에 있을 때 호출됩니다. + /// 드래그 중 마우스가 다른 아이템(또는 빈 영역) 위에 있을 때 호출됩니다. + /// 상태를 변경하지 않고, 단순히 현재 hover 대상을 이벤트로 통지합니다. /// - /// 현재 마우스 위에 있는 아이템 + /// 현재 마우스 아래의 아이템. 빈 공간이면 null. public void OnDragOver(TreeListItemData? targetItem) { if (!IsDragging || DraggedItem == null) @@ -83,7 +104,7 @@ namespace UVC.UI.List.Tree } /// - /// 드래그를 종료합니다. + /// 드래그를 종료합니다. 드래그 중이 아니면 아무 동작도 하지 않습니다. /// public void EndDrag() { @@ -99,11 +120,19 @@ namespace UVC.UI.List.Tree } /// - /// 드래그된 아이템을 대상 아이템에 드롭합니다. + /// 드래그된 아이템을 대상 아이템에 드롭 시도합니다. + /// 유효성 검사(자기 자신/조상에게 드롭 금지)를 통과한 경우에만 OnDropped를 발생시킵니다. /// - /// 드롭 대상 아이템 (null이면 루트 레벨) - /// 대상 부모 내에서의 삽입 위치 (-1이면 끝에 추가) - /// 드롭 성공 여부 + /// 드롭 대상 아이템. 루트 레벨로 드롭하려면 null. + /// + /// 대상 부모 내 삽입 위치. -1이면 끝에 추가 의도. + /// 현재 구현에서는 이 값이 내부에서 사용되지 않으며, 필요 시 이벤트 모델 확장이 필요합니다. + /// + /// 드롭을 성공적으로 수락하여 이벤트를 발생시켰으면 true, 그 외는 false. + /// + /// 성공 시 순서: OnDropped(once) → EndDrag → OnDragEnded.
+ /// 실패/무효 시: EndDrag → OnDragEnded. + ///
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 /// /// 첫 번째 아이템이 두 번째 아이템의 조상인지 확인합니다. - /// 순환 참조를 방지하기 위해 사용됩니다. /// - /// 조상일 가능성이 있는 아이템 - /// 후손일 가능성이 있는 아이템 - /// 조상-후손 관계이면 true + /// 조상일 가능성이 있는 아이템. + /// 후손일 가능성이 있는 아이템. + /// 조상-후손 관계이면 true, 아니면 false. + /// + /// 상향 탐색으로 O(h) 시간 복잡도입니다(h: 트리 높이). + /// public static bool IsAncestorOf(TreeListItemData potentialAncestor, TreeListItemData potentialDescendant) { var current = potentialDescendant.Parent; @@ -157,7 +189,7 @@ namespace UVC.UI.List.Tree /// - /// 모든 드래그 & 드롭 상태를 리셋합니다. + /// 드래그 & 드롭 상태를 초기화합니다. /// public void Reset() { diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs index e8a6bb02..aabe3745 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItem.cs @@ -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": 아이템 클릭 버튼 ///
public class TreeListItem : MonoBehaviour { #region UI 컴포넌트 참조 (UI Component References) /// - /// 이 아이템을 관리하는 부모 TreeList입니다. - /// - /// 용도: - /// - 선택 상태 변경 시 TreeList에 알림 - /// - 새 자식 아이템 생성 시 프리팹 가져오기 - /// - 키보드 입력(Ctrl, Shift) 상태 전달 - /// - /// [SerializeField]란? - /// Inspector에서 이 값을 직접 할당할 수 있게 해줍니다. - /// (private이지만 Unity가 특별히 접근 가능) + /// 이 아이템을 관리하는 상위 컨트롤입니다. (선택/평탄화/프리팹 액세스 등에 사용) /// [SerializeField] protected TreeList treeList; /// - /// 이 아이템의 이름을 표시하는 텍스트 UI입니다. - /// - /// TMPro.TextMeshProUGUI란? - /// TextMeshPro는 Unity의 고급 텍스트 시스템입니다. - /// 일반 Text보다 더 예쁘고 빠릅니다. - /// - /// 사용: - /// valueText.text = "새로운 이름"; // 화면에 표시되는 텍스트 변경 + /// 아이템 이름 텍스트(UI). /// [SerializeField] protected TMPro.TextMeshProUGUI valueText; /// - /// 트리의 자식을 펼침/접힘하는 화살표 버튼입니다. - /// - /// 특징: - /// - 자식이 없으면 숨겨집니다 - /// - 자식이 펼쳐지면 ▼ 모양 - /// - 자식이 접혀있으면 ▶ 모양 - /// - 클릭 시 ToggleChild() 메서드 호출 - /// - /// 회전 애니메이션: - /// DORotate()를 사용해 부드럽게 회전합니다. + /// 자식 펼침/접힘 버튼. onClick에서 ToggleChild를 호출합니다. /// [SerializeField] protected Button childExpand; /// - /// 모든 자식 아이템들을 담는 컨테이너 GameObject입니다. - /// - /// 역할: - /// - 자식 아이템들을 묶음으로 보관 - /// - 펼침/접힘 시 이 컨테이너 전체를 표시/숨김 - /// - SetActive(true/false)로 온/오프 제어 - /// - /// GameObject란? - /// Unity의 모든 객체(씬의 모든 것)의 기본 단위입니다. - /// 게임 오브젝트는 여러 컴포넌트를 가질 수 있습니다. + /// 자식 아이템들을 포함하는 컨테이너 GameObject. /// [SerializeField] protected GameObject childContainer; /// - /// 자식 아이템들이 실제로 배치되는 부모 Transform입니다. - /// - /// RectTransform란? - /// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다. - /// Canvas 아래의 모든 UI 요소가 RectTransform을 가집니다. - /// - /// 용도: - /// - childRoot.childCount: 현재 몇 개의 자식이 있는지 확인 - /// - new TreeListItem을 Instantiate할 때 부모로 지정 - /// - /// 구조 예: - /// childRoot (이것) - /// ├─ 자식1 (TreeListItem) - /// ├─ 자식2 (TreeListItem) - /// └─ 자식3 (TreeListItem) + /// 자식 TreeListItem이 배치될 부모 RectTransform. /// [SerializeField] protected RectTransform childRoot; /// - /// 아이템이 선택되었을 때 배경으로 표시되는 이미지입니다. - /// - /// 예: 파일 탐색기에서 파일을 선택했을 때 파일 이름 뒤의 파란 배경 - /// - /// 동작: - /// - IsSelected가 true면 selectedBg.gameObject.SetActive(true) - /// - IsSelected가 false면 selectedBg.gameObject.SetActive(false) - /// - 모든 레벨의 선택된 아이템의 selectedBg 왼쪽이 정렬됨 + /// 선택 상태를 표시하는 배경 이미지. /// [SerializeField] protected Image selectedBg; /// - /// 아이템 전체를 클릭 가능하게 하는 버튼 컴포넌트입니다. - /// - /// Button이란? - /// 사용자가 클릭하면 onClick 이벤트를 발생시킵니다. - /// 이 경우 OnItemClicked() 메서드가 호출됩니다. - /// - /// onClick.AddListener는? - /// 버튼이 클릭되면 리스너 함수를 호출하라는 뜻입니다. - /// 예: itemButton.onClick.AddListener(OnItemClicked); - /// → itemButton을 클릭하면 OnItemClicked() 실행 + /// 아이템 전체 클릭 버튼. onClick에서 OnItemClicked를 호출합니다. /// [SerializeField] protected Button itemButton; @@ -146,62 +77,17 @@ namespace UVC.UI.List.Tree #region 데이터 필드 (Data Fields) /// - /// 이 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는 이 데이터의 변경 이벤트에 반응합니다. /// protected TreeListItemData? data; /// - /// data 속성을 읽기 전용으로 공개합니다. - /// - /// 문법: - /// public TreeListItemData? Data => data; - /// = getter 전용 프로퍼티 - /// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음 - /// - /// 사용 예: - /// if (treeItem.Data != null) - /// { - /// Debug.Log(treeItem.Data.Name); // 읽기만 가능 - /// } + /// 표시 데이터의 읽기 전용 접근자. /// public TreeListItemData? Data => data; /// - /// 펼침/접힘 애니메이션이 진행 중인지를 나타내는 플래그입니다. - /// - /// 용도: 애니메이션이 끝나기 전에 다시 클릭하는 것을 방지합니다. - /// - /// 시나리오: - /// 1. 사용자가 화살표 버튼 클릭 - /// 2. isAnimating = true (애니메이션 시작) - /// 3. 0.3초 동안 화살표가 회전 - /// 4. 애니메이션 완료 → isAnimating = false - /// - /// 중간에 다시 클릭해도: - /// if (isAnimating) return; ← 여기서 무시됨 - /// - /// 이렇게 하는 이유: 애니메이션이 겹치면 버그가 발생할 수 있음 + /// 펼침/접힘 애니메이션 진행 여부(중복 입력 방지). /// protected bool isAnimating = false; @@ -210,27 +96,20 @@ namespace UVC.UI.List.Tree #region 초기화 (Initialization) /// - /// 이 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: 드래그 & 드롭 매니저 /// 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) /// - /// 데이터가 변경되었을 때 호출되는 메서드입니다. + /// 데이터 변경에 반응하여 UI를 갱신합니다. /// - /// 호출되는 경우: - /// 1. data.Name이 변경됨 - /// 2. data.Children이 추가/제거됨 - /// 3. data.IsExpanded가 변경됨 - /// - /// 역할: - /// - 화면에 표시된 내용과 데이터를 동기화 - /// - 필요한 UI 컴포넌트 업데이트 - /// - /// 데이터 바인딩이란? - /// 데이터가 변경되면 UI가 자동으로 업데이트되는 패턴입니다. - /// 이렇게 하면 데이터와 UI를 동기화 유지하기 쉽습니다. + /// 매개변수: + /// - changedType: 변경 종류(이름/확장/자식 추가 등) + /// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효) + /// - index: 삽입/이동 시 기준 인덱스(해당되는 경우) /// 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(); - 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) /// - /// 아이템의 선택 상태가 변경되었을 때 호출됩니다. - /// - /// 호출 시기: - /// - data.IsSelected = true/false 일 때 - /// - /// 동작: - /// - UpdateSelectionUI() 호출해서 화면 업데이트 + /// 선택 상태 변경에 반응하여 UI를 갱신합니다. /// /// 매개변수: - /// - changedData: 변경된 데이터 (이 경우 항상 this.data와 같음) - /// - isSelected: 새로운 선택 상태 (true = 선택됨, false = 해제됨) + /// - changedData: 변경된 데이터(자기 자신) + /// - isSelected: 선택 여부 /// private void OnSelectionChanged(TreeListItemData changedData, bool isSelected) { @@ -439,18 +326,7 @@ namespace UVC.UI.List.Tree } /// - /// 선택 상태에 따라 UI를 업데이트합니다. - /// - /// 동작: - /// - data.IsSelected = true → selectedBg 표시 (배경 보이기) - /// - data.IsSelected = false → selectedBg 숨김 (배경 숨기기) - /// - /// 예시: - /// data.IsSelected = true - /// ↓ - /// selectedBg.gameObject.SetActive(true) - /// ↓ - /// 화면에 파란 배경이 나타남 + /// 선택 상태에 따라 배경 표시/숨김을 갱신합니다. /// private void UpdateSelectionUI() { @@ -464,25 +340,7 @@ namespace UVC.UI.List.Tree #region 위치 정렬 (Position Alignment) /// - /// 모든 선택 배경(selectedBg)의 왼쪽 위치를 정렬합니다. - /// - /// 목표: - /// 📌 폴더1 - /// 📌├─ 파일1 - /// 📌├─ 파일2 - /// 📌└─ 폴더2 - /// 📌├─ 파일3 - /// - /// 모든 선택 배경의 왼쪽이 📌 위치에서 시작하도록 합니다. - /// - /// 이렇게 하는 이유: - /// - 깔끔한 UI 표현 - /// - 다중 레벨 트리에서 시각적 일관성 유지 - /// - /// 좌표 변환: - /// - 월드 좌표(World Coordinate): 게임 전체에서의 절대 위치 - /// - 로컬 좌표(Local Coordinate): 부모 기준 상대 위치 - /// - TransformPoint: 로컬 좌표를 월드 좌표로 변환 + /// 모든 레벨에서 선택 배경의 왼쪽 정렬을 루트와 맞춥니다. /// private void AlignSelectedBgToRoot() { @@ -523,25 +381,10 @@ namespace UVC.UI.List.Tree #region 계층 구조 탐색 (Hierarchy Navigation) /// - /// 최상위 부모(루트)의 TreeListItem을 찾습니다. + /// 현재 아이템의 루트 TreeListItem을 반환합니다. /// - /// 트리 구조: - /// TreeListItem1 (루트) - /// ├─ TreeListItem2 (부모) - /// │ └─ TreeListItem3 (자식 ← 이 메서드를 호출하면) - /// └─ TreeListItem4 - /// - /// 반환값: TreeListItem1 - /// - /// 동작: - /// 1. 현재 객체의 부모 확인 - /// 2. 부모가 TreeListItem을 가지는지 확인 - /// 3. 가지면 그 부모의 GetRootTreeListItem() 재귀 호출 - /// 4. 루트에 도달할 때까지 반복 - /// - /// 재귀(Recursion)란? - /// 함수가 자기 자신을 호출하는 것입니다. - /// 계층 구조를 탐색하는 데 효과적입니다. + /// 반환: + /// - 루트 TreeListItem, 없으면 null /// private TreeListItem? GetRootTreeListItem() { @@ -580,35 +423,18 @@ namespace UVC.UI.List.Tree #region 입력 처리 (Input Handling) /// - /// 아이템을 클릭했을 때 호출됩니다. + /// 아이템 클릭 시 호출됩니다. /// - /// 호출 시점: - /// - itemButton.onClick 이벤트 발생 시 - /// = 사용자가 이 아이템을 마우스로 클릭했을 때 - /// - /// 처리 순서: - /// 1. 데이터의 OnClickAction 실행 (있으면) - /// 2. Ctrl, Shift 키 상태 감지 - /// 3. TreeList에 클릭 정보 전달 (다중 선택 로직) - /// - /// 키 입력: - /// - Ctrl 클릭: 현재 선택을 유지하면서 이 아이템 토글 (다중 선택) - /// - Shift 클릭: 마지막 선택부터 이 아이템까지 범위 선택 - /// - 일반 클릭: 이 아이템만 선택 - /// - /// 파일 탐색기와 동일한 동작: - /// Windows 탐색기를 생각하면 쉽습니다. - /// - 클릭: 한 파일만 선택 - /// - Ctrl+클릭: 여러 파일 선택 (기존 선택 유지) - /// - Shift+클릭: 범위 선택 + /// 동작: + /// - 등록된 OnClickAction 실행(있으면) + /// - Ctrl/Shift 상태를 읽어 TreeList에 전달 + /// (일반/토글/범위 선택) /// 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) /// - /// 자식 아이템의 펼침/접힘을 토글합니다. (펼쳐있으면 접고, 접혀있으면 펼침) - /// - /// 호출 시점: - /// - childExpand 버튼을 클릭했을 때 - /// - /// 동작: - /// 1. 애니메이션이 진행 중이면 무시 (중복 클릭 방지) - /// 2. data.IsExpanded 토글 (true ↔ false) - /// 3. childContainer 활성화/비활성화 - /// 4. 0.3초에 걸쳐 화살표 회전 애니메이션 실행 - /// - /// UI 피드백: - /// 애니메이션이 있으면 사용자가 반응을 확인할 수 있습니다. - /// 즉시 완료되는 것보다 더 자연스럽고 좋은 경험입니다. + /// 자식의 펼침/접힘을 토글합니다. + /// (중복 입력은 애니메이션 종료까지 무시) /// public void ToggleChild() { @@ -659,26 +473,9 @@ namespace UVC.UI.List.Tree } /// - /// 펼침/접힘 화살표의 회전 애니메이션을 실행합니다. - /// - /// 파라미터: - /// - duration: 애니메이션 지속 시간 (초) - /// 기본값 0.0 = 즉시 완료 (애니메이션 없음) - /// 0.3 = 0.3초에 걸쳐 회전 - /// - /// 동작: - /// - 펼쳐짐 (IsExpanded=true): 화살표를 0도로 회전 (▼ 모양) - /// - 접혀짐 (IsExpanded=false): 화살표를 90도로 회전 (▶ 모양) - /// - 애니메이션 완료 후 isAnimating 플래그 리셋 - /// - /// DORotate란? (Tweening 라이브러리) - /// 부드럽게 회전하는 애니메이션을 쉽게 만들어줍니다. - /// duration 시간에 걸쳐 지정된 각도까지 회전합니다. - /// - /// OnComplete란? - /// 애니메이션이 완료되면 호출되는 콜백입니다. - /// 람다 식(=>)으로 익명 함수를 정의합니다. + /// 펼침/접힘 화살표 회전과 컨테이너 표시를 갱신합니다. /// + /// 회전 애니메이션 시간(초). 0이면 즉시 적용. 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) /// - /// 새로운 자식 아이템을 추가합니다. - /// - /// 호출 시점: - /// - 프로그램이 런타임에 새 자식을 추가할 때 - /// - 예: 폴더에 파일을 추가했을 때 - /// - /// 동작: - /// 1. 새 자식 UI 생성 (CreateItem) - /// 2. 자식 컨테이너 활성화 - /// 3. 펼침 애니메이션 (자동으로 펼침) - /// - /// 매개변수: - /// - data: 추가할 자식 데이터 - /// - /// 사용 예: - /// treeItem.AddChild(newChildData); - /// // → 새 자식이 자동으로 UI에 추가되고 펼쳐짐 + /// 새로운 자식 아이템을 추가하고 UI를 갱신합니다. /// + /// 추가할 자식 데이터 public void AddChild(TreeListItemData data) { // 1️. 새 자식 UI 생성 @@ -735,32 +518,10 @@ namespace UVC.UI.List.Tree } /// - /// 자식 데이터를 받아 UI TreeListItem으로 생성합니다. - /// - /// 호출 시점: - /// - Init()에서 기존 자식들을 UI로 생성할 때 - /// - OnDataChanged()에서 추가된 자식을 UI로 생성할 때 - /// - AddChild()에서 새 자식을 추가할 때 - /// - /// 동작: - /// 1. treeList.ItemPrefab를 childRoot 아래에 인스턴스화 - /// 2. 생성된 item의 Init() 메서드 호출 - /// 3. 생성된 item 반환 - /// - /// Instantiate란? - /// 프리팹(템플릿)을 복제해서 새로운 객체를 만드는 함수입니다. - /// Instantiate(프리팹, 부모, 복제) - /// - /// 매개변수: - /// - data: 생성할 아이템의 데이터 - /// - /// 반환값: - /// - 생성된 TreeListItem 컴포넌트 - /// - /// 재귀 구조: - /// CreateItem은 계속 자식을 생성하므로 - /// 깊이 있는 트리 구조를 만들 수 있습니다. + /// 자식 데이터를 받아 UI TreeListItem을 생성합니다. /// + /// 생성할 아이템 데이터 + /// 생성된 TreeListItem protected TreeListItem CreateItem(TreeListItemData data) { // 1️. 프리팹을 복제해서 새로운 TreeListItem 생성 @@ -784,27 +545,9 @@ namespace UVC.UI.List.Tree #region 제거 (Deletion) /// - /// 이 TreeListItem과 관련된 모든 리소스를 정리하고 삭제합니다. - /// - /// 호출 시점: - /// - 트리에서 아이템을 제거하고 싶을 때 - /// - 프로그램이 명시적으로 아이템을 삭제할 때 - /// - /// 정리 작업: - /// 1. 데이터 변경 이벤트 구독 해제 - /// 2. 버튼 클릭 이벤트 구독 해제 - /// 3. GameObject 삭제 - /// - /// 왜 구독을 해제하나? - /// - 이벤트를 구독하는 것은 메모리 연결을 만듭니다. - /// - 해제하지 않으면 메모리 누수가 발생할 수 있습니다. - /// - 따라서 삭제 전에 반드시 해제해야 합니다. - /// - /// 메모리 누수란? - /// 불필요한 메모리가 해제되지 않고 계속 사용되는 문제입니다. - /// 게임이 계속 실행되면서 메모리 사용량이 증가해서 - /// 결국 게임이 느려지거나 충돌할 수 있습니다. + /// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 삭제합니다. /// + /// 데이터도 삭제할지 여부 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 } /// - /// GameObject가 파괴될 때 자동으로 호출되는 Unity 메서드입니다. - /// - /// 호출 시점: - /// - Destroy(gameObject) 호출 후 실제 삭제되기 직전 - /// - 게임이 종료될 때 - /// - 씬이 언로드될 때 - /// - /// 용도: - /// - 정리 작업 (Clean-up) - /// - 리소스 해제 - /// - 이벤트 구독 해제 - /// - /// Delete()와의 차이: - /// - Delete(): 명시적으로 호출하는 메서드 - /// - OnDestroy(): Unity에서 자동으로 호출하는 메서드 - /// - 둘 다 같은 정리 작업을 합니다 (중복 방지) + /// Unity 파괴 시점에 정리합니다. (중복 정리 방지) /// 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; } diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs index 00de7869..48a9b300 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs @@ -6,106 +6,74 @@ using System.Collections.Generic; namespace UVC.UI.List.Tree { /// - /// 트리 구조 리스트에서 각 아이템이 갖는 데이터 클래스입니다. + /// 트리 리스트 아이템의 데이터 모델. /// - /// 트리 구조란? 폴더-파일처럼 상위(부모)와 하위(자식) 관계를 가진 계층 구조입니다. - /// 예: 📁 폴더 - /// ├─ 📄 파일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한 뒤 복제본을 추가합니다(파괴적). /// public class TreeListItemData: IDisposable { #region 이벤트 (Events) /// - /// 데이터가 변경되었을 때 발생하는 이벤트입니다. - /// - /// 용도: 이 데이터의 속성(Name, Option 등)이 변경되면 - /// UI에 자동으로 반영되도록 통지합니다. - /// - /// 사용 예: - /// treeItem.OnDataChanged += (data) => Debug.Log("데이터 변경됨!"); + /// 데이터/구조 변경 통지 이벤트. + /// 시그니처: (ChangedType type, TreeListItemData changed, int index) + /// - type: 변경 종류 + /// - changed: 변경 대상(보통 this 또는 특정 자식) + /// - index: 위치 관련 변경 시 사용(해당 없으면 -1) /// public Action? OnDataChanged; /// - /// 선택 상태가 변경되었을 때 발생하는 이벤트입니다. - /// - /// 용도: 사용자가 이 아이템을 클릭해서 선택 또는 선택 해제했을 때 - /// 다른 시스템(예: 옵션 창)에 알려줍니다. - /// - /// 매개변수: - /// - TreeListItemData: 변경된 아이템 자신 - /// - bool: true면 선택됨, false면 선택 해제됨 - /// - /// 사용 예: - /// treeItem.OnSelectionChanged += (data, isSelected) => { - /// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}"); - /// }; + /// 선택 상태 변경 통지 이벤트. + /// 시그니처: (TreeListItemData data, bool isSelected) /// public Action? OnSelectionChanged; + /// + /// 아이템 클릭 시 실행할 사용자 정의 동작. + /// (예: 속성 패널 열기, 포커스 이동 등. 확장/축소와는 별개) + /// + public Action? OnClickAction; + #endregion #region 내부 필드 (Private Fields) - /// - /// 아이템의 이름을 저장하는 비공개 필드입니다. - /// - /// '_' 접두사를 붙인 이유: - /// 실제 데이터는 여기 저장하고, public 프로퍼티(Name)를 통해 - /// 접근을 제어합니다. (캡슐화) - /// - /// 예: _name = "폴더", Name 프로퍼티로 접근 - /// + /// 아이템 이름. private string _name = string.Empty; - /// - /// 아이템의 추가 옵션 정보를 저장합니다. - /// - /// 용도: 읽기 전용, 숨김 속성 등의 추가 설정 정보 - /// 예: "readonly", "hidden", "locked" 등 - /// + /// 추가 옵션 문자열. private string _option = string.Empty; - /// - /// 이 아이템의 자식들이 펼쳐져 있는지 여부를 나타냅니다. - /// - /// true: 자식들이 표시됨 (▼ 펼침 상태) - /// false: 자식들이 숨겨짐 (▶ 접혀있는 상태) - /// - /// 자식이 없으면 이 값은 의미가 없습니다. - /// + /// 자식 펼침 여부. private bool _isExpanded = false; - /// - /// 현재 아이템이 사용자에게 선택되어 있는지를 나타냅니다. - /// - /// true: 선택됨 (보통 배경색이 다르게 표시) - /// false: 선택 안 됨 (기본 상태) - /// - /// 예: 파일 탐색기에서 파일을 클릭했을 때 그 파일의 _isSelected = true - /// + /// 선택 여부. private bool _isSelected = false; + /// 부모 private TreeListItemData? _parent; - /// - /// 이 아이템의 하위 아이템들을 모두 저장하는 리스트입니다. - /// - /// 트리 구조 예: - /// 부모 (이 객체) - /// ├─ 자식1 - /// ├─ 자식2 - /// └─ 자식3 - /// - /// _children 리스트에 [자식1, 자식2, 자식3]이 저장됩니다. - /// 자식이 없으면 빈 리스트입니다. - /// + /// 자식 리스트. private List _children = new List(); #endregion @@ -113,20 +81,8 @@ namespace UVC.UI.List.Tree #region 공개 프로퍼티 (Public Properties) /// - /// 아이템의 이름을 가져오거나 설정합니다. - /// - /// 동작: - /// - 가져올 때(get): _name의 값을 반환합니다. - /// - 설정할 때(set): - /// 1. 기존 값과 비교해서 정말 달라졌는지 확인 - /// 2. 다르면 새 값으로 변경 - /// 3. OnDataChanged 이벤트를 발생시켜 UI에 알림 - /// - /// 이렇게 하는 이유: 같은 값으로 변경되는 불필요한 갱신을 피합니다. - /// - /// 사용 예: - /// treeItem.Name = "새로운 이름"; // 자동으로 UI 업데이트 - /// string currentName = treeItem.Name; // 이름 읽기 + /// 아이템 이름. + /// 변경 시 OnDataChanged(Name) 발생. /// public string Name { @@ -142,13 +98,8 @@ namespace UVC.UI.List.Tree } /// - /// 아이템의 추가 옵션을 가져오거나 설정합니다. - /// - /// 동작: Name 프로퍼티와 동일하게 작동합니다. - /// 변경될 때마다 OnDataChanged 이벤트를 발생시킵니다. - /// - /// 사용 예: - /// treeItem.Option = "readonly"; // 읽기 전용으로 설정 + /// 옵션 문자열. + /// 변경 시 OnDataChanged(Option) 발생. /// public string Option { @@ -164,17 +115,8 @@ namespace UVC.UI.List.Tree } /// - /// 이 아이템의 자식들이 펼쳐져 있는지 여부를 가져오거나 설정합니다. - /// - /// internal 접근제한자 이유: - /// 이것은 UI 시스템에서만 관리해야 하므로 외부에서 직접 접근할 수 없습니다. - /// (같은 어셈블리 내부에서만 접근 가능) - /// - /// true: 자식들이 표시됨 (트리 펼침) - /// false: 자식들이 숨겨짐 (트리 접힘) - /// - /// 사용 예: (UI 시스템에서만) - /// treeItem.IsExpanded = true; // 자식들을 표시 + /// 펼침 상태(같은 어셈블리 내 전용). + /// 변경 시 OnDataChanged(Expanded) 발생. /// internal bool IsExpanded { @@ -190,16 +132,8 @@ namespace UVC.UI.List.Tree } /// - /// 이 아이템이 선택되어 있는지 여부를 가져오거나 설정합니다. - /// - /// 중요한 차이점: 다른 프로퍼티는 OnDataChanged를 호출하지만, - /// 이것은 OnSelectionChanged를 호출합니다. - /// 왜? 선택 상태는 UI 갱신이 아니라 - /// 선택 이벤트 처리가 필요하기 때문입니다. - /// - /// 사용 예: - /// treeItem.IsSelected = true; // 아이템 선택 - /// if (treeItem.IsSelected) { ... } // 선택 여부 확인 + /// 선택 상태. + /// 변경 시 OnSelectionChanged 발생. /// public bool IsSelected { @@ -216,21 +150,9 @@ namespace UVC.UI.List.Tree } } - /// - /// 사용자가 확장/축소 버튼을 클릭했을 때 호출될 함수입니다. - /// - /// 용도: 트리의 화살표(▼/▶) 버튼을 클릭했을 때 - /// IsExpanded 상태를 변경하는 로직을 실행합니다. - /// - /// 누가 등록하나? UI 시스템 (TreeListItem 클래스) - /// - /// 사용 예: - /// treeItem.OnClickAction = (data) => { - /// data.IsExpanded = !data.IsExpanded; // 펼침/접힘 토글 - /// }; - /// - public Action? OnClickAction; + + /// 부모 데이터(내부 전용). internal TreeListItemData? Parent { get => _parent; @@ -238,22 +160,8 @@ namespace UVC.UI.List.Tree } /// - /// 이 아이템의 모든 자식 아이템들을 가져오거나 설정합니다. - /// - /// internal 접근제한자 이유: - /// 자식 리스트는 AddChild, RemoveChild, ClearChildren 메서드로만 - /// 수정되어야 데이터 일관성이 보장됩니다. - /// - /// 동작: - /// - 가져올 때(get): _children 리스트 반환 - /// - 설정할 때(set): - /// 1. null이면 빈 리스트로 설정 (null 방지) - /// 2. OnDataChanged 이벤트 발생 - /// - /// ?? 연산자 설명: - /// childrenItemData ?? new List<>() - /// = childrenItemData가 null이면 빈 리스트 사용 - /// null이 아니면 childrenItemData 사용 + /// 자식 컬렉션(내부 전용). + /// set 시 null을 빈 리스트로 대체하고 OnDataChanged(ResetChildren) 발생. /// internal List Children { @@ -270,47 +178,23 @@ namespace UVC.UI.List.Tree #region 생성자 (Constructors) /// - /// 빈 TreeListItemData를 생성합니다. - /// - /// 초기값: - /// - Name: 빈 문자열 - /// - Option: 빈 문자열 - /// - IsExpanded: false (접혀있음) - /// - Children: 빈 리스트 (자식 없음) - /// - /// 사용 예: - /// var item = new TreeListItemData(); - /// item.Name = "새 폴더"; + /// 기본 생성자. + /// 초기값: Name="", Option="", IsExpanded=false, IsSelected=false, Children=[] /// public TreeListItemData() { _name = string.Empty; _option = string.Empty; _isExpanded = false; + _isSelected = false; _children = new List(); } /// - /// 이름과 선택적으로 자식 목록을 지정하여 TreeListItemData를 생성합니다. - /// - /// 매개변수: - /// - generalName: 아이템의 이름 (필수) - /// - childrenItemData: 초기 자식 목록 (선택사항, null 가능) - /// - /// 초기값: - /// - Name: generalName - /// - Option: 빈 문자열 - /// - IsExpanded: false - /// - Children: childrenItemData (null이면 빈 리스트) - /// - /// 사용 예: - /// // 간단한 아이템 생성 - /// var item1 = new TreeListItemData("폴더"); - /// - /// // 자식을 포함해서 생성 - /// var children = new List { item1 }; - /// var parent = new TreeListItemData("부모 폴더", children); + /// 이름과 초기 자식으로 생성합니다. /// + /// 아이템 이름(필수). + /// 초기 자식 목록(null 허용). public TreeListItemData(string generalName, List? childrenItemData = null) { _name = generalName; @@ -324,29 +208,21 @@ namespace UVC.UI.List.Tree #region 자식 관리 메서드 (Child Management Methods) /// - /// 이 아이템에 자식 아이템을 추가합니다. - /// - /// 동작: - /// 1. 자식을 _children 리스트에 추가 - /// 2. OnDataChanged 이벤트 발생 (UI 트리 구조 갱신) - /// - /// 사용 예: - /// parent.AddChild(child); // 부모에 자식 추가 - /// - /// 트리 구조 변화: - /// 변경 전: 변경 후: - /// 부모 부모 - /// ├─ 자식1 ├─ 자식1 - /// └─ 자식2 ├─ 자식2 - /// └─ 새자식 + /// 자식을 끝에 추가합니다. /// + /// 추가할 자식. public void AddChild(TreeListItemData child) { - child._parent = this; + child.Parent = this; _children.Add(child); NotifyDataChanged(ChangedType.AddChild, child); // UI에 트리 구조 변경 알림 } + /// + /// 자식을 지정 인덱스에 삽입합니다. + /// + /// 삽입할 자식. + /// 삽입 인덱스(0 기반). public void AddChildAt(TreeListItemData child, int index) { child._parent = this; @@ -355,64 +231,51 @@ namespace UVC.UI.List.Tree } /// - /// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하지만 아이템 자체는 삭제하지 않습니다. - /// - /// 동작: - /// 1. 자식을 _children 리스트에서 제거 - /// 2. OnDataChanged 이벤트 발생 - /// - /// 주의: 같은 이름의 첫 번째 자식만 제거됩니다. - /// (TreeListItemData의 == 연산자가 Name으로 비교하기 때문) - /// - /// 사용 예: - /// parent.RemoveChild(child); // 부모에서 자식 제거 - /// - /// 트리 구조 변화: - /// 변경 전: 변경 후: - /// 부모 부모 - /// ├─ 자식1 ├─ 자식1 - /// ├─ 자식2 - /// └─ 자식3 └─ 자식3 - /// (자식2 제거) + /// 전달된 자식을 복제하여 추가합니다. + /// 주의: 원본 child는 Dispose됩니다(파괴적). /// + /// 복제 및 대체할 원본 자식. + public void AddCloneChild(TreeListItemData child) + { + NotifyDataChanged(ChangedType.AddCloneChild, child); // UI에 트리 구조 변경 알림 + } + + /// + /// 전달된 자식을 복제하여 지정 인덱스에 삽입합니다. + /// 주의: 원본 child는 Dispose됩니다(파괴적). + /// + /// 복제 및 대체할 원본 자식. + /// 삽입 인덱스(0 기반). + public void AddCloneAtChild(TreeListItemData child, int index) + { + NotifyDataChanged(ChangedType.AddCloneAtChild, child, index); // UI에 트리 구조 변경 알림 + } + + /// + /// 지정 자식을 지정 인덱스 위치에 삽입합니다. + /// 주의: 기존 위치에서 제거하지 않으므로 "이동"이 아닌 "삽입" 동작입니다. + /// (호출 측에서 기존 위치 정리는 별도로 해야 합니다) + /// + /// 삽입할 기존 자식 참조. + /// 삽입 인덱스(0 기반). + 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에 트리 구조 변경 알림 + } } /// - /// 이 아이템에서 지정된 자식 아이템을 자식 목록에서 제거하고 아이템 자체도 삭제합니다. - /// - /// - public void DeleteChild(TreeListItemData child) - { - RemoveChild(child); - child.Dispose(); - } - - /// - /// 이 아이템의 모든 자식을 한 번에 제거합니다. - /// - /// 동작: - /// 1. _children 리스트를 완전히 비움 - /// 2. OnDataChanged 이벤트 발생 - /// - /// 주의: 자식들이 메모리에서 삭제되는 것은 아니고, - /// 이 아이템과의 연결만 끊어집니다. - /// (C#의 가비지 컬렉션이 필요 없으면 나중에 정리) - /// - /// 사용 예: - /// parent.ClearChildren(); // 모든 자식 제거 - /// - /// 트리 구조 변화: - /// 변경 전: 변경 후: - /// 부모 부모 - /// ├─ 자식1 (모든 자식 제거됨) - /// ├─ 자식2 - /// └─ 자식3 + /// 모든 자식을 제거하고 정리합니다. + /// 각 자식에 대해 Parent=null 및 Dispose()를 호출한 뒤 리스트를 비웁니다. /// public void ClearChildren() { @@ -430,19 +293,11 @@ namespace UVC.UI.List.Tree #region 내부 메서드 (Internal Methods) /// - /// 데이터가 변경되었음을 UI 시스템에 알립니다. - /// - /// 동작: OnDataChanged 이벤트를 발생시킵니다. - /// 이를 통해 UI는 자동으로 이 아이템의 정보를 갱신합니다. - /// - /// 호출되는 시점: - /// - Name이나 Option이 변경될 때 - /// - 자식이 추가/제거될 때 - /// - IsExpanded 상태가 변경될 때 - /// - /// 왜 protected인가? - /// 이 클래스를 상속받은 자식 클래스에서도 호출할 수 있도록 하기 위함입니다. + /// 변경을 구독자에게 알립니다. /// + /// 변경 종류. + /// 변경 대상(null이면 this). + /// 위치 관련 인덱스(없으면 -1). internal void NotifyDataChanged(ChangedType changedType, TreeListItemData? target = null, int index = -1) { // OnDataChanged가 등록되어 있으면 실행 @@ -455,28 +310,7 @@ namespace UVC.UI.List.Tree #region 비교 연산자 (Comparison Operators) /// - /// 두 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) 기반 동등 비교. /// public static bool operator ==(TreeListItemData? left, TreeListItemData? right) { @@ -497,12 +331,7 @@ namespace UVC.UI.List.Tree } /// - /// 두 TreeListItemData 객체가 다른지 비교합니다. (!= 연산자) - /// - /// 동작: == 연산자의 결과를 반대(!)로 반환합니다. - /// - /// 사용 예: - /// if (item1 != item2) { ... } // 다른 아이템이면 실행 + /// 이름 기반 비동등 비교. /// public static bool operator !=(TreeListItemData? left, TreeListItemData? right) { @@ -514,23 +343,7 @@ namespace UVC.UI.List.Tree #region 객체 메서드 (Object Methods) /// - /// 이 객체의 고유한 해시 코드를 반환합니다. - /// - /// 용도: 이 객체를 Dictionary나 HashSet 같은 컬렉션에 저장할 때 사용합니다. - /// - /// 해시 코드란? 객체를 빠르게 비교/검색하기 위한 고유 숫자입니다. - /// 같은 내용이면 같은 해시 코드를 반환해야 합니다. - /// - /// 우리의 기준: - /// Name의 해시 코드 = 이 객체의 해시 코드 - /// 왜? == 연산자에서 Name으로 비교하기 때문입니다. - /// - /// 사용 예: - /// int hash = item.GetHashCode(); - /// - /// // Dictionary에 저장 - /// Dictionary dict = new(); - /// dict[item] = "값"; // 내부적으로 GetHashCode() 사용 + /// 이름 기반 해시 코드 반환. /// public override int GetHashCode() { @@ -538,29 +351,9 @@ namespace UVC.UI.List.Tree } /// - /// 이 객체가 다른 객체와 같은지 비교합니다. (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, 해시 코드도 같음 - /// - 이는 일관성 있게 설계되어 있습니다. + /// 이름 기반 동등성 비교(== 사용). /// + /// 비교 대상. public override bool Equals(object? obj) { // obj가 TreeListItemData 타입인지 확인 @@ -574,24 +367,49 @@ namespace UVC.UI.List.Tree return false; } + /// + /// 이벤트/참조/자식 컬렉션을 정리합니다. + /// - Parent=null, 이벤트 핸들러 해제, Children.Clear() + /// - 하위 항목을 재귀적으로 Dispose하지는 않습니다 + /// (재귀 정리는 ClearChildren에서 수행). + /// 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(); } } + /// + /// 깊은 복제본을 생성합니다(자식까지 재귀 복제). + /// + /// 복제된 새 인스턴스. + 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 } + /// + /// 데이터 변경 종류 열거형. + /// public enum ChangedType { Name, @@ -599,7 +417,10 @@ namespace UVC.UI.List.Tree Expanded, ResetChildren, AddChild, + RemoveChild, AddAtChild, - RemoveChild + AddCloneChild, + AddCloneAtChild, + SwapChild, } } \ No newline at end of file diff --git a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs index 292a77d2..8424ce39 100644 --- a/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs +++ b/Assets/Scripts/UVC/UI/List/Tree/TreeListItemDragHandler.cs @@ -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(얇은 선 또는 블록)로 삽입 위치/자식 투입 위치를 표시 /// public class TreeListItemDragHandler : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler { - /// - /// 부모 TreeListItem입니다. - /// + /// 부모 TreeListItem. private TreeListItem? treeListItem; - /// - /// 부모 TreeList입니다. - /// + /// 부모 TreeList. private TreeList? treeList; - /// - /// 드래그 & 드롭 매니저입니다. - /// + /// 드래그 & 드롭 매니저. private TreeListDragDropManager? dragDropManager; private RectTransform? rectTransform; - /// - /// 드래그 중 시각 피드백을 위한 CanvasGroup입니다. - /// + /// 드래그 시각 피드백용 CanvasGroup. private CanvasGroup? canvasGroup; - /// - /// 드래그 시작 시 원본 알파값입니다. - /// + /// 드래그 시작 전 원본 알파값. private float originalAlpha = 1f; private Vector2 dragOffset = Vector2.zero; - /// - /// 드래그 중 적용할 알파값입니다. - /// + /// 드래그 중 적용할 알파값. [SerializeField] private float dragAlpha = 0.5f; - /// - /// 드래그 활성화 여부입니다. - /// + /// 드래그 활성화 여부. [SerializeField] private bool enableDragDrop = true; - /// - /// 드롭 위치 표시 막대 프리팹입니다. - /// + /// 드롭 위치 표시 라인/블록. private Image? dropIndicator; private RectTransform? dropIndicatorRect; - /// - /// 드롭 표시기의 부모 (Content 또는 EntryRoot) - /// + /// 드롭 표시기의 부모(RectTransform). private RectTransform? dropIndicatorParent; - /// - /// 드래그 시작 시 아이템의 원본 부모입니다. - /// 드래그 후 원래 위치로 복구할 때 사용합니다. - /// + /// 드래그 시작 시의 원본 부모/순서. private Transform? originalParent; private int originalSiblingIndex; @@ -95,6 +78,12 @@ namespace UVC.UI.List.Tree originalAlpha = canvasGroup.alpha; } + /// + /// 외부에서 드래그 매니저 및 상위 컨텍스트를 주입합니다. + /// + /// 이 핸들러가 속한 아이템. + /// 아이템 컨테이너(TreeList). + /// 드래그 & 드롭 매니저. public void SetDragDropManager(TreeListItem item, TreeList list, TreeListDragDropManager manager) { treeListItem = item; @@ -105,9 +94,10 @@ namespace UVC.UI.List.Tree } /// - /// 드래그가 시작될 때 호출됩니다. (IBeginDragHandler) - /// OnPointerDown 이후 마우스가 약간 움직이면 자동으로 호출됩니다. + /// 드래그가 시작될 때 호출됩니다. + /// OnPointerDown 이후 마우스가 임계치 이상 이동하면 자동 호출됩니다. /// + /// 포인터 이벤트 데이터. 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 } /// - /// 드래그 중에 마우스가 이동할 때 호출됩니다. (IDragHandler) + /// 드래그 중 프레임마다 호출됩니다. + /// 최초 1프레임에 드래그 상태 진입 및 표시기 생성, 이후 hover 대상/표시기 갱신을 수행합니다. /// + /// 포인터 이벤트 데이터. 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); } /// - /// 드래그가 종료될 때 호출됩니다. (IEndDragHandler) - /// 마우스 버튼을 놓으면 자동으로 호출됩니다. + /// 드래그가 종료될 때 호출됩니다(마우스 버튼 해제). + /// 상태/시각 피드백 복구 후 TryDrop을 수행합니다. /// + /// 포인터 이벤트 데이터. 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 /// /// 드래그 중 아이템이 마우스를 따라다니도록 위치를 업데이트합니다. - /// Y축만 이동 (X축은 고정) + /// Y축만 이동(X축 고정). /// + /// 포인터 이벤트 데이터. private void UpdateDragPosition(PointerEventData eventData) { if (rectTransform == null || treeList == null) @@ -281,7 +271,7 @@ namespace UVC.UI.List.Tree } /// - /// 드롭 위치 표시 막대를 생성합니다. + /// 드롭 위치 표시기(DropIndicator)를 생성하거나 재사용합니다. /// private void CreateDropIndicator() { @@ -340,9 +330,11 @@ namespace UVC.UI.List.Tree } /// - /// 드롭 위치 표시 막대를 업데이트합니다. - /// VerticalLayoutGroup 환경에서도 정확하게 위치를 계산합니다. + /// 드롭 위치 표시기를 갱신합니다. + /// 복잡도: 대상 RectTransform의 월드 코너를 부모 기준 로컬 좌표로 환산하여, + /// 위/아래/자식(블록) 위치를 정확히 계산합니다. /// + /// 현재 마우스 아래의 대상 아이템(null이면 숨김). private void UpdateDropIndicator(TreeListItem? targetItem) { if (dropIndicator == null || dropIndicatorRect == null || dropIndicatorParent == null) @@ -437,7 +429,7 @@ namespace UVC.UI.List.Tree } /// - /// 드롭 위치 표시 막대를 숨깁니다. + /// 드롭 위치 표시기를 숨깁니다. /// private void HideDropIndicator() { @@ -448,9 +440,9 @@ namespace UVC.UI.List.Tree } /// - /// treeListRootParent의 자식 중에서 "DropIndicator"라는 이름의 GameObject를 찾습니다. + /// treeListRootParent의 직접 자식 중 "DropIndicator" GameObject를 찾습니다. /// - /// 찾은 GameObject (없으면 null) + /// 찾은 GameObject. 없으면 null. private GameObject? FindDropIndicatorInRoot() { if (treeListRootParent == null) @@ -477,10 +469,10 @@ namespace UVC.UI.List.Tree } /// - /// 주어진 스크린 좌표에 있는 TreeListItem을 찾습니다. + /// 스크린 좌표에 위치한 TreeListItem을 찾습니다. /// - /// 스크린 좌표 - /// 찾은 TreeListItem (없으면 null) + /// 스크린 좌표. + /// 찾은 TreeListItem. 없으면 null. private TreeListItem? GetItemAtMousePosition(Vector2 screenPosition) { if (treeList == null) @@ -514,10 +506,11 @@ namespace UVC.UI.List.Tree /// - /// 드롭 성공 후 데이터를 동기화합니다. + /// 드롭 성공 후 데이터 구조를 갱신합니다. + /// 루트 드롭/형제 간 이동/자식으로 이동 등을 처리합니다. /// - /// 드래그된 아이템 - /// 드롭 대상 UI 아이템 + /// 드래그한 데이터. + /// 드롭 대상 UI 아이템(null: 루트로 드롭). private void HandleDropSuccess(TreeListItemData? draggedData, TreeListItem? targetItem) { if (draggedData == null || treeList == null) @@ -567,10 +560,11 @@ namespace UVC.UI.List.Tree } /// - /// 드롭 위치를 판단합니다. + /// 대상 RectTransform 내 마우스 Y 위치로 위/아래/자식 드롭 위치를 판정합니다. + /// 상/하 30% → 위/아래, 중간 40% → 자식. /// - /// 대상 아이템의 RectTransform - /// 드롭 위치 + /// 대상 아이템 RectTransform. + /// 드롭 위치. private DropPosition GetDropPosition(RectTransform targetRect) { // 목표: 마우스 Y 위치를 기반으로 위/아래/안쪽 판단 @@ -610,18 +604,13 @@ namespace UVC.UI.List.Tree /// /// 아이템을 루트 레벨로 이동합니다. + /// 이미 루트면 순서만 변경합니다. /// + /// 드래그된 데이터. 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()); - 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}을(를) 루트 레벨의 끝에 추가"); } /// - /// 아이템을 다른 아이템의 자식으로 이동합니다. + /// 아이템을 대상의 자식으로 이동합니다. /// + /// 이동할 데이터. + /// 대상 데이터. private void MoveAsChild(TreeListItemData draggedData, TreeListItemData targetData) { - //RemoveFromParent(draggedData); - targetData.AddChild(draggedData); + targetData.AddCloneChild(draggedData); Debug.Log($"[MoveAsChild] {draggedData.Name}을(를) {targetData.Name}의 자식으로 이동"); } /// - /// 아이템을 다른 아이템 앞으로 이동합니다. + /// 아이템을 대상 앞(위)으로 이동합니다. + /// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다. /// + /// 이동할 데이터. + /// 기준 데이터. 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} 이동 (루트 레벨)"); + } + } + } } } /// - /// 아이템을 다른 아이템 뒤로 이동합니다. + /// 아이템을 대상 뒤(아래)로 이동합니다. + /// 같은 부모면 재정렬, 다르면 부모 교체 후 삽입합니다. /// + /// 이동할 데이터. + /// 기준 데이터. 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} 이동 (루트 레벨)"); + } } } } } - /// - /// 아이템을 현재 부모에서 제거합니다. - /// - 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(); - } /// - /// 주어진 아이템의 부모를 찾습니다. - /// - private TreeListItemData? FindParentOfItem(TreeListItemData item) - { - if (treeList == null) - { - return null; - } - - foreach (TreeListItemData data in treeList!.AllItemsFlattened) - { - if (data == item) - { - return data.Parent; - } - } - return null; - } - - - /// - /// 드롭 위치를 나타내는 열거형입니다. + /// 드롭 위치를 나타냅니다. /// private enum DropPosition {