UTKAccordionList 개발 완료

This commit is contained in:
logonkhi
2026-01-05 16:52:06 +09:00
parent 430fb27a10
commit b7776f3af0
37 changed files with 5629 additions and 715 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 11db88c46df8e6242bad6972aa1db2f7

View File

@@ -0,0 +1,520 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UVC.UIToolkit.List
{
/// <summary>
/// 콘텐츠 종류를 정의합니다.
/// </summary>
public enum UTKAccordionContentKind
{
None,
Text,
Image,
IconButton
}
/// <summary>
/// 아이템 노드 타입을 정의합니다.
/// </summary>
public enum UTKAccordionNodeType
{
/// <summary>섹션 (펼침/접힘 가능한 부모 노드)</summary>
Section,
/// <summary>수평 아이템 (Head-Content-Tail)</summary>
HorizontalItem,
/// <summary>그리드 아이템 (Image + Caption)</summary>
GridItem,
/// <summary>그리드 행 (2개의 GridItem을 포함하는 컨테이너)</summary>
GridRow
}
/// <summary>
/// 콘텐츠 명세 (Head/Content/Tail 각 요소 정의)
/// </summary>
[Serializable]
public sealed class UTKAccordionContentSpec
{
/// <summary>콘텐츠 종류</summary>
public UTKAccordionContentKind Kind { get; set; } = UTKAccordionContentKind.None;
/// <summary>표시 텍스트</summary>
public string? Text { get; set; }
/// <summary>이미지 경로 (Resources 기준)</summary>
public string? ImagePath { get; set; }
/// <summary>이미지 색상</summary>
public Color ImageColor { get; set; } = Color.white;
/// <summary>아이콘 이름 또는 경로</summary>
public string? IconName { get; set; }
/// <summary>툴팁 텍스트</summary>
public string? Tooltip { get; set; }
/// <summary>클릭 이벤트 식별용 액션 ID</summary>
public string? ActionId { get; set; }
/// <summary>사용자 정의 데이터</summary>
public object? UserData { get; set; }
/// <summary>텍스트 콘텐츠 스펙을 생성합니다.</summary>
public static UTKAccordionContentSpec FromText(string text, string? actionId = null, string? tooltip = null)
=> new UTKAccordionContentSpec
{
Kind = UTKAccordionContentKind.Text,
Text = text,
ActionId = actionId,
Tooltip = tooltip
};
/// <summary>이미지 콘텐츠 스펙을 생성합니다.</summary>
public static UTKAccordionContentSpec FromImage(string path, Color? color = null, string? tooltip = null)
=> new UTKAccordionContentSpec
{
Kind = UTKAccordionContentKind.Image,
ImagePath = path,
ImageColor = color ?? Color.white,
Tooltip = tooltip
};
/// <summary>아이콘 버튼 스펙을 생성합니다.</summary>
public static UTKAccordionContentSpec FromIconButton(string iconName, string? actionId = null, string? tooltip = null, object? userData = null)
=> new UTKAccordionContentSpec
{
Kind = UTKAccordionContentKind.IconButton,
IconName = iconName,
ActionId = actionId,
Tooltip = tooltip,
UserData = userData
};
}
/// <summary>
/// 통합 아코디언 아이템 데이터 (TreeView용)
/// 섹션, 수평 아이템, 그리드 아이템을 하나의 타입으로 통합합니다.
/// </summary>
[Serializable]
public sealed class UTKAccordionItemData : IDisposable
{
#region
/// <summary>내부 고유 ID (TreeView용, 자동 할당)</summary>
public int id;
/// <summary>외부 시스템 연동용 식별자</summary>
public string? externalId;
/// <summary>노드 타입 (Section, HorizontalItem, GridItem)</summary>
public UTKAccordionNodeType nodeType;
/// <summary>표시 이름 (섹션 제목 또는 아이템 이름)</summary>
public string name = string.Empty;
/// <summary>자식 아이템 리스트 (섹션인 경우)</summary>
public List<UTKAccordionItemData>? children;
/// <summary>펼침 상태 (섹션인 경우)</summary>
public bool isExpanded = true;
/// <summary>사용자 정의 태그</summary>
public string? tag;
/// <summary>사용자 정의 데이터</summary>
public object? userData;
#endregion
#region
/// <summary>헤드 영역 콘텐츠 (수평 아이템)</summary>
public UTKAccordionContentSpec? head;
/// <summary>콘텐츠 영역 (수평 아이템)</summary>
public UTKAccordionContentSpec? content;
/// <summary>테일 영역 아이콘 버튼 리스트 (수평 아이템)</summary>
public List<UTKAccordionContentSpec>? tail;
#endregion
#region
/// <summary>이미지 경로 (그리드 아이템)</summary>
public string? imagePath;
/// <summary>캡션 텍스트 (그리드 아이템)</summary>
public string? caption;
/// <summary>캡션 색상 (그리드 아이템)</summary>
public Color? captionColor;
/// <summary>캡션 폰트 크기 (그리드 아이템)</summary>
public int? captionSize;
/// <summary>프리팹 경로 (드래그 앤 드롭용)</summary>
public string? prefabPath;
#endregion
#region
/// <summary>왼쪽 그리드 아이템 (GridRow 전용)</summary>
public UTKAccordionItemData? leftGridItem;
/// <summary>오른쪽 그리드 아이템 (GridRow 전용)</summary>
public UTKAccordionItemData? rightGridItem;
#endregion
#region
/// <summary>섹션을 생성합니다.</summary>
public static UTKAccordionItemData CreateSection(string title, bool isExpanded = true)
=> new UTKAccordionItemData
{
nodeType = UTKAccordionNodeType.Section,
name = title,
isExpanded = isExpanded,
children = new List<UTKAccordionItemData>()
};
/// <summary>수평 아이템을 생성합니다.</summary>
public static UTKAccordionItemData CreateHorizontalItem(
UTKAccordionContentSpec? head = null,
UTKAccordionContentSpec? content = null,
List<UTKAccordionContentSpec>? tail = null,
string? tag = null,
object? userData = null)
=> new UTKAccordionItemData
{
nodeType = UTKAccordionNodeType.HorizontalItem,
name = content?.Text ?? string.Empty,
head = head,
content = content,
tail = tail,
tag = tag,
userData = userData
};
/// <summary>그리드 아이템을 생성합니다.</summary>
public static UTKAccordionItemData CreateGridItem(
string caption,
string? imagePath = null,
string? prefabPath = null,
string? tag = null,
object? userData = null)
=> new UTKAccordionItemData
{
nodeType = UTKAccordionNodeType.GridItem,
name = caption,
caption = caption,
imagePath = imagePath,
prefabPath = prefabPath,
tag = tag,
userData = userData
};
#endregion
#region
/// <summary>섹션에 자식 아이템을 추가합니다.</summary>
public UTKAccordionItemData AddChild(UTKAccordionItemData child)
{
children ??= new List<UTKAccordionItemData>();
children.Add(child);
return this;
}
/// <summary>섹션에 수평 아이템을 추가합니다.</summary>
public UTKAccordionItemData AddHorizontalItem(
UTKAccordionContentSpec? head = null,
UTKAccordionContentSpec? content = null,
List<UTKAccordionContentSpec>? tail = null,
string? tag = null)
{
return AddChild(CreateHorizontalItem(head, content, tail, tag));
}
/// <summary>섹션에 그리드 아이템을 추가합니다.</summary>
public UTKAccordionItemData AddGridItem(
string caption,
string? imagePath = null,
string? prefabPath = null,
string? tag = null)
{
return AddChild(CreateGridItem(caption, imagePath, prefabPath, tag));
}
/// <summary>자식이 있는지 확인합니다.</summary>
public bool HasChildren => children != null && children.Count > 0;
/// <summary>섹션인지 확인합니다.</summary>
public bool IsSection => nodeType == UTKAccordionNodeType.Section;
/// <summary>수평 아이템인지 확인합니다.</summary>
public bool IsHorizontalItem => nodeType == UTKAccordionNodeType.HorizontalItem;
/// <summary>그리드 아이템인지 확인합니다.</summary>
public bool IsGridItem => nodeType == UTKAccordionNodeType.GridItem;
/// <summary>그리드 행인지 확인합니다.</summary>
public bool IsGridRow => nodeType == UTKAccordionNodeType.GridRow;
#endregion
#region IDisposable
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
head = null;
content = null;
tail?.Clear();
tail = null;
if (children != null)
{
foreach (var child in children)
{
child.Dispose();
}
children.Clear();
children = null;
}
externalId = null;
name = string.Empty;
tag = null;
userData = null;
imagePath = null;
caption = null;
prefabPath = null;
// GridRow 정리
leftGridItem?.Dispose();
leftGridItem = null;
rightGridItem?.Dispose();
rightGridItem = null;
}
#endregion
}
#region (Backward Compatibility)
// 기존 API 호환성을 위해 레거시 타입 유지
// 새 코드에서는 UTKAccordionItemData 사용 권장
/// <summary>
/// [레거시] 수평 아이템 데이터 - UTKAccordionItemData.CreateHorizontalItem() 사용 권장
/// </summary>
[Serializable]
public sealed class UTKAccordionHorizontalItemData : IDisposable
{
public int Id { get; set; }
public string? ExternalId { get; set; }
public UTKAccordionContentSpec? Head { get; set; }
public UTKAccordionContentSpec? Content { get; set; }
public List<UTKAccordionContentSpec>? Tail { get; set; }
public string? Tag { get; set; }
public object? UserData { get; set; }
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
Head = null;
Content = null;
Tail?.Clear();
Tail = null;
Tag = null;
ExternalId = null;
UserData = null;
}
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
public UTKAccordionItemData ToItemData()
{
return new UTKAccordionItemData
{
id = Id,
externalId = ExternalId,
nodeType = UTKAccordionNodeType.HorizontalItem,
name = Content?.Text ?? string.Empty,
head = Head,
content = Content,
tail = Tail,
tag = Tag,
userData = UserData
};
}
}
/// <summary>
/// [레거시] 그리드 아이템 데이터 - UTKAccordionItemData.CreateGridItem() 사용 권장
/// </summary>
[Serializable]
public sealed class UTKAccordionGridItemData : IDisposable
{
public int Id { get; set; }
public string? ExternalId { get; set; }
public string? ImagePath { get; set; }
public string Caption { get; set; } = string.Empty;
public Color? CaptionColor { get; set; }
public int? CaptionSize { get; set; }
public string? Tag { get; set; }
public object? UserData { get; set; }
public string? PrefabPath { get; set; }
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
ExternalId = null;
ImagePath = null;
Caption = string.Empty;
Tag = null;
UserData = null;
PrefabPath = null;
}
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
public UTKAccordionItemData ToItemData()
{
return new UTKAccordionItemData
{
id = Id,
externalId = ExternalId,
nodeType = UTKAccordionNodeType.GridItem,
name = Caption,
caption = Caption,
imagePath = ImagePath,
captionColor = CaptionColor,
captionSize = CaptionSize,
prefabPath = PrefabPath,
tag = Tag,
userData = UserData
};
}
}
/// <summary>
/// [레거시] 섹션 데이터 - UTKAccordionItemData.CreateSection() 사용 권장
/// </summary>
[Serializable]
public sealed class UTKAccordionSectionData : IDisposable
{
public int Id { get; set; }
public string Title { get; set; } = string.Empty;
public bool IsExpanded { get; set; } = true;
public UTKAccordionLayoutType LayoutType { get; set; } = UTKAccordionLayoutType.Horizontal;
public List<UTKAccordionHorizontalItemData>? HorizontalItems { get; set; }
public List<UTKAccordionGridItemData>? GridItems { get; set; }
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (HorizontalItems != null)
{
foreach (var item in HorizontalItems)
item.Dispose();
HorizontalItems.Clear();
HorizontalItems = null;
}
if (GridItems != null)
{
foreach (var item in GridItems)
item.Dispose();
GridItems.Clear();
GridItems = null;
}
Title = string.Empty;
}
/// <summary>UTKAccordionItemData로 변환합니다.</summary>
public UTKAccordionItemData ToItemData()
{
var section = UTKAccordionItemData.CreateSection(Title, IsExpanded);
section.id = Id;
if (LayoutType == UTKAccordionLayoutType.Horizontal && HorizontalItems != null)
{
foreach (var item in HorizontalItems)
{
section.AddChild(item.ToItemData());
}
}
else if (LayoutType == UTKAccordionLayoutType.Grid && GridItems != null)
{
foreach (var item in GridItems)
{
section.AddChild(item.ToItemData());
}
}
return section;
}
}
/// <summary>
/// [레거시] 아코디언 전체 데이터 - List&lt;UTKAccordionItemData&gt; 사용 권장
/// </summary>
[Serializable]
public sealed class UTKAccordionData : IDisposable
{
public List<UTKAccordionSectionData> Sections { get; set; } = new();
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (var section in Sections)
section.Dispose();
Sections.Clear();
}
/// <summary>UTKAccordionItemData 리스트로 변환합니다.</summary>
public List<UTKAccordionItemData> ToItemDataList()
{
var result = new List<UTKAccordionItemData>();
foreach (var section in Sections)
{
result.Add(section.ToItemData());
}
return result;
}
}
/// <summary>
/// [레거시] 아이템 레이아웃 타입 - UTKAccordionNodeType 사용 권장
/// </summary>
public enum UTKAccordionLayoutType
{
Horizontal,
Grid
}
#endregion
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 793db34bde0a1a145ac4e4ae31b7c452

View File

@@ -249,6 +249,7 @@ namespace UVC.UIToolkit.List
// 검색어 지우기 버튼
if(_clearButton != null)
{
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
_clearButton.clicked += () =>
{
if (_searchField.value.Length > 0)
@@ -256,6 +257,7 @@ namespace UVC.UIToolkit.List
_searchField.value = string.Empty;
OnSearch(string.Empty);
}
_clearButton.style.display = DisplayStyle.None; // 클리어 후 숨김
};
}
@@ -1255,6 +1257,12 @@ namespace UVC.UIToolkit.List
/// <param name="query">검색어</param>
private void OnSearch(string query)
{
// Clear 버튼 가시성 토글
if (_clearButton != null)
{
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
}
// 검색어가 없으면 원본 데이터 복원
if (string.IsNullOrEmpty(query))
{

View File

@@ -59,7 +59,7 @@ namespace UVC.UIToolkit.List
private const string ITEM_UXML_PATH = "UIToolkit/List/UTKImageListItem";
/// <summary>최소 검색어 길이</summary>
private const int MIN_SEARCH_LENGTH = 3;
private const int MIN_SEARCH_LENGTH = 2;
/// <summary>이미지 로딩 디바운스 시간 (ms)</summary>
private const int IMAGE_LOAD_DEBOUNCE_MS = 50;
@@ -118,6 +118,9 @@ namespace UVC.UIToolkit.List
/// <summary>현재 로딩 중인 이미지 경로 추적 (중복 로딩 방지)</summary>
private readonly HashSet<string> _loadingImages = new();
/// <summary>RowData 객체 풀 (GC 부담 감소)</summary>
private readonly Queue<RowData> _rowDataPool = new();
#endregion
#region (Internal Classes)
@@ -148,6 +151,13 @@ namespace UVC.UIToolkit.List
/// </summary>
public bool DragImageFollowCursor { get; set; } = true;
/// <summary>
/// 드래그 영역 체크에 사용할 요소를 설정합니다.
/// null이면 자신(UTKImageList)의 worldBound를 사용합니다.
/// 부모 윈도우 등 다른 요소의 영역을 체크하려면 해당 요소를 설정하세요.
/// </summary>
public VisualElement? DragBoundsElement { get; set; }
/// <summary>
/// 현재 검색어를 가져오거나 설정합니다.
/// 설정 시 검색 필드의 값만 변경하고 검색은 실행하지 않습니다.
@@ -198,6 +208,20 @@ namespace UVC.UIToolkit.List
/// </summary>
public Action<UTKImageListItemData, Vector2>? OnItemEndDrag;
/// <summary>
/// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다.
/// (아이템 데이터, 화면 좌표)
/// 3D 프리팹 미리보기를 표시하는데 사용합니다.
/// </summary>
public Action<UTKImageListItemData, Vector2>? OnDragExitList;
/// <summary>
/// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다.
/// (아이템 데이터, 화면 좌표)
/// 3D 프리팹 미리보기를 숨기는데 사용합니다.
/// </summary>
public Action<UTKImageListItemData, Vector2>? OnDragEnterList;
#endregion
#region (Constructor)
@@ -243,20 +267,135 @@ namespace UVC.UIToolkit.List
/// </summary>
private void InitializeLogic()
{
// 검색 필드 이벤트 등록
// 검색 필드 이벤트 등록 (Enter 키 또는 포커스 잃을 때 검색)
if (_searchField != null)
{
_searchField.RegisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.RegisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
}
// 검색어 지우기 버튼 이벤트 등록
if (_clearButton != null)
{
_clearButton.clicked += OnClearButtonClicked;
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
}
// ListView 설정
SetupListView();
// 루트 레벨 포인터 이벤트 등록 (드래그 처리용)
RegisterRootPointerEvents();
}
/// <summary>
/// 루트 레벨 포인터 이벤트를 등록합니다.
/// 포인터 캡처 후에도 이벤트를 받을 수 있도록 합니다.
/// </summary>
private void RegisterRootPointerEvents()
{
// UTKImageList 자체에서 포인터 이벤트 수신 (캡처 후에도 받을 수 있음)
RegisterCallback<PointerMoveEvent>(OnRootPointerMove);
RegisterCallback<PointerUpEvent>(OnRootPointerUp);
}
/// <summary>
/// 루트 레벨 포인터 이동 이벤트 핸들러
/// </summary>
private void OnRootPointerMove(PointerMoveEvent evt)
{
if (_dragData == null) return;
// 드래그 시작 임계값 체크 (5픽셀)
if (!_isDragging)
{
var distance = Vector2.Distance(_dragStartPosition, evt.position);
if (distance < 5f) return;
// 드래그 시작
_isDragging = true;
_isInsideListArea = true; // 시작 시 항상 내부
OnItemBeginDrag?.Invoke(_dragData, evt.position);
// 드래그 고스트 생성
if (DragImageFollowCursor)
{
CreateDragGhost(_dragData, evt.position);
}
}
// 드래그 중 이벤트
OnItemDrag?.Invoke(_dragData, evt.position);
// 리스트 영역 체크 (진입/이탈 이벤트 발생)
CheckListAreaBounds(evt.position);
// 드래그 고스트 이동 (리스트 영역 내부일 때만 표시)
if (_dragGhost != null)
{
UpdateDragGhostPosition(evt.position);
}
}
/// <summary>
/// 포인터가 리스트 영역 내부인지 체크하고, 진입/이탈 이벤트를 발생시킵니다.
/// </summary>
private void CheckListAreaBounds(Vector2 position)
{
if (_dragData == null || !_isDragging) return;
// 현재 포인터 위치가 리스트 영역 내부인지 체크
// DragBoundsElement가 설정되어 있으면 해당 요소의 영역을, 아니면 자신의 영역을 사용
var boundsElement = DragBoundsElement ?? this;
bool isInside = boundsElement.worldBound.Contains(position);
// 상태 변경 시 이벤트 발생
if (_isInsideListArea && !isInside)
{
// 내부 → 외부로 이동 (3D 프리팹 표시)
_isInsideListArea = false;
if (_dragGhost != null)
{
_dragGhost.style.visibility = Visibility.Hidden;
}
OnDragExitList?.Invoke(_dragData, position);
}
else if (!_isInsideListArea && isInside)
{
// 외부 → 내부로 이동 (고스트 이미지 표시)
_isInsideListArea = true;
if (_dragGhost != null)
{
_dragGhost.style.visibility = Visibility.Visible;
}
OnDragEnterList?.Invoke(_dragData, position);
}
}
/// <summary>
/// 루트 레벨 포인터 업 이벤트 핸들러
/// </summary>
private void OnRootPointerUp(PointerUpEvent evt)
{
if (_dragData == null) return;
// 포인터 캡처 해제
if (this.HasPointerCapture(_dragPointerId))
{
this.ReleasePointer(_dragPointerId);
}
if (_isDragging)
{
OnItemEndDrag?.Invoke(_dragData, evt.position);
OnItemDrop?.Invoke(_dragData);
}
// 드래그 상태 초기화
DestroyDragGhost();
_isDragging = false;
_isInsideListArea = false;
_dragData = null;
}
/// <summary>
@@ -432,23 +571,46 @@ namespace UVC.UIToolkit.List
/// <summary>
/// 아이템 데이터를 행 단위로 묶습니다.
/// 2개씩 묶어서 RowData 리스트를 생성합니다.
/// ObjectPool 패턴으로 GC 부담을 줄입니다.
/// </summary>
/// <param name="items">원본 아이템 리스트</param>
private void BuildRowData(List<UTKImageListItemData> items)
{
_rowData.Clear();
// 기존 RowData를 풀에 반환
ReturnRowDataToPool();
for (int i = 0; i < items.Count; i += ITEMS_PER_ROW)
{
var row = new RowData
{
LeftItem = items[i],
RightItem = (i + 1 < items.Count) ? items[i + 1] : null
};
var row = GetRowDataFromPool();
row.LeftItem = items[i];
row.RightItem = (i + 1 < items.Count) ? items[i + 1] : null;
_rowData.Add(row);
}
}
/// <summary>
/// ObjectPool에서 RowData를 가져옵니다.
/// 풀이 비어있으면 새로 생성합니다.
/// </summary>
private RowData GetRowDataFromPool()
{
return _rowDataPool.Count > 0 ? _rowDataPool.Dequeue() : new RowData();
}
/// <summary>
/// 사용 중인 RowData를 모두 풀에 반환합니다.
/// </summary>
private void ReturnRowDataToPool()
{
foreach (var row in _rowData)
{
row.LeftItem = null;
row.RightItem = null;
_rowDataPool.Enqueue(row);
}
_rowData.Clear();
}
#endregion
#region ListView (ListView Binding)
@@ -578,9 +740,7 @@ namespace UVC.UIToolkit.List
if (itemRoot.userData is ItemEventHandlers handlers)
{
itemRoot.UnregisterCallback<ClickEvent>(handlers.ClickHandler);
itemRoot.UnregisterCallback<PointerDownEvent>(handlers.PointerDownHandler);
itemRoot.UnregisterCallback<PointerMoveEvent>(handlers.PointerMoveHandler);
itemRoot.UnregisterCallback<PointerUpEvent>(handlers.PointerUpHandler);
itemRoot.UnregisterCallback<PointerDownEvent>(handlers.PointerDownHandler, TrickleDown.TrickleDown);
itemRoot.userData = null;
}
}
@@ -596,25 +756,20 @@ namespace UVC.UIToolkit.List
if (itemRoot.userData is ItemEventHandlers oldHandlers)
{
itemRoot.UnregisterCallback<ClickEvent>(oldHandlers.ClickHandler);
itemRoot.UnregisterCallback<PointerDownEvent>(oldHandlers.PointerDownHandler);
itemRoot.UnregisterCallback<PointerMoveEvent>(oldHandlers.PointerMoveHandler);
itemRoot.UnregisterCallback<PointerUpEvent>(oldHandlers.PointerUpHandler);
itemRoot.UnregisterCallback<PointerDownEvent>(oldHandlers.PointerDownHandler, TrickleDown.TrickleDown);
}
// 핸들러 생성 및 등록
// 핸들러 생성 및 등록 (PointerMove/Up은 루트 레벨에서 처리)
var handlers = new ItemEventHandlers
{
Data = data,
ClickHandler = evt => HandleItemClick(data),
PointerDownHandler = evt => HandlePointerDown(evt, data),
PointerMoveHandler = evt => HandlePointerMove(evt, data),
PointerUpHandler = evt => HandlePointerUp(evt, data)
PointerDownHandler = evt => HandlePointerDown(evt, data)
};
itemRoot.RegisterCallback(handlers.ClickHandler);
itemRoot.RegisterCallback(handlers.PointerDownHandler);
itemRoot.RegisterCallback(handlers.PointerMoveHandler);
itemRoot.RegisterCallback(handlers.PointerUpHandler);
// TrickleDown으로 등록하여 ListView 선택 전에 이벤트를 받음
itemRoot.RegisterCallback(handlers.PointerDownHandler, TrickleDown.TrickleDown);
// 나중에 해제할 수 있도록 저장
itemRoot.userData = handlers;
@@ -629,8 +784,6 @@ namespace UVC.UIToolkit.List
public UTKImageListItemData? Data;
public EventCallback<ClickEvent>? ClickHandler;
public EventCallback<PointerDownEvent>? PointerDownHandler;
public EventCallback<PointerMoveEvent>? PointerMoveHandler;
public EventCallback<PointerUpEvent>? PointerUpHandler;
}
#endregion
@@ -762,7 +915,8 @@ namespace UVC.UIToolkit.List
private UTKImageListItemData? _dragData;
private Vector2 _dragStartPosition;
private VisualElement? _dragGhost;
private VisualElement? _capturedElement;
private int _dragPointerId;
private bool _isInsideListArea; // 드래그 중 리스트 영역 내부 여부
/// <summary>
/// 포인터 다운 이벤트를 처리합니다 (드래그 시작 준비).
@@ -772,68 +926,10 @@ namespace UVC.UIToolkit.List
_dragData = data;
_dragStartPosition = evt.position;
_isDragging = false;
_dragPointerId = evt.pointerId;
// 포인터 캡처하여 요소 외부에서도 이벤트 수신
_capturedElement = evt.target as VisualElement;
_capturedElement?.CapturePointer(evt.pointerId);
}
/// <summary>
/// 포인터 이동 이벤트를 처리합니다 (드래그 중).
/// </summary>
private void HandlePointerMove(PointerMoveEvent evt, UTKImageListItemData data)
{
if (_dragData == null) return;
// 드래그 시작 임계값 체크 (5픽셀)
if (!_isDragging)
{
var distance = Vector2.Distance(_dragStartPosition, evt.position);
if (distance < 5f) return;
// 드래그 시작
_isDragging = true;
OnItemBeginDrag?.Invoke(_dragData, evt.position);
// 드래그 고스트 생성
if (DragImageFollowCursor)
{
CreateDragGhost(_dragData, evt.position);
}
}
// 드래그 중 이벤트
OnItemDrag?.Invoke(_dragData, evt.position);
// 드래그 고스트 이동
if (_dragGhost != null)
{
UpdateDragGhostPosition(evt.position);
}
}
/// <summary>
/// 포인터 업 이벤트를 처리합니다 (드래그 종료).
/// </summary>
private void HandlePointerUp(PointerUpEvent evt, UTKImageListItemData data)
{
// 포인터 캡처 해제
if (_capturedElement != null && _capturedElement.HasPointerCapture(evt.pointerId))
{
_capturedElement.ReleasePointer(evt.pointerId);
}
_capturedElement = null;
if (_dragData != null && _isDragging)
{
OnItemEndDrag?.Invoke(_dragData, evt.position);
OnItemDrop?.Invoke(_dragData);
}
// 드래그 상태 초기화
DestroyDragGhost();
_isDragging = false;
_dragData = null;
// UTKImageList 자체에서 포인터 캡처하여 영역 외부에서도 이벤트 수신
this.CapturePointer(evt.pointerId);
}
#endregion
@@ -847,13 +943,27 @@ namespace UVC.UIToolkit.List
{
// 이미지만 표시하는 고스트 생성
_dragGhost = new VisualElement();
_dragGhost.AddToClassList("drag-ghost");
_dragGhost.name = "drag-ghost";
_dragGhost.pickingMode = PickingMode.Ignore;
// 고스트 컨테이너 스타일 (인라인) - UTKAccordionList와 동일한 크기
_dragGhost.style.width = 116;
_dragGhost.style.height = 87;
_dragGhost.style.position = Position.Absolute;
_dragGhost.style.opacity = 0.8f;
// 이미지 요소
var image = new VisualElement();
image.AddToClassList("item-image");
image.name = "drag-ghost-image";
image.pickingMode = PickingMode.Ignore;
image.style.width = Length.Percent(100);
image.style.height = Length.Percent(100);
image.style.backgroundPositionX = new BackgroundPosition(BackgroundPositionKeyword.Center);
image.style.backgroundPositionY = new BackgroundPosition(BackgroundPositionKeyword.Center);
image.style.backgroundRepeat = new BackgroundRepeat(Repeat.NoRepeat, Repeat.NoRepeat);
image.style.backgroundSize = new BackgroundSize(BackgroundSizeType.Contain);
// 스프라이트 캐시에서 이미지 로드
if (!string.IsNullOrEmpty(data.imagePath) && _spriteCache.TryGetValue(data.imagePath, out var sprite) && sprite != null)
{
image.style.backgroundImage = new StyleBackground(sprite);
@@ -861,12 +971,23 @@ namespace UVC.UIToolkit.List
_dragGhost.Add(image);
// 위치 설정
_dragGhost.style.position = Position.Absolute;
UpdateDragGhostPosition(position);
// 루트에 추가
var root = this.panel?.visualTree;
root?.Add(_dragGhost);
if (root != null)
{
root.Add(_dragGhost);
#if UNITY_EDITOR && UTK_DEBUG
UnityEngine.Debug.Log($"[UTKImageList] 드래그 고스트 생성됨, 루트에 추가: {root.name}");
#endif
}
#if UNITY_EDITOR && UTK_DEBUG
else
{
UnityEngine.Debug.LogWarning("[UTKImageList] 드래그 고스트 생성 실패: panel.visualTree가 null입니다.");
}
#endif
}
/// <summary>
@@ -876,13 +997,10 @@ namespace UVC.UIToolkit.List
{
if (_dragGhost == null) return;
// 리스트 영역 외부에서만 고스트 표시 (AccordionGridItemView와 동일한 동작)
var listBounds = this.worldBound;
bool isInsideList = listBounds.Contains(position);
_dragGhost.style.visibility = isInsideList ? Visibility.Hidden : Visibility.Visible;
_dragGhost.style.left = position.x - 40;
_dragGhost.style.top = position.y - 40;
// 위치만 업데이트 (visibility는 CheckListAreaBounds에서 관리)
// 고스트 크기: 116x87, 커서를 이미지 중앙에 배치
_dragGhost.style.left = position.x - 58;
_dragGhost.style.top = position.y - 43;
}
/// <summary>
@@ -913,6 +1031,14 @@ namespace UVC.UIToolkit.List
}
}
/// <summary>
/// 검색 필드가 포커스를 잃었을 때 검색을 실행합니다.
/// </summary>
private void OnSearchFieldFocusOut(FocusOutEvent evt)
{
OnSearch(_searchField?.value ?? string.Empty);
}
/// <summary>
/// 검색어 지우기 버튼 클릭 이벤트를 처리합니다.
/// </summary>
@@ -923,15 +1049,27 @@ namespace UVC.UIToolkit.List
_searchField.value = string.Empty;
OnSearch(string.Empty);
}
// 클리어 후 버튼 숨김
if (_clearButton != null)
{
_clearButton.style.display = DisplayStyle.None;
}
}
/// <summary>
/// 검색을 실행합니다.
/// 성능 최적화: LINQ 대신 직접 반복, 소문자 변환 최소화
/// 성능 최적화: LINQ 대신 직접 반복, StringComparison 사용
/// </summary>
/// <param name="query">검색어</param>
private void OnSearch(string query)
{
// Clear 버튼 가시성 토글
if (_clearButton != null)
{
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
}
// 빈 검색어 → 원본 데이터 복원
if (string.IsNullOrEmpty(query))
{
@@ -942,18 +1080,20 @@ namespace UVC.UIToolkit.List
// 최소 검색어 길이 체크
if (query.Length < MIN_SEARCH_LENGTH)
{
#if UNITY_EDITOR && UTK_DEBUG
Debug.Log($"[UTKImageList] 검색어는 {MIN_SEARCH_LENGTH}글자 이상 입력해주세요.");
#endif
return;
}
// 검색 실행 (대소문자 무시)
var queryLower = query.Trim().ToLowerInvariant();
// 검색 실행 (대소문자 무시, 문화권 독립적)
var trimmedQuery = query.Trim();
_filteredData.Clear();
// 성능: foreach 루프 사용 (LINQ 오버헤드 방지)
// 성능: foreach 루프 + StringComparison.OrdinalIgnoreCase (ToLower 할당 방지)
foreach (var item in _originalData)
{
if (item.itemName.ToLowerInvariant().Contains(queryLower))
if (item.itemName.Contains(trimmedQuery, StringComparison.OrdinalIgnoreCase))
{
_filteredData.Add(item);
}
@@ -1017,10 +1157,21 @@ namespace UVC.UIToolkit.List
// 이미지 로딩 취소
CancelImageLoading();
// 루트 레벨 포인터 이벤트 해제 (메모리 누수 방지)
UnregisterCallback<PointerMoveEvent>(OnRootPointerMove);
UnregisterCallback<PointerUpEvent>(OnRootPointerUp);
// 포인터 캡처 해제
if (_dragPointerId != 0 && this.HasPointerCapture(_dragPointerId))
{
this.ReleasePointer(_dragPointerId);
}
// 이벤트 핸들러 해제
if (_searchField != null)
{
_searchField.UnregisterCallback<KeyDownEvent>(OnSearchFieldKeyDown);
_searchField.UnregisterCallback<FocusOutEvent>(OnSearchFieldFocusOut);
}
if (_clearButton != null)
@@ -1040,6 +1191,9 @@ namespace UVC.UIToolkit.List
_filteredData.Clear();
_rowData.Clear();
// ObjectPool 정리 (메모리 해제)
_rowDataPool.Clear();
// 캐시 정리
ClearSpriteCache();
@@ -1049,6 +1203,8 @@ namespace UVC.UIToolkit.List
OnItemBeginDrag = null;
OnItemDrag = null;
OnItemEndDrag = null;
OnDragExitList = null;
OnDragEnterList = null;
// UI 참조 정리
_searchField = null;
@@ -1056,6 +1212,7 @@ namespace UVC.UIToolkit.List
_clearButton = null;
_searchResultLabel = null;
_itemTemplate = null;
DragBoundsElement = null;
}
#endregion

View File

@@ -0,0 +1,358 @@
#nullable enable
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;
using UVC.UIToolkit.List;
namespace UVC.UIToolkit.Window
{
/// <summary>
/// UTKAccordionList를 래핑하여 윈도우 형태로 제공하는 컴포넌트입니다.
/// 기존 AccordionWindow의 UIToolkit 버전입니다.
///
/// <para><b>개요:</b></para>
/// <para>
/// UTKAccordionListWindow는 UTKAccordionList를 내부에 포함하고 헤더(타이틀, 닫기 버튼)를 추가한
/// 윈도우 형태의 컴포넌트입니다. 모든 리스트 관련 기능은 내부 UTKAccordionList에 위임됩니다.
/// </para>
///
/// <para><b>주요 기능:</b></para>
/// <list type="bullet">
/// <item>윈도우 형태의 UI 제공 (헤더 + 닫기 버튼)</item>
/// <item>내부 UTKAccordionList의 모든 기능 위임</item>
/// <item>드래그 앤 드롭 이벤트 전달</item>
/// </list>
///
/// <para><b>관련 리소스:</b></para>
/// <list type="bullet">
/// <item>Resources/UIToolkit/Window/UTKAccordionListWindow.uxml - 윈도우 레이아웃</item>
/// <item>Resources/UIToolkit/Window/UTKAccordionListWindow.uss - 윈도우 스타일</item>
/// </list>
/// </summary>
[UxmlElement]
public partial class UTKAccordionListWindow : VisualElement, IDisposable
{
#region (Constants)
private const string UXML_PATH = "UIToolkit/Window/UTKAccordionListWindow";
#endregion
#region UI (UI Component References)
private UTKAccordionList? _accordionList;
private Button? _closeButton;
private Label? _titleLabel;
#endregion
#region IDisposable
private bool _disposed;
#endregion
#region (Public Properties)
/// <summary>드래그 시 고스트 이미지 표시 여부</summary>
public bool ShowDragGhost
{
get => _accordionList?.ShowDragGhost ?? true;
set { if (_accordionList != null) _accordionList.ShowDragGhost = value; }
}
/// <summary>드래그 영역 체크에 사용할 요소 (기본: 윈도우 자체)</summary>
public VisualElement? DragBoundsElement
{
get => _accordionList?.DragBoundsElement;
set { if (_accordionList != null) _accordionList.DragBoundsElement = value; }
}
/// <summary>검색 기능 활성화 여부</summary>
public bool EnableSearch
{
get => _accordionList?.EnableSearch ?? true;
set { if (_accordionList != null) _accordionList.EnableSearch = value; }
}
/// <summary>윈도우 제목</summary>
public string Title
{
get => _titleLabel?.text ?? string.Empty;
set { if (_titleLabel != null) _titleLabel.text = value; }
}
/// <summary>닫기 버튼 표시 여부</summary>
public bool ShowCloseButton
{
get => _closeButton?.style.display == DisplayStyle.Flex;
set { if (_closeButton != null) _closeButton.style.display = value ? DisplayStyle.Flex : DisplayStyle.None; }
}
#endregion
#region (Event Delegation)
/// <summary>수평 아이템 클릭 이벤트</summary>
public event Action<UTKAccordionHorizontalItemData, UTKAccordionContentSpec?>? OnHorizontalItemClick
{
add { if (_accordionList != null) _accordionList.OnHorizontalItemClick += value; }
remove { if (_accordionList != null) _accordionList.OnHorizontalItemClick -= value; }
}
/// <summary>수평 아이템 아이콘 버튼 클릭 이벤트</summary>
public event Action<UTKAccordionHorizontalItemData, UTKAccordionContentSpec>? OnHorizontalItemIconClick
{
add { if (_accordionList != null) _accordionList.OnHorizontalItemIconClick += value; }
remove { if (_accordionList != null) _accordionList.OnHorizontalItemIconClick -= value; }
}
/// <summary>그리드 아이템 클릭 이벤트</summary>
public event Action<UTKAccordionGridItemData>? OnGridItemClick
{
add { if (_accordionList != null) _accordionList.OnGridItemClick += value; }
remove { if (_accordionList != null) _accordionList.OnGridItemClick -= value; }
}
/// <summary>그리드 아이템 드롭 이벤트</summary>
public event Action<UTKAccordionGridItemData>? OnGridItemDrop
{
add { if (_accordionList != null) _accordionList.OnGridItemDrop += value; }
remove { if (_accordionList != null) _accordionList.OnGridItemDrop -= value; }
}
/// <summary>드래그 시작 이벤트</summary>
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemBeginDrag
{
add { if (_accordionList != null) _accordionList.OnGridItemBeginDrag += value; }
remove { if (_accordionList != null) _accordionList.OnGridItemBeginDrag -= value; }
}
/// <summary>드래그 중 이벤트</summary>
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemDrag
{
add { if (_accordionList != null) _accordionList.OnGridItemDrag += value; }
remove { if (_accordionList != null) _accordionList.OnGridItemDrag -= value; }
}
/// <summary>드래그 종료 이벤트</summary>
public event Action<UTKAccordionGridItemData, Vector2>? OnGridItemEndDrag
{
add { if (_accordionList != null) _accordionList.OnGridItemEndDrag += value; }
remove { if (_accordionList != null) _accordionList.OnGridItemEndDrag -= value; }
}
/// <summary>드래그 중 리스트 영역 이탈 이벤트 (3D 미리보기용)</summary>
public event Action<UTKAccordionGridItemData, Vector2>? OnDragExitList
{
add { if (_accordionList != null) _accordionList.OnDragExitList += value; }
remove { if (_accordionList != null) _accordionList.OnDragExitList -= value; }
}
/// <summary>드래그 중 리스트 영역 진입 이벤트</summary>
public event Action<UTKAccordionGridItemData, Vector2>? OnDragEnterList
{
add { if (_accordionList != null) _accordionList.OnDragEnterList += value; }
remove { if (_accordionList != null) _accordionList.OnDragEnterList -= value; }
}
/// <summary>섹션 펼침/접힘 이벤트</summary>
public event Action<UTKAccordionSectionData, bool>? OnSectionToggled
{
add { if (_accordionList != null) _accordionList.OnSectionToggled += value; }
remove { if (_accordionList != null) _accordionList.OnSectionToggled -= value; }
}
/// <summary>윈도우가 닫힐 때 발생</summary>
public event Action? OnClosed;
// ========================================
// 통합 API 이벤트 (New Unified Events)
// ========================================
/// <summary>통합 아이템 클릭 이벤트</summary>
public event Action<UTKAccordionItemData>? OnItemClick
{
add { if (_accordionList != null) _accordionList.OnItemClick += value; }
remove { if (_accordionList != null) _accordionList.OnItemClick -= value; }
}
/// <summary>통합 아이템 아이콘 클릭 이벤트</summary>
public event Action<UTKAccordionItemData, UTKAccordionContentSpec>? OnItemIconClick
{
add { if (_accordionList != null) _accordionList.OnItemIconClick += value; }
remove { if (_accordionList != null) _accordionList.OnItemIconClick -= value; }
}
/// <summary>통합 아이템 드래그 시작 이벤트</summary>
public event Action<UTKAccordionItemData, Vector2>? OnItemBeginDrag
{
add { if (_accordionList != null) _accordionList.OnItemBeginDrag += value; }
remove { if (_accordionList != null) _accordionList.OnItemBeginDrag -= value; }
}
/// <summary>통합 아이템 드래그 중 이벤트</summary>
public event Action<UTKAccordionItemData, Vector2>? OnItemDrag
{
add { if (_accordionList != null) _accordionList.OnItemDrag += value; }
remove { if (_accordionList != null) _accordionList.OnItemDrag -= value; }
}
/// <summary>통합 아이템 드래그 종료 이벤트</summary>
public event Action<UTKAccordionItemData, Vector2>? OnItemEndDrag
{
add { if (_accordionList != null) _accordionList.OnItemEndDrag += value; }
remove { if (_accordionList != null) _accordionList.OnItemEndDrag -= value; }
}
/// <summary>통합 아이템 드롭 이벤트</summary>
public event Action<UTKAccordionItemData>? OnItemDrop
{
add { if (_accordionList != null) _accordionList.OnItemDrop += value; }
remove { if (_accordionList != null) _accordionList.OnItemDrop -= value; }
}
#endregion
#region (Constructor)
public UTKAccordionListWindow()
{
// UXML 로드
var visualTree = Resources.Load<VisualTreeAsset>(UXML_PATH);
if (visualTree == null)
{
Debug.LogError($"[UTKAccordionListWindow] UXML not found at: {UXML_PATH}");
return;
}
visualTree.CloneTree(this);
// 내부 UTKAccordionList 찾기
_accordionList = this.Q<UTKAccordionList>();
if (_accordionList == null)
{
Debug.LogError("[UTKAccordionListWindow] UTKAccordionList not found in UXML");
}
else
{
// 드래그 영역 체크에 윈도우 전체 영역을 사용
_accordionList.DragBoundsElement = this;
}
// 헤더 요소 참조
_titleLabel = this.Q<Label>("title");
_closeButton = this.Q<Button>("close-btn");
// 닫기 버튼 이벤트
if (_closeButton != null)
{
_closeButton.clicked += OnCloseButtonClicked;
}
}
#endregion
#region (Public Methods)
/// <summary>윈도우를 화면에 표시합니다.</summary>
public void Show()
{
style.display = DisplayStyle.Flex;
_accordionList?.Show();
}
/// <summary>윈도우를 화면에서 숨깁니다.</summary>
public void Close()
{
style.display = DisplayStyle.None;
OnClosed?.Invoke();
}
/// <summary>레거시 API: 아코디언 데이터를 설정합니다.</summary>
public void SetData(UTKAccordionData data)
{
_accordionList?.SetData(data);
}
/// <summary>통합 API: UTKAccordionItemData 리스트를 설정합니다.</summary>
public void SetData(List<UTKAccordionItemData> roots)
{
_accordionList?.SetData(roots);
}
/// <summary>모든 데이터를 제거합니다.</summary>
public void Clear()
{
_accordionList?.Clear();
}
/// <summary>특정 섹션을 펼치거나 접습니다.</summary>
public void SetSectionExpanded(int sectionId, bool expanded)
{
_accordionList?.SetSectionExpanded(sectionId, expanded);
}
/// <summary>모든 섹션을 펼칩니다.</summary>
public void ExpandAll()
{
_accordionList?.ExpandAll();
}
/// <summary>모든 섹션을 접습니다.</summary>
public void CollapseAll()
{
_accordionList?.CollapseAll();
}
/// <summary>검색어로 필터링합니다.</summary>
public void Search(string query)
{
_accordionList?.Search(query);
}
/// <summary>검색을 초기화합니다.</summary>
public void ClearSearch()
{
_accordionList?.ClearSearch();
}
#endregion
#region (Event Handlers)
private void OnCloseButtonClicked()
{
Close();
}
#endregion
#region IDisposable
public void Dispose()
{
if (_disposed) return;
_disposed = true;
// 내부 UTKAccordionList 정리
_accordionList?.Dispose();
_accordionList = null;
// 닫기 버튼 이벤트 해제
if (_closeButton != null)
{
_closeButton.clicked -= OnCloseButtonClicked;
}
// 외부 이벤트 정리
OnClosed = null;
// UI 참조 정리
_closeButton = null;
_titleLabel = null;
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c4a8ae56f0677484f87647337ff3fc03

View File

@@ -83,6 +83,16 @@ namespace UVC.UIToolkit.Window
set { if (_imageList != null) _imageList.DragImageFollowCursor = value; }
}
/// <summary>
/// 드래그 영역 체크에 사용할 요소를 설정합니다.
/// 기본값은 윈도우 자체입니다.
/// </summary>
public VisualElement? DragBoundsElement
{
get => _imageList?.DragBoundsElement;
set { if (_imageList != null) _imageList.DragBoundsElement = value; }
}
/// <summary>
/// 윈도우 제목을 가져오거나 설정합니다.
/// </summary>
@@ -159,6 +169,28 @@ namespace UVC.UIToolkit.Window
set { if (_imageList != null) _imageList.OnItemEndDrag = value; }
}
/// <summary>
/// 드래그 중 리스트 영역을 벗어났을 때 발생하는 이벤트입니다.
/// (아이템 데이터, 화면 좌표)
/// 3D 프리팹 미리보기를 표시하는데 사용합니다.
/// </summary>
public Action<UTKImageListItemData, Vector2>? OnDragExitList
{
get => _imageList?.OnDragExitList;
set { if (_imageList != null) _imageList.OnDragExitList = value; }
}
/// <summary>
/// 드래그 중 리스트 영역에 다시 진입했을 때 발생하는 이벤트입니다.
/// (아이템 데이터, 화면 좌표)
/// 3D 프리팹 미리보기를 숨기는데 사용합니다.
/// </summary>
public Action<UTKImageListItemData, Vector2>? OnDragEnterList
{
get => _imageList?.OnDragEnterList;
set { if (_imageList != null) _imageList.OnDragEnterList = value; }
}
/// <summary>
/// 윈도우가 닫힐 때(숨겨질 때) 발생합니다.
/// 닫기 버튼 클릭 또는 Close() 메서드 호출 시 트리거됩니다.
@@ -190,6 +222,11 @@ namespace UVC.UIToolkit.Window
{
Debug.LogError("[UTKImageListWindow] UTKImageList not found in UXML");
}
else
{
// 드래그 영역 체크에 윈도우 전체 영역을 사용하도록 설정
_imageList.DragBoundsElement = this;
}
// 3. 헤더 요소 참조
_titleLabel = this.Q<Label>("title");

View File

@@ -228,6 +228,7 @@ namespace UVC.UIToolkit.Window
// 검색어 지우기 버튼
if(_clearButton != null)
{
_clearButton.style.display = DisplayStyle.None; // 초기에는 숨김
_clearButton.clicked += () =>
{
if (_searchField.value.Length > 0)
@@ -235,6 +236,7 @@ namespace UVC.UIToolkit.Window
_searchField.value = string.Empty;
OnSearch(string.Empty);
}
_clearButton.style.display = DisplayStyle.None; // 클리어 후 숨김
};
}
@@ -1123,6 +1125,12 @@ namespace UVC.UIToolkit.Window
/// <param name="query">검색어</param>
private void OnSearch(string query)
{
// Clear 버튼 가시성 토글
if (_clearButton != null)
{
_clearButton.style.display = string.IsNullOrEmpty(query) ? DisplayStyle.None : DisplayStyle.Flex;
}
// 검색어가 없으면 원본 데이터 복원
if (string.IsNullOrEmpty(query))
{