using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; using UVC.Extension; using UVC.Locale; using UVC.UI.Toolbar.Model; using UVC.UI.ToolBar; using UVC.UI.ToolBar.View; using UVC.UI.Tooltip; namespace UVC.UI.Toolbar.View { /// /// ToolbarModel에 정의된 데이터를 기반으로 실제 툴바 UI를 생성하고 관리하는 MonoBehaviour 클래스입니다. /// 사용자의 UI 상호작용을 감지하여 모델의 상태를 변경하거나 커맨드를 실행하고, /// 모델의 상태 변경에 따라 UI를 업데이트합니다. /// /// /// 주요 역할: /// - UI 렌더링: ToolbarModel의 Items 리스트를 순회하며 각 항목에 맞는 UI GameObject(버튼, 구분선 등)를 프리팹으로부터 생성하여 화면에 표시합니다. /// - 이벤트 바인딩: 생성된 UI 요소(예: Unity Button의 onClick)와 ToolbarButtonBase 모델의 ExecuteClick 메서드를 연결합니다. /// 또한, 모델의 OnStateChanged, OnToggleStateChanged 등의 이벤트를 구독하여 모델 상태 변경 시 UI를 업데이트합니다. /// - UI 업데이트: 모델의 상태(텍스트, 아이콘, 활성화 상태, 선택 상태 등)가 변경되면 해당 UI 요소의 시각적 표현을 업데이트합니다. /// - 리소스 관리: 생성된 UI GameObject들과 이벤트 구독을 관리하며, 툴바가 파괴되거나 다시 렌더링될 때 정리합니다(ClearToolbar). /// - 하위 메뉴 관리: ToolbarExpandableButton 클릭 시 하위 메뉴 UI를 생성하고 토글합니다. /// - 툴팁 관리: 각 버튼에 TooltipHandler를 추가하여 툴팁 기능을 제공합니다. /// /// 이 클래스의 인스턴스는 Unity 씬 내의 GameObject에 컴포넌트로 추가되어야 하며, /// Inspector를 통해 필요한 프리팹들(standardButtonPrefab, toggleButtonPrefab 등)과 /// UI 요소들이 배치될 부모 Transform(toolbarContainer), LayoutGroup 등을 할당받아야 합니다. /// /// /// /// // 1. Unity 에디터에서 빈 GameObject를 만들고 ToolbarView 스크립트를 추가합니다. /// // 2. Inspector에서 ToolbarView 컴포넌트의 다음 필드들을 설정합니다: /// // - Standard Button Prefab: 표준 버튼 UI 프리팹 /// // - Toggle Button Prefab: 토글 버튼 UI 프리팹 /// // - Radio Button Prefab: 라디오 버튼 UI 프리팹 /// // - Expandable Button Prefab: 확장 버튼 UI 프리팹 /// // - Separator Prefab: 구분선 UI 프리팹 /// // - Sub Menu Panel Prefab: 확장 버튼의 하위 메뉴 패널 UI 프리팹 /// // - Toolbar Container: 생성된 툴바 항목 UI들이 자식으로 추가될 Transform (보통 이 GameObject 자체 또는 자식 Panel) /// // - Layout Group: Toolbar Container에 연결된 LayoutGroup (예: VerticalLayoutGroup, HorizontalLayoutGroup) /// // - Root Canvas: (선택 사항, TooltipManager 초기화에 필요하며 보통 자동으로 찾음) /// // /// // 3. Toolbar (또는 유사 클래스)에서 ToolbarModel을 설정한 후, /// // ToolbarView의 Initialize 메서드를 호출하여 툴바 UI를 생성합니다. /// // Toolbar controller = GetComponent(); // 또는 다른 방식으로 참조 /// // ToolbarModel model = controller.GetConfiguredModel(); // 가정: 컨트롤러가 모델을 반환 /// // Initialize(model); /// /// public class ToolbarView : MonoBehaviour { /// /// 현재 툴바 UI의 기반이 되는 데이터 모델입니다. /// Initialize 메서드를 통해 외부(주로 Toolbar)에서 주입받습니다. /// protected ToolbarModel ToolbarModel { get; private set; } // --- Inspector에서 할당할 프리팹 및 UI 요소들 --- [Header("UI Prefabs")] /// /// 표준 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("표준 버튼 UI에 사용될 프리팹입니다.")] public GameObject standardButtonPrefab; /// /// 토글 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("토글 버튼 UI에 사용될 프리팹입니다.")] public GameObject toggleButtonPrefab; /// /// 라디오 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("라디오 버튼 UI에 사용될 프리팹입니다.")] public GameObject radioButtonPrefab; /// /// 확장 가능한 버튼 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("확장 가능한 버튼 UI에 사용될 프리팹입니다.")] public GameObject expandableButtonPrefab; /// /// 구분선 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("구분선 UI에 사용될 프리팹입니다.")] public GameObject separatorPrefab; /// /// 확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("확장 버튼의 하위 메뉴 패널 UI에 사용될 프리팹입니다.")] public GameObject subMenuPanelPrefab; [Header("UI Layout")] /// /// 생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("생성된 툴바 항목 UI들이 자식으로 추가될 부모 Transform입니다.")] public Transform toolbarContainer; /// /// 툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다. Inspector를 통해 할당해야 합니다. /// [Tooltip("툴바 항목들의 자동 배치를 담당하는 LayoutGroup 컴포넌트입니다 (예: VerticalLayoutGroup).")] public LayoutGroup layoutGroup; /// /// 툴바 버튼 모델(ToolbarButtonBase)과 해당 모델을 나타내는 실제 UI GameObject를 매핑하는 딕셔너리입니다. /// 모델 상태 변경 시 해당 GameObject를 찾아 UI를 업데이트하거나, UI 정리 시 사용됩니다. /// protected Dictionary _modelToGameObjectMap = new Dictionary(); /// /// 버튼 타입별 UI 처리기를 저장하는 딕셔너리입니다. /// private Dictionary _buttonViewProcessors = new Dictionary(); /// /// 툴팁 표시에 사용될 루트 Canvas입니다. /// Inspector에서 할당하거나, Awake에서 자동으로 찾으려고 시도합니다. /// protected Canvas rootCanvas; /// /// MonoBehaviour의 Awake 메서드입니다. /// toolbarContainer, layoutGroup, rootCanvas 등의 필수 참조를 초기화하고, /// TooltipManager를 초기화합니다. /// protected virtual void Awake() { if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null || expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null) { Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this); } // toolbarContainer 자동 할당 (Inspector에서 할당되지 않은 경우) if (toolbarContainer == null) toolbarContainer = GetComponent(); if (toolbarContainer == null) toolbarContainer = GetComponentInChildren(true); // 비활성 자식도 포함 if (toolbarContainer == null) Debug.LogError("ToolbarView: toolbarContainer가 할당되지 않았습니다. Inspector에서 설정하거나 현재 GameObject에 Transform이 있어야 합니다.", this); // layoutGroup 자동 할당 및 추가 (Inspector에서 할당되지 않은 경우) if (layoutGroup == null && toolbarContainer != null) { layoutGroup = toolbarContainer.gameObject.GetComponent(); if (layoutGroup == null) { // 기본으로 VerticalLayoutGroup 추가. 필요시 프로젝트에 맞게 수정. layoutGroup = toolbarContainer.gameObject.AddComponent(); Debug.LogWarning("ToolbarView: LayoutGroup이 toolbarContainer에 없어 새로 추가합니다. (기본 VerticalLayoutGroup)", this); } } if (layoutGroup == null) Debug.LogError("ToolbarView: layoutGroup이 할당되지 않았습니다. toolbarContainer에 LayoutGroup 컴포넌트를 추가해주세요.", this); // rootCanvas 자동 찾기 (TooltipManager 초기화에 필요) if (rootCanvas == null) rootCanvas = GetComponentInParent(); if (rootCanvas == null) { Canvas[] canvases = FindObjectsByType(FindObjectsSortMode.None); // Unity 2023부터 FindObjectsByType 사용 권장 foreach (Canvas c in canvases) { if (c.isRootCanvas) { rootCanvas = c; break; } } if (rootCanvas == null && canvases.Length > 0) rootCanvas = canvases[0]; // 최후의 수단 } // TooltipManager 초기화 if (rootCanvas != null) { if (!TooltipManager.Instance.IsInitialized) TooltipManager.Instance.Initialize(rootCanvas.transform, rootCanvas); } else { Debug.LogError("ToolbarView: rootCanvas를 찾을 수 없어 TooltipVisualManager를 초기화할 수 없습니다."); } // 버튼 뷰 처리기 등록 _buttonViewProcessors[typeof(ToolbarStandardButton)] = new ToolbarStandardButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarToggleButton)] = new ToolbarToggleButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarRadioButton)] = new ToolbarRadioButtonViewProcessor(); _buttonViewProcessors[typeof(ToolbarExpandableButton)] = new ToolbarExpandableButtonViewProcessor(); // 새로운 버튼 타입이 추가되면 여기에 처리기를 등록합니다. } /// /// 지정된 ToolbarModel을 사용하여 툴바 UI를 초기화하고 렌더링합니다. /// 기존 UI가 있다면 정리(ClearToolbar)한 후, 새 모델에 따라 UI를 다시 생성(RenderToolbar)합니다. /// /// 화면에 표시할 툴바의 데이터 모델입니다. public virtual void Initialize(ToolbarModel toolbarModel) { this.ToolbarModel = toolbarModel; if (standardButtonPrefab == null || toggleButtonPrefab == null || radioButtonPrefab == null || expandableButtonPrefab == null || separatorPrefab == null || subMenuPanelPrefab == null) { Debug.LogError("ToolbarView: 필수 프리팹이 할당되지 않았습니다. Inspector에서 모든 프리팹을 설정해주세요.", this); return; } if (toolbarContainer == null) { Debug.LogError("ToolbarView: Initialize 실패. toolbarContainer가 할당되지 않았습니다.", this); return; } if (layoutGroup == null) // layoutGroup도 필수적이므로 확인 { Debug.LogError("ToolbarView: Initialize 실패. layoutGroup이 할당되지 않았습니다.", this); return; } if (this.ToolbarModel == null) { Debug.LogError("ToolbarView: Initialize 실패. 제공된 toolbarModel이 null입니다.", this); ClearToolbar(); // 모델이 없으면 기존 UI라도 정리 return; } RenderToolbar(); } /// /// 현재 툴바에 표시된 모든 UI 요소들을 제거하고, 관련된 이벤트 구독을 해제합니다. /// _modelToGameObjectMap, _toggleGroups를 비우고, 열려있는 하위 메뉴(currentSubMenu)도 파괴합니다. /// 툴팁도 숨깁니다. /// protected virtual void ClearToolbar() { if (_modelToGameObjectMap != null) { foreach (var pair in _modelToGameObjectMap) { if (pair.Key != null) // 모델(버튼)에 연결된 이벤트 구독 해제 { // ToolbarButtonBase 및 파생 클래스에 정의된 ClearEventHandlers를 호출하여 // 모델에 연결된 모든 이벤트 구독을 명시적으로 해제합니다. pair.Key.ClearEventHandlers(); } if (pair.Value != null) // UI GameObject 파괴 { TooltipHandler handler = pair.Value.GetComponent(); if (handler != null) // 툴팁 핸들러 이벤트도 정리 (필요시) { handler.OnPointerEnterAction = null; handler.OnPointerExitAction = null; } Destroy(pair.Value); } } _modelToGameObjectMap.Clear(); } if (_toggleGroups != null) // 토글 그룹 GameObject들도 정리 { foreach (var groupObj in _toggleGroups.Values) { if (groupObj != null && groupObj.gameObject != null) Destroy(groupObj.gameObject); } _toggleGroups.Clear(); } if (currentSubMenu != null) // 열려있는 하위 메뉴 파괴 { Destroy(currentSubMenu); currentSubMenu = null; } if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) // 툴팁 숨기기 { TooltipManager.Instance.HideTooltip(); } } /// /// 지정된 버튼 모델 타입에 맞는 IButtonViewProcessor를 반환합니다. /// /// 버튼 모델의 System.Type 객체입니다. /// 해당 타입의 처리기. 없으면 null을 반환합니다. protected IButtonViewProcessor GetButtonViewProcessor(System.Type buttonModelType) { if (_buttonViewProcessors.TryGetValue(buttonModelType, out var processor)) { return processor; } // 상속 관계를 고려하여 부모 타입의 처리기를 찾을 수도 있습니다. // 예를 들어, MyCustomButton : ToolbarStandardButton 인 경우, ToolbarStandardButton 처리기를 반환할 수 있습니다. // 이 로직은 필요에 따라 확장할 수 있습니다. // 현재는 정확한 타입 매칭만 지원합니다. return null; } /// /// ToolbarModel에 정의된 항목들을 기반으로 실제 UI 요소들을 생성하여 툴바를 구성합니다. /// 먼저 ClearToolbar를 호출하여 기존 UI를 정리한 후, 모델의 각 항목에 대해 적절한 프리팹을 사용하여 /// UI GameObject를 생성하고, 필요한 설정(이벤트 바인딩, 툴팁 등)을 수행합니다. /// 버튼 항목의 경우 IButtonViewProcessor 구현체에 UI 생성을 위임합니다. /// protected void RenderToolbar() { ClearToolbar(); // 기존 UI 및 이벤트 구독 정리 if (ToolbarModel == null || ToolbarModel.Items == null) return; if (toolbarContainer == null) { Debug.LogError("ToolbarView: RenderToolbar 중단. toolbarContainer가 null입니다.", this); return; } foreach (var itemModel in ToolbarModel.Items) { GameObject itemUIObject = null;// 생성될 UI GameObject if (itemModel is ToolbarSeparator) { if (separatorPrefab != null) itemUIObject = Instantiate(separatorPrefab, toolbarContainer); else Debug.LogError("ToolbarView: separatorPrefab이 할당되지 않았습니다.", this); } else if (itemModel is ToolbarButtonBase buttonModel) { IButtonViewProcessor processor = GetButtonViewProcessor(buttonModel.GetType()); if (processor != null) { itemUIObject = processor.CreateButtonUI(buttonModel, toolbarContainer, this); if (itemUIObject != null) { _modelToGameObjectMap[buttonModel] = itemUIObject; processor.SetupButtonInteractions(buttonModel, itemUIObject, this); // 모델 상태 변경 이벤트 구독 (공통) buttonModel.OnStateChanged += () => processor.UpdateCommonButtonVisuals(buttonModel, itemUIObject, this); // 토글 버튼/라디오 버튼의 경우 추가적인 이벤트 구독 if (buttonModel is ToolbarToggleButton toggleModel) // ToolbarRadioButton도 ToolbarToggleButton을 상속 { toggleModel.OnToggleStateChanged += (isSelected) => processor.UpdateToggleStateVisuals(toggleModel, itemUIObject, isSelected, this); } // 툴팁 핸들러 추가 및 설정 if (!string.IsNullOrEmpty(buttonModel.Tooltip)) { TooltipHandler tooltipHandler = itemUIObject.GetComponent(); if (tooltipHandler == null) tooltipHandler = itemUIObject.AddComponent(); tooltipHandler.Tooltip = buttonModel.Tooltip; if (TooltipManager.Instance != null && TooltipManager.Instance.IsInitialized) { tooltipHandler.OnPointerEnterAction = TooltipManager.Instance.HandlePointerEnter; tooltipHandler.OnPointerExitAction = TooltipManager.Instance.HandlePointerExit; } } } else { Debug.LogError($"ToolbarView: {buttonModel.GetType().Name}의 UI를 생성하지 못했습니다 (Processor 반환 null).", this); continue; } } else { Debug.LogError($"ToolbarView: {buttonModel.GetType().Name}에 대한 프리팹이 할당되지 않았거나 알 수 없는 버튼 타입입니다.", this); continue; } } else { Debug.LogWarning($"ToolbarView: 알 수 없는 IToolbarItem 타입 ({itemModel.GetType().Name})이거나, UI 생성이 지원되지 않습니다.", this); } if (itemUIObject != null) { itemUIObject.name = $"ToolbarItem_{itemModel.GetType().Name}_{(itemModel is ToolbarButtonBase b ? b.Text : "")}"; } } // 모든 UI 항목이 추가된 후 레이아웃 그룹 업데이트 if (layoutGroup != null) { // LayoutRebuilder.ForceRebuildLayoutImmediate(layoutGroup.GetComponent()); // 즉시 업데이트 // 또는 FitToChildren 확장 메서드 사용 (UVC.Extension에 정의되어 있다고 가정) layoutGroup.FitToChildren(width: layoutGroup is VerticalLayoutGroup, height: true); } } /// /// 버튼 모델과 UI GameObject를 기반으로 공통적인 시각적 요소(텍스트, 아이콘, 활성화 상태)를 업데이트합니다. /// 이 메서드는 각 IButtonViewProcessor 구현체에서 호출됩니다. /// /// 업데이트할 버튼의 데이터 모델입니다. /// 업데이트할 UI GameObject입니다. internal void InternalUpdateCommonButtonVisuals(ToolbarButtonBase model, GameObject itemObj) { if (model == null || itemObj == null) return; // 1. 텍스트 업데이트 TextMeshProUGUI buttonTextComponent = itemObj.GetComponentInChildren(true); if (buttonTextComponent != null) { string displayText = model.Text; if (LocalizationManager.Instance != null && !string.IsNullOrEmpty(model.Text)) { displayText = LocalizationManager.Instance.GetString(model.Text, model.Text); } buttonTextComponent.text = displayText; } // 2. 아이콘 업데이트 Image iconImageComponent = null; Transform iconTransform = itemObj.transform.Find("Icon"); if (iconTransform != null) iconImageComponent = iconTransform.GetComponent(); if (iconImageComponent == null) iconImageComponent = itemObj.GetComponentInChildren(true); if (iconImageComponent != null) { string iconPathToLoad = model.IconSpritePath; if (model is ToolbarToggleButton toggleButton) { iconPathToLoad = toggleButton.IsSelected ? toggleButton.IconSpritePath : toggleButton.OffIconSpritePath; } if (!string.IsNullOrEmpty(iconPathToLoad)) { Sprite iconSprite = LoadSpriteFromResources(iconPathToLoad); iconImageComponent.sprite = iconSprite; iconImageComponent.enabled = (iconSprite != null); } else { iconImageComponent.sprite = null; iconImageComponent.enabled = false; } } // 3. 활성화 상태(interactable) 업데이트 Selectable selectableComponent = itemObj.GetComponent(); if (selectableComponent != null) { selectableComponent.interactable = model.IsEnabled; } } protected Dictionary _toggleGroups = new Dictionary(); /// /// 라디오 버튼 그룹 이름에 해당하는 UnityEngine.UI.ToggleGroup을 가져오거나, 없으면 새로 생성합니다. /// 생성된 ToggleGroup GameObject는 toolbarContainer의 자식으로 추가되며, 레이아웃에 영향을 주지 않도록 설정됩니다. /// /// 찾거나 생성할 토글 그룹의 이름입니다. /// 해당 이름의 ToggleGroup 컴포넌트입니다. internal ToggleGroup GetOrCreateToggleGroup(string groupName) { if (string.IsNullOrEmpty(groupName)) { Debug.LogError("ToolbarView: GetOrCreateToggleGroup - groupName이 null이거나 비어있습니다.", this); // 임시 그룹 또는 null 반환 등의 예외 처리 필요 GameObject tempGroupObj = new GameObject($"ToggleGroup_UnnamedDynamic"); tempGroupObj.transform.SetParent(toolbarContainer, false); return tempGroupObj.AddComponent(); } if (!_toggleGroups.TryGetValue(groupName, out ToggleGroup group)) { // 새 ToggleGroup GameObject 생성 GameObject groupObj = new GameObject($"ToggleGroup_{groupName}"); groupObj.transform.SetParent(toolbarContainer); // RectTransform 설정 (크기가 0이 되도록 하여 레이아웃에 영향 최소화) RectTransform groupRect = groupObj.AddComponent(); groupRect.sizeDelta = new Vector2(0, 0); // 크기는 필요에 따라 조정 groupRect.anchoredPosition = Vector2.zero; groupRect.sizeDelta = Vector2.zero; groupRect.anchorMin = Vector2.zero; groupRect.anchorMax = Vector2.one; group = groupObj.AddComponent(); group.allowSwitchOff = false; // 라디오 버튼 그룹은 일반적으로 하나는 선택되어 있도록 함 LayoutElement element = groupObj.AddComponent(); element.ignoreLayout = true; // 레이아웃 그룹에서 무시하도록 설정 _toggleGroups.Add(groupName, group); } return group; } /// /// 현재 열려있는 하위 메뉴 UI GameObject입니다. 없으면 null입니다. /// protected GameObject currentSubMenu = null; /// /// 확장 가능한 버튼(ToolbarExpandableButton)에 대한 하위 메뉴 UI를 토글합니다. /// 이미 열려있으면 닫고, 닫혀있으면 엽니다. /// 하위 메뉴는 subMenuPanelPrefab을 사용하여 생성되며, expandableButtonObj의 위치를 기준으로 표시됩니다. /// /// 하위 버튼 목록을 가진 확장 버튼 모델입니다. /// 주 확장 버튼의 UI GameObject로, 하위 메뉴 위치 결정에 사용됩니다. internal void ToggleSubMenu(ToolbarExpandableButton expandableButtonModel, GameObject expandableButtonObj) { // 이미 다른 하위 메뉴가 열려있거나, 현재 클릭한 버튼의 하위 메뉴가 열려있다면 닫습니다. if (currentSubMenu != null) { Destroy(currentSubMenu); currentSubMenu = null; // 만약 현재 클릭한 버튼의 하위 메뉴가 이미 열려있어서 닫는 경우라면, 여기서 함수 종료 // (다시 열리지 않도록). 이 로직은 currentSubMenu가 어떤 버튼에 의해 열렸는지 알아야 함. // 간단하게는, 어떤 확장 버튼이든 클릭하면 기존 서브메뉴는 닫고, 필요하면 새로 연다. // 지금은 그냥 닫고, 아래 로직에서 새로 열 조건이 되면 열도록 함. // 만약 클릭한 버튼이 이미 열린 메뉴의 주인이면, 그냥 닫기만 하고 리턴하는게 자연스러울 수 있음. // 이 부분은 UI/UX 정책에 따라 상세 조정 필요. 현재는 "토글"이므로, 다시 누르면 닫히고, 없으면 연다. // 즉, 아래 로직에서 currentSubMenu가 null이 되었으므로, 다시 열릴 수 있음. // 만약 "닫기만" 하고 싶다면, 여기서 return 해야 함. // 현재 로직: 이미 열려있으면 무조건 닫고, 아래에서 다시 열지 결정. // 수정 제안: 만약 currentSubMenu가 이 expandableButtonModel에 의해 열린 것이라면, 닫고 return. // (이를 위해서는 currentSubMenu가 어떤 모델에 연결되었는지 정보가 필요) // 현재는 간단히 토글로 동작: 열려있으면 닫고, 닫혀있으면 연다. // 즉, if (currentSubMenu != null) { Destroy; currentSubMenu = null; return; } 이면 닫기만 하고 끝. // 지금은 닫고, 아래에서 다시 열 수 있게 함. } // 새 하위 메뉴를 열 조건 확인 (닫혀 있었고, 프리팹과 하위 버튼이 있는 경우) if (subMenuPanelPrefab == null) { Debug.LogError("ToolbarView: subMenuPanelPrefab이 할당되지 않아 하위 메뉴를 열 수 없습니다.", this); return; } if (expandableButtonModel.SubButtons == null || expandableButtonModel.SubButtons.Count == 0) { // Debug.LogWarning($"ToolbarView: 확장 버튼 '{expandableButtonModel.Text}'에 하위 버튼이 없어 메뉴를 열지 않습니다.", this); return; // 하위 버튼 없으면 메뉴 열지 않음 } // 하위 메뉴 패널 UI 생성 // 생성 위치는 일단 ToolbarView의 자식으로 하고, 이후 위치 조정 currentSubMenu = Instantiate(subMenuPanelPrefab, transform); currentSubMenu.name = $"SubMenu_{expandableButtonModel.Text}"; RectTransform panelRect = currentSubMenu.GetComponent(); if (panelRect == null) { Debug.LogError("ToolbarView: subMenuPanelPrefab에 RectTransform이 없습니다.", currentSubMenu); Destroy(currentSubMenu); currentSubMenu = null; return; } // 하위 메뉴 패널 위치 설정 (확장 버튼 UI 오른쪽 또는 아래 등) // 이 로직은 툴바의 방향(수평/수직)과 UI 디자인에 따라 매우 달라질 수 있습니다. // 여기서는 간단한 예시로, 확장 버튼의 오른쪽에 표시한다고 가정합니다. RectTransform expandableButtonRect = expandableButtonObj.GetComponent(); // panelRect의 앵커 및 피벗 설정 (예: 좌상단 기준) panelRect.anchorMin = new Vector2(0, 1); panelRect.anchorMax = new Vector2(0, 1); panelRect.pivot = new Vector2(0, 1); // 좌상단 피벗 currentSubMenu.SetActive(true); // 부모 내에서 가장 마지막 자식으로 만들어 최상단에 표시되도록 합니다. if (panelRect.parent != null) panelRect.SetAsLastSibling(); LayoutGroup subMenuLayoutGroup = currentSubMenu.GetComponent(); if (subMenuLayoutGroup == null) { // 하위 메뉴 패널에 LayoutGroup이 없다면 추가합니다. subMenuLayoutGroup = currentSubMenu.AddComponent(); } // 패널 위치를 확장 버튼의 위치에 맞춥니다. Vector2 offset = new Vector2(expandableButtonRect.anchoredPosition.x, expandableButtonRect.anchoredPosition.y); if (layoutGroup is VerticalLayoutGroup) { offset.x += expandableButtonRect.rect.width; // 수직 레이아웃인 경우 버튼 오른쪽에 표시 되도록 } else { if (subMenuLayoutGroup != null) offset.x -= subMenuLayoutGroup.padding.left; // 수평 레이아웃인 경우 패딩을 고려 offset.y -= expandableButtonRect.rect.height; // 수평 레이아웃인 경우 버튼 아래에 표시 되도록 } panelRect.anchoredPosition = offset; // 위치 조정 // 하위 버튼들 생성. 하위 메뉴 패널에 LayoutGroup이 있다면 자식 버튼들이 자동으로 정렬됩니다. foreach (var subItemBase in expandableButtonModel.SubButtons) { if (subItemBase is ToolbarButtonBase subItemModel) // 모든 하위 아이템은 ToolbarButtonBase라고 가정 { IButtonViewProcessor subProcessor = GetButtonViewProcessor(subItemModel.GetType()); // 하위 버튼도 적절한 프리팹을 사용해야 합니다. // 여기서는 모든 하위 버튼이 standardButtonPrefab을 사용한다고 가정합니다. // 실제로는 subItemModel의 타입에 따라 다른 프리팹을 선택할 수 있습니다. GameObject subButtonPrefabToUse = standardButtonPrefab; // 기본값 if (subProcessor != null) // 하위 버튼도 Processor를 통해 생성 시도 { if (subItemModel is ToolbarToggleButton) subButtonPrefabToUse = toggleButtonPrefab; else subButtonPrefabToUse = standardButtonPrefab; // 기본은 표준 버튼 // ... 다른 타입에 대한 처리 ... if (subButtonPrefabToUse == null) { Debug.LogError($"ToolbarView: 하위 버튼 '{subItemModel.Text}'에 대한 프리팹을 결정할 수 없습니다 (standardButtonPrefab 사용 시도).", this); continue; } GameObject subButtonObj = Instantiate(subButtonPrefabToUse, panelRect); // 하위 메뉴 패널의 자식으로 생성 subButtonObj.name = $"SubItem_{subItemModel.Text}"; // 하위 버튼의 시각적 요소 설정 및 상호작용 연결 InternalUpdateCommonButtonVisuals(subItemModel, subButtonObj); // 공통 UI 업데이트 // 하위 버튼 클릭 이벤트 연결 Button subUiButton = subButtonObj.GetComponent