#nullable enable using Gpm.Ui; using System; using System.Collections.Generic; namespace UVC.UI.List.Tree { /// /// 트리 리스트 아이템의 데이터 모델. /// /// 목적: /// - 이름/옵션/선택/펼침 상태 및 계층(부모-자식) 보유 /// - 상태 변경 시 UI에 알릴 이벤트 제공(데이터 변경/선택 변경) /// - 자식 추가/삽입/복제/재배치/초기화 등 계층 편집 기능 /// /// 이벤트 설계: /// - OnDataChanged(ChangedType type, TreeListItemData changed, int index): /// 데이터/구조 변경 통지. changed는 변동 주체(보통 this 또는 대상 자식), /// index는 삽입/이동 시 사용(해당 없으면 -1). /// - OnSelectionChanged(TreeListItemData data, bool isSelected): /// 선택 상태 변경 통지(선택 UI/로직 처리용). /// /// 동등성/해시: /// - 이름(Name) 기반 비교/해시 코드. 이름이 같으면 같은 개체로 간주(대소문자 구분). /// - 해시/Equals가 이름에 의존하므로 Name 변경 시 컬렉션 키로 사용 중이면 주의. /// /// 주의사항: /// - ClearChildren은 각 자식에 Dispose를 호출하여 이벤트/내부 상태를 정리한 뒤 /// 리스트를 비웁니다(연결만 끊는 수준을 넘어 자식도 정리됨). /// - AddClone계열은 전달된 원본 child를 Dispose한 뒤 복제본을 추가합니다(파괴적). /// public class TreeListItemData : IDisposable { #region 이벤트 (Events) /// /// 데이터/구조 변경 통지 이벤트. /// 시그니처: (ChangedType type, TreeListItemData changed, int index) /// - type: 변경 종류 /// - changed: 변경 대상(보통 this 또는 특정 자식) /// - index: 위치 관련 변경 시 사용(해당 없으면 -1) /// public Action? OnDataChanged; /// /// 선택 상태 변경 통지 이벤트. /// 시그니처: (TreeListItemData data, bool isSelected) /// public Action? OnSelectionChanged; /// /// 아이템 클릭 시 실행할 사용자 정의 동작. /// (예: 속성 패널 열기, 포커스 이동 등. 확장/축소와는 별개) /// public Action? OnClickAction; #endregion #region 내부 필드 (Private Fields) /// 고유 식별자(Id). private readonly Guid _id = Guid.NewGuid(); /// 아이템 이름. private string _name = string.Empty; /// 추가 옵션 문자열. private string _option = string.Empty; /// 자식 펼침 여부. private bool _isExpanded = false; /// 선택 여부. private bool _isSelected = false; /// 부모 private TreeListItemData? _parent; /// 자식 리스트. private List _children = new List(); #endregion #region 공개 프로퍼티 (Public Properties) /// /// 아이템 이름. /// 변경 시 OnDataChanged(Name) 발생. /// public string Name { get => _name; set { if (_name != value) { _name = value; NotifyDataChanged(ChangedType.Name); // UI에 변경을 알림 } } } /// /// 옵션 문자열. /// 변경 시 OnDataChanged(Option) 발생. /// public string Option { get => _option; set { if (_option != value) { _option = value; NotifyDataChanged(ChangedType.Option); } } } /// /// 펼침 상태(같은 어셈블리 내 전용). /// 변경 시 OnDataChanged(Expanded) 발생. /// internal bool IsExpanded { get => _isExpanded; set { if (_isExpanded != value) { _isExpanded = value; NotifyDataChanged(ChangedType.Expanded); // 트리 구조 UI 갱신 } } } /// /// 선택 상태. /// 변경 시 OnSelectionChanged 발생. /// public bool IsSelected { get => _isSelected; set { if (_isSelected != value) { _isSelected = value; // OnSelectionChanged 이벤트 발생 // 예: "폴더가 선택되었습니다" 같은 처리를 수행 OnSelectionChanged?.Invoke(this, value); } } } /// 부모 데이터(내부 전용). internal TreeListItemData? Parent { get => _parent; set => _parent = value; } /// /// 자식 컬렉션(내부 전용). /// set 시 null을 빈 리스트로 대체하고 OnDataChanged(ResetChildren) 발생. /// internal List Children { get => _children; set { _children = value ?? new List(); NotifyDataChanged(ChangedType.ResetChildren); } } /// 고유 식별자(Id). public Guid Id => _id; #endregion #region 생성자 (Constructors) /// /// 기본 생성자. /// 초기값: Name="", Option="", IsExpanded=false, IsSelected=false, Children=[] /// public TreeListItemData() { _name = string.Empty; _option = string.Empty; _isExpanded = false; _isSelected = false; _children = new List(); } /// /// 이름과 초기 자식으로 생성합니다. /// /// 아이템 이름(필수). /// 초기 자식 목록(null 허용). public TreeListItemData(string generalName, List? childrenItemData = null) { _name = generalName; _option = string.Empty; _isExpanded = false; _children = childrenItemData ?? new List(); } #endregion #region 자식 관리 메서드 (Child Management Methods) /// /// 자식을 끝에 추가합니다. /// /// 추가할 자식. public void AddChild(TreeListItemData child) { child.Parent = this; _children.Add(child); NotifyDataChanged(ChangedType.AddChild, child); // UI에 트리 구조 변경 알림 } /// /// 자식을 지정 인덱스에 삽입합니다. /// /// 삽입할 자식. /// 삽입 인덱스(0 기반). public void AddChildAt(TreeListItemData child, int index) { child._parent = this; _children.Insert(index, child); NotifyDataChanged(ChangedType.AddAtChild, child, index); // UI에 트리 구조 변경 알림 } /// /// 전달된 자식을 복제하여 추가합니다. /// 주의: 원본 child는 Dispose됩니다(파괴적). /// /// 복제 및 대체할 원본 자식. public void AddCloneChild(TreeListItemData child) { NotifyDataChanged(ChangedType.AddCloneChild, child); // UI에 트리 구조 변경 알림 } /// /// 전달된 자식을 복제하여 지정 인덱스에 삽입합니다. /// 주의: 원본 child는 Dispose됩니다(파괴적). /// /// 복제 및 대체할 원본 자식. /// 삽입 인덱스(0 기반). public void AddCloneAtChild(TreeListItemData child, int index) { NotifyDataChanged(ChangedType.AddCloneAtChild, child, index); // UI에 트리 구조 변경 알림 } /// /// 지정 자식을 지정 인덱스 위치에 삽입합니다. /// 주의: 기존 위치에서 제거하지 않으므로 "이동"이 아닌 "삽입" 동작입니다. /// (호출 측에서 기존 위치 정리는 별도로 해야 합니다) /// /// 삽입할 기존 자식 참조. /// 삽입 인덱스(0 기반). public void SwapChild(TreeListItemData child, int index) { _children.Insert(index, child); NotifyDataChanged(ChangedType.SwapChild, child, index); // UI에 트리 구조 변경 알림 } public void RemoveChild(TreeListItemData child) { if (_children.Remove(child)) { child._parent = null; NotifyDataChanged(ChangedType.RemoveChild, child); // UI에 트리 구조 변경 알림 } } /// /// 모든 자식을 제거하고 정리합니다. /// 각 자식에 대해 Parent=null 및 Dispose()를 호출한 뒤 리스트를 비웁니다. /// public void ClearChildren() { foreach (var child in _children) { child._parent = null; child.Dispose(); } _children.Clear(); NotifyDataChanged(ChangedType.ResetChildren); // UI에 트리 구조 변경 알림 } #endregion #region 내부 메서드 (Internal Methods) /// /// 변경을 구독자에게 알립니다. /// /// 변경 종류. /// 변경 대상(null이면 this). /// 위치 관련 인덱스(없으면 -1). internal void NotifyDataChanged(ChangedType changedType, TreeListItemData? target = null, int index = -1) { // OnDataChanged가 등록되어 있으면 실행 // ?. 연산자: null이면 실행하지 않음 (null reference exception 방지) OnDataChanged?.Invoke(changedType, (target == null ? this : target), index); } #endregion #region 비교 연산자 (Comparison Operators) /// /// 이름(대소문자 구분, Ordinal) 기반 동등 비교. /// 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; } // 고유 식별자(Id)로 비교 return left.Id == right.Id; } /// /// 이름 기반 비동등 비교. /// public static bool operator !=(TreeListItemData? left, TreeListItemData? right) { return !(left == right); } #endregion #region 객체 메서드 (Object Methods) /// /// 이름 기반 해시 코드 반환. /// public override int GetHashCode() { return Id.GetHashCode(); } /// /// 이름 기반 동등성 비교(== 사용). /// /// 비교 대상. public override bool Equals(object? obj) { // obj가 TreeListItemData 타입인지 확인 if (obj is TreeListItemData other) { // TreeListItemData면 == 연산자로 비교 return this == other; } // 다른 타입이면 false return false; } /// /// 이벤트/참조/자식 컬렉션을 정리합니다. /// - Parent=null, 이벤트 핸들러 해제, Children.Clear() /// - 하위 항목을 재귀적으로 Dispose하지는 않습니다 /// (재귀 정리는 ClearChildren에서 수행). /// public void Dispose() { if (_parent != null && _parent.Children.Contains(this)) _parent.Children.Remove(this); _parent = null; if (OnDataChanged != null) OnDataChanged = null; if (OnSelectionChanged != null) OnSelectionChanged = null; if (OnClickAction != null) OnClickAction = null; if (Children != null) { Children.Clear(); } } /// /// 깊은 복제본을 생성합니다(자식까지 재귀 복제). /// /// 복제된 새 인스턴스. public TreeListItemData CloneWithChild() { TreeListItemData clone = new TreeListItemData(); clone.Name = this.Name; clone.Option = this.Option; clone.IsExpanded = this.IsExpanded; clone.IsSelected = this.IsSelected; foreach (var child in this.Children) { clone.AddChild(child.CloneWithChild()); } return clone; } /// /// 현재 인스턴스의 복사본인 의 새 인스턴스를 생성합니다. /// /// 현재 인스턴스와 동일한 속성 값을 가진 새 객체를 생성합니다. public TreeListItemData Clone() { TreeListItemData clone = new TreeListItemData(); clone.Name = this.Name; clone.Option = this.Option; clone.IsExpanded = this.IsExpanded; clone.IsSelected = this.IsSelected; return clone; } #endregion } /// /// 데이터 변경 종류 열거형. /// public enum ChangedType { Name, Option, Expanded, ResetChildren, AddChild, RemoveChild, AddAtChild, AddCloneChild, AddCloneAtChild, SwapChild, } }