Files
XRLib/Assets/Scripts/UVC/UI/List/Tree/TreeListItemData.cs

546 lines
20 KiB
C#
Raw Normal View History

2025-10-28 15:36:55 +09:00
#nullable enable
using Gpm.Ui;
using System;
using System.Collections.Generic;
namespace UVC.UI.List.Tree
{
2025-10-28 20:10:51 +09:00
/// <summary>
/// 트리 구조 리스트에서 각 아이템이 갖는 데이터 클래스입니다.
///
/// 트리 구조란? 폴더-파일처럼 상위(부모)와 하위(자식) 관계를 가진 계층 구조입니다.
/// 예: 📁 폴더
/// ├─ 📄 파일1
/// ├─ 📄 파일2
/// └─ 📁 하위폴더
/// └─ 📄 파일3
///
/// 이 클래스는 InfiniteScrollData를 상속하여 UI 스크롤 리스트와 연동됩니다.
/// </summary>
public class TreeListItemData
2025-10-28 15:36:55 +09:00
{
2025-10-28 20:10:51 +09:00
#region (Events)
/// <summary>
/// 데이터가 변경되었을 때 발생하는 이벤트입니다.
///
/// 용도: 이 데이터의 속성(Name, Option 등)이 변경되면
/// UI에 자동으로 반영되도록 통지합니다.
///
/// 사용 예:
/// treeItem.OnDataChanged += (data) => Debug.Log("데이터 변경됨!");
/// </summary>
public Action<TreeListItemData>? OnDataChanged;
/// <summary>
/// 선택 상태가 변경되었을 때 발생하는 이벤트입니다.
///
/// 용도: 사용자가 이 아이템을 클릭해서 선택 또는 선택 해제했을 때
/// 다른 시스템(예: 옵션 창)에 알려줍니다.
///
/// 매개변수:
/// - TreeListItemData: 변경된 아이템 자신
/// - bool: true면 선택됨, false면 선택 해제됨
///
/// 사용 예:
/// treeItem.OnSelectionChanged += (data, isSelected) => {
/// Debug.Log($"{data.Name}이 {(isSelected ? "선택됨" : "해제됨")}");
/// };
/// </summary>
public Action<TreeListItemData, bool>? OnSelectionChanged;
#endregion
#region (Private Fields)
/// <summary>
/// 아이템의 이름을 저장하는 비공개 필드입니다.
///
/// '_' 접두사를 붙인 이유:
/// 실제 데이터는 여기 저장하고, public 프로퍼티(Name)를 통해
/// 접근을 제어합니다. (캡슐화)
///
/// 예: _name = "폴더", Name 프로퍼티로 접근
/// </summary>
private string _name = string.Empty;
/// <summary>
/// 아이템의 추가 옵션 정보를 저장합니다.
///
/// 용도: 읽기 전용, 숨김 속성 등의 추가 설정 정보
/// 예: "readonly", "hidden", "locked" 등
/// </summary>
private string _option = string.Empty;
/// <summary>
/// 이 아이템의 자식들이 펼쳐져 있는지 여부를 나타냅니다.
///
/// true: 자식들이 표시됨 (▼ 펼침 상태)
/// false: 자식들이 숨겨짐 (▶ 접혀있는 상태)
///
/// 자식이 없으면 이 값은 의미가 없습니다.
/// </summary>
private bool _isExpanded = false;
/// <summary>
/// 현재 아이템이 사용자에게 선택되어 있는지를 나타냅니다.
///
/// true: 선택됨 (보통 배경색이 다르게 표시)
/// false: 선택 안 됨 (기본 상태)
///
/// 예: 파일 탐색기에서 파일을 클릭했을 때 그 파일의 _isSelected = true
/// </summary>
private bool _isSelected = false;
/// <summary>
/// 이 아이템의 하위 아이템들을 모두 저장하는 리스트입니다.
///
/// 트리 구조 예:
/// 부모 (이 객체)
/// ├─ 자식1
/// ├─ 자식2
/// └─ 자식3
///
/// _children 리스트에 [자식1, 자식2, 자식3]이 저장됩니다.
/// 자식이 없으면 빈 리스트입니다.
/// </summary>
private List<TreeListItemData> _children = new List<TreeListItemData>();
#endregion
#region (Public Properties)
/// <summary>
/// 아이템의 이름을 가져오거나 설정합니다.
///
/// 동작:
/// - 가져올 때(get): _name의 값을 반환합니다.
/// - 설정할 때(set):
/// 1. 기존 값과 비교해서 정말 달라졌는지 확인
/// 2. 다르면 새 값으로 변경
/// 3. OnDataChanged 이벤트를 발생시켜 UI에 알림
///
/// 이렇게 하는 이유: 같은 값으로 변경되는 불필요한 갱신을 피합니다.
///
/// 사용 예:
/// treeItem.Name = "새로운 이름"; // 자동으로 UI 업데이트
/// string currentName = treeItem.Name; // 이름 읽기
/// </summary>
public string Name
{
get => _name;
set
{
if (_name != value)
{
_name = value;
NotifyDataChanged(); // UI에 변경을 알림
}
}
}
2025-10-28 15:36:55 +09:00
/// <summary>
2025-10-28 20:10:51 +09:00
/// 아이템의 추가 옵션을 가져오거나 설정합니다.
///
/// 동작: Name 프로퍼티와 동일하게 작동합니다.
/// 변경될 때마다 OnDataChanged 이벤트를 발생시킵니다.
///
/// 사용 예:
/// treeItem.Option = "readonly"; // 읽기 전용으로 설정
2025-10-28 15:36:55 +09:00
/// </summary>
2025-10-28 20:10:51 +09:00
public string Option
{
get => _option;
set
{
if (_option != value)
{
_option = value;
NotifyDataChanged();
}
}
}
2025-10-28 15:36:55 +09:00
/// <summary>
2025-10-28 20:10:51 +09:00
/// 이 아이템의 자식들이 펼쳐져 있는지 여부를 가져오거나 설정합니다.
///
/// internal 접근제한자 이유:
/// 이것은 UI 시스템에서만 관리해야 하므로 외부에서 직접 접근할 수 없습니다.
/// (같은 어셈블리 내부에서만 접근 가능)
///
/// true: 자식들이 표시됨 (트리 펼침)
/// false: 자식들이 숨겨짐 (트리 접힘)
///
/// 사용 예: (UI 시스템에서만)
/// treeItem.IsExpanded = true; // 자식들을 표시
2025-10-28 15:36:55 +09:00
/// </summary>
2025-10-28 20:10:51 +09:00
internal bool IsExpanded
{
get => _isExpanded;
set
{
if (_isExpanded != value)
{
_isExpanded = value;
NotifyDataChanged(); // 트리 구조 UI 갱신
}
}
}
2025-10-28 15:36:55 +09:00
/// <summary>
2025-10-28 20:10:51 +09:00
/// 이 아이템이 선택되어 있는지 여부를 가져오거나 설정합니다.
///
/// 중요한 차이점: 다른 프로퍼티는 OnDataChanged를 호출하지만,
/// 이것은 OnSelectionChanged를 호출합니다.
/// 왜? 선택 상태는 UI 갱신이 아니라
/// 선택 이벤트 처리가 필요하기 때문입니다.
///
/// 사용 예:
/// treeItem.IsSelected = true; // 아이템 선택
/// if (treeItem.IsSelected) { ... } // 선택 여부 확인
2025-10-28 15:36:55 +09:00
/// </summary>
2025-10-28 20:10:51 +09:00
public bool IsSelected
{
get => _isSelected;
set
{
if (_isSelected != value)
{
_isSelected = value;
// OnSelectionChanged 이벤트 발생
// 예: "폴더가 선택되었습니다" 같은 처리를 수행
OnSelectionChanged?.Invoke(this, value);
}
}
}
2025-10-28 15:36:55 +09:00
/// <summary>
2025-10-28 20:10:51 +09:00
/// 사용자가 확장/축소 버튼을 클릭했을 때 호출될 함수입니다.
///
/// 용도: 트리의 화살표(▼/▶) 버튼을 클릭했을 때
/// IsExpanded 상태를 변경하는 로직을 실행합니다.
///
/// 누가 등록하나? UI 시스템 (TreeListItem 클래스)
///
/// 사용 예:
/// treeItem.OnClickAction = (data) => {
/// data.IsExpanded = !data.IsExpanded; // 펼침/접힘 토글
/// };
2025-10-28 15:36:55 +09:00
/// </summary>
public Action<TreeListItemData>? OnClickAction;
2025-10-28 20:10:51 +09:00
/// <summary>
/// 이 아이템의 모든 자식 아이템들을 가져오거나 설정합니다.
///
/// internal 접근제한자 이유:
/// 자식 리스트는 AddChild, RemoveChild, ClearChildren 메서드로만
/// 수정되어야 데이터 일관성이 보장됩니다.
///
/// 동작:
/// - 가져올 때(get): _children 리스트 반환
/// - 설정할 때(set):
/// 1. null이면 빈 리스트로 설정 (null 방지)
/// 2. OnDataChanged 이벤트 발생
///
/// ?? 연산자 설명:
/// childrenItemData ?? new List<>()
/// = childrenItemData가 null이면 빈 리스트 사용
/// null이 아니면 childrenItemData 사용
/// </summary>
internal List<TreeListItemData> Children
{
get => _children;
set
{
_children = value ?? new List<TreeListItemData>();
NotifyDataChanged();
}
}
#endregion
2025-10-28 15:36:55 +09:00
2025-10-28 20:10:51 +09:00
#region (Constructors)
2025-10-28 15:36:55 +09:00
2025-10-28 20:10:51 +09:00
/// <summary>
/// 빈 TreeListItemData를 생성합니다.
///
/// 초기값:
/// - Name: 빈 문자열
/// - Option: 빈 문자열
/// - IsExpanded: false (접혀있음)
/// - Children: 빈 리스트 (자식 없음)
///
/// 사용 예:
/// var item = new TreeListItemData();
/// item.Name = "새 폴더";
/// </summary>
public TreeListItemData()
{
_name = string.Empty;
_option = string.Empty;
_isExpanded = false;
_children = new List<TreeListItemData>();
}
/// <summary>
/// 이름과 선택적으로 자식 목록을 지정하여 TreeListItemData를 생성합니다.
///
/// 매개변수:
/// - generalName: 아이템의 이름 (필수)
/// - childrenItemData: 초기 자식 목록 (선택사항, null 가능)
///
/// 초기값:
/// - Name: generalName
/// - Option: 빈 문자열
/// - IsExpanded: false
/// - Children: childrenItemData (null이면 빈 리스트)
///
/// 사용 예:
/// // 간단한 아이템 생성
/// var item1 = new TreeListItemData("폴더");
///
/// // 자식을 포함해서 생성
/// var children = new List<TreeListItemData> { item1 };
/// var parent = new TreeListItemData("부모 폴더", children);
/// </summary>
2025-10-28 15:36:55 +09:00
public TreeListItemData(string generalName, List<TreeListItemData>? childrenItemData = null)
{
2025-10-28 20:10:51 +09:00
_name = generalName;
_option = string.Empty;
_isExpanded = false;
_children = childrenItemData ?? new List<TreeListItemData>();
}
#endregion
#region (Child Management Methods)
/// <summary>
/// 이 아이템에 자식 아이템을 추가합니다.
///
/// 동작:
/// 1. 자식을 _children 리스트에 추가
/// 2. OnDataChanged 이벤트 발생 (UI 트리 구조 갱신)
///
/// 사용 예:
/// parent.AddChild(child); // 부모에 자식 추가
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 ├─ 자식1
/// └─ 자식2 ├─ 자식2
/// └─ 새자식
/// </summary>
public void AddChild(TreeListItemData child)
{
_children.Add(child);
NotifyDataChanged(); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 이 아이템에서 지정된 자식 아이템을 제거합니다.
///
/// 동작:
/// 1. 자식을 _children 리스트에서 제거
/// 2. OnDataChanged 이벤트 발생
///
/// 주의: 같은 이름의 첫 번째 자식만 제거됩니다.
/// (TreeListItemData의 == 연산자가 Name으로 비교하기 때문)
///
/// 사용 예:
/// parent.RemoveChild(child); // 부모에서 자식 제거
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 ├─ 자식1
/// ├─ 자식2
/// └─ 자식3 └─ 자식3
/// (자식2 제거)
/// </summary>
public void RemoveChild(TreeListItemData child)
{
_children.Remove(child);
NotifyDataChanged(); // UI에 트리 구조 변경 알림
}
/// <summary>
/// 이 아이템의 모든 자식을 한 번에 제거합니다.
///
/// 동작:
/// 1. _children 리스트를 완전히 비움
/// 2. OnDataChanged 이벤트 발생
///
/// 주의: 자식들이 메모리에서 삭제되는 것은 아니고,
/// 이 아이템과의 연결만 끊어집니다.
/// (C#의 가비지 컬렉션이 필요 없으면 나중에 정리)
///
/// 사용 예:
/// parent.ClearChildren(); // 모든 자식 제거
///
/// 트리 구조 변화:
/// 변경 전: 변경 후:
/// 부모 부모
/// ├─ 자식1 (모든 자식 제거됨)
/// ├─ 자식2
/// └─ 자식3
/// </summary>
public void ClearChildren()
{
_children.Clear();
NotifyDataChanged(); // UI에 트리 구조 변경 알림
}
#endregion
#region (Internal Methods)
/// <summary>
/// 데이터가 변경되었음을 UI 시스템에 알립니다.
///
/// 동작: OnDataChanged 이벤트를 발생시킵니다.
/// 이를 통해 UI는 자동으로 이 아이템의 정보를 갱신합니다.
///
/// 호출되는 시점:
/// - Name이나 Option이 변경될 때
/// - 자식이 추가/제거될 때
/// - IsExpanded 상태가 변경될 때
///
/// 왜 protected인가?
/// 이 클래스를 상속받은 자식 클래스에서도 호출할 수 있도록 하기 위함입니다.
/// </summary>
internal void NotifyDataChanged()
{
// OnDataChanged가 등록되어 있으면 실행
// ?. 연산자: null이면 실행하지 않음 (null reference exception 방지)
OnDataChanged?.Invoke(this);
}
#endregion
#region (Comparison Operators)
/// <summary>
/// 두 TreeListItemData 객체가 같은지 비교합니다. (== 연산자)
///
/// 비교 기준: Name (아이템의 이름)
/// 즉, 이름이 같으면 같은 아이템으로 간주합니다.
///
/// 비교 로직:
/// 1. 같은 객체인가? (메모리 주소가 같음) → true
/// 2. 둘 다 null이거나 하나가 null? → false (둘 다 null이면 true인데, 1번에서 처리)
/// 3. Name이 같은가? → true/false
///
/// 사용 예:
/// var item1 = new TreeListItemData("파일");
/// var item2 = new TreeListItemData("파일");
/// var item3 = new TreeListItemData("폴더");
///
/// item1 == item2 // true (Name이 "파일"로 같음)
/// item1 == item3 // false (Name이 다름)
/// item1 == null // false
///
/// 주의: 같은 이름이면 같은 아이템으로 취급되므로,
/// 실제로 다른 객체임에도 true가 될 수 있습니다.
/// 이는 의도된 설계입니다.
/// </summary>
public static bool operator ==(TreeListItemData? left, TreeListItemData? right)
{
// 같은 객체인지 확인 (메모리 주소 비교)
if (ReferenceEquals(left, right))
{
return true;
}
// 하나 이상이 null이면 false (ReferenceEquals에서 둘 다 null인 경우는 true 반환)
if (left is null || right is null)
{
return false;
}
// 이름으로 비교
return left.Name == right.Name;
}
/// <summary>
/// 두 TreeListItemData 객체가 다른지 비교합니다. (!= 연산자)
///
/// 동작: == 연산자의 결과를 반대(!)로 반환합니다.
///
/// 사용 예:
/// if (item1 != item2) { ... } // 다른 아이템이면 실행
/// </summary>
public static bool operator !=(TreeListItemData? left, TreeListItemData? right)
{
return !(left == right);
}
#endregion
#region (Object Methods)
/// <summary>
/// 이 객체의 고유한 해시 코드를 반환합니다.
///
/// 용도: 이 객체를 Dictionary나 HashSet 같은 컬렉션에 저장할 때 사용합니다.
///
/// 해시 코드란? 객체를 빠르게 비교/검색하기 위한 고유 숫자입니다.
/// 같은 내용이면 같은 해시 코드를 반환해야 합니다.
///
/// 우리의 기준:
/// Name의 해시 코드 = 이 객체의 해시 코드
/// 왜? == 연산자에서 Name으로 비교하기 때문입니다.
///
/// 사용 예:
/// int hash = item.GetHashCode();
///
/// // Dictionary에 저장
/// Dictionary<TreeListItemData, string> dict = new();
/// dict[item] = "값"; // 내부적으로 GetHashCode() 사용
/// </summary>
public override int GetHashCode()
{
return _name.GetHashCode();
}
/// <summary>
/// 이 객체가 다른 객체와 같은지 비교합니다. (Equals 메서드)
///
/// 용도: 모든 C# 객체는 Equals 메서드를 가집니다.
/// 이 메서드를 오버라이드하여 우리의 비교 로직을 정의합니다.
///
/// 동작:
/// 1. 다른 객체가 TreeListItemData 타입인지 확인
/// 2. 맞으면 == 연산자로 비교 (Name으로 비교)
/// 3. 아니면 false 반환
///
/// 사용 예:
/// var item1 = new TreeListItemData("파일");
/// var item2 = new TreeListItemData("파일");
///
/// item1.Equals(item2) // true
/// item1.Equals("파일") // false (문자열은 다른 타입)
/// item1 == item2 // true (== 연산자와 동일)
///
/// GetHashCode()와 Equals의 관계:
/// - 같은 객체면 같은 해시 코드를 가져야 함
/// - 우리의 경우: Name이 같으면 Equals는 true, 해시 코드도 같음
/// - 이는 일관성 있게 설계되어 있습니다.
/// </summary>
public override bool Equals(object? obj)
{
// obj가 TreeListItemData 타입인지 확인
if (obj is TreeListItemData other)
2025-10-28 15:36:55 +09:00
{
2025-10-28 20:10:51 +09:00
// TreeListItemData면 == 연산자로 비교
return this == other;
2025-10-28 15:36:55 +09:00
}
2025-10-28 20:10:51 +09:00
// 다른 타입이면 false
return 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
}