2025-10-28 15:36:55 +09:00
|
|
|
#nullable enable
|
2025-10-30 18:36:26 +09:00
|
|
|
using Cysharp.Threading.Tasks;
|
2025-10-28 20:10:51 +09:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
2025-10-28 15:36:55 +09:00
|
|
|
using UnityEngine;
|
|
|
|
|
|
|
|
|
|
namespace UVC.UI.List.Tree
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 트리 구조의 리스트를 관리하고 제어하는 클래스입니다.
|
|
|
|
|
///
|
|
|
|
|
/// 역할:
|
|
|
|
|
/// 1. 아이템 추가/제거 관리
|
|
|
|
|
/// 2. 선택 상태 관리 (단일, 다중 선택)
|
|
|
|
|
/// 3. 키 입력(Ctrl, Shift)에 따른 선택 로직 처리
|
|
|
|
|
/// 4. 선택 상태 변경 이벤트 발생
|
|
|
|
|
///
|
|
|
|
|
/// 파일 탐색기와 비슷한 동작:
|
|
|
|
|
/// - 클릭: 한 항목 선택
|
|
|
|
|
/// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지)
|
|
|
|
|
/// - Shift+클릭: 범위 선택 (시작~끝)
|
|
|
|
|
///
|
|
|
|
|
/// MonoBehaviour란?
|
|
|
|
|
/// Unity의 모든 게임 로직이 상속받는 기본 클래스입니다.
|
|
|
|
|
/// Inspector에서 설정할 수 있고, Update 같은 생명주기 함수를 사용할 수 있습니다.
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
public class TreeList : MonoBehaviour
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// UI 아이템(TreeListItem)의 프리팹(템플릿)입니다.
|
|
|
|
|
///
|
|
|
|
|
/// 프리팹이란?
|
|
|
|
|
/// 미리 만들어둔 UI 템플릿입니다.
|
|
|
|
|
/// 이것을 복제(Instantiate)해서 새로운 아이템을 여러 개 만들 수 있습니다.
|
|
|
|
|
///
|
|
|
|
|
/// [SerializeField]란?
|
|
|
|
|
/// 이 필드를 Inspector(게임 편집기)에서 직접 설정할 수 있게 해줍니다.
|
|
|
|
|
/// (private이지만 Unity에서만 접근 가능)
|
|
|
|
|
///
|
|
|
|
|
/// 사용 흐름:
|
|
|
|
|
/// 1. Inspector에서 프리팹을 이 필드에 드래그&드롭
|
|
|
|
|
/// 2. AddItem()이 호출되면 이 프리팹을 복제해서 새 아이템 생성
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
[SerializeField]
|
|
|
|
|
protected TreeListItem itemPrefab;
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// itemPrefab을 읽기만 가능하게 공개합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 문법: public TreeListItem ItemPrefab => itemPrefab;
|
|
|
|
|
/// = 프로퍼티 (getter 전용)
|
|
|
|
|
/// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
public TreeListItem ItemPrefab => itemPrefab;
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 아이템을 담는 부모 컨테이너입니다.
|
|
|
|
|
///
|
|
|
|
|
/// RectTransform이란?
|
|
|
|
|
/// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다.
|
|
|
|
|
/// Canvas(캔버스) 아래의 모든 UI 요소가 이것을 가집니다.
|
|
|
|
|
///
|
|
|
|
|
/// 역할:
|
|
|
|
|
/// - AddItem()에서 새 아이템의 부모로 지정
|
2025-10-30 18:36:26 +09:00
|
|
|
/// - UpdateFlattenedItemDataList()에서 모든 자식을 순회
|
2025-10-28 20:10:51 +09:00
|
|
|
///
|
|
|
|
|
/// 구조 예:
|
|
|
|
|
/// root (이것)
|
|
|
|
|
/// ├─ 아이템1
|
|
|
|
|
/// ├─ 아이템2
|
|
|
|
|
/// └─ 아이템3
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
[SerializeField]
|
|
|
|
|
protected RectTransform root;
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
public RectTransform Root => root;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 여러 개 선택이 가능한지 여부를 나타냅니다.
|
|
|
|
|
///
|
|
|
|
|
/// true: 여러 개 선택 가능 (Ctrl+클릭, Shift+클릭 작동)
|
|
|
|
|
/// false: 한 개만 선택 가능 (최신 선택이 기존 선택을 덮어씀)
|
|
|
|
|
///
|
|
|
|
|
/// 예시:
|
|
|
|
|
/// - true인 경우: 파일 탐색기 (여러 파일 선택 가능)
|
|
|
|
|
/// - false인 경우: 라디오 버튼 (하나만 선택)
|
|
|
|
|
///
|
|
|
|
|
/// SetAllowMultipleSelection() 메서드로 런타임에 변경 가능합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[SerializeField]
|
|
|
|
|
protected bool allowMultipleSelection = true;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 드래그 & 드롭 기능 활성화 여부입니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
[SerializeField]
|
|
|
|
|
protected bool enableDragDrop = true;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 현재 선택된 아이템들을 저장하는 리스트입니다.
|
|
|
|
|
///
|
|
|
|
|
/// 역할:
|
|
|
|
|
/// - SelectItem(): 선택된 아이템 추가
|
|
|
|
|
/// - DeselectItem(): 선택된 아이템 제거
|
|
|
|
|
/// - ClearSelection(): 모두 해제
|
|
|
|
|
/// - SelectedItems 프로퍼티: 외부에 읽기 전용으로 제공
|
|
|
|
|
///
|
|
|
|
|
/// 예:
|
|
|
|
|
/// 사용자가 3개 파일을 선택하면 이 리스트에 [파일1, 파일2, 파일3]이 저장됩니다.
|
|
|
|
|
///
|
|
|
|
|
/// List란?
|
|
|
|
|
/// 배열처럼 여러 개 항목을 저장하는 컨테이너입니다.
|
|
|
|
|
/// 크기가 자동으로 조정되므로 개수를 미리 정할 필요가 없습니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected List<TreeListItemData> selectedItems = new List<TreeListItemData>();
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 사용자가 마지막으로 선택한 아이템입니다.
|
|
|
|
|
///
|
|
|
|
|
/// 용도: Shift+클릭으로 범위 선택할 때 시작점으로 사용
|
|
|
|
|
///
|
|
|
|
|
/// 시나리오:
|
|
|
|
|
/// 1. 파일1 클릭 → lastSelectedItem = 파일1
|
|
|
|
|
/// 2. Shift+파일5 클릭 → 파일1부터 파일5까지 모두 선택
|
|
|
|
|
///
|
|
|
|
|
/// ? 연산자 (nullable)란?
|
|
|
|
|
/// 이 변수는 null(값 없음)일 수 있습니다.
|
|
|
|
|
/// 예: lastSelectedItem = null (아직 선택한 게 없음)
|
|
|
|
|
///
|
|
|
|
|
/// null 체크:
|
|
|
|
|
/// if (lastSelectedItem != null) { ... }
|
|
|
|
|
/// = 값이 있는지 확인 후에 사용
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected TreeListItemData? lastSelectedItem = null;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 아이템을 1차원 리스트로 변환한 것입니다. (평탄화)
|
|
|
|
|
///
|
|
|
|
|
/// "평탄화"란?
|
|
|
|
|
/// 원본 트리 구조:
|
|
|
|
|
/// 폴더1
|
|
|
|
|
/// ├─ 파일1
|
|
|
|
|
/// └─ 폴더2
|
|
|
|
|
/// └─ 파일2
|
|
|
|
|
///
|
|
|
|
|
/// 평탄화된 리스트:
|
|
|
|
|
/// [폴더1, 파일1, 폴더2, 파일2] (계층 무시, 선형으로 배열)
|
|
|
|
|
///
|
|
|
|
|
/// 용도:
|
|
|
|
|
/// - SelectRange()에서 두 아이템 사이의 인덱스를 빠르게 찾기
|
|
|
|
|
/// - Shift+클릭 범위 선택 구현
|
|
|
|
|
///
|
2025-10-30 18:36:26 +09:00
|
|
|
/// UpdateFlattenedItemDataList() 메서드로 항상 최신 상태로 유지됩니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
/// </summary>
|
2025-10-30 18:36:26 +09:00
|
|
|
protected List<TreeListItemData> allItemDatasFlattened = new List<TreeListItemData>();
|
2025-10-28 20:10:51 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
public List<TreeListItemData> AllItemsFlattened => allItemDatasFlattened;
|
2025-10-29 20:12:11 +09:00
|
|
|
|
|
|
|
|
protected List<TreeListItemData> items = new List<TreeListItemData>();
|
|
|
|
|
|
|
|
|
|
public List<TreeListItemData> Items => items;
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
protected List<TreeListItem> allItemFlattened = new List<TreeListItem>();
|
|
|
|
|
|
|
|
|
|
public List<TreeListItem> AllItemFlattened => allItemFlattened;
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 드래그 & 드롭 매니저입니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
protected TreeListDragDropManager dragDropManager = new TreeListDragDropManager();
|
|
|
|
|
|
|
|
|
|
public TreeListDragDropManager DragDropManager => dragDropManager;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템의 선택 상태가 변경되었을 때 발생하는 이벤트입니다.
|
|
|
|
|
///
|
|
|
|
|
/// 이벤트란?
|
|
|
|
|
/// 무언가 일어났을 때 다른 코드에 알려주는 메커니즘입니다.
|
|
|
|
|
/// 구독자(listener)가 등록되어 있으면 자동으로 호출됩니다.
|
|
|
|
|
///
|
2025-10-30 18:36:26 +09:00
|
|
|
/// 문법: Action<TreeListItemData, bool>?
|
2025-10-28 20:10:51 +09:00
|
|
|
/// = TreeListItemData 1개, bool 1개를 매개변수로 받는 함수들을 등록
|
|
|
|
|
///
|
|
|
|
|
/// 호출되는 경우:
|
|
|
|
|
/// - SelectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, true);
|
|
|
|
|
/// - DeselectItem() 호출 시 → OnItemSelectionChanged?.Invoke(data, false);
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// treeList.OnItemSelectionChanged += (data, isSelected) => {
|
|
|
|
|
/// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}");
|
|
|
|
|
/// };
|
|
|
|
|
/// </summary>
|
2025-10-30 18:36:26 +09:00
|
|
|
public Action<TreeListItemData, bool>? OnItemSelectionChanged;
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 현재 선택된 아이템 목록을 반환합니다. (읽기 전용)
|
|
|
|
|
///
|
|
|
|
|
/// 문법: public IReadOnlyList<TreeListItemData> SelectedItems => ...
|
|
|
|
|
/// = 프로퍼티 (getter 전용)
|
|
|
|
|
/// = 외부에서 읽을 수 있지만 변경할 수 없음
|
|
|
|
|
///
|
|
|
|
|
/// AsReadOnly()란?
|
|
|
|
|
/// 리스트를 읽기 전용으로 변환합니다.
|
2025-10-30 18:36:26 +09:00
|
|
|
/// 따라서 외부에서 SelectedItems.AddChild()같은 수정이 불가능합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
///
|
|
|
|
|
/// 왜 이렇게 하나?
|
|
|
|
|
/// 클래스 내부에서만 selectedItems를 제어하고,
|
|
|
|
|
/// 외부는 결과만 볼 수 있게 하기 위함입니다. (데이터 무결성)
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// foreach (var item in treeList.SelectedItems)
|
|
|
|
|
/// {
|
|
|
|
|
/// Debug.Log(item.Name); // 읽기만 가능
|
|
|
|
|
/// }
|
|
|
|
|
/// </summary>
|
|
|
|
|
public IReadOnlyList<TreeListItemData> SelectedItems => selectedItems.AsReadOnly();
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
public List<TreeListItem> TreeLists => root.GetComponentsInChildren<TreeListItem>().ToList();
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
private void Awake()
|
|
|
|
|
{
|
|
|
|
|
// 드래그 & 드롭 이벤트 구독
|
|
|
|
|
if (enableDragDrop)
|
|
|
|
|
{
|
|
|
|
|
dragDropManager.OnDropped += HandleItemDropped;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
{
|
|
|
|
|
if (enableDragDrop)
|
|
|
|
|
{
|
|
|
|
|
dragDropManager.OnDropped -= HandleItemDropped;
|
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
|
|
|
|
if(OnItemSelectionChanged != null) OnItemSelectionChanged = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void Update()
|
|
|
|
|
{
|
|
|
|
|
// Delete 키 입력 감지
|
|
|
|
|
if (Input.GetKeyDown(KeyCode.Delete))
|
|
|
|
|
{
|
|
|
|
|
HandleDeleteKeyPressed();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Delete 키가 눌렸을 때 선택된 아이템들을 삭제합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. 현재 선택된 아이템 목록을 복사 (선택 상태 변경 중 문제 방지)
|
|
|
|
|
/// 2. 복사본을 역순으로 순회 (뒤에서 앞으로)
|
|
|
|
|
/// 3. 각 아이템을 RemoveItem()으로 삭제
|
|
|
|
|
///
|
|
|
|
|
/// 역순 삭제 이유:
|
|
|
|
|
/// - 앞에서부터 삭제하면 인덱스가 변경되어 뒤의 아이템을 놓칠 수 있음
|
|
|
|
|
/// - 뒤에서부터 삭제하면 앞의 인덱스는 영향을 받지 않음
|
|
|
|
|
///
|
|
|
|
|
/// ToList()로 복사하는 이유:
|
|
|
|
|
/// - RemoveItem() 호출 중 selectedItems가 수정될 수 있음
|
|
|
|
|
/// - 복사본을 순회하면 안전하게 모든 아이템을 삭제 가능
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void HandleDeleteKeyPressed()
|
|
|
|
|
{
|
|
|
|
|
// 선택된 아이템이 없으면 아무것도 하지 않음
|
|
|
|
|
if (selectedItems.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 선택된 아이템 목록을 복사 (역순 삭제 시 안전)
|
|
|
|
|
var itemsToDelete = selectedItems.ToList();
|
|
|
|
|
|
|
|
|
|
// 디버그 로그
|
|
|
|
|
Debug.Log($"Delete key pressed. Removing {itemsToDelete.Count} selected item(s).");
|
|
|
|
|
|
|
|
|
|
// 각 선택된 아이템을 삭제
|
|
|
|
|
foreach (var item in itemsToDelete)
|
|
|
|
|
{
|
|
|
|
|
DeleteItem(item);
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 새로운 아이템을 트리 리스트에 추가합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
/// - 프로그램이 새 데이터를 UI에 표시하고 싶을 때
|
|
|
|
|
///
|
|
|
|
|
/// 처리 순서:
|
|
|
|
|
/// 1. itemPrefab을 복제해서 새 UI 객체 생성 (Instantiate)
|
|
|
|
|
/// 2. 복제된 객체를 root(부모 컨테이너) 아래에 배치
|
|
|
|
|
/// 3. 복제된 객체의 Init() 메서드 호출해서 초기화
|
|
|
|
|
/// 4. 평탄화 리스트 업데이트 (범위 선택 시 사용)
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 표시할 데이터 (이름, 자식 등의 정보 포함)
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// var newItem = new TreeListItemData("새 폴더");
|
|
|
|
|
/// treeList.AddItem(newItem); // 화면에 표시됨
|
|
|
|
|
///
|
|
|
|
|
/// Instantiate란?
|
|
|
|
|
/// 프리팹(템플릿)을 복제해서 새로운 객체를 만듭니다.
|
|
|
|
|
/// 게임의 모든 객체는 이 방식으로 생성됩니다.
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
public void AddItem(TreeListItemData data)
|
|
|
|
|
{
|
2025-10-29 20:12:11 +09:00
|
|
|
data.Parent = null;
|
2025-10-30 18:36:26 +09:00
|
|
|
//data에 해당하는 TreeListItem 찾기
|
|
|
|
|
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
|
|
|
|
if (item != null)
|
|
|
|
|
{
|
|
|
|
|
item.transform.SetParent(root);
|
|
|
|
|
item.SetExpand();
|
|
|
|
|
|
|
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
|
|
|
|
{
|
|
|
|
|
//gameObject 순서 조절
|
|
|
|
|
item.transform.SetAsLastSibling();
|
2025-10-28 20:10:51 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
// 범위 선택에 필요한 평탄화 리스트 업데이트
|
|
|
|
|
UpdateFlattenedItemDataList();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Instantiate(템플릿, 부모 Transform)
|
|
|
|
|
// = 템플릿을 복제하고 부모의 자식으로 설정
|
|
|
|
|
item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
// 생성된 아이템 초기화
|
|
|
|
|
// 데이터를 UI에 바인딩하고 이벤트 리스너 등록
|
|
|
|
|
item.Init(data, this, dragDropManager);
|
|
|
|
|
items.Add(data);
|
2025-10-29 20:12:11 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
// 범위 선택에 필요한 평탄화 리스트 업데이트
|
|
|
|
|
UpdateFlattenedItemDataList();
|
|
|
|
|
}
|
2025-10-29 20:12:11 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void AddItemAt(TreeListItemData data, int index)
|
|
|
|
|
{
|
|
|
|
|
data.Parent = null;
|
2025-10-30 18:36:26 +09:00
|
|
|
//data에 해당하는 TreeListItem 찾기
|
|
|
|
|
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
|
|
|
|
if (item != null)
|
|
|
|
|
{
|
|
|
|
|
item.transform.SetParent(root);
|
|
|
|
|
item.SetExpand();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
item = GameObject.Instantiate<TreeListItem>(ItemPrefab, root);
|
|
|
|
|
item.Init(data, this, dragDropManager);
|
|
|
|
|
items.Insert(index, data);
|
|
|
|
|
}
|
2025-10-29 20:12:11 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
|
|
|
|
{
|
|
|
|
|
//gameObject 순서 조절
|
|
|
|
|
item.transform.SetSiblingIndex(index);
|
|
|
|
|
|
|
|
|
|
// 범위 선택에 필요한 평탄화 리스트 업데이트
|
|
|
|
|
UpdateFlattenedItemDataList();
|
|
|
|
|
});
|
2025-10-29 20:12:11 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
/// <summary>
|
|
|
|
|
/// 해당 항목을 트리 리스트에서 제거합니다. date 자체를 삭제하지 않습니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
2025-10-29 20:12:11 +09:00
|
|
|
public void RemoveItem(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
items.Remove(data);
|
2025-10-30 18:36:26 +09:00
|
|
|
|
2025-10-29 20:12:11 +09:00
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
// selectedItems에서도 제거
|
|
|
|
|
if (selectedItems.Contains(data))
|
|
|
|
|
{
|
|
|
|
|
selectedItems.Remove(data);
|
2025-10-29 20:12:11 +09:00
|
|
|
}
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
TreeListItem? item = allItemFlattened.FirstOrDefault(x => x.Data == data);
|
|
|
|
|
if (item != null)
|
|
|
|
|
{
|
|
|
|
|
item.Delete();
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
// 범위 선택에 필요한 평탄화 리스트 업데이트
|
2025-10-30 18:36:26 +09:00
|
|
|
UpdateFlattenedItemDataList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 해당 항목을 트리 리스트에서 제거며 date 자체도 삭제합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
/// <param name="data"></param>
|
|
|
|
|
public void DeleteItem(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
RemoveItem(data);
|
|
|
|
|
data.Dispose();
|
2025-10-28 20:10:51 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 아이템을 평탄화된 1차원 리스트로 재구성합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. 기존 평탄화 리스트를 비움 (Clear)
|
|
|
|
|
/// 2. root의 모든 직접 자식을 순회 (foreach)
|
|
|
|
|
/// 3. 각 자식의 TreeListItem 컴포넌트 획득 (GetComponent)
|
2025-10-30 18:36:26 +09:00
|
|
|
/// 4. AddItemDataToFlattened() 호출해서 자식과 손자까지 재귀 추가
|
2025-10-28 20:10:51 +09:00
|
|
|
///
|
|
|
|
|
/// 호출되는 시점:
|
|
|
|
|
/// - AddItem() 실행 후 (새 아이템 추가됨)
|
|
|
|
|
/// - OnDataChanged 이벤트 발생 시 (자식 구조 변경됨)
|
|
|
|
|
///
|
|
|
|
|
/// Transform이란?
|
|
|
|
|
/// 게임 오브젝트의 위치, 회전, 크기 정보를 가진 컴포넌트입니다.
|
|
|
|
|
/// 계층 구조(부모-자식 관계)의 중심입니다.
|
|
|
|
|
/// root.childCount: 몇 개의 자식이 있는지
|
|
|
|
|
/// for (Transform child in root): 모든 자식을 순회
|
|
|
|
|
///
|
|
|
|
|
/// GetComponent<T>() 란?
|
|
|
|
|
/// 게임 오브젝트에 붙어있는 특정 컴포넌트를 찾아서 반환합니다.
|
|
|
|
|
/// 찾지 못하면 null 반환
|
|
|
|
|
///
|
|
|
|
|
/// 예: child.GetComponent<TreeListItem>()
|
|
|
|
|
/// = child 게임 오브젝트에서 TreeListItem 컴포넌트 찾기
|
|
|
|
|
/// </summary>
|
2025-10-30 18:36:26 +09:00
|
|
|
internal void UpdateFlattenedItemDataList()
|
2025-10-28 20:10:51 +09:00
|
|
|
{
|
|
|
|
|
// 기존 평탄화 리스트 비우기
|
2025-10-30 18:36:26 +09:00
|
|
|
allItemDatasFlattened.Clear();
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
// root의 모든 직접 자식을 순회
|
|
|
|
|
// (손자, 증손자는 재귀로 처리됨)
|
2025-10-29 20:12:11 +09:00
|
|
|
foreach (TreeListItemData itemData in items)
|
|
|
|
|
{
|
|
|
|
|
// 이 아이템과 자식들을 재귀적으로 평탄화 리스트에 추가
|
2025-10-30 18:36:26 +09:00
|
|
|
AddItemDataToFlattened(itemData);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
allItemFlattened.Clear();
|
|
|
|
|
foreach (var data in allItemDatasFlattened)
|
|
|
|
|
{
|
|
|
|
|
var item = root.GetComponentsInChildren<TreeListItem>().FirstOrDefault(x => x.Data == data);
|
|
|
|
|
if(item != null)
|
|
|
|
|
{
|
|
|
|
|
allItemFlattened.Add(item);
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템과 그 모든 자식 아이템들을 평탄화 리스트에 재귀적으로 추가합니다.
|
|
|
|
|
///
|
|
|
|
|
/// "재귀"란?
|
|
|
|
|
/// 함수가 자기 자신을 호출하는 것입니다.
|
|
|
|
|
/// 계층 구조(깊이가 정해지지 않은)를 탐색하는 데 유용합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. 현재 data를 allItemsFlattened에 추가
|
|
|
|
|
/// 2. data의 모든 자식 Children을 순회
|
2025-10-30 18:36:26 +09:00
|
|
|
/// 3. 각 자식에 대해 AddItemDataToFlattened() 재귀 호출
|
2025-10-28 20:10:51 +09:00
|
|
|
///
|
|
|
|
|
/// 예시 - 트리 구조를 평탄화하는 과정:
|
|
|
|
|
///
|
|
|
|
|
/// 트리: 재귀 호출 흐름: 결과:
|
2025-10-30 18:36:26 +09:00
|
|
|
/// 폴더1 AddItemDataToFlattened(폴더1) [폴더1,
|
|
|
|
|
/// ├─ 파일1 → AddItemDataToFlattened(파일1) 파일1,
|
|
|
|
|
/// ├─ 파일2 → AddItemDataToFlattened(파일2) 파일2,
|
|
|
|
|
/// └─ 폴더2 → AddItemDataToFlattened(폴더2) 폴더2,
|
|
|
|
|
/// └─ 파일3 → AddItemDataToFlattened(파일3) 파일3]
|
2025-10-28 20:10:51 +09:00
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 현재 처리할 아이템
|
|
|
|
|
/// </summary>
|
2025-10-30 18:36:26 +09:00
|
|
|
private void AddItemDataToFlattened(TreeListItemData data)
|
2025-10-28 20:10:51 +09:00
|
|
|
{
|
|
|
|
|
// 현재 아이템을 평탄화 리스트에 추가
|
2025-10-30 18:36:26 +09:00
|
|
|
allItemDatasFlattened.Add(data);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
// 현재 아이템의 모든 자식을 순회
|
|
|
|
|
foreach (var child in data.Children)
|
|
|
|
|
{
|
|
|
|
|
// 각 자식에 대해 재귀 호출
|
|
|
|
|
// = 자식의 자식들도 모두 추가됨
|
2025-10-30 18:36:26 +09:00
|
|
|
AddItemDataToFlattened(child);
|
2025-10-28 20:10:51 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템이 클릭되었을 때 호출됩니다.
|
|
|
|
|
///
|
|
|
|
|
/// 역할:
|
|
|
|
|
/// - 사용자가 어떤 키를 누르고 있는지 확인
|
|
|
|
|
/// - 그에 맞는 선택 로직 실행
|
|
|
|
|
/// - 마지막 선택 아이템 기억
|
|
|
|
|
///
|
|
|
|
|
/// 파일 탐색기와 동일한 동작:
|
|
|
|
|
/// - 일반 클릭: 한 항목만 선택
|
|
|
|
|
/// - Ctrl+클릭: 여러 항목 선택 (기존 선택 유지)
|
|
|
|
|
/// - Shift+클릭: 범위 선택 (마지막 선택~현재 선택)
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 클릭된 아이템의 데이터
|
|
|
|
|
/// - ctrlPressed: Ctrl 키가 눌렸는지 여부
|
|
|
|
|
/// - shiftPressed: Shift 키가 눌렸는지 여부
|
|
|
|
|
/// </summary>
|
|
|
|
|
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 키를 누르고 클릭
|
|
|
|
|
// ============================================================
|
|
|
|
|
if (ctrlPressed)
|
|
|
|
|
{
|
|
|
|
|
// 기존 선택을 유지하면서 현재 아이템을 토글
|
|
|
|
|
// 이미 선택됨 → 해제
|
|
|
|
|
// 선택 안 됨 → 선택
|
|
|
|
|
ToggleItemSelection(data);
|
|
|
|
|
|
|
|
|
|
// 이 아이템을 "마지막 선택"으로 기억
|
|
|
|
|
// (다음 Shift+클릭의 시작점)
|
|
|
|
|
lastSelectedItem = data;
|
|
|
|
|
}
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 경우 3: Shift 키를 누르고 클릭
|
|
|
|
|
// ============================================================
|
|
|
|
|
else if (shiftPressed)
|
|
|
|
|
{
|
|
|
|
|
if (lastSelectedItem != null)
|
|
|
|
|
{
|
|
|
|
|
// 마지막 선택부터 현재 아이템까지 범위 선택
|
|
|
|
|
SelectRange(lastSelectedItem, data);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 아직 선택한 게 없으면 단순 선택
|
|
|
|
|
ClearSelection();
|
|
|
|
|
SelectItem(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 이 아이템을 "마지막 선택"으로 기억
|
|
|
|
|
lastSelectedItem = data;
|
|
|
|
|
}
|
|
|
|
|
// ============================================================
|
|
|
|
|
// 경우 4: 아무 키도 누르지 않고 클릭 (일반 클릭)
|
|
|
|
|
// ============================================================
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (data.IsSelected)
|
|
|
|
|
{
|
|
|
|
|
// 이미 선택된 아이템을 다시 클릭 → 선택 해제
|
|
|
|
|
DeselectItem(data);
|
|
|
|
|
lastSelectedItem = null;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 선택되지 않은 아이템 클릭 → 다른 선택 모두 해제하고 이것 선택
|
|
|
|
|
ClearSelection();
|
|
|
|
|
SelectItem(data);
|
|
|
|
|
lastSelectedItem = data;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-28 15:36:55 +09:00
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 두 아이템 사이의 모든 아이템을 선택합니다. (범위 선택)
|
|
|
|
|
///
|
|
|
|
|
/// 시나리오:
|
|
|
|
|
/// 평탄화 리스트: [파일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
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - startItem: 범위의 시작점 (사용자가 먼저 선택한 아이템)
|
|
|
|
|
/// - endItem: 범위의 끝점 (Shift+클릭한 아이템)
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void SelectRange(TreeListItemData startItem, TreeListItemData endItem)
|
|
|
|
|
{
|
|
|
|
|
// 평탄화 리스트에서 시작 아이템의 위치(인덱스) 찾기
|
2025-10-30 18:36:26 +09:00
|
|
|
int startIndex = allItemDatasFlattened.IndexOf(startItem);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
// 평탄화 리스트에서 끝 아이템의 위치(인덱스) 찾기
|
2025-10-30 18:36:26 +09:00
|
|
|
int endIndex = allItemDatasFlattened.IndexOf(endItem);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
// 두 아이템 모두 리스트에 없으면 종료
|
|
|
|
|
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++)
|
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
SelectItem(allItemDatasFlattened[i]);
|
2025-10-28 20:10:51 +09:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템의 선택 상태를 반대로 바꿉니다. (토글)
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// - 선택됨 → 선택 해제
|
|
|
|
|
/// - 선택 안 됨 → 선택
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// treeItem.IsSelected = false
|
|
|
|
|
/// treeList.ToggleItemSelection(treeItem)
|
|
|
|
|
/// // 이후 treeItem.IsSelected = true
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 토글할 아이템
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ToggleItemSelection(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
if (data.IsSelected)
|
|
|
|
|
{
|
|
|
|
|
// 선택됨 → 해제
|
|
|
|
|
DeselectItem(data);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// 선택 안 됨 → 선택
|
|
|
|
|
SelectItem(data);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템을 선택합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. 이미 선택되어 있으면 아무것도 안 함 (중복 선택 방지)
|
|
|
|
|
/// 2. 다중 선택이 비활성화되어 있으면 기존 선택 모두 해제
|
|
|
|
|
/// 3. 현재 아이템 선택 표시
|
|
|
|
|
/// 4. selectedItems 리스트에 추가
|
|
|
|
|
/// 5. OnItemSelectionChanged 이벤트 발생
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 선택할 아이템
|
|
|
|
|
///
|
|
|
|
|
/// 이벤트란?
|
|
|
|
|
/// SelectItem이 호출되면 이 이벤트를 구독한 다른 코드들이
|
|
|
|
|
/// 자동으로 실행됩니다. (옵션 창 업데이트 등)
|
|
|
|
|
/// </summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 아이템의 선택을 해제합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. 선택되어 있지 않으면 아무것도 안 함 (중복 해제 방지)
|
|
|
|
|
/// 2. 아이템의 선택 상태를 false로 설정
|
|
|
|
|
/// 3. selectedItems 리스트에서 제거
|
|
|
|
|
/// 4. OnItemSelectionChanged 이벤트 발생
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - data: 선택 해제할 아이템
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void DeselectItem(TreeListItemData data)
|
|
|
|
|
{
|
|
|
|
|
// 이미 선택 해제되어 있으면 아무것도 안 함
|
|
|
|
|
if (!data.IsSelected)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 아이템의 선택 상태를 false로 설정
|
|
|
|
|
data.IsSelected = false;
|
|
|
|
|
|
|
|
|
|
// selectedItems 리스트에서 제거
|
|
|
|
|
selectedItems.Remove(data);
|
|
|
|
|
|
|
|
|
|
// 이벤트 발생
|
|
|
|
|
OnItemSelectionChanged?.Invoke(data, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 모든 선택 상태를 한 번에 해제합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. selectedItems를 복사 (ToList())
|
|
|
|
|
/// 왜? 반복 중에 리스트를 수정하면 버그가 발생할 수 있으므로
|
|
|
|
|
/// 2. 복사본을 순회하면서 각각 DeselectItem() 호출
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// treeList.ClearSelection();
|
|
|
|
|
/// // 이후 selectedItems는 비어있음
|
|
|
|
|
///
|
|
|
|
|
/// 결과:
|
|
|
|
|
/// - 모든 아이템의 IsSelected = false
|
|
|
|
|
/// - selectedItems 리스트 비어있음
|
|
|
|
|
/// - 각 아이템마다 OnItemSelectionChanged 이벤트 발생
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void ClearSelection()
|
|
|
|
|
{
|
|
|
|
|
// ToList()로 복사
|
|
|
|
|
// = selectedItems의 현재 상태를 새로운 리스트로 만듦
|
|
|
|
|
// 반복 중에 원본을 수정해도 안전하게 함
|
|
|
|
|
foreach (var item in selectedItems.ToList())
|
|
|
|
|
{
|
|
|
|
|
DeselectItem(item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 다중 선택 가능 여부를 설정합니다.
|
|
|
|
|
///
|
|
|
|
|
/// 동작:
|
|
|
|
|
/// 1. allowMultipleSelection 값 변경
|
|
|
|
|
/// 2. 만약 false로 변경하면서 여러 개가 선택되어 있으면:
|
|
|
|
|
/// - 첫 번째 아이템만 유지
|
|
|
|
|
/// - 나머지 모두 해제
|
|
|
|
|
///
|
|
|
|
|
/// 시나리오:
|
|
|
|
|
/// 현재: 3개 선택된 상태 (파일1, 파일2, 파일3)
|
|
|
|
|
/// SetAllowMultipleSelection(false) 호출
|
|
|
|
|
/// 결과: 파일1만 선택, 파일2와 파일3은 해제
|
|
|
|
|
///
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
/// - allow: true면 다중 선택 가능, false면 단일 선택만 가능
|
|
|
|
|
///
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
/// // 라디오 버튼 같은 단일 선택 모드로 변경
|
|
|
|
|
/// treeList.SetAllowMultipleSelection(false);
|
|
|
|
|
/// </summary>
|
|
|
|
|
public void SetAllowMultipleSelection(bool allow)
|
|
|
|
|
{
|
|
|
|
|
// 다중 선택 가능 여부 설정
|
|
|
|
|
allowMultipleSelection = allow;
|
|
|
|
|
|
|
|
|
|
// false로 변경되었는데 여러 개가 선택되어 있으면 정리
|
|
|
|
|
if (!allow && selectedItems.Count > 1)
|
|
|
|
|
{
|
|
|
|
|
// 첫 번째 아이템 보관
|
|
|
|
|
var firstItem = selectedItems[0];
|
|
|
|
|
|
|
|
|
|
// 모든 선택 해제
|
|
|
|
|
ClearSelection();
|
|
|
|
|
|
|
|
|
|
// 첫 번째 아이템만 다시 선택
|
|
|
|
|
SelectItem(firstItem);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// 드롭 완료 후 UI를 업데이트합니다.
|
|
|
|
|
/// </summary>
|
|
|
|
|
private void HandleItemDropped(TreeListItemData draggedItem, TreeListItemData? targetItem)
|
|
|
|
|
{
|
|
|
|
|
// 평탄화 리스트 업데이트
|
2025-10-30 18:36:26 +09:00
|
|
|
UpdateFlattenedItemDataList();
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
// 필요시 UI 재구성 (예: 자식 컨테이너 위치 변경 등)
|
|
|
|
|
Debug.Log($"Item '{draggedItem.Name}' dropped on '{(targetItem?.Name ?? "Root")}'");
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-28 15:36:55 +09:00
|
|
|
}
|
|
|
|
|
}
|