menu 개발 중. 언어 변경 시 반영 않됨

This commit is contained in:
logonkhi
2025-06-11 19:24:08 +09:00
parent cd8c5e177b
commit 2614470f13
92 changed files with 1199947 additions and 188 deletions

View File

@@ -0,0 +1,131 @@
using UnityEngine;
using UVC.Locale;
using System.Collections.Generic;
using UVC.UI.Commands;
using UVC.Log;
using System;
namespace UVC.UI.Menu
{
public class TopMenuController : MonoBehaviour
{
protected TopMenuView view;
protected TopMenuModel model;
protected LocalizationManager _locManager;
protected virtual void Awake()
{
// 1. 동일한 GameObject에서 TopMenuView 컴포넌트 검색
view = GetComponent<TopMenuView>();
// 2. 동일한 GameObject에 없다면, 자식 GameObject에서 검색
if (view == null)
{
view = GetComponentInChildren<TopMenuView>();
}
}
protected virtual void Start()
{
model = new TopMenuModel();
_locManager = LocalizationManager.Instance;
if (view == null)
{
ULog.Error("TopMenuView가 Inspector에서 할당되지 않았습니다.");
return;
}
if (_locManager == null)
{
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다. 메뉴 텍스트가 올바르게 표시되지 않을 수 있습니다.");
// _locManager가 null이어도 메뉴 구조는 생성될 수 있도록 진행합니다.
// TopMenuView에서 _locManager null 체크를 통해 텍스트 처리를 합니다.
}
InitializeMenuItems();
view.ClearMenuItems();
// menuContainer가 TopMenuView 내부에 public으로 노출되어 있다고 가정합니다.
// 만약 private이라면, TopMenuView에 menuContainer를 전달받는 public 메서드가 필요할 수 있습니다.
view.CreateMenuItems(model.MenuItems, view.MenuContainer);
view.OnMenuItemClicked += HandleMenuItemClicked;
if (_locManager != null)
{
_locManager.OnLanguageChanged += HandleLanguageChanged;
}
}
protected virtual void OnDestroy()
{
if (view != null)
{
view.OnMenuItemClicked -= HandleMenuItemClicked;
}
if (_locManager != null)
{
_locManager.OnLanguageChanged -= HandleLanguageChanged;
}
}
protected virtual void InitializeMenuItems()
{
model.MenuItems.Clear();
model.MenuItems.Add(new MenuItemData("file", "menu_file", subMenuItems: new List<MenuItemData>
{
new MenuItemData("file_new", "menu_file_new", subMenuItems: new List<MenuItemData>
{
new MenuItemData("file_new_project", "menu_file_new_project", new DebugLogCommand("새 프로젝트 선택됨 (Command)")),
new MenuItemData("file_new_file", "menu_file_new_file", new DebugLogCommand("새 파일 선택됨 (Command)"))
}),
new MenuItemData("file_open", "menu_file_open", new DebugLogCommand("파일 열기 선택됨 (Command)")),
MenuItemData.CreateSeparator("file_sep1"), // 구분선 추가
new MenuItemData("file_save", "menu_file_save", command: new DebugLogCommand("저장 선택됨 (Command)") , subMenuItems: new List<MenuItemData> // 저장 메뉴에 Command 추가
{
new MenuItemData("file_save_as", "menu_file_save_as", new DebugLogCommand("다른 이름으로 저장 선택됨 (Command)"))
}),
MenuItemData.CreateSeparator("file_sep2"), // 또 다른 구분선 추가
new MenuItemData("file_exit", "menu_file_exit", new QuitApplicationCommand())
}));
model.MenuItems.Add(new MenuItemData("edit", "menu_edit", subMenuItems: new List<MenuItemData>
{
new MenuItemData("edit_undo", "menu_edit_undo", new DebugLogCommand("실행 취소 선택됨 (Command)")),
new MenuItemData("edit_redo", "menu_edit_redo", new DebugLogCommand("다시 실행 선택됨 (Command)")),
MenuItemData.CreateSeparator("edit_sep1"),
new MenuItemData("preferences", "menu_preferences", new DebugLogCommand("환경설정 선택됨 (Command)"))
}));
if (_locManager != null)
{
model.MenuItems.Add(new MenuItemData("language", "menu_language", subMenuItems: new List<MenuItemData>
{
new MenuItemData("lang_ko", "menu_lang_korean", new ChangeLanguageCommand("ko-KR", _locManager)),
new MenuItemData("lang_en", "menu_lang_english", new ChangeLanguageCommand("en-US", _locManager))
}));
}
else
{
ULog.Warning("LocalizationManager가 null이므로 언어 변경 메뉴를 초기화할 수 없습니다.");
}
}
protected virtual void HandleMenuItemClicked(MenuItemData clickedItemData)
{
if (clickedItemData.IsSeparator) return; // 구분선은 클릭 액션이 없음
ULog.Debug($"메뉴 아이템 클릭됨: {clickedItemData.ItemId} (Key: {clickedItemData.DisplayNameKey})");
clickedItemData.Command?.Execute();
}
protected virtual void HandleLanguageChanged(string newLanguageCode)
{
ULog.Debug($"언어 변경 감지: {newLanguageCode}. 메뉴 텍스트 업데이트 중...");
if (view != null && model != null)
{
view.UpdateAllMenuTexts(model.MenuItems);
}
}
}
}

View File

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

View File

@@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using UVC.UI.Commands;
namespace UVC.UI.Menu
{
// 메뉴 아이템 하나를 나타내는 데이터 구조체 또는 클래스
public class MenuItemData
{
public string ItemId { get; private set; }
public string DisplayNameKey { get; private set; } // 다국어 처리를 위한 키
public ICommand Command { get; private set; } // Action 대신 ICommand 사용
public List<MenuItemData> SubMenuItems { get; private set; } // 하위 메뉴 아이템 목록
public bool IsSeparator { get; private set; } // 구분선 여부 플래그
public MenuItemData(string itemId, string displayNameKey, ICommand command = null, List<MenuItemData> subMenuItems = null, bool isSeparator = false)
{
ItemId = itemId;
DisplayNameKey = displayNameKey;
Command = command;
SubMenuItems = subMenuItems ?? new List<MenuItemData>();
IsSeparator = isSeparator;
}
public void AddSubMenuItem(MenuItemData subItem)
{
if (IsSeparator) return; // 구분선에는 하위 메뉴를 추가할 수 없음
SubMenuItems.Add(subItem);
}
/// <summary>
/// 구분선 역할을 하는 MenuItemData 객체를 생성합니다.
/// </summary>
/// <param name="itemId">구분선의 고유 ID. null일 경우 GUID로 자동 생성됩니다.</param>
/// <returns>구분선 MenuItemData 객체입니다.</returns>
public static MenuItemData CreateSeparator(string itemId = null)
{
return new MenuItemData(itemId ?? $"separator_{Guid.NewGuid()}", string.Empty, null, null, true);
}
}
public class TopMenuModel
{
public List<MenuItemData> MenuItems { get; private set; }
public TopMenuModel()
{
MenuItems = new List<MenuItemData>();
}
// LoadMenuItems 메서드는 Controller에서 직접 데이터를 채움
//public void LoadMenuItems(string jsonString) { }
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 44674fdbeb5fa51409a23e6544757068

View File

@@ -0,0 +1,603 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UVC.Locale;
using UVC.Log; // 다국어 처리 네임스페이스 사용
namespace UVC.UI.Menu
{
public class TopMenuView : MonoBehaviour
{
// 프리팹의 Resources 폴더 내 경로 (예시 경로이므로 실제 프로젝트에 맞게 수정 필요)
private const string MenuItemPrefabPath = "Prefabs/UI/Menu/MenuItem";
private const string SubMenuItemPrefabPath = "Prefabs/UI/Menu/SubMenuItem";
private const string MenuSeparatorPrefabPath = "Prefabs/UI/Menu/MenuSeparator";
private const string MenuContainerName = "TopMenu";
private const string SubMenuContainerName = "SubMenuContainer";
private const string SubMenuArrowName = "SubMenuArrow";
protected GameObject menuItemPrefab;
protected GameObject subMenuItemPrefab;
protected GameObject menuSeparatorPrefab;
protected Transform menuContainer;
protected int menuItemInitX = 0; // 1depth 메뉴 아이템의 초기 X 위치
protected int menuItemInitY = 10; // 1depth 메뉴 아이템의 초기 Y 위치
protected int menuItemWidth = 100;
protected int menuItemHeight = 30;
protected int subMenuItemWidth = 100;
protected int subMenuItemHeight = 30;
protected int menuItemSeparatorWidth = 100;
protected int menuItemSeparatorHeight = 1;
protected int menuItemSeparatorVerticalMargin = 4;
protected int menuItemHorizontalGap = 30;
protected int menuItemVerticalGap = 2; // 사용자가 제공한 컨텍스트 값 사용
protected int menuDepthHorizontalGap = 100 - 5;
protected int menuDepthVerticalGap = 10;
protected int subContainerHorizontalPadding = 4;
protected int subContainerVerticalPadding = 10;
public Transform MenuContainer { get => menuContainer; }
public event Action<MenuItemData> OnMenuItemClicked;
protected Dictionary<string, GameObject> _menuItemObjects = new Dictionary<string, GameObject>();
protected LocalizationManager _locManager;
protected GameObject uiBlockerInstance;
protected bool isAnySubMenuOpen = false;
protected virtual void Awake()
{
_locManager = LocalizationManager.Instance;
if (_locManager == null)
{
ULog.Error("LocalizationManager 인스턴스를 찾을 수 없습니다.", new InvalidOperationException("LocalizationManager 인스턴스를 찾을 수 없습니다."));
}
menuItemPrefab = Resources.Load<GameObject>(MenuItemPrefabPath);
if (menuItemPrefab == null)
{
ULog.Error($"메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuItemPrefabPath}"));
}
subMenuItemPrefab = Resources.Load<GameObject>(SubMenuItemPrefabPath);
if (subMenuItemPrefab == null)
{
ULog.Error($"서브 메뉴 아이템 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {SubMenuItemPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{SubMenuItemPrefabPath}"));
}
menuSeparatorPrefab = Resources.Load<GameObject>(MenuSeparatorPrefabPath);
if (menuSeparatorPrefab == null)
{
ULog.Error($"메뉴 구분선 프리팹을 Resources 폴더에서 로드할 수 없습니다. 경로: {MenuSeparatorPrefabPath}", new System.IO.FileNotFoundException($"Prefab not found at Resources/{MenuSeparatorPrefabPath}"));
}
Transform containerTransform = transform.Find(MenuContainerName);
if (containerTransform != null)
{
menuContainer = containerTransform;
}
else
{
ULog.Warning($"'{MenuContainerName}'이라는 이름의 자식 GameObject를 찾을 수 없어 현재 Transform을 menuContainer로 사용합니다.");
menuContainer = transform;
}
if (menuContainer == null)
{
ULog.Error("menuContainer를 설정할 수 없습니다.", new InvalidOperationException("menuContainer could not be set."));
}
// UI 블로커 생성 및 설정
CreateUIBlocker();
}
protected virtual void CreateUIBlocker()
{
uiBlockerInstance = new GameObject("TopMenuUIBlocker");
// Canvas를 찾아 그 자식으로 설정합니다. Canvas가 여러 개일 경우, 적절한 Canvas를 찾는 로직이 필요할 수 있습니다.
Canvas canvas = FindFirstObjectByType<Canvas>();
Transform blockerParent = canvas != null ? canvas.transform : transform.parent;
if (blockerParent == null)
{
blockerParent = transform;
ULog.Warning("TopMenuUIBlocker의 부모를 TopMenuView 자신으로 설정합니다. Canvas 하위에 배치하는 것을 권장합니다.");
uiBlockerInstance.transform.SetParent(blockerParent, false);
uiBlockerInstance.transform.SetAsFirstSibling();
}
else
{
uiBlockerInstance.transform.SetParent(blockerParent, false);
// TopMenuView GameObject (또는 menuContainer) 바로 앞에 오도록 sibling index 설정
// menuContainer가 Canvas의 직계 자식이 아닐 수 있으므로, menuContainer의 sibling index를 기준으로 합니다.
if (menuContainer != null && menuContainer.parent == blockerParent)
{
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex());
}
else
{
// menuContainer가 blockerParent의 직계 자식이 아니거나 다른 복잡한 구조일 경우,
// 블로커를 가장 뒤(UI 요소들 중에서는 가장 앞)로 보내는 것이 안전할 수 있습니다.
// 또는, TopMenuView 자체가 Canvas의 자식이라면 그 기준으로 설정합니다.
if (transform.parent == blockerParent)
{
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex());
}
else
{
// 최후의 수단으로 가장 뒤로 보냅니다. (UI상 가장 앞으로)
// 하지만 이렇게 되면 다른 UI를 가릴 수 있으므로 주의해야 합니다.
// uiBlockerInstance.transform.SetAsLastSibling();
// 더 나은 방법은 Canvas 내에서 TopMenu 시스템 전체의 루트 바로 앞에 두는 것입니다.
// 여기서는 TopMenuView의 sibling index를 기준으로 합니다.
uiBlockerInstance.transform.SetSiblingIndex(transform.GetSiblingIndex());
}
}
}
RectTransform blockerRect = uiBlockerInstance.AddComponent<RectTransform>();
blockerRect.anchorMin = Vector2.zero;
blockerRect.anchorMax = Vector2.one;
blockerRect.offsetMin = Vector2.zero;
blockerRect.offsetMax = Vector2.zero;
Image blockerImage = uiBlockerInstance.AddComponent<Image>();
blockerImage.color = new Color(0, 0, 0, 0.001f);
blockerImage.raycastTarget = true;
Button blockerButton = uiBlockerInstance.AddComponent<Button>();
blockerButton.transition = Selectable.Transition.None;
blockerButton.onClick.AddListener(CloseAllOpenSubMenus);
uiBlockerInstance.SetActive(false);
}
public virtual Vector2 CreateMenuItems(List<MenuItemData> items, Transform parentContainer, int depth = 0)
{
if (items == null || parentContainer == null)
{
ULog.Error("메뉴 아이템 생성에 필요한 'items' 또는 'parentContainer'가 누락되었습니다.", new ArgumentNullException(items == null ? "items" : "parentContainer"));
return Vector2.zero;
}
float currentLevelCalculatedWidth = 0;
float currentLevelCalculatedHeight = 0;
if (items.Count == 0 && depth > 0)
{
return Vector2.zero;
}
for (int i = 0; i < items.Count; i++)
{
MenuItemData itemData = items[i];
GameObject instanceToLayout = null;
GameObject prefabToUse = null;
if (itemData.IsSeparator)
{
if (menuSeparatorPrefab == null)
{
ULog.Error("menuSeparatorPrefab이 할당되지 않았습니다. 구분선을 생성할 수 없습니다.", new InvalidOperationException("menuSeparatorPrefab is not loaded."));
continue;
}
GameObject separatorInstance = Instantiate(menuSeparatorPrefab, parentContainer);
separatorInstance.name = $"Separator_{itemData.ItemId}_Depth{depth}";
_menuItemObjects[itemData.ItemId] = separatorInstance;
instanceToLayout = separatorInstance;
}
else
{
if (depth == 0) // 1차 깊이 메뉴
{
prefabToUse = menuItemPrefab;
if (prefabToUse == null)
{
ULog.Error("menuItemPrefab이 할당되지 않았습니다. 1차 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("menuItemPrefab is not loaded."));
continue;
}
}
else // 2차 깊이 이상 하위 메뉴
{
prefabToUse = subMenuItemPrefab;
if (prefabToUse == null)
{
ULog.Error("subMenuItemPrefab이 할당되지 않았습니다. 하위 메뉴 아이템을 생성할 수 없습니다.", new InvalidOperationException("subMenuItemPrefab is not loaded."));
continue;
}
}
GameObject menuItemInstance = Instantiate(prefabToUse, parentContainer);
menuItemInstance.name = $"MenuItem_{itemData.ItemId}_Depth{depth}";
_menuItemObjects[itemData.ItemId] = menuItemInstance;
instanceToLayout = menuItemInstance;
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
Button button = menuItemInstance.GetComponent<Button>();
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
{
if (_locManager != null)
{
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
}
else
{
ULog.Warning($"LocalizationManager 인스턴스가 없어 메뉴 아이템 텍스트를 키 값으로 설정합니다: {itemData.DisplayNameKey}");
buttonText.text = $"[{itemData.DisplayNameKey}]";
}
}
if (button != null)
{
button.onClick.RemoveAllListeners();
button.onClick.AddListener(() =>
{
OnMenuItemClicked?.Invoke(itemData);
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
{
ToggleSubMenuDisplay(itemData, menuItemInstance);
}
else
{
// 하위 메뉴가 없는 아이템 클릭 시 모든 메뉴 닫기 (선택적)
CloseAllOpenSubMenus();
}
});
}
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
{
Transform subMenuArrowTransform = menuItemInstance.transform.Find(SubMenuArrowName);
if (subMenuArrowTransform != null)
{
subMenuArrowTransform.gameObject.SetActive(true);
}
Transform subMenuContainerTransform = menuItemInstance.transform.Find(SubMenuContainerName);
if (subMenuContainerTransform != null)
{
RectTransform subMenuRect = subMenuContainerTransform.GetComponent<RectTransform>();
if (subMenuRect == null)
{
ULog.Warning($"{SubMenuContainerName} for '{menuItemInstance.name}' is missing RectTransform. Adding one.");
subMenuRect = subMenuContainerTransform.gameObject.AddComponent<RectTransform>();
}
subMenuRect.anchorMin = new Vector2(0, 1);
subMenuRect.anchorMax = new Vector2(0, 1);
subMenuRect.pivot = new Vector2(0, 1);
// 하위 메뉴 위치 조정: 1차 메뉴는 오른쪽, 2차 이상은 아래쪽으로 펼쳐지도록 수정
if (depth == 0)
{
subMenuRect.anchoredPosition = new Vector2(menuDepthHorizontalGap, -menuDepthVerticalGap);
}
else
{
// 2차 이상 메뉴의 하위 메뉴는 부모 아이템의 오른쪽에 표시되도록 anchoredPosition 조정
// 부모 아이템의 너비만큼 이동 (subMenuItemWidth 사용)
subMenuRect.anchoredPosition = new Vector2(subMenuItemWidth, 0);
}
Vector2 subContentSize = CreateMenuItems(itemData.SubMenuItems, subMenuContainerTransform, depth + 1);
float containerWidth = subContentSize.x + (2 * subContainerHorizontalPadding);
float containerHeight = subContentSize.y + (2 * subContainerVerticalPadding);
subMenuRect.sizeDelta = new Vector2(containerWidth, containerHeight);
subMenuContainerTransform.gameObject.SetActive(false);
}
else
{
ULog.Warning($"'{menuItemInstance.name}' 내부에 '{SubMenuContainerName}'를 찾을 수 없습니다. 하위 메뉴가 표시되지 않습니다.");
}
}
else
{
Transform existingArrow = menuItemInstance.transform.Find(SubMenuArrowName);
if (existingArrow != null)
{
existingArrow.gameObject.SetActive(false);
}
}
}
if (instanceToLayout != null)
{
LayoutMenuItem(instanceToLayout, itemData, depth, i, items);
}
// 현재 레벨의 전체 너비와 높이 계산
if (depth == 0) // 최상위 메뉴 (수평)
{
if (i > 0) currentLevelCalculatedWidth += menuItemHorizontalGap;
currentLevelCalculatedWidth += itemData.IsSeparator ? menuItemSeparatorWidth : menuItemWidth;
currentLevelCalculatedHeight = Mathf.Max(currentLevelCalculatedHeight, itemData.IsSeparator ? menuItemSeparatorHeight : menuItemHeight);
}
else // 하위 메뉴 (수직)
{
if (i > 0) currentLevelCalculatedHeight += menuItemVerticalGap;
if (itemData.IsSeparator)
{
currentLevelCalculatedHeight += menuItemSeparatorHeight + (2 * menuItemSeparatorVerticalMargin);
}
else
{
currentLevelCalculatedHeight += subMenuItemHeight; // 하위 메뉴 아이템 높이 사용
}
currentLevelCalculatedWidth = Mathf.Max(currentLevelCalculatedWidth, subMenuItemWidth); // 하위 메뉴 아이템 너비 사용
}
}
return new Vector2(currentLevelCalculatedWidth, currentLevelCalculatedHeight);
}
protected virtual void LayoutMenuItem(GameObject uiElement, MenuItemData itemData, int depth, int itemIndex, List<MenuItemData> siblingItems)
{
RectTransform rectTransform = uiElement.GetComponent<RectTransform>();
if (rectTransform == null)
{
ULog.Error($"GameObject '{uiElement.name}' is missing a RectTransform component.");
return;
}
rectTransform.anchorMin = new Vector2(0, 1);
rectTransform.anchorMax = new Vector2(0, 1);
rectTransform.pivot = new Vector2(0, 1);
float currentX = 0;
float currentY = 0;
float currentItemWidth = (depth == 0) ? menuItemWidth : subMenuItemWidth;
float currentItemHeight = (depth == 0) ? menuItemHeight : subMenuItemHeight;
if (depth == 0) // 1차 깊이 메뉴 (수평 레이아웃)
{
currentX = menuItemInitX;
currentY = -menuItemInitY;
for (int k = 0; k < itemIndex; k++)
{
MenuItemData sibling = siblingItems[k];
if (sibling.IsSeparator)
{
currentX += menuItemSeparatorWidth;
}
else
{
currentX += menuItemWidth; // 1차 메뉴 너비
}
currentX += menuItemHorizontalGap;
}
if (itemData.IsSeparator)
{
rectTransform.sizeDelta = new Vector2(menuItemSeparatorWidth, menuItemSeparatorHeight);
}
else
{
rectTransform.sizeDelta = new Vector2(menuItemWidth, menuItemHeight);
}
}
else // 2차 깊이 이상 하위 메뉴 (수직 레이아웃)
{
currentX = subContainerHorizontalPadding; // 하위 메뉴 컨테이너 내부 패딩 적용
currentY = -subContainerVerticalPadding; // 하위 메뉴 컨테이너 내부 패딩 적용
for (int k = 0; k < itemIndex; k++)
{
MenuItemData sibling = siblingItems[k];
if (sibling.IsSeparator)
{
currentY -= (menuItemSeparatorVerticalMargin + menuItemSeparatorHeight + menuItemSeparatorVerticalMargin);
}
else
{
currentY -= subMenuItemHeight; // 하위 메뉴 아이템 높이
}
currentY -= menuItemVerticalGap;
}
if (itemData.IsSeparator)
{
currentY -= menuItemSeparatorVerticalMargin; // 구분선 위쪽 마진
// 하위 메뉴의 구분선 너비는 subMenuItemWidth를 따르도록 수정
rectTransform.sizeDelta = new Vector2(subMenuItemWidth, menuItemSeparatorHeight);
}
else
{
rectTransform.sizeDelta = new Vector2(subMenuItemWidth, subMenuItemHeight);
}
}
rectTransform.anchoredPosition = new Vector2(currentX, currentY);
}
protected virtual void ToggleSubMenuDisplay(MenuItemData itemData, GameObject menuItemInstance)
{
if (itemData.IsSeparator) return;
Transform subMenuContainer = menuItemInstance.transform.Find("SubMenuContainer");
if (subMenuContainer != null)
{
bool isActive = subMenuContainer.gameObject.activeSelf;
bool becomingActive = !isActive;
if (becomingActive)
{
CloseOtherSubMenusInParent(menuItemInstance.transform.parent, subMenuContainer);
// 하위 메뉴를 포함하는 menuItemInstance를 가장 앞으로 가져와서 다른 1단계 메뉴에 가려지지 않도록 합니다.
// 이는 menuItemInstance가 menuContainer의 직계 자식일 때 (depth 0 메뉴 아이템) 가장 효과적입니다.
// 더 깊은 레벨의 하위 메뉴는 이미 부모 SubMenuContainer 내에 있으므로,
// 해당 SubMenuContainer를 포함하는 상위 레벨의 menuItemInstance가 앞으로 와야 합니다.
// 최상위 메뉴 아이템(depth 0)의 경우, 그 부모는 menuContainer입니다.
// 하위 메뉴 아이템의 경우, 그 부모는 다른 SubMenuContainer입니다.
// 현재 로직은 클릭된 menuItemInstance를 그 부모 컨테이너 내에서 맨 앞으로 가져옵니다.
menuItemInstance.transform.SetAsLastSibling();
}
subMenuContainer.gameObject.SetActive(!isActive);
if (becomingActive)
{
isAnySubMenuOpen = true;
}
else
{
CheckIfAnySubMenuRemainsOpen();
}
UpdateBlockerVisibility();
}
}
protected virtual void CloseOtherSubMenusInParent(Transform currentMenuItemsParent, Transform subMenuToExclude)
{
if (currentMenuItemsParent == null)
{
ULog.Warning("CloseOtherSubMenusInParent 호출 시 currentMenuItemsParent가 null입니다.");
return;
}
for (int i = 0; i < currentMenuItemsParent.childCount; i++)
{
Transform siblingMenuItemTransform = currentMenuItemsParent.GetChild(i);
if (siblingMenuItemTransform == null || siblingMenuItemTransform == subMenuToExclude?.parent) continue;
Transform potentialSubMenu = siblingMenuItemTransform.Find("SubMenuContainer");
if (potentialSubMenu != null && potentialSubMenu != subMenuToExclude && potentialSubMenu.gameObject.activeSelf)
{
potentialSubMenu.gameObject.SetActive(false);
}
}
}
public virtual void CloseAllOpenSubMenus()
{
bool anyActuallyClosed = false;
// _menuItemObjects에는 모든 레벨의 메뉴 아이템 GameObject가 포함될 수 있습니다.
// 최상위 메뉴 아이템부터 순회하며 하위 메뉴를 닫는 것이 더 안전할 수 있습니다.
// 또는, 모든 SubMenuContainer를 직접 찾아 닫습니다.
// 현재 구현은 _menuItemObjects를 순회합니다.
foreach (GameObject menuItemGO in _menuItemObjects.Values)
{
if (menuItemGO == null) continue;
// IsSeparator인 경우 SubMenuContainer가 없을 것이므로, itemData를 통해 확인하거나,
// 단순히 Find로 null 체크만 해도 됩니다.
MenuItemData data = null; // 실제로는 itemID로 _menuItemData 딕셔너리에서 찾아야 함 (현재 없음)
// 여기서는 itemID로 _menuItemObjects에서 가져온 GO이므로, 해당 GO가 separator인지 알 수 없음.
// 따라서, 그냥 Find로 처리합니다.
// if(menuItemGO.name.StartsWith("Separator_")) continue; // 이름 기반으로 구분선 건너뛰기 (덜 안전함)
Transform subMenuContainer = menuItemGO.transform.Find("SubMenuContainer");
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
{
subMenuContainer.gameObject.SetActive(false);
anyActuallyClosed = true;
}
}
if (anyActuallyClosed || isAnySubMenuOpen)
{
isAnySubMenuOpen = false;
UpdateBlockerVisibility();
}
}
protected virtual void CheckIfAnySubMenuRemainsOpen()
{
isAnySubMenuOpen = false;
foreach (GameObject menuItemGO in _menuItemObjects.Values)
{
if (menuItemGO == null) continue;
// if(menuItemGO.name.StartsWith("Separator_")) continue;
Transform subMenuContainer = menuItemGO.transform.Find("SubMenuContainer");
if (subMenuContainer != null && subMenuContainer.gameObject.activeSelf)
{
isAnySubMenuOpen = true;
break;
}
}
}
protected virtual void UpdateBlockerVisibility()
{
if (uiBlockerInstance != null)
{
uiBlockerInstance.SetActive(isAnySubMenuOpen);
if (isAnySubMenuOpen)
{
// 블로커가 활성화될 때, 메뉴 시스템 바로 뒤(UI상 바로 앞)에 오도록 합니다.
// menuContainer가 Canvas의 직계 자식이라고 가정하고, 그 앞에 블로커를 둡니다.
// 더 복잡한 계층 구조에서는 이 부분이 조정되어야 할 수 있습니다.
if (menuContainer != null && uiBlockerInstance.transform.parent == menuContainer.parent)
{
uiBlockerInstance.transform.SetSiblingIndex(menuContainer.GetSiblingIndex());
}
// 만약 uiBlockerInstance가 Canvas 직속이고, menuContainer는 다른 곳에 있다면,
// menuContainer를 포함하는 최상위 UI 요소의 sibling index를 기준으로 해야 합니다.
// 여기서는 CreateUIBlocker에서 설정한 부모와 sibling index를 유지하거나,
// 필요시 menuContainer의 루트 캔버스 부모를 기준으로 다시 설정합니다.
}
}
}
public virtual void ClearMenuItems()
{
CloseAllOpenSubMenus();
foreach (var pair in _menuItemObjects)
{
if (pair.Value != null) Destroy(pair.Value);
}
_menuItemObjects.Clear();
if (menuContainer != null)
{
foreach (Transform child in menuContainer)
{
Destroy(child.gameObject);
}
}
}
public virtual void UpdateAllMenuTexts(List<MenuItemData> items)
{
if (_locManager == null)
{
ULog.Warning("LocalizationManager가 없어 메뉴 텍스트를 업데이트할 수 없습니다.");
return;
}
UpdateMenuTextsRecursive(items);
}
protected virtual void UpdateMenuTextsRecursive(List<MenuItemData> items)
{
if (items == null) return;
foreach (var itemData in items)
{
if (itemData.IsSeparator) continue;
if (_menuItemObjects.TryGetValue(itemData.ItemId, out GameObject menuItemInstance))
{
TextMeshProUGUI buttonText = menuItemInstance.GetComponentInChildren<TextMeshProUGUI>(true);
if (buttonText != null && !string.IsNullOrEmpty(itemData.DisplayNameKey))
{
buttonText.text = _locManager.GetString(itemData.DisplayNameKey);
}
}
if (itemData.SubMenuItems != null && itemData.SubMenuItems.Count > 0)
{
UpdateMenuTextsRecursive(itemData.SubMenuItems);
}
}
}
}
}

View File

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