#nullable enable using System; using System.Collections.Generic; using System.Linq; using System.Threading; using Cysharp.Threading.Tasks; using UnityEngine; using UnityEngine.UIElements; using UVC.Util; namespace UVC.UIToolkit { /// /// 이미지가 포함된 그리드/리스트를 표시하는 UIToolkit 컴포넌트입니다. /// PrefabGrid의 UIToolkit 버전입니다. /// /// 개요: /// /// UTKImageList는 Unity UI Toolkit의 ListView를 래핑하여 이미지+텍스트 형태의 /// 아이템 목록을 2열 그리드로 표시합니다. 검색 필터링, 드래그 앤 드롭 기능을 제공합니다. /// /// /// 주요 기능: /// /// 이미지+텍스트 형태의 아이템을 2열 그리드로 표시 /// 실시간 검색 필터링 (3글자 이상) /// 드래그 앤 드롭 지원 /// 가상화를 통한 대량 데이터 성능 최적화 /// /// /// UXML에서 사용: /// /// /// /// /// 코드에서 사용: /// /// var list = root.Q(); /// list.OnItemClick += (item) => Debug.Log($"클릭: {item.itemName}"); /// list.SetData(imageItems); /// /// /// 관련 리소스: /// /// Resources/UIToolkit/List/UTKImageList.uxml - 메인 레이아웃 /// Resources/UIToolkit/List/UTKImageListItem.uxml - 행 템플릿 (2열) /// Resources/UIToolkit/List/UTKImageList.uss - 스타일 /// /// [UxmlElement] public partial class UTKImageList : VisualElement, IDisposable { #region 상수 (Constants) /// 메인 UXML 파일 경로 (Resources 폴더 기준) private const string UXML_PATH = "UIToolkit/List/UTKImageList"; /// 아이템 UXML 파일 경로 (Resources 폴더 기준) private const string ITEM_UXML_PATH = "UIToolkit/List/UTKImageListItem"; /// 최소 검색어 길이 private const int MIN_SEARCH_LENGTH = 2; /// 이미지 로딩 디바운스 시간 (ms) private const int IMAGE_LOAD_DEBOUNCE_MS = 50; /// 한 행에 표시할 아이템 수 private const int ITEMS_PER_ROW = 2; #endregion #region 캐싱된 리소스 (Cached Resources) /// 아이템 UXML 템플릿 (재사용을 위해 캐싱) private VisualTreeAsset? _itemTemplate; #endregion #region UI 컴포넌트 참조 (UI Component References) /// 검색어 입력 필드 private TextField? _searchField; /// Unity UI Toolkit의 ListView 컴포넌트 private ListView? _listView; /// 검색어 지우기 버튼 private Button? _clearButton; /// 검색 결과 건수 라벨 private Label? _searchResultLabel; #endregion #region 내부 데이터 (Internal Data) /// 원본 데이터 (검색 필터 해제 시 복원용) private List _originalData = new(); /// 필터링된 데이터 (검색 결과) private List _filteredData = new(); /// 행 단위로 묶인 데이터 (ListView용) private List _rowData = new(); /// 항목 ID 자동 생성을 위한 시드 값 private int _idSeed = 1; /// 현재 검색 모드 여부 private bool _isSearchMode; /// 이미지 로딩 취소 토큰 소스 (메모리 누수 방지) private CancellationTokenSource? _imageLoadCts; /// 로드된 이미지 스프라이트 캐시 (경로 → 스프라이트) private readonly Dictionary _spriteCache = new(); /// 현재 로딩 중인 이미지 경로 추적 (중복 로딩 방지) private readonly HashSet _loadingImages = new(); /// RowData 객체 풀 (GC 부담 감소) private readonly Queue _rowDataPool = new(); #endregion #region 내부 클래스 (Internal Classes) /// /// 행 데이터를 저장하는 클래스입니다. /// 한 행에 최대 2개의 아이템을 포함합니다. /// private class RowData { public UTKImageListItemData? LeftItem; public UTKImageListItemData? RightItem; } #endregion #region IDisposable private bool _disposed; #endregion #region 공개 속성 (Public Properties) /// /// 드래그 시 이미지가 커서를 따라다니도록 설정합니다. /// 기본값은 true입니다. /// public bool DragImageFollowCursor { get; set; } = true; /// /// 드래그 영역 체크에 사용할 요소를 설정합니다. /// null이면 자신(UTKImageList)의 worldBound를 사용합니다. /// 부모 윈도우 등 다른 요소의 영역을 체크하려면 해당 요소를 설정하세요. /// public VisualElement? DragBoundsElement { get; set; } /// /// 현재 검색어를 가져오거나 설정합니다. /// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다. /// public string SearchQuery { get => _searchField?.value ?? string.Empty; set { if (_searchField != null) _searchField.value = value; } } /// /// 현재 표시 중인 아이템 수를 반환합니다. /// 검색 모드에서는 필터링된 결과 수를, 아니면 전체 수를 반환합니다. /// public int ItemCount => _isSearchMode ? _filteredData.Count : _originalData.Count; #endregion #region 외부 이벤트 (Public Events) /// /// 아이템 클릭 이벤트입니다. /// 아이템을 클릭하면 해당 데이터와 함께 발생합니다. /// public Action? OnItemClick; /// /// 아이템 드롭 이벤트입니다. /// 드래그 앤 드롭이 완료되면 발생합니다. /// public Action? OnItemDrop; /// /// 드래그 시작 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemBeginDrag; /// /// 드래그 중 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemDrag; /// /// 드래그 종료 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// public Action? OnItemEndDrag; /// /// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// 3D 프리팹 미리보기를 표시하는데 사용합니다. /// public Action? OnDragExitList; /// /// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다. /// (아이템 데이터, 화면 좌표) /// 3D 프리팹 미리보기를 숨기는데 사용합니다. /// public Action? OnDragEnterList; #endregion #region 생성자 (Constructor) /// /// UTKImageList 컴포넌트를 초기화합니다. /// UXML 템플릿을 로드하고 내부 컴포넌트를 설정합니다. /// public UTKImageList() { // 1. 메인 UXML 로드 및 복제 var visualTree = Resources.Load(UXML_PATH); if (visualTree == null) { Debug.LogError($"[UTKImageList] UXML not found at: {UXML_PATH}"); return; } visualTree.CloneTree(this); // 2. 아이템 템플릿 로드 (성능: 한 번만 로드하여 재사용) _itemTemplate = Resources.Load(ITEM_UXML_PATH); if (_itemTemplate == null) { Debug.LogError($"[UTKImageList] Item UXML not found at: {ITEM_UXML_PATH}"); } // 3. UI 요소 참조 획득 _searchField = this.Q("search-field"); _listView = this.Q("main-list-view"); _clearButton = this.Q