#nullable enable using Cysharp.Threading.Tasks; using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UVC.UI.Modal.DatePicker; namespace UVC.UI.List.Tree { /// /// 트리 구조의 리스트를 관리하고 제어하는 클래스입니다. /// /// 주요 기능: /// - 계층 구조 아이템의 추가/제거 관리 /// - 단일 및 다중 선택 지원 (파일 탐색기와 동일) /// - 키보드 입력(Delete, 화살표, Ctrl, Shift) 처리 /// - 드래그 & 드롭 기능 지원 /// - 선택 상태 변경 이벤트 제공 /// /// 선택 동작: /// - 클릭: 한 항목 선택 /// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지) /// - Shift+클릭: 범위 선택 (시작~끝) /// - 화살표: 선택된 아이템 이동 /// - Delete: 선택된 아이템 삭제 /// public class TreeList : MonoBehaviour { /// /// UI 아이템 프리팹 (복제할 템플릿) /// [SerializeField] protected TreeListItem itemPrefab; /// /// itemPrefab 프로퍼티 (읽기 전용) /// public TreeListItem ItemPrefab => itemPrefab; /// /// 모든 아이템을 담는 부모 컨테이너 (RectTransform) /// [SerializeField] protected RectTransform root; /// /// root 프로퍼티 (읽기 전용) /// public RectTransform Root => root; /// /// 다중 선택 허용 여부 /// (true: 여러 개 선택 가능, false: 한 개만 선택 가능) /// [SerializeField] protected bool allowMultipleSelection = true; /// /// 드래그 & 드롭 기능 활성화 여부 /// [SerializeField] protected bool enableDragDrop = true; /// /// 현재 선택된 아이템 리스트 /// protected List selectedItems = new List(); /// /// 마지막으로 선택한 아이템 (Shift+클릭 범위 선택의 시작점) /// protected TreeListItemData? lastSelectedItem = null; /// /// 모든 아이템을 1차원으로 평탄화한 리스트 (범위 선택용) /// protected List allItemDataFlattened = new List(); /// /// 평탄화된 아이템 데이터 리스트 (읽기 전용) /// public List AllItemDataFlattened => allItemDataFlattened; /// /// 루트 레벨 아이템 리스트 /// 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; /// /// 아이템 선택 상태 변경 이벤트 (data, isSelected) /// public Action? OnItemSelectionChanged; /// /// 현재 선택된 아이템 목록 (읽기 전용) /// public IReadOnlyList SelectedItems => selectedItems.AsReadOnly(); /// /// root 아래의 모든 TreeListItem 컴포넌트 /// public List TreeLists => root.GetComponentsInChildren().ToList(); // 뷰 매핑: 데이터 ↔ View O(1) 조회 private readonly Dictionary _viewMap = new Dictionary(); // 플래튼 업데이트 스로틀링 플래그 private bool _flattenScheduled; private void Awake() { // 드래그 & 드롭 이벤트 구독 if (enableDragDrop) { dragDropManager.OnDropped += HandleItemDropped; } } private void OnDestroy() { if (enableDragDrop) { dragDropManager.OnDropped -= HandleItemDropped; } if (OnItemSelectionChanged != null) OnItemSelectionChanged = null; } private void Update() { // Escape 키 입력 감지 - 선택 해제 if (Input.GetKeyDown(KeyCode.Escape)) { HandleEscapeKeyPressed(); } // Delete 키 입력 감지 if (Input.GetKeyDown(KeyCode.Delete)) { 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(); } } /// /// View 등록(데이터↔View 맵). Init 시점에 호출됩니다. /// internal void RegisterView(TreeListItemData data, TreeListItem view) { if (data == null || view == null) return; _viewMap[data] = view; } /// /// View 등록 해제. Delete/OnDestroy 시점에 호출됩니다. /// internal void UnregisterView(TreeListItemData data, TreeListItem view) { if (data == null) return; if (_viewMap.TryGetValue(data, out var existing) && existing == view) { _viewMap.Remove(data); } } /// /// 다음 프레임에 평탄화 리스트 업데이트를 예약합니다. /// 과도한 재계산을 방지하기 위한 스로틀링 유틸입니다. /// public void ScheduleFlattenedUpdate() { if (_flattenScheduled) return; _flattenScheduled = true; UniTask.NextFrame().ContinueWith(() => { _flattenScheduled = false; UpdateFlattenedItemDataList(); }); } /// /// Escape 키 입력 시 모든 선택 해제 /// /// 동작: /// 1. 선택된 아이템이 있는지 확인 /// 2. ClearSelection() 호출하여 모든 선택 해제 /// 3. lastSelectedItem 초기화 /// /// 용도: 사용자가 선택을 취소하고 싶을 때 빠른 해제 /// private void HandleEscapeKeyPressed() { // 선택된 아이템이 없으면 아무것도 하지 않음 if (selectedItems.Count == 0) { return; } // 디버그 로그 Debug.Log($"Escape key pressed. Clearing {selectedItems.Count} selected item(s)."); // 모든 선택 해제 ClearSelection(); // 마지막 선택 아이템 초기화 lastSelectedItem = null; } /// /// Delete 키 입력 시 선택된 모든 아이템 삭제 /// /// 동작: /// 1. 선택된 아이템 목록 복사 /// 2. 각 아이템에 대해 DeleteItem() 호출 /// 3. 평탄화 리스트 업데이트 /// /// 주의: 반복 중 리스트 수정으로 인한 문제 방지를 위해 /// ToList()로 복사 후 순회합니다. /// private void HandleDeleteKeyPressed() { // 선택된 아이템이 없으면 아무것도 하지 않음 if (selectedItems.Count == 0) { return; } // 선택된 아이템 목록을 복사 (역순 삭제 시 안전) var itemsToDelete = selectedItems.ToList(); // 디버그 로그 Debug.Log($"Delete key pressed. Removing {itemsToDelete.Count} selected item(s)."); // 각 선택된 아이템을 삭제 foreach (var item in itemsToDelete) { DeleteItem(item); } UpdateFlattenedItemDataList(); } /// /// 펼쳐진 아이템들만 포함하는 가시 리스트 생성 /// /// 동작: /// 1. 루트 레벨 아이템부터 시작 /// 2. IsExpanded=true인 아이템의 자식들만 포함 /// 3. 재귀적으로 모든 가시 아이템 수집 /// /// 용도: 화살표 키 네비게이션 시 보이는 아이템만 선택 가능 /// private List GetVisibleFlattenedItems() { List visibleItems = new List(); // 루트 레벨 아이템들부터 시작 foreach (var rootItem in items) { AddVisibleItemsRecursive(rootItem, visibleItems); } return visibleItems; } /// /// 가시 아이템을 재귀적으로 추가 /// /// 동작: /// 1. 현재 아이템을 리스트에 추가 /// 2. IsExpanded=true이면 모든 자식 재귀 처리 /// 3. IsExpanded=false이면 자식 무시 (접혀있음) /// /// 현재 처리할 아이템 /// 수집 결과 리스트 private void AddVisibleItemsRecursive(TreeListItemData item, List visibleItems) { // 현재 아이템을 리스트에 추가 visibleItems.Add(item); // IsExpanded=true인 경우에만 자식들 처리 if (item.IsExpanded) { 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; TreeListItem item = GameObject.Instantiate(ItemPrefab, root); item.Init(data, this, dragDropManager); index = Mathf.Clamp(index, 0, items.Count); items.Insert(index, data); UniTask.DelayFrame(1).ContinueWith(() => { //gameObject 순서 조절 item.transform.SetSiblingIndex(index); // 범위 선택에 필요한 평탄화 리스트 업데이트 UpdateFlattenedItemDataList(); }); } /// /// 아이템 복제 추가 /// /// 복제할 원본 아이템 데이터 public void AddCloneItem(TreeListItemData data) { TreeListItemData clone = data.CloneWithChild(); //호출 순서 중요 //data에 해당하는 TreeListItem 찾기 TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); //changedData 부모에게 알림 if (data.Parent != null) { data.Parent.RemoveChild(data); } else { RemoveItem(data); } if (item != null) item.Delete(true); AddItem(clone); } /// /// 아이템 복제 후 지정 인덱스에 추가 /// /// 복제할 원본 아이템 데이터 /// 삽입 위치 public void AddCloneItemAt(TreeListItemData data, int index) { TreeListItemData clone = data.CloneWithChild(); //호출 순서 중요 //data에 해당하는 TreeListItem 찾기 TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); //changedData 부모에게 알림 if (data.Parent != null) { data.Parent.RemoveChild(data); } else { RemoveItem(data); } if (item != null) item.Delete(true); AddItemAt(clone, index); } /// /// 아이템을 지정 인덱스로 이동합니다(기존 위치 제거 후 삽입). /// public void SwapItem(TreeListItemData data, int index) { // 기존 위치 제거 items.Remove(data); // 인덱스 보정 후 삽입 index = Mathf.Clamp(index, 0, items.Count); items.Insert(index, data); TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); UniTask.DelayFrame(1).ContinueWith(() => { 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)) { selectedItems.Remove(data); } TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data); if (item != null) { item.Delete(true); } } /// /// 컬렉션에서 모든 항목을 지우고 관련 리소스를 해제합니다. /// /// 이 메서드는 컬렉션에서 모든 항목을 제거하고, 내부 데이터 구조를 지우고, /// 메서드를 매개변수로 호출하여 각 항목을 삭제합니다. public void ClearItems() { foreach (var item in allItemFlattened) { item.Delete(true); } allItemFlattened.Clear(); allItemDataFlattened.Clear(); items.Clear(); selectedItems.Clear(); _viewMap.Clear(); } /// /// 모든 아이템을 1차원 리스트로 평탄화 /// /// 동작: /// 1. 평탄화 리스트 초기화 /// 2. 루트 아이템부터 재귀적으로 추가 /// 3. 각 데이터에 대응하는 UI 컴포넌트 수집 /// /// 용도: 범위 선택 및 화살표 네비게이션 /// internal void UpdateFlattenedItemDataList() { // 기존 평탄화 리스트 비우기 allItemDataFlattened.Clear(); // root의 모든 직접 자식을 순회 // (손자, 증손자는 재귀로 처리됨) foreach (TreeListItemData itemData in items) { // 이 아이템과 자식들을 재귀적으로 평탄화 리스트에 추가 AddItemDataToFlattened(itemData); } allItemFlattened.Clear(); foreach (var data in allItemDataFlattened) { if (_viewMap.TryGetValue(data, out var item) && item != null) { allItemFlattened.Add(item); } } } /// /// 아이템과 자식들을 재귀적으로 평탄화 /// /// 동작: /// 1. 현재 아이템 추가 /// 2. 모든 자식에 대해 재귀 호출 /// /// 트리 구조를 1차원 배열로 변환하여 /// 빠른 인덱스 검색 및 범위 선택을 가능하게 합니다. /// /// 현재 처리할 아이템 private void AddItemDataToFlattened(TreeListItemData data) { // 현재 아이템을 평탄화 리스트에 추가 allItemDataFlattened.Add(data); // 현재 아이템의 모든 자식을 순회 foreach (var child in data.Children) { // 각 자식에 대해 재귀 호출 // = 자식의 자식들도 모두 추가됨 AddItemDataToFlattened(child); } } /// /// 아이템 클릭 처리 /// /// 동작: /// 1. 키 입력 상태 확인 (Ctrl, Shift) /// 2. 선택 모드에 따라 로직 실행: /// - 일반 클릭: 이 아이템만 선택 /// - Ctrl+클릭: 토글 (기존 선택 유지) /// - Shift+클릭: 범위 선택 /// 3. 마지막 선택 아이템 기록 /// /// 매개변수: /// - 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}"); // 다중 선택 비활성화 if (!allowMultipleSelection) { ToggleItemSelection(data); return; } // Ctrl+클릭: 토글 if (ctrlPressed) { ToggleItemSelection(data); lastSelectedItem = data; } // Shift+클릭: 범위 선택 else if (shiftPressed) { if (lastSelectedItem != null) { // 마지막 선택부터 현재 아이템까지 범위 선택 SelectRange(lastSelectedItem, data); } else { // 아직 선택한 게 없으면 단순 선택 ClearSelection(); SelectItem(data); } // 이 아이템을 "마지막 선택"으로 기억 lastSelectedItem = data; } // 일반 클릭: 단일 선택 else { if (data.IsSelected) { // 이미 선택된 아이템을 다시 클릭 → 선택 해제 DeselectItem(data); lastSelectedItem = null; } else { // 선택되지 않은 아이템 클릭 → 다른 선택 모두 해제하고 이것 선택 ClearSelection(); SelectItem(data); lastSelectedItem = data; } } } /// /// 두 아이템 사이의 범위 선택 /// /// 동작: /// 1. 평탄화 리스트에서 두 아이템의 인덱스 찾기 /// 2. 범위 정렬 (시작 ≤ 끝) /// 3. 범위 내 모든 아이템 선택 /// /// 매개변수: /// - startItem: 범위 시작 아이템 /// - endItem: 범위 끝 아이템 /// /// 범위 시작 아이템 /// 범위 끝 아이템 private void SelectRange(TreeListItemData startItem, TreeListItemData endItem) { // 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기 int startIndex = allItemDataFlattened.IndexOf(startItem); // 평탄화 리스트에서 끝 아이템의 위치(인덱스) 찾기 int endIndex = allItemDataFlattened.IndexOf(endItem); // 두 아이템 모두 리스트에 없으면 종료 if (startIndex == -1 || endIndex == -1) { return; } // 시작과 끝의 순서를 정렬 // 사용자가 거꾸로 선택할 수도 있으므로 (끝→시작) // 항상 작은 인덱스 = 시작, 큰 인덱스 = 끝으로 정렬 int minIndex = Mathf.Min(startIndex, endIndex); int maxIndex = Mathf.Max(startIndex, endIndex); // 범위 내의 모든 아이템 선택 // i = minIndex부터 i = maxIndex까지 (포함) for (int i = minIndex; i <= maxIndex; i++) { SelectItem(allItemDataFlattened[i]); } } /// /// 아이템 선택 상태 토글 /// /// 동작: /// - 선택됨 → 해제 /// - 미선택 → 선택 /// /// 토글할 아이템 public void ToggleItemSelection(TreeListItemData data) { if (data.IsSelected) { // 선택됨 → 해제 DeselectItem(data); } else { // 선택 안 됨 → 선택 SelectItem(data); } } /// /// 아이템 선택 /// /// 동작: /// 1. 중복 선택 방지 /// 2. 다중 선택 비활성화 시 기존 선택 해제 /// 3. 아이템 선택 표시 /// 4. 선택 리스트에 추가 /// 5. 선택 변경 이벤트 발생 /// /// 선택할 아이템 public void SelectItem(TreeListItemData data) { // 이미 선택되어 있으면 중복 선택 방지 if (data.IsSelected) { return; } // 다중 선택이 불가능한 경우, 기존 선택 해제 // (새 선택만 유지) if (!allowMultipleSelection && selectedItems.Count > 0) { // 첫 번째(유일한) 선택 항목 해제 DeselectItem(selectedItems[0]); } // 아이템의 선택 상태를 true로 설정 data.IsSelected = true; // 아직 리스트에 없으면 추가 if (!selectedItems.Contains(data)) { selectedItems.Add(data); } // 이벤트 발생 // ?. 연산자: null 체크 후 호출 // (구독자가 없을 수도 있으므로) OnItemSelectionChanged?.Invoke(data, true); } /// /// 아이템 선택 해제 /// /// 동작: /// 1. 중복 해제 방지 /// 2. 아이템 선택 상태 해제 /// 3. 선택 리스트에서 제거 /// 4. 선택 변경 이벤트 발생 /// /// 선택 해제할 아이템 public void DeselectItem(TreeListItemData data) { // 이미 선택 해제되어 있으면 아무것도 안 함 if (!data.IsSelected) { return; } // 아이템의 선택 상태를 false로 설정 data.IsSelected = false; // selectedItems 리스트에서 제거 selectedItems.Remove(data); // 이벤트 발생 OnItemSelectionChanged?.Invoke(data, false); } /// /// 모든 아이템 선택 해제 /// /// 동작: /// 1. 선택 리스트 복사 (반복 중 수정 방지) /// 2. 각 아이템에 대해 DeselectItem() 호출 /// public void ClearSelection() { // ToList()로 복사 // = selectedItems의 현재 상태를 새로운 리스트로 만듦 // 반복 중에 원본을 수정해도 안전하게 함 foreach (var item in selectedItems.ToList()) { DeselectItem(item); } } /// /// 다중 선택 활성화/비활성화 설정 /// /// 동작: /// 1. allowMultipleSelection 값 변경 /// 2. false로 변경 시 여러 선택이 있으면 첫 번째만 유지 /// /// true: 다중 선택 허용, false: 단일 선택만 허용 public void SetAllowMultipleSelection(bool allow) { // 다중 선택 가능 여부 설정 allowMultipleSelection = allow; // false로 변경되었는데 여러 개가 선택되어 있으면 정리 if (!allow && selectedItems.Count > 1) { // 첫 번째 아이템 보관 var firstItem = selectedItems[0]; // 모든 선택 해제 ClearSelection(); // 첫 번째 아이템만 다시 선택 SelectItem(firstItem); } } /// /// 드롭 완료 후 UI 업데이트 /// /// 드래그된 아이템 /// 드롭 대상 아이템 (null: 루트) private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem) { // 평탄화 리스트 업데이트 UpdateFlattenedItemDataList(); // 필요시 UI 재구성 (예: 자식 컨테이너 위치 변경 등) Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'"); } /// /// 지정한 데이터 항목을 트리에서 찾아 부모 체인을 펼치고 선택합니다. /// 기존 선택이 있으면 해제합니다. /// /// 선택 및 표시할 데이터 (트리의 실제 데이터 참조) /// true면 기존 선택을 모두 해제 public void RevealAndSelectItem(TreeListItemData? data, bool clearExisting = true) { if (data == null) return; if (clearExisting) { ClearSelection(); } // 부모 체인 펼치기 var parent = data.Parent; while (parent != null) { parent.IsExpanded = true; parent = parent.Parent; } // 평탄화 리스트 갱신 및 대상 선택 UpdateFlattenedItemDataList(); SelectItem(data); } } }