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 15:36:55 +09:00
|
|
|
|
using DG.Tweening;
|
2025-10-29 20:12:11 +09:00
|
|
|
|
using System.Linq;
|
2025-10-28 15:36:55 +09:00
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace UVC.UI.List.Tree
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 트리 리스트의 개별 아이템을 표시하고 관리하는 UI 클래스입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 역할:
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// - 아이템 이름 표시
|
|
|
|
|
|
/// - 자식 아이템 펼침/접힘 관리
|
|
|
|
|
|
/// - 선택 상태 UI 표시 및 입력 처리
|
|
|
|
|
|
/// - 데이터 변경 감지 후 UI 동기화
|
|
|
|
|
|
/// - 드래그 & 드롭 핸들러 연결
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 구성:
|
|
|
|
|
|
/// "TreeListItem"
|
|
|
|
|
|
/// ├─ "valueText": 이름 텍스트
|
|
|
|
|
|
/// ├─ "childExpand": 펼침/접힘 버튼
|
|
|
|
|
|
/// ├─ "childContainer": 자식 컨테이너
|
|
|
|
|
|
/// ├─ "childRoot": 자식 배치 부모
|
|
|
|
|
|
/// ├─ "selectedBg": 선택 배경
|
|
|
|
|
|
/// └─ "itemButton": 아이템 클릭 버튼
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
public class TreeListItem : MonoBehaviour
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#region UI 컴포넌트 참조 (UI Component References)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 이 아이템을 관리하는 상위 컨트롤입니다. (선택/평탄화/프리팹 액세스 등에 사용)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
2025-10-30 18:36:26 +09:00
|
|
|
|
protected TreeList treeList;
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 아이템 이름 텍스트(UI).
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected TMPro.TextMeshProUGUI valueText;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 자식 펼침/접힘 버튼. onClick에서 ToggleChild를 호출합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Button childExpand;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 자식 아이템들을 포함하는 컨테이너 GameObject.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected GameObject childContainer;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 자식 TreeListItem이 배치될 부모 RectTransform.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected RectTransform childRoot;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 선택 상태를 표시하는 배경 이미지.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Image selectedBg;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 아이템 전체 클릭 버튼. onClick에서 OnItemClicked를 호출합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Button itemButton;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 데이터 필드 (Data Fields)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 이 UI가 표시하는 데이터(Nullable). UI는 이 데이터의 변경 이벤트에 반응합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
protected TreeListItemData? data;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 표시 데이터의 읽기 전용 접근자.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TreeListItemData? Data => data;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 펼침/접힘 애니메이션 진행 여부(중복 입력 방지).
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
protected bool isAnimating = false;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 초기화 (Initialization)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// TreeListItem을 초기화합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 절차:
|
|
|
|
|
|
/// 1) 데이터/컨트롤 연결 및 이름 표시
|
|
|
|
|
|
/// 2) 자식 UI 생성
|
|
|
|
|
|
/// 3) 데이터/선택 변경 이벤트 구독
|
|
|
|
|
|
/// 4) 버튼(onClick) 이벤트 연결
|
|
|
|
|
|
/// 5) 선택 UI 반영 및 선택 배경 정렬
|
|
|
|
|
|
/// 6) 드래그 핸들러 연결
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// - data: 표시할 데이터
|
|
|
|
|
|
/// - control: 상위 TreeList
|
|
|
|
|
|
/// - dragDropManager: 드래그 & 드롭 매니저
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void Init(TreeListItemData data, TreeList control, TreeListDragDropManager dragDropManager)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1. 기본 정보 할당
|
2025-10-30 18:36:26 +09:00
|
|
|
|
this.treeList = control;
|
2025-10-28 15:36:55 +09:00
|
|
|
|
this.data = data;
|
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
gameObject.name = "TreeListItem_" + data.Name;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 2. 아이템 이름을 UI에 표시
|
|
|
|
|
|
valueText.text = data.Name;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 자식 아이템들을 UI로 생성
|
|
|
|
|
|
Debug.Log("Creating Children for " + data.Name + ", " + data.Children.Count);
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
if (data.Children.Count > 0)
|
2025-10-31 19:55:14 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 각 자식 데이터에 대해 UI 생성
|
|
|
|
|
|
foreach (var childData in data.Children)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
CreateItem(childData); // 재귀적으로 트리 구조 생성
|
2025-10-28 15:36:55 +09:00
|
|
|
|
}
|
2025-10-31 19:55:14 +09:00
|
|
|
|
|
2025-10-28 15:36:55 +09:00
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// 화살표 방향 설정 (초기에는 펼쳐짐)
|
|
|
|
|
|
SetExpand();
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 4. 데이터 변경 감지 구독
|
|
|
|
|
|
// 데이터의 이름, 자식 목록 등이 변경되면 OnDataChanged 호출
|
|
|
|
|
|
data.OnDataChanged += OnDataChanged;
|
|
|
|
|
|
|
|
|
|
|
|
// 데이터의 선택 상태가 변경되면 OnSelectionChanged 호출
|
|
|
|
|
|
data.OnSelectionChanged += OnSelectionChanged;
|
|
|
|
|
|
|
|
|
|
|
|
// 5. 버튼 클릭 이벤트 구독
|
|
|
|
|
|
// 아이템을 클릭하면 OnItemClicked 메서드 호출
|
|
|
|
|
|
if (itemButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
itemButton.onClick.AddListener(OnItemClicked);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 현재 선택 상태 UI에 반영
|
|
|
|
|
|
// data.IsSelected가 true이면 배경 표시
|
|
|
|
|
|
UpdateSelectionUI();
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 선택 배경의 왼쪽 정렬 (모든 레벨에서 일직선)
|
|
|
|
|
|
AlignSelectedBgToRoot();
|
|
|
|
|
|
|
|
|
|
|
|
// ✅ 드래그 & 드롭 핸들러 설정
|
|
|
|
|
|
var dragHandler = gameObject.GetComponent<TreeListItemDragHandler>();
|
|
|
|
|
|
if (dragHandler == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
dragHandler = gameObject.AddComponent<TreeListItemDragHandler>();
|
|
|
|
|
|
Debug.Log($"[TreeListItem.Init] 새로운 TreeListItemDragHandler 추가: {data.Name}");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
dragHandler.SetDragDropManager(this, control, dragDropManager);
|
|
|
|
|
|
dragHandler.enabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
Debug.Log($"[TreeListItem.Init] 초기화 완료: {data.Name}");
|
2025-10-28 15:36:55 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#region 데이터 변경 처리 (Data Change Handlers)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 데이터 변경에 반응하여 UI를 갱신합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 매개변수:
|
|
|
|
|
|
/// - changedType: 변경 종류(이름/확장/자식 추가 등)
|
|
|
|
|
|
/// - changedData: 변경 대상 데이터(자식 추가 등일 때 유효)
|
|
|
|
|
|
/// - index: 삽입/이동 시 기준 인덱스(해당되는 경우)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-30 18:36:26 +09:00
|
|
|
|
private void OnDataChanged(ChangedType changedType, TreeListItemData changedData, int index)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
if (changedType == ChangedType.Expanded)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
childContainer.SetActive(data.IsExpanded);
|
|
|
|
|
|
// 펼침/접힘 상태 변경 처리
|
|
|
|
|
|
SetExpand();
|
|
|
|
|
|
return;
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
|
|
|
|
|
|
if (changedType == ChangedType.Name)
|
2025-10-31 19:55:14 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// 이름이 변경된 경우
|
|
|
|
|
|
if (valueText.text != data.Name)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
valueText.text = data.Name;
|
2025-10-29 20:12:11 +09:00
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
return;
|
2025-10-29 20:12:11 +09:00
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
if (changedType == ChangedType.ResetChildren)
|
2025-10-29 20:12:11 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// 전체 리셋 처리
|
|
|
|
|
|
// 자식 모두 삭제 후 재생성
|
|
|
|
|
|
for (int i = childRoot.childCount - 1; i >= 0; i--)
|
2025-10-29 20:12:11 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
var childItem = childRoot.GetChild(i).GetComponent<TreeListItem>();
|
|
|
|
|
|
if (childItem != null)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
childItem.Delete(true);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-10-29 20:12:11 +09:00
|
|
|
|
foreach (var childData in data.Children)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
CreateItem(childData);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
treeList.UpdateFlattenedItemDataList();
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (changedType == ChangedType.AddChild)
|
|
|
|
|
|
{
|
2025-10-31 19:55:14 +09:00
|
|
|
|
CreateItem(changedData);
|
|
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
2025-10-29 20:12:11 +09:00
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
treeList.UpdateFlattenedItemDataList();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (changedType == ChangedType.AddAtChild)
|
|
|
|
|
|
{
|
2025-10-31 19:55:14 +09:00
|
|
|
|
TreeListItem item = CreateItem(changedData);
|
|
|
|
|
|
|
|
|
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
item.transform.SetSiblingIndex(index);
|
|
|
|
|
|
treeList.UpdateFlattenedItemDataList();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (changedType == ChangedType.AddCloneChild)
|
|
|
|
|
|
{
|
|
|
|
|
|
//데이터 복사
|
|
|
|
|
|
TreeListItemData clone = changedData.Clone();
|
|
|
|
|
|
|
|
|
|
|
|
//changedData 부모에게 알림 - UI 갱신 용
|
|
|
|
|
|
if (changedData.Parent != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
changedData.Parent.RemoveChild(changedData);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
treeList.RemoveItem(changedData);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TreeListItem 제거
|
2025-10-30 18:36:26 +09:00
|
|
|
|
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
2025-10-31 19:55:14 +09:00
|
|
|
|
if (item != null) item.Delete(true);
|
|
|
|
|
|
|
|
|
|
|
|
data.AddChild(clone);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (changedType == ChangedType.AddCloneAtChild)
|
|
|
|
|
|
{
|
|
|
|
|
|
//데이터 복사
|
|
|
|
|
|
TreeListItemData clone = changedData.Clone();
|
|
|
|
|
|
|
|
|
|
|
|
//changedData 부모에게 알림
|
|
|
|
|
|
if (changedData.Parent != null)
|
2025-10-30 18:36:26 +09:00
|
|
|
|
{
|
2025-10-31 19:55:14 +09:00
|
|
|
|
changedData.Parent.RemoveChild(changedData);
|
2025-10-29 20:12:11 +09:00
|
|
|
|
}
|
2025-10-30 18:36:26 +09:00
|
|
|
|
else
|
2025-10-29 20:12:11 +09:00
|
|
|
|
{
|
2025-10-31 19:55:14 +09:00
|
|
|
|
treeList.RemoveItem(changedData);
|
2025-10-29 20:12:11 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-31 19:55:14 +09:00
|
|
|
|
// 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)
|
|
|
|
|
|
{
|
2025-10-30 18:36:26 +09:00
|
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
2025-10-29 20:12:11 +09:00
|
|
|
|
{
|
2025-10-31 19:55:14 +09:00
|
|
|
|
TreeListItem? item = treeList.AllItemFlattened.FirstOrDefault(x => x.Data == changedData);
|
|
|
|
|
|
if (item != null) item.transform.SetSiblingIndex(index);
|
2025-10-30 18:36:26 +09:00
|
|
|
|
treeList.UpdateFlattenedItemDataList();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (changedType == ChangedType.RemoveChild)
|
2025-10-31 19:55:14 +09:00
|
|
|
|
{
|
|
|
|
|
|
//따로 할것 없음 - 펼침 버튼 갱신 용
|
|
|
|
|
|
Debug.Log($"RemoveChild 처리 완료 {changedData.Name}, {index}");
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
2025-10-29 20:12:11 +09:00
|
|
|
|
|
2025-10-31 19:55:14 +09:00
|
|
|
|
UniTask.DelayFrame(1).ContinueWith(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 5️. 펼침 버튼 표시 여부 결정
|
|
|
|
|
|
childExpand.gameObject.SetActive(data.Children.Count > 0);
|
|
|
|
|
|
});
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 선택 상태 관리 (Selection Management)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 선택 상태 변경에 반응하여 UI를 갱신합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// - changedData: 변경된 데이터(자기 자신)
|
|
|
|
|
|
/// - isSelected: 선택 여부
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnSelectionChanged(TreeListItemData changedData, bool isSelected)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 선택 상태 UI 업데이트 (배경 표시/숨김)
|
|
|
|
|
|
UpdateSelectionUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 선택 상태에 따라 배경 표시/숨김을 갱신합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void UpdateSelectionUI()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
// IsSelected 상태에 따라 배경 표시/숨김
|
|
|
|
|
|
selectedBg.gameObject.SetActive(data.IsSelected);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 위치 정렬 (Position Alignment)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 모든 레벨에서 선택 배경의 왼쪽 정렬을 루트와 맞춥니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void AlignSelectedBgToRoot()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (selectedBg == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 최상위 부모(루트)의 TreeListItem 찾기
|
|
|
|
|
|
TreeListItem? rootItem = GetRootTreeListItem();
|
|
|
|
|
|
|
|
|
|
|
|
// 루트가 없거나 이 아이템이 루트면 정렬할 필요 없음
|
|
|
|
|
|
if (rootItem == null || rootItem == this)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 루트의 selectedBg와 현재 selectedBg의 RectTransform 가져오기
|
|
|
|
|
|
RectTransform rootSelectedBgRect = rootItem.selectedBg.rectTransform;
|
|
|
|
|
|
RectTransform currentSelectedBgRect = selectedBg.rectTransform;
|
|
|
|
|
|
|
|
|
|
|
|
// 루트의 배경 왼쪽 끝 위치를 월드 좌표로 계산
|
|
|
|
|
|
// TransformPoint: 로컬 좌표 → 월드 좌표 변환
|
|
|
|
|
|
// xMin: RectTransform의 왼쪽 끝 위치
|
|
|
|
|
|
Vector3 rootLeftPos = rootSelectedBgRect.TransformPoint(new Vector3(rootSelectedBgRect.rect.xMin, 0, 0));
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 배경의 왼쪽 끝 위치를 월드 좌표로 계산
|
|
|
|
|
|
Vector3 currentLeftPos = currentSelectedBgRect.TransformPoint(new Vector3(currentSelectedBgRect.rect.xMin, 0, 0));
|
|
|
|
|
|
|
|
|
|
|
|
// 두 위치의 차이 계산
|
|
|
|
|
|
// 루트 위치 - 현재 위치 = 조정해야 할 거리
|
|
|
|
|
|
Vector3 offset = rootLeftPos - currentLeftPos;
|
|
|
|
|
|
|
|
|
|
|
|
// 현재 배경의 위치를 조정
|
|
|
|
|
|
// anchoredPosition: 부모 기준 위치 (로컬 좌표)
|
|
|
|
|
|
currentSelectedBgRect.anchoredPosition += new Vector2(offset.x, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 계층 구조 탐색 (Hierarchy Navigation)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 현재 아이템의 루트 TreeListItem을 반환합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 반환:
|
|
|
|
|
|
/// - 루트 TreeListItem, 없으면 null
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private TreeListItem? GetRootTreeListItem()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 현재 객체의 부모부터 시작
|
|
|
|
|
|
Transform current = transform.parent;
|
|
|
|
|
|
|
|
|
|
|
|
// 루트에 도달할 때까지 계속 탐색
|
|
|
|
|
|
while (current != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 현재 레벨에 TreeListItem 컴포넌트가 있는지 확인
|
|
|
|
|
|
TreeListItem parentItem = current.GetComponent<TreeListItem>();
|
|
|
|
|
|
if (parentItem != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// TreeListItem을 찾았으므로, 그 부모의 루트를 다시 찾음 (재귀)
|
|
|
|
|
|
TreeListItem? grandParent = parentItem.GetRootTreeListItem();
|
|
|
|
|
|
if (grandParent != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 할아버지 레벨의 루트 반환
|
|
|
|
|
|
return grandParent;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// 할아버지가 없으면 이 부모가 루트
|
|
|
|
|
|
return parentItem;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 현재 레벨에 TreeListItem이 없으면 더 상위로
|
|
|
|
|
|
current = current.parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 루트까지 탐색해도 TreeListItem을 찾지 못함 (이 객체가 루트)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 입력 처리 (Input Handling)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 아이템 클릭 시 호출됩니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
///
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// - 등록된 OnClickAction 실행(있으면)
|
|
|
|
|
|
/// - Ctrl/Shift 상태를 읽어 TreeList에 전달
|
|
|
|
|
|
/// (일반/토글/범위 선택)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnItemClicked()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1️. 데이터에 등록된 클릭 액션 실행 (있으면)
|
|
|
|
|
|
data.OnClickAction?.Invoke(data);
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. Ctrl 키 상태 감지
|
|
|
|
|
|
// LeftControl(왼쪽) 또는 RightControl(오른쪽) 중 하나라도 누르고 있으면 true
|
|
|
|
|
|
bool ctrlPressed = Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
|
|
|
|
|
|
|
|
|
|
|
|
// 3️. Shift 키 상태 감지
|
|
|
|
|
|
bool shiftPressed = Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
|
|
|
|
|
|
|
|
|
|
|
|
// 4️. 부모 TreeList에 클릭 정보 전달
|
|
|
|
|
|
// TreeList는 이 정보를 받아서 선택 로직을 처리합니다.
|
|
|
|
|
|
// (단일 선택 / 다중 선택 / 범위 선택 등)
|
2025-10-30 18:36:26 +09:00
|
|
|
|
treeList.OnItemClicked(data, ctrlPressed, shiftPressed);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 펼침/접힘 (Expand/Collapse)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 자식의 펼침/접힘을 토글합니다.
|
|
|
|
|
|
/// (중복 입력은 애니메이션 종료까지 무시)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
public void ToggleChild()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1️. 애니메이션 진행 중이면 중복 호출 방지
|
|
|
|
|
|
if (isAnimating) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 플래그 설정: 애니메이션 시작
|
2025-10-28 15:36:55 +09:00
|
|
|
|
isAnimating = true;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 2️. 펼침/접힘 상태 토글
|
|
|
|
|
|
// ! 연산자: 반대로 변경 (true → false, false → true)
|
|
|
|
|
|
data!.IsExpanded = !data.IsExpanded;
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 펼침/접힘 화살표 회전과 컨테이너 표시를 갱신합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// <param name="duration">회전 애니메이션 시간(초). 0이면 즉시 적용.</param>
|
2025-10-30 18:36:26 +09:00
|
|
|
|
internal void SetExpand(float duration = 0.0f)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// 펼침 버튼
|
|
|
|
|
|
childExpand.gameObject.SetActive(data!.Children.Count > 0);
|
|
|
|
|
|
childContainer.SetActive(data!.Children.Count > 0 && data!.IsExpanded);
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// DORotate(목표 각도, 지속 시간)
|
|
|
|
|
|
// IsExpanded가 true면 0도 (▼), false면 90도 (▶)
|
|
|
|
|
|
childExpand.transform.DOKill();
|
|
|
|
|
|
childExpand.transform.DORotate(new Vector3(0, 0, data!.IsExpanded ? 0 : 90), duration)
|
|
|
|
|
|
.OnComplete(() =>
|
|
|
|
|
|
{
|
|
|
|
|
|
// 4️. 애니메이션이 완료되면 플래그 리셋
|
|
|
|
|
|
// 이제 다시 ToggleChild() 호출 가능
|
|
|
|
|
|
isAnimating = false;
|
2025-10-31 19:55:14 +09:00
|
|
|
|
LayoutRebuilder.ForceRebuildLayoutImmediate(transform as RectTransform);
|
2025-10-30 18:36:26 +09:00
|
|
|
|
});
|
2025-10-28 15:36:55 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#region 자식 관리 (Child Management)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 새로운 자식 아이템을 추가하고 UI를 갱신합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// <param name="data">추가할 자식 데이터</param>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
public void AddChild(TreeListItemData data)
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1️. 새 자식 UI 생성
|
2025-10-28 15:36:55 +09:00
|
|
|
|
CreateItem(data);
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 2️. 자식 컨테이너 활성화 (표시)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
childContainer.SetActive(true);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 3️. 0.3초에 걸쳐 펼침 애니메이션 실행
|
|
|
|
|
|
// 사용자가 새 자식이 추가되었음을 자연스럽게 인식
|
2025-10-28 15:36:55 +09:00
|
|
|
|
SetExpand(0.3f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 자식 데이터를 받아 UI TreeListItem을 생성합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// <param name="data">생성할 아이템 데이터</param>
|
|
|
|
|
|
/// <returns>생성된 TreeListItem</returns>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
protected TreeListItem CreateItem(TreeListItemData data)
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1️. 프리팹을 복제해서 새로운 TreeListItem 생성
|
|
|
|
|
|
// Instantiate<T>(원본, 부모, 옵션)
|
2025-10-30 18:36:26 +09:00
|
|
|
|
// treeList.ItemPrefab: UI 아이템 템플릿
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// childRoot: 새 아이템의 부모 Transform
|
|
|
|
|
|
TreeListItem item = GameObject.Instantiate<TreeListItem>(
|
2025-10-30 18:36:26 +09:00
|
|
|
|
treeList.ItemPrefab, // 복제할 프리팹
|
2025-10-28 20:10:51 +09:00
|
|
|
|
childRoot // 부모로 배치할 위치
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. 생성된 아이템 초기화
|
2025-10-30 18:36:26 +09:00
|
|
|
|
item.Init(data, treeList, treeList.DragDropManager);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 3️. 생성된 아이템 반환
|
2025-10-28 15:36:55 +09:00
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 제거 (Deletion)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// 이벤트 구독 해제, 애니메이션 중지 후 GameObject를 삭제합니다.
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// <param name="deleteData">데이터도 삭제할지 여부</param>
|
2025-10-30 18:36:26 +09:00
|
|
|
|
public void Delete(bool deleteData = false)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1️. 데이터 변경 이벤트 구독 해제
|
|
|
|
|
|
if (data != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// -= 연산자: 이벤트에서 리스너 제거
|
|
|
|
|
|
data.OnDataChanged -= OnDataChanged;
|
|
|
|
|
|
data.OnSelectionChanged -= OnSelectionChanged;
|
2025-10-31 19:55:14 +09:00
|
|
|
|
data.Dispose();
|
2025-10-30 18:36:26 +09:00
|
|
|
|
data = null;
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. 버튼 클릭 이벤트 구독 해제
|
|
|
|
|
|
if (itemButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
itemButton.onClick.RemoveListener(OnItemClicked);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-29 20:12:11 +09:00
|
|
|
|
if (childExpand != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
childExpand.transform.DOKill(); // ✅ 진행 중인 회전 애니메이션 중단
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 3️. 이 GameObject 삭제
|
|
|
|
|
|
// 게임 실행 중에 오브젝트를 제거합니다.
|
2025-10-28 15:36:55 +09:00
|
|
|
|
GameObject.Destroy(gameObject);
|
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-10-31 19:55:14 +09:00
|
|
|
|
/// Unity 파괴 시점에 정리합니다. (중복 정리 방지)
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1️. 데이터 변경 이벤트 구독 해제
|
|
|
|
|
|
if (data != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
data.OnDataChanged -= OnDataChanged;
|
|
|
|
|
|
data.OnSelectionChanged -= OnSelectionChanged;
|
2025-10-30 18:36:26 +09:00
|
|
|
|
data.Dispose();
|
|
|
|
|
|
data = null;
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. 버튼 클릭 이벤트 구독 해제
|
|
|
|
|
|
if (itemButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
itemButton.onClick.RemoveListener(OnItemClicked);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
2025-10-28 15:36:55 +09:00
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
}
|