Files
XRLib/Assets/Scripts/UVC/UIToolkit/Menu/UTKTopMenuModel.cs
2026-02-13 20:27:31 +09:00

315 lines
10 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
namespace UVC.UIToolkit
{
/// <summary>
/// UIToolkit 메뉴 시스템의 데이터 모델입니다.
/// 메뉴 아이템 컬렉션을 관리하고 검색 기능을 제공합니다.
/// </summary>
/// <remarks>
/// 이 클래스는 메뉴 아이템을 효율적으로 관리하기 위해 Dictionary 캐싱을 사용합니다.
/// 검색 시간 복잡도: O(1) (Dictionary 사용)
/// </remarks>
/// <example>
/// <code>
/// // 모델 생성
/// var model = new UTKTopMenuModel();
///
/// // 메뉴 아이템 추가
/// var fileMenu = new UTKMenuItemData("file", "menu_file");
/// model.AddMenuItem(fileMenu);
///
/// // 메뉴 아이템 검색
/// var found = model.FindMenuItem("file");
///
/// // 메뉴 아이템 제거
/// model.RemoveMenuItem("file");
///
/// // 모든 메뉴 정리
/// model.ClearMenuItems();
///
/// // 사용 후 정리
/// model.Dispose();
/// </code>
/// </example>
public class UTKTopMenuModel : IDisposable
{
#region Fields
/// <summary>빠른 검색을 위한 메뉴 아이템 인덱스 (ItemId -> MenuItemData)</summary>
private readonly Dictionary<string, UTKMenuItemData> _menuItemIndex;
#endregion
#region Properties
/// <summary>최상위 메뉴 아이템 리스트</summary>
public List<UTKMenuItemData> MenuItems { get; private set; }
#endregion
#region Constructor
/// <summary>
/// UTKTopMenuModel의 새 인스턴스를 초기화합니다.
/// </summary>
public UTKTopMenuModel()
{
MenuItems = new List<UTKMenuItemData>();
_menuItemIndex = new Dictionary<string, UTKMenuItemData>(StringComparer.Ordinal);
}
#endregion
#region Methods
/// <summary>
/// 메뉴 아이템을 추가합니다.
/// </summary>
/// <param name="item">추가할 메뉴 아이템</param>
/// <exception cref="ArgumentNullException">item이 null인 경우</exception>
/// <exception cref="ObjectDisposedException">이미 정리된 객체에 접근하는 경우</exception>
public void AddMenuItem(UTKMenuItemData item)
{
if (_disposed)
throw new ObjectDisposedException(nameof(UTKTopMenuModel), "이미 정리된 모델에 메뉴 아이템을 추가할 수 없습니다.");
if (item == null)
throw new ArgumentNullException(nameof(item), "추가할 메뉴 아이템이 null입니다.");
MenuItems.Add(item);
// 인덱스에 추가 (재귀적으로 하위 메뉴도 인덱싱)
AddToIndex(item);
}
/// <summary>
/// 메뉴 아이템을 제거합니다.
/// </summary>
/// <param name="itemId">제거할 메뉴 아이템의 ID</param>
/// <returns>제거 성공 시 true, 그렇지 않으면 false</returns>
/// <exception cref="ObjectDisposedException">이미 정리된 객체에 접근하는 경우</exception>
/// <remarks>
/// 시간 복잡도: O(n) (n = MenuItems.Count)
/// 하위 메뉴도 함께 제거되며, 제거된 아이템은 Dispose됩니다.
/// </remarks>
public bool RemoveMenuItem(string itemId)
{
if (_disposed)
throw new ObjectDisposedException(nameof(UTKTopMenuModel), "이미 정리된 모델에서 메뉴 아이템을 제거할 수 없습니다.");
if (string.IsNullOrEmpty(itemId))
return false;
// 최상위 메뉴에서 제거 시도
for (int i = 0; i < MenuItems.Count; i++)
{
if (string.Equals(MenuItems[i].ItemId, itemId, StringComparison.Ordinal))
{
var item = MenuItems[i];
MenuItems.RemoveAt(i);
// 인덱스에서 제거 (재귀적으로 하위 메뉴도 제거)
RemoveFromIndex(item);
// 메모리 정리
item.Dispose();
return true;
}
}
// 최상위에서 찾지 못했으면 재귀적으로 하위 메뉴에서 검색
return RemoveMenuItemRecursive(MenuItems, itemId);
}
/// <summary>
/// 재귀적으로 메뉴 아이템을 검색합니다.
/// </summary>
/// <param name="itemId">검색할 메뉴 아이템의 ID</param>
/// <returns>찾은 메뉴 아이템, 없으면 null</returns>
/// <exception cref="ObjectDisposedException">이미 정리된 객체에 접근하는 경우</exception>
/// <remarks>
/// 시간 복잡도: O(1) (Dictionary 캐싱 사용)
/// </remarks>
public UTKMenuItemData? FindMenuItem(string itemId)
{
if (_disposed)
throw new ObjectDisposedException(nameof(UTKTopMenuModel), "이미 정리된 모델에서 메뉴 아이템을 검색할 수 없습니다.");
if (string.IsNullOrEmpty(itemId))
return null;
// Dictionary 캐싱으로 O(1) 검색
return _menuItemIndex.TryGetValue(itemId, out var item) ? item : null;
}
/// <summary>
/// 모든 메뉴 아이템을 초기화합니다.
/// </summary>
/// <exception cref="ObjectDisposedException">이미 정리된 객체에 접근하는 경우</exception>
/// <remarks>
/// 모든 메뉴 아이템을 재귀적으로 Dispose합니다.
/// </remarks>
public void ClearMenuItems()
{
if (_disposed)
throw new ObjectDisposedException(nameof(UTKTopMenuModel), "이미 정리된 모델의 메뉴 아이템을 초기화할 수 없습니다.");
// 모든 메뉴 아이템 정리
foreach (var item in MenuItems)
{
item?.Dispose();
}
MenuItems.Clear();
_menuItemIndex.Clear();
}
#endregion
#region Private Methods
/// <summary>
/// 메뉴 아이템과 그 하위 메뉴들을 인덱스에 추가합니다.
/// </summary>
/// <param name="item">인덱싱할 메뉴 아이템</param>
private void AddToIndex(UTKMenuItemData item)
{
if (item == null)
return;
// 중복 체크 (성능 최적화: ContainsKey 대신 TryAdd 사용)
_menuItemIndex[item.ItemId] = item;
// 하위 메뉴 재귀적 인덱싱
if (item.SubMenuItems != null)
{
foreach (var subItem in item.SubMenuItems)
{
AddToIndex(subItem);
}
}
}
/// <summary>
/// 메뉴 아이템과 그 하위 메뉴들을 인덱스에서 제거합니다.
/// </summary>
/// <param name="item">제거할 메뉴 아이템</param>
private void RemoveFromIndex(UTKMenuItemData item)
{
if (item == null)
return;
_menuItemIndex.Remove(item.ItemId);
// 하위 메뉴 재귀적 제거
if (item.SubMenuItems != null)
{
foreach (var subItem in item.SubMenuItems)
{
RemoveFromIndex(subItem);
}
}
}
/// <summary>
/// 재귀적으로 메뉴 아이템을 제거합니다.
/// </summary>
/// <param name="items">검색할 메뉴 아이템 리스트</param>
/// <param name="itemId">제거할 메뉴 아이템 ID</param>
/// <returns>제거 성공 시 true, 그렇지 않으면 false</returns>
private bool RemoveMenuItemRecursive(List<UTKMenuItemData> items, string itemId)
{
if (items == null || string.IsNullOrEmpty(itemId))
return false;
foreach (var item in items)
{
if (item.SubMenuItems != null && item.SubMenuItems.Count > 0)
{
// 하위 메뉴에서 제거 시도
for (int i = 0; i < item.SubMenuItems.Count; i++)
{
if (string.Equals(item.SubMenuItems[i].ItemId, itemId, StringComparison.Ordinal))
{
var subItem = item.SubMenuItems[i];
item.SubMenuItems.RemoveAt(i);
// 인덱스에서 제거
RemoveFromIndex(subItem);
// 메모리 정리
subItem.Dispose();
return true;
}
}
// 더 깊은 하위 메뉴에서 재귀 검색
if (RemoveMenuItemRecursive(item.SubMenuItems, itemId))
return true;
}
}
return false;
}
#endregion
#region IDisposable
private bool _disposed;
/// <summary>
/// 모든 메뉴 아이템을 정리합니다.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// 리소스를 정리합니다.
/// </summary>
/// <param name="disposing">관리되는 리소스를 정리할지 여부</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// 모든 메뉴 아이템 재귀적으로 정리
if (MenuItems != null)
{
foreach (var item in MenuItems)
{
item?.Dispose();
}
MenuItems.Clear();
}
// 인덱스 정리
_menuItemIndex?.Clear();
}
_disposed = true;
}
/// <summary>
/// 소멸자
/// </summary>
~UTKTopMenuModel()
{
Dispose(false);
}
#endregion
}
}