2025-10-28 15:36:55 +09:00
|
|
|
|
#nullable enable
|
|
|
|
|
|
using DG.Tweening;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.UI;
|
|
|
|
|
|
|
|
|
|
|
|
namespace UVC.UI.List.Tree
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 트리 리스트의 개별 아이템을 표시하고 관리하는 UI 클래스입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 역할:
|
|
|
|
|
|
/// 1. 아이템의 이름을 화면에 표시
|
|
|
|
|
|
/// 2. 자식 아이템 펼침/접힘 기능 관리
|
|
|
|
|
|
/// 3. 아이템 선택/선택 해제 표시
|
|
|
|
|
|
/// 4. 사용자 입력(클릭, 키) 처리
|
|
|
|
|
|
/// 5. 데이터 변경 감지 및 UI 업데이트
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 구조:
|
|
|
|
|
|
/// 📦 TreeListItem (이 클래스)
|
|
|
|
|
|
/// ├─ 📝 valueText: 아이템 이름 표시 (TextMeshPro)
|
|
|
|
|
|
/// ├─ 🔘 childExpand: 펼침/접힘 버튼
|
|
|
|
|
|
/// ├─ 📦 childContainer: 자식 아이템들을 담는 컨테이너
|
|
|
|
|
|
/// ├─ 🎨 selectedBg: 선택됨 표시 배경
|
|
|
|
|
|
/// └─ 🔘 itemButton: 클릭 감지 버튼
|
|
|
|
|
|
///
|
|
|
|
|
|
/// MonoBehaviour란?
|
|
|
|
|
|
/// Unity의 모든 GameObject가 가져야 할 기본 클래스입니다.
|
|
|
|
|
|
/// Update, OnDestroy 같은 Unity의 생명주기 메서드를 사용할 수 있습니다.
|
|
|
|
|
|
/// </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>
|
|
|
|
|
|
/// 이 아이템을 관리하는 부모 TreeList입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 용도:
|
|
|
|
|
|
/// - 선택 상태 변경 시 TreeList에 알림
|
|
|
|
|
|
/// - 새 자식 아이템 생성 시 프리팹 가져오기
|
|
|
|
|
|
/// - 키보드 입력(Ctrl, Shift) 상태 전달
|
|
|
|
|
|
///
|
|
|
|
|
|
/// [SerializeField]란?
|
|
|
|
|
|
/// Inspector에서 이 값을 직접 할당할 수 있게 해줍니다.
|
|
|
|
|
|
/// (private이지만 Unity가 특별히 접근 가능)
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected TreeList control;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 이 아이템의 이름을 표시하는 텍스트 UI입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// TMPro.TextMeshProUGUI란?
|
|
|
|
|
|
/// TextMeshPro는 Unity의 고급 텍스트 시스템입니다.
|
|
|
|
|
|
/// 일반 Text보다 더 예쁘고 빠릅니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 사용:
|
|
|
|
|
|
/// valueText.text = "새로운 이름"; // 화면에 표시되는 텍스트 변경
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected TMPro.TextMeshProUGUI valueText;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 트리의 자식을 펼침/접힘하는 화살표 버튼입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 특징:
|
|
|
|
|
|
/// - 자식이 없으면 숨겨집니다
|
|
|
|
|
|
/// - 자식이 펼쳐지면 ▼ 모양
|
|
|
|
|
|
/// - 자식이 접혀있으면 ▶ 모양
|
|
|
|
|
|
/// - 클릭 시 ToggleChild() 메서드 호출
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 회전 애니메이션:
|
|
|
|
|
|
/// DORotate()를 사용해 부드럽게 회전합니다.
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Button childExpand;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 모든 자식 아이템들을 담는 컨테이너 GameObject입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 역할:
|
|
|
|
|
|
/// - 자식 아이템들을 묶음으로 보관
|
|
|
|
|
|
/// - 펼침/접힘 시 이 컨테이너 전체를 표시/숨김
|
|
|
|
|
|
/// - SetActive(true/false)로 온/오프 제어
|
|
|
|
|
|
///
|
|
|
|
|
|
/// GameObject란?
|
|
|
|
|
|
/// Unity의 모든 객체(씬의 모든 것)의 기본 단위입니다.
|
|
|
|
|
|
/// 게임 오브젝트는 여러 컴포넌트를 가질 수 있습니다.
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected GameObject childContainer;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 자식 아이템들이 실제로 배치되는 부모 Transform입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// RectTransform란?
|
|
|
|
|
|
/// UI 요소의 위치, 크기, 회전을 관리하는 컴포넌트입니다.
|
|
|
|
|
|
/// Canvas 아래의 모든 UI 요소가 RectTransform을 가집니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 용도:
|
|
|
|
|
|
/// - childRoot.childCount: 현재 몇 개의 자식이 있는지 확인
|
|
|
|
|
|
/// - new TreeListItem을 Instantiate할 때 부모로 지정
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 구조 예:
|
|
|
|
|
|
/// childRoot (이것)
|
|
|
|
|
|
/// ├─ 자식1 (TreeListItem)
|
|
|
|
|
|
/// ├─ 자식2 (TreeListItem)
|
|
|
|
|
|
/// └─ 자식3 (TreeListItem)
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected RectTransform childRoot;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 아이템이 선택되었을 때 배경으로 표시되는 이미지입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 예: 파일 탐색기에서 파일을 선택했을 때 파일 이름 뒤의 파란 배경
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// - IsSelected가 true면 selectedBg.gameObject.SetActive(true)
|
|
|
|
|
|
/// - IsSelected가 false면 selectedBg.gameObject.SetActive(false)
|
|
|
|
|
|
/// - 모든 레벨의 선택된 아이템의 selectedBg 왼쪽이 정렬됨
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Image selectedBg;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 아이템 전체를 클릭 가능하게 하는 버튼 컴포넌트입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Button이란?
|
|
|
|
|
|
/// 사용자가 클릭하면 onClick 이벤트를 발생시킵니다.
|
|
|
|
|
|
/// 이 경우 OnItemClicked() 메서드가 호출됩니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// onClick.AddListener는?
|
|
|
|
|
|
/// 버튼이 클릭되면 리스너 함수를 호출하라는 뜻입니다.
|
|
|
|
|
|
/// 예: itemButton.onClick.AddListener(OnItemClicked);
|
|
|
|
|
|
/// → itemButton을 클릭하면 OnItemClicked() 실행
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[SerializeField]
|
|
|
|
|
|
protected Button itemButton;
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 데이터 필드 (Data Fields)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 이 UI 아이템이 표시하는 데이터 객체입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 구조:
|
|
|
|
|
|
/// TreeListItem (UI - 화면에 보이는 것)
|
|
|
|
|
|
/// ↓
|
|
|
|
|
|
/// TreeListItemData (데이터 - 실제 정보)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 관계:
|
|
|
|
|
|
/// - TreeListItemData에 이름, 자식 목록, 선택 상태 등이 저장됨
|
|
|
|
|
|
/// - TreeListItem은 이 데이터를 화면에 표시하고 상호작용 처리
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 예:
|
|
|
|
|
|
/// data.Name = "폴더"
|
|
|
|
|
|
/// → valueText.text = "폴더"로 화면에 표시
|
|
|
|
|
|
///
|
|
|
|
|
|
/// data.IsSelected = true
|
|
|
|
|
|
/// → selectedBg가 활성화되어 선택 표시
|
|
|
|
|
|
///
|
|
|
|
|
|
/// ? 연산자 (nullable):
|
|
|
|
|
|
/// data는 null일 수 있습니다.
|
|
|
|
|
|
/// 따라서 사용 전에 null 체크를 해야 합니다.
|
|
|
|
|
|
/// if (data == null) return;
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
protected TreeListItemData? data;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// data 속성을 읽기 전용으로 공개합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 문법:
|
|
|
|
|
|
/// public TreeListItemData? Data => data;
|
|
|
|
|
|
/// = getter 전용 프로퍼티
|
|
|
|
|
|
/// = 외부에서 이 값을 읽을 수 있지만 변경할 수 없음
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
|
/// if (treeItem.Data != null)
|
|
|
|
|
|
/// {
|
|
|
|
|
|
/// Debug.Log(treeItem.Data.Name); // 읽기만 가능
|
|
|
|
|
|
/// }
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public TreeListItemData? Data => data;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 펼침/접힘 애니메이션이 진행 중인지를 나타내는 플래그입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 용도: 애니메이션이 끝나기 전에 다시 클릭하는 것을 방지합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 시나리오:
|
|
|
|
|
|
/// 1. 사용자가 화살표 버튼 클릭
|
|
|
|
|
|
/// 2. isAnimating = true (애니메이션 시작)
|
|
|
|
|
|
/// 3. 0.3초 동안 화살표가 회전
|
|
|
|
|
|
/// 4. 애니메이션 완료 → isAnimating = false
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 중간에 다시 클릭해도:
|
|
|
|
|
|
/// if (isAnimating) return; ← 여기서 무시됨
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 이렇게 하는 이유: 애니메이션이 겹치면 버그가 발생할 수 있음
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
protected bool isAnimating = false;
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 초기화 (Initialization)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 이 TreeListItem을 초기화합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - 새로운 TreeListItem이 생성될 때
|
|
|
|
|
|
/// - Instantiate 직후 에 Init()이 호출됨
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 초기화 과정:
|
|
|
|
|
|
/// 1. UI 요소 설정 (이름 표시, 이벤트 연결)
|
|
|
|
|
|
/// 2. 자식이 있으면 자식 UI 생성
|
|
|
|
|
|
/// 3. 데이터 변경 이벤트 구독
|
|
|
|
|
|
/// 4. 버튼 클릭 이벤트 구독
|
|
|
|
|
|
/// 5. 선택 상태 UI 업데이트
|
|
|
|
|
|
/// 6. 선택된 배경 위치 정렬
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
|
/// - data: 표시할 데이터
|
|
|
|
|
|
/// - control: 부모 TreeList (선택 관리, 클릭 처리 등)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
|
/// var item = Instantiate(prefab);
|
|
|
|
|
|
/// item.Init(treeData, treeList);
|
|
|
|
|
|
/// </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-28 15:36:55 +09:00
|
|
|
|
this.control = control;
|
|
|
|
|
|
this.data = data;
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// 자식이 없는 경우
|
|
|
|
|
|
if (data.Children.Count == 0)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 펼침 버튼 숨기기 (자식이 없으니까 펼칠 게 없음)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
childExpand.gameObject.SetActive(false);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 자식 컨테이너도 숨기기
|
2025-10-28 15:36:55 +09:00
|
|
|
|
childContainer.SetActive(false);
|
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 자식이 있는 경우
|
2025-10-28 15:36:55 +09:00
|
|
|
|
else
|
|
|
|
|
|
{
|
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-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 펼침 버튼과 컨테이너 표시
|
2025-10-28 15:36:55 +09:00
|
|
|
|
childExpand.gameObject.SetActive(true);
|
|
|
|
|
|
childContainer.SetActive(true);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 화살표 방향 설정 (초기에는 펼쳐짐)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
SetExpand();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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>
|
|
|
|
|
|
/// 데이터가 변경되었을 때 호출되는 메서드입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출되는 경우:
|
|
|
|
|
|
/// 1. data.Name이 변경됨
|
|
|
|
|
|
/// 2. data.Children이 추가/제거됨
|
|
|
|
|
|
/// 3. data.IsExpanded가 변경됨
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 역할:
|
|
|
|
|
|
/// - 화면에 표시된 내용과 데이터를 동기화
|
|
|
|
|
|
/// - 필요한 UI 컴포넌트 업데이트
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 데이터 바인딩이란?
|
|
|
|
|
|
/// 데이터가 변경되면 UI가 자동으로 업데이트되는 패턴입니다.
|
|
|
|
|
|
/// 이렇게 하면 데이터와 UI를 동기화 유지하기 쉽습니다.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnDataChanged(TreeListItemData changedData)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 이름이 변경된 경우
|
|
|
|
|
|
if (valueText.text != data.Name)
|
|
|
|
|
|
{
|
|
|
|
|
|
valueText.text = data.Name;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 자식 목록이 변경된 경우
|
|
|
|
|
|
// UI의 자식 개수와 데이터의 자식 개수 비교
|
|
|
|
|
|
int currentChildCount = childRoot.childCount;
|
|
|
|
|
|
if (currentChildCount != data.Children.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 자식이 추가된 경우
|
|
|
|
|
|
if (data.Children.Count > currentChildCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 새로운 자식들을 UI로 생성
|
|
|
|
|
|
for (int i = currentChildCount; i < data.Children.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
CreateItem(data.Children[i]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
// 자식이 제거된 경우
|
|
|
|
|
|
else if (data.Children.Count < currentChildCount)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 필요에 따라 UI 아이템 제거 처리
|
|
|
|
|
|
while (childRoot.childCount > data.Children.Count)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 마지막 자식부터 하나씩 삭제
|
|
|
|
|
|
Destroy(childRoot.GetChild(childRoot.childCount - 1).gameObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 펼침 버튼 표시 여부 결정
|
|
|
|
|
|
// 자식이 있으면 버튼 표시, 없으면 숨김
|
|
|
|
|
|
childExpand.gameObject.SetActive(data.Children.Count > 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 자식이 있고 컨테이너가 비활성화 상태면 활성화
|
|
|
|
|
|
if (data.Children.Count > 0 && !childContainer.activeSelf)
|
|
|
|
|
|
{
|
|
|
|
|
|
childContainer.SetActive(true);
|
|
|
|
|
|
SetExpand();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 선택 상태 관리 (Selection Management)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 아이템의 선택 상태가 변경되었을 때 호출됩니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시기:
|
|
|
|
|
|
/// - data.IsSelected = true/false 일 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// - UpdateSelectionUI() 호출해서 화면 업데이트
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
|
/// - changedData: 변경된 데이터 (이 경우 항상 this.data와 같음)
|
|
|
|
|
|
/// - isSelected: 새로운 선택 상태 (true = 선택됨, false = 해제됨)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnSelectionChanged(TreeListItemData changedData, bool isSelected)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 선택 상태 UI 업데이트 (배경 표시/숨김)
|
|
|
|
|
|
UpdateSelectionUI();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 선택 상태에 따라 UI를 업데이트합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// - data.IsSelected = true → selectedBg 표시 (배경 보이기)
|
|
|
|
|
|
/// - data.IsSelected = false → selectedBg 숨김 (배경 숨기기)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 예시:
|
|
|
|
|
|
/// data.IsSelected = true
|
|
|
|
|
|
/// ↓
|
|
|
|
|
|
/// selectedBg.gameObject.SetActive(true)
|
|
|
|
|
|
/// ↓
|
|
|
|
|
|
/// 화면에 파란 배경이 나타남
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void UpdateSelectionUI()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
// IsSelected 상태에 따라 배경 표시/숨김
|
|
|
|
|
|
selectedBg.gameObject.SetActive(data.IsSelected);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 위치 정렬 (Position Alignment)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 모든 선택 배경(selectedBg)의 왼쪽 위치를 정렬합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 목표:
|
|
|
|
|
|
/// 📌 폴더1
|
|
|
|
|
|
/// 📌├─ 파일1
|
|
|
|
|
|
/// 📌├─ 파일2
|
|
|
|
|
|
/// 📌└─ 폴더2
|
|
|
|
|
|
/// 📌├─ 파일3
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 모든 선택 배경의 왼쪽이 📌 위치에서 시작하도록 합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 이렇게 하는 이유:
|
|
|
|
|
|
/// - 깔끔한 UI 표현
|
|
|
|
|
|
/// - 다중 레벨 트리에서 시각적 일관성 유지
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 좌표 변환:
|
|
|
|
|
|
/// - 월드 좌표(World Coordinate): 게임 전체에서의 절대 위치
|
|
|
|
|
|
/// - 로컬 좌표(Local Coordinate): 부모 기준 상대 위치
|
|
|
|
|
|
/// - TransformPoint: 로컬 좌표를 월드 좌표로 변환
|
|
|
|
|
|
/// </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>
|
|
|
|
|
|
/// 최상위 부모(루트)의 TreeListItem을 찾습니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 트리 구조:
|
|
|
|
|
|
/// TreeListItem1 (루트)
|
|
|
|
|
|
/// ├─ TreeListItem2 (부모)
|
|
|
|
|
|
/// │ └─ TreeListItem3 (자식 ← 이 메서드를 호출하면)
|
|
|
|
|
|
/// └─ TreeListItem4
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 반환값: TreeListItem1
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// 1. 현재 객체의 부모 확인
|
|
|
|
|
|
/// 2. 부모가 TreeListItem을 가지는지 확인
|
|
|
|
|
|
/// 3. 가지면 그 부모의 GetRootTreeListItem() 재귀 호출
|
|
|
|
|
|
/// 4. 루트에 도달할 때까지 반복
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 재귀(Recursion)란?
|
|
|
|
|
|
/// 함수가 자기 자신을 호출하는 것입니다.
|
|
|
|
|
|
/// 계층 구조를 탐색하는 데 효과적입니다.
|
|
|
|
|
|
/// </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>
|
|
|
|
|
|
/// 아이템을 클릭했을 때 호출됩니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - itemButton.onClick 이벤트 발생 시
|
|
|
|
|
|
/// = 사용자가 이 아이템을 마우스로 클릭했을 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 처리 순서:
|
|
|
|
|
|
/// 1. 데이터의 OnClickAction 실행 (있으면)
|
|
|
|
|
|
/// 2. Ctrl, Shift 키 상태 감지
|
|
|
|
|
|
/// 3. TreeList에 클릭 정보 전달 (다중 선택 로직)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 키 입력:
|
|
|
|
|
|
/// - Ctrl 클릭: 현재 선택을 유지하면서 이 아이템 토글 (다중 선택)
|
|
|
|
|
|
/// - Shift 클릭: 마지막 선택부터 이 아이템까지 범위 선택
|
|
|
|
|
|
/// - 일반 클릭: 이 아이템만 선택
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 파일 탐색기와 동일한 동작:
|
|
|
|
|
|
/// Windows 탐색기를 생각하면 쉽습니다.
|
|
|
|
|
|
/// - 클릭: 한 파일만 선택
|
|
|
|
|
|
/// - Ctrl+클릭: 여러 파일 선택 (기존 선택 유지)
|
|
|
|
|
|
/// - Shift+클릭: 범위 선택
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnItemClicked()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (data == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 1️. 데이터에 등록된 클릭 액션 실행 (있으면)
|
|
|
|
|
|
// ?. 연산자: null이면 실행 안 함
|
|
|
|
|
|
// 예: 펼침/접힘 버튼 클릭 시 자동으로 호출되는 액션
|
|
|
|
|
|
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는 이 정보를 받아서 선택 로직을 처리합니다.
|
|
|
|
|
|
// (단일 선택 / 다중 선택 / 범위 선택 등)
|
|
|
|
|
|
control.OnItemClicked(data, ctrlPressed, shiftPressed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 펼침/접힘 (Expand/Collapse)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 자식 아이템의 펼침/접힘을 토글합니다. (펼쳐있으면 접고, 접혀있으면 펼침)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - childExpand 버튼을 클릭했을 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// 1. 애니메이션이 진행 중이면 무시 (중복 클릭 방지)
|
|
|
|
|
|
/// 2. data.IsExpanded 토글 (true ↔ false)
|
|
|
|
|
|
/// 3. childContainer 활성화/비활성화
|
|
|
|
|
|
/// 4. 0.3초에 걸쳐 화살표 회전 애니메이션 실행
|
|
|
|
|
|
///
|
|
|
|
|
|
/// UI 피드백:
|
|
|
|
|
|
/// 애니메이션이 있으면 사용자가 반응을 확인할 수 있습니다.
|
|
|
|
|
|
/// 즉시 완료되는 것보다 더 자연스럽고 좋은 경험입니다.
|
|
|
|
|
|
/// </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
|
|
|
|
// 3️. 자식 컨테이너 표시/숨김
|
|
|
|
|
|
// IsExpanded가 true면 표시, false면 숨김
|
|
|
|
|
|
childContainer.SetActive(data.IsExpanded);
|
2025-10-28 15:36:55 +09:00
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 4️. 0.3초에 걸쳐 펼침/접힘 애니메이션 실행
|
2025-10-28 15:36:55 +09:00
|
|
|
|
SetExpand(0.3f);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 펼침/접힘 화살표의 회전 애니메이션을 실행합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 파라미터:
|
|
|
|
|
|
/// - duration: 애니메이션 지속 시간 (초)
|
|
|
|
|
|
/// 기본값 0.0 = 즉시 완료 (애니메이션 없음)
|
|
|
|
|
|
/// 0.3 = 0.3초에 걸쳐 회전
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// - 펼쳐짐 (IsExpanded=true): 화살표를 0도로 회전 (▼ 모양)
|
|
|
|
|
|
/// - 접혀짐 (IsExpanded=false): 화살표를 90도로 회전 (▶ 모양)
|
|
|
|
|
|
/// - 애니메이션 완료 후 isAnimating 플래그 리셋
|
|
|
|
|
|
///
|
|
|
|
|
|
/// DORotate란? (Tweening 라이브러리)
|
|
|
|
|
|
/// 부드럽게 회전하는 애니메이션을 쉽게 만들어줍니다.
|
|
|
|
|
|
/// duration 시간에 걸쳐 지정된 각도까지 회전합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// OnComplete란?
|
|
|
|
|
|
/// 애니메이션이 완료되면 호출되는 콜백입니다.
|
|
|
|
|
|
/// 람다 식(=>)으로 익명 함수를 정의합니다.
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
private void SetExpand(float duration = 0.0f)
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1️. 자식이 있는지 확인해서 펼침 버튼 표시 여부 결정
|
2025-10-28 15:36:55 +09:00
|
|
|
|
childExpand.gameObject.SetActive(childRoot.childCount > 0);
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
// 2️. 자식이 있는 경우에만 애니메이션 실행
|
2025-10-28 15:36:55 +09:00
|
|
|
|
if (childRoot.childCount > 0)
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// UI 상태와 데이터 동기화
|
|
|
|
|
|
if (data != null) data.IsExpanded = childContainer.activeSelf == true;
|
|
|
|
|
|
// 3️. 화살표 회전 애니메이션 실행
|
|
|
|
|
|
// DORotate(목표 각도, 지속 시간)
|
|
|
|
|
|
// IsExpanded가 true면 0도 (▼), false면 90도 (▶)
|
|
|
|
|
|
childExpand.transform.DORotate(new Vector3(0, 0, data.IsExpanded ? 0 : 90), duration)
|
2025-10-28 15:36:55 +09:00
|
|
|
|
.OnComplete(() =>
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 4️. 애니메이션이 완료되면 플래그 리셋
|
|
|
|
|
|
// 이제 다시 ToggleChild() 호출 가능
|
2025-10-28 15:36:55 +09:00
|
|
|
|
isAnimating = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 자식이 없으면 IsExpanded는 항상 false
|
|
|
|
|
|
if (data != null) data.IsExpanded = false;
|
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>
|
|
|
|
|
|
/// 새로운 자식 아이템을 추가합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - 프로그램이 런타임에 새 자식을 추가할 때
|
|
|
|
|
|
/// - 예: 폴더에 파일을 추가했을 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// 1. 새 자식 UI 생성 (CreateItem)
|
|
|
|
|
|
/// 2. 자식 컨테이너 활성화
|
|
|
|
|
|
/// 3. 펼침 애니메이션 (자동으로 펼침)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
|
/// - data: 추가할 자식 데이터
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 사용 예:
|
|
|
|
|
|
/// treeItem.AddChild(newChildData);
|
|
|
|
|
|
/// // → 새 자식이 자동으로 UI에 추가되고 펼쳐짐
|
|
|
|
|
|
/// </summary>
|
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>
|
|
|
|
|
|
/// 자식 데이터를 받아 UI TreeListItem으로 생성합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - Init()에서 기존 자식들을 UI로 생성할 때
|
|
|
|
|
|
/// - OnDataChanged()에서 추가된 자식을 UI로 생성할 때
|
|
|
|
|
|
/// - AddChild()에서 새 자식을 추가할 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 동작:
|
|
|
|
|
|
/// 1. control.ItemPrefab를 childRoot 아래에 인스턴스화
|
|
|
|
|
|
/// 2. 생성된 item의 Init() 메서드 호출
|
|
|
|
|
|
/// 3. 생성된 item 반환
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Instantiate란?
|
|
|
|
|
|
/// 프리팹(템플릿)을 복제해서 새로운 객체를 만드는 함수입니다.
|
|
|
|
|
|
/// Instantiate(프리팹, 부모, 복제)
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 매개변수:
|
|
|
|
|
|
/// - data: 생성할 아이템의 데이터
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 반환값:
|
|
|
|
|
|
/// - 생성된 TreeListItem 컴포넌트
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 재귀 구조:
|
|
|
|
|
|
/// CreateItem은 계속 자식을 생성하므로
|
|
|
|
|
|
/// 깊이 있는 트리 구조를 만들 수 있습니다.
|
|
|
|
|
|
/// </summary>
|
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>(원본, 부모, 옵션)
|
|
|
|
|
|
// control.ItemPrefab: UI 아이템 템플릿
|
|
|
|
|
|
// childRoot: 새 아이템의 부모 Transform
|
|
|
|
|
|
TreeListItem item = GameObject.Instantiate<TreeListItem>(
|
|
|
|
|
|
control.ItemPrefab, // 복제할 프리팹
|
|
|
|
|
|
childRoot // 부모로 배치할 위치
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. 생성된 아이템 초기화
|
|
|
|
|
|
item.Init(data, control, control.DragDropManager);
|
|
|
|
|
|
|
|
|
|
|
|
// 3️. 생성된 아이템 반환
|
2025-10-28 15:36:55 +09:00
|
|
|
|
return item;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-28 20:10:51 +09:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 제거 (Deletion)
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 이 TreeListItem과 관련된 모든 리소스를 정리하고 삭제합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - 트리에서 아이템을 제거하고 싶을 때
|
|
|
|
|
|
/// - 프로그램이 명시적으로 아이템을 삭제할 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 정리 작업:
|
|
|
|
|
|
/// 1. 데이터 변경 이벤트 구독 해제
|
|
|
|
|
|
/// 2. 버튼 클릭 이벤트 구독 해제
|
|
|
|
|
|
/// 3. GameObject 삭제
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 왜 구독을 해제하나?
|
|
|
|
|
|
/// - 이벤트를 구독하는 것은 메모리 연결을 만듭니다.
|
|
|
|
|
|
/// - 해제하지 않으면 메모리 누수가 발생할 수 있습니다.
|
|
|
|
|
|
/// - 따라서 삭제 전에 반드시 해제해야 합니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 메모리 누수란?
|
|
|
|
|
|
/// 불필요한 메모리가 해제되지 않고 계속 사용되는 문제입니다.
|
|
|
|
|
|
/// 게임이 계속 실행되면서 메모리 사용량이 증가해서
|
|
|
|
|
|
/// 결국 게임이 느려지거나 충돌할 수 있습니다.
|
|
|
|
|
|
/// </summary>
|
2025-10-28 15:36:55 +09:00
|
|
|
|
public void Delete()
|
|
|
|
|
|
{
|
2025-10-28 20:10:51 +09:00
|
|
|
|
// 1️. 데이터 변경 이벤트 구독 해제
|
|
|
|
|
|
if (data != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// -= 연산자: 이벤트에서 리스너 제거
|
|
|
|
|
|
data.OnDataChanged -= OnDataChanged;
|
|
|
|
|
|
data.OnSelectionChanged -= OnSelectionChanged;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2️. 버튼 클릭 이벤트 구독 해제
|
|
|
|
|
|
if (itemButton != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
itemButton.onClick.RemoveListener(OnItemClicked);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3️. 이 GameObject 삭제
|
|
|
|
|
|
// 게임 실행 중에 오브젝트를 제거합니다.
|
2025-10-28 15:36:55 +09:00
|
|
|
|
GameObject.Destroy(gameObject);
|
|
|
|
|
|
}
|
2025-10-28 20:10:51 +09:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// GameObject가 파괴될 때 자동으로 호출되는 Unity 메서드입니다.
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 호출 시점:
|
|
|
|
|
|
/// - Destroy(gameObject) 호출 후 실제 삭제되기 직전
|
|
|
|
|
|
/// - 게임이 종료될 때
|
|
|
|
|
|
/// - 씬이 언로드될 때
|
|
|
|
|
|
///
|
|
|
|
|
|
/// 용도:
|
|
|
|
|
|
/// - 정리 작업 (Clean-up)
|
|
|
|
|
|
/// - 리소스 해제
|
|
|
|
|
|
/// - 이벤트 구독 해제
|
|
|
|
|
|
///
|
|
|
|
|
|
/// Delete()와의 차이:
|
|
|
|
|
|
/// - Delete(): 명시적으로 호출하는 메서드
|
|
|
|
|
|
/// - OnDestroy(): Unity에서 자동으로 호출하는 메서드
|
|
|
|
|
|
/// - 둘 다 같은 정리 작업을 합니다 (중복 방지)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private void OnDestroy()
|
|
|
|
|
|
{
|
|
|
|
|
|
// 1️. 데이터 변경 이벤트 구독 해제
|
|
|
|
|
|
if (data != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
data.OnDataChanged -= OnDataChanged;
|
|
|
|
|
|
data.OnSelectionChanged -= OnSelectionChanged;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|