Files
2026-03-09 01:05:56 +09:00

173 lines
7.6 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using DTNavigation.Model;
using UnityEngine.UIElements;
namespace DTNavigation.View
{
/// <summary>
/// 사이드바 UI DOM 빌더.
/// UXML 쿼리, 네비게이션 항목 생성, 상태 클래스 전환을 담당합니다.
/// </summary>
public sealed class SidebarView : IDisposable
{
// ── 상태별 CSS 클래스 ──────────────────────────────────────
private static readonly string[] StateClasses =
{ "sidebar--one-depth", "sidebar--two-depth", "sidebar--icon-only" };
// ── UXML 요소 참조 ─────────────────────────────────────────
private readonly VisualElement _sidebarRoot;
private readonly VisualElement _navList;
private readonly Button _toggleBtn;
// ── 토글 버튼 콜백 (RegisterCallback 대칭 해제용) ──────────
private EventCallback<ClickEvent>? _toggleBtnCallback;
// ── 외부 이벤트 ────────────────────────────────────────────
public event Action? OnToggle;
/// <summary>1뎁스 그룹 헤더 클릭 시 발행 (그룹 모델 전달)</summary>
public event Action<NavItemModel>? OnNavGroupClicked;
/// <summary>2뎁스 자식 항목 클릭 시 발행 (itemId 전달)</summary>
public event Action<string>? OnNavItemSelected;
// ──────────────────────────────────────────────────────────
public SidebarView(VisualElement documentRoot)
{
_sidebarRoot = documentRoot.Q<VisualElement>("sidebar-root")
?? throw new InvalidOperationException("'sidebar-root' 요소를 찾을 수 없습니다.");
_navList = documentRoot.Q<VisualElement>("nav-list")
?? throw new InvalidOperationException("'nav-list' 요소를 찾을 수 없습니다.");
_toggleBtn = documentRoot.Q<Button>("sidebar-toggle-btn")
?? throw new InvalidOperationException("'sidebar-toggle-btn' 요소를 찾을 수 없습니다.");
_toggleBtnCallback = _ => OnToggle?.Invoke();
_toggleBtn.RegisterCallback(_toggleBtnCallback);
}
// ──────────────────────────────────────────────────────────
/// <summary>상태, 데이터, 선택 항목을 받아 사이드바 전체를 다시 그립니다.</summary>
public void Render(SidebarState state, IReadOnlyList<NavItemModel> items, string? selectedItemId)
{
ApplyStateClass(state);
UpdateToggleButton(state);
BuildNavList(state, items, selectedItemId);
}
// ── 상태 클래스 전환 ───────────────────────────────────────
private void ApplyStateClass(SidebarState state)
{
foreach (var cls in StateClasses)
_sidebarRoot.RemoveFromClassList(cls);
_sidebarRoot.AddToClassList(state switch
{
SidebarState.OneDepth => "sidebar--one-depth",
SidebarState.TwoDepth => "sidebar--two-depth",
SidebarState.IconOnly => "sidebar--icon-only",
_ => "sidebar--one-depth",
});
}
// ── 토글 버튼 텍스트 ───────────────────────────────────────
private void UpdateToggleButton(SidebarState state)
=> _toggleBtn.text = state == SidebarState.IconOnly ? ">" : "<";
// ── 네비게이션 목록 빌드 ───────────────────────────────────
private void BuildNavList(SidebarState state, IReadOnlyList<NavItemModel> items, string? selectedItemId)
{
_navList.Clear();
for (var i = 0; i < items.Count; i++)
_navList.Add(CreateNavItem(state, items[i], selectedItemId));
}
private VisualElement CreateNavItem(SidebarState state, NavItemModel item, string? selectedItemId)
{
var container = new VisualElement();
container.AddToClassList("nav-item");
container.Add(CreateNavItemHeader(state, item, selectedItemId));
if (item.HasChildren && state == SidebarState.TwoDepth)
container.Add(CreateChildList(item, selectedItemId));
return container;
}
private VisualElement CreateNavItemHeader(SidebarState state, NavItemModel item, string? selectedItemId)
{
var header = new VisualElement();
header.AddToClassList("nav-item__header");
if (item.IsTopLevel)
{
header.AddToClassList("nav-item__header--top");
var label = new Label(item.Label);
label.AddToClassList("nav-item__label");
label.AddToClassList("nav-item__label--top");
header.Add(label);
// 최상위 항목은 선택 가능한 단독 항목으로 처리
if (item.Id == selectedItemId)
header.AddToClassList("nav-item__header--selected");
header.RegisterCallback<ClickEvent>(_ => OnNavItemSelected?.Invoke(item.Id));
}
else
{
var icon = new Label("i");
icon.AddToClassList("nav-item__icon");
header.Add(icon);
var labelText = state == SidebarState.OneDepth
? $"{item.Label} OVERVIEW"
: item.Label;
var label = new Label(labelText);
label.AddToClassList("nav-item__label");
header.Add(label);
// 1뎁스 그룹 헤더 클릭 → 자식 탭 추가 이벤트
header.RegisterCallback<ClickEvent>(_ => OnNavGroupClicked?.Invoke(item));
}
return header;
}
private VisualElement CreateChildList(NavItemModel item, string? selectedItemId)
{
var childList = new VisualElement();
childList.AddToClassList("nav-item__children");
for (var i = 0; i < item.Children.Count; i++)
{
var child = item.Children[i];
var childEl = new Label(child.Label);
childEl.AddToClassList("nav-child-item");
if (child.Id == selectedItemId)
childEl.AddToClassList("nav-child-item--selected");
childEl.RegisterCallback<ClickEvent>(_ => OnNavItemSelected?.Invoke(child.Id));
childList.Add(childEl);
}
return childList;
}
// ── IDisposable ────────────────────────────────────────────
public void Dispose()
{
if (_toggleBtnCallback != null)
{
_toggleBtn.UnregisterCallback(_toggleBtnCallback);
_toggleBtnCallback = null;
}
}
}
}