ui 구조 변경 임시저장
This commit is contained in:
parent
0cce2efe62
commit
6f4417cd98
@ -6311,7 +6311,10 @@ PrefabInstance:
|
|||||||
m_AddedComponents:
|
m_AddedComponents:
|
||||||
- targetCorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
- targetCorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||||
insertIndex: -1
|
insertIndex: -1
|
||||||
addedObject: {fileID: 7197937761328488698}
|
addedObject: {fileID: 3190533777077691364}
|
||||||
|
- targetCorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||||
|
insertIndex: -1
|
||||||
|
addedObject: {fileID: 245423958264921561}
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
m_SourcePrefab: {fileID: 100100000, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||||
--- !u!224 &4720669467062659157 stripped
|
--- !u!224 &4720669467062659157 stripped
|
||||||
RectTransform:
|
RectTransform:
|
||||||
@ -6323,7 +6326,7 @@ GameObject:
|
|||||||
m_CorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
m_CorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||||
m_PrefabInstance: {fileID: 4463400116329503023}
|
m_PrefabInstance: {fileID: 4463400116329503023}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
--- !u!114 &7197937761328488698
|
--- !u!114 &3190533777077691364
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
@ -6332,10 +6335,11 @@ MonoBehaviour:
|
|||||||
m_GameObject: {fileID: 6740783381500491556}
|
m_GameObject: {fileID: 6740783381500491556}
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
m_EditorHideFlags: 0
|
m_EditorHideFlags: 0
|
||||||
m_Script: {fileID: 11500000, guid: 8a2e0954aa144633aad86e53dc80a46a, type: 3}
|
m_Script: {fileID: 11500000, guid: 46c8396c996c804449b383960b44e812, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
_enableBlockImage: 0
|
_enableBlockImage: 0
|
||||||
|
InputActionMaps: 3
|
||||||
_uiActionsInputBinding: {fileID: 11400000, guid: 8073fcaf56fc7c34e996d0d47044f146, type: 2}
|
_uiActionsInputBinding: {fileID: 11400000, guid: 8073fcaf56fc7c34e996d0d47044f146, type: 2}
|
||||||
_checklistView: {fileID: 7075966153492927588}
|
_checklistView: {fileID: 7075966153492927588}
|
||||||
_inventoryView: {fileID: 3570087040626823091}
|
_inventoryView: {fileID: 3570087040626823091}
|
||||||
@ -6346,6 +6350,18 @@ MonoBehaviour:
|
|||||||
_menuCategoryTabs: {fileID: 6805049896193344908}
|
_menuCategoryTabs: {fileID: 6805049896193344908}
|
||||||
_cookwareCategoryTabs: {fileID: 6628923975427483430}
|
_cookwareCategoryTabs: {fileID: 6628923975427483430}
|
||||||
_completeBatchFilledImage: {fileID: 2965326806322860544}
|
_completeBatchFilledImage: {fileID: 2965326806322860544}
|
||||||
|
--- !u!114 &245423958264921561
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 6740783381500491556}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: 80dee5e1862248aab26236036049e5fc, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier:
|
||||||
--- !u!1001 &4530765275021007961
|
--- !u!1001 &4530765275021007961
|
||||||
PrefabInstance:
|
PrefabInstance:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
BIN
Assets/_DDD/_Addressables/So/PopupUiState.asset
(Stored with Git LFS)
BIN
Assets/_DDD/_Addressables/So/PopupUiState.asset
(Stored with Git LFS)
Binary file not shown.
@ -1,23 +1,34 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
using DDD.MVVM;
|
||||||
|
|
||||||
namespace DDD
|
namespace DDD
|
||||||
{
|
{
|
||||||
public abstract class BaseUi : MonoBehaviour
|
public abstract class BaseUi : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
[SerializeField] protected bool _enableBlockImage;
|
||||||
|
|
||||||
protected CanvasGroup _canvasGroup;
|
protected CanvasGroup _canvasGroup;
|
||||||
protected GameObject _blockImage;
|
protected GameObject _blockImage;
|
||||||
protected GameObject _panel;
|
protected GameObject _panel;
|
||||||
public virtual bool IsBlockingTime => false;
|
protected BindingContext _bindingContext;
|
||||||
public virtual bool IsOpen => _panel.activeSelf;
|
|
||||||
|
|
||||||
[SerializeField] protected bool _enableBlockImage;
|
public virtual bool IsBlockingTime => false;
|
||||||
|
public virtual bool IsOpen => _panel != null && _panel.activeSelf;
|
||||||
|
|
||||||
protected virtual void Awake()
|
protected virtual void Awake()
|
||||||
{
|
{
|
||||||
_canvasGroup = GetComponent<CanvasGroup>();
|
_canvasGroup = GetComponent<CanvasGroup>();
|
||||||
_panel = transform.Find(CommonConstants.Panel).gameObject;
|
_panel = transform.Find(CommonConstants.Panel)?.gameObject;
|
||||||
_blockImage = transform.Find(CommonConstants.BlockImage).gameObject;
|
_blockImage = transform.Find(CommonConstants.BlockImage)?.gameObject;
|
||||||
|
|
||||||
|
_bindingContext = new BindingContext();
|
||||||
|
SetupAutoBindings();
|
||||||
|
SetupBindings();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnEnable()
|
protected virtual void OnEnable()
|
||||||
@ -44,11 +55,13 @@ protected virtual void OnDisable()
|
|||||||
protected virtual void OnDestroy()
|
protected virtual void OnDestroy()
|
||||||
{
|
{
|
||||||
TryUnregister();
|
TryUnregister();
|
||||||
|
_bindingContext?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void TryRegister() { }
|
protected virtual void TryRegister() { }
|
||||||
protected virtual void TryUnregister() { }
|
protected virtual void TryUnregister() { }
|
||||||
|
|
||||||
|
// BaseUi 메서드들을 직접 구현
|
||||||
public virtual void OpenPanel()
|
public virtual void OpenPanel()
|
||||||
{
|
{
|
||||||
if (_enableBlockImage)
|
if (_enableBlockImage)
|
||||||
@ -71,10 +84,127 @@ public virtual void ClosePanel()
|
|||||||
|
|
||||||
public virtual void SetUiInteractable(bool active)
|
public virtual void SetUiInteractable(bool active)
|
||||||
{
|
{
|
||||||
_canvasGroup.interactable = active;
|
if (_canvasGroup != null)
|
||||||
_canvasGroup.blocksRaycasts = active;
|
{
|
||||||
|
_canvasGroup.interactable = active;
|
||||||
|
_canvasGroup.blocksRaycasts = active;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsOpenPanel() => _panel.activeInHierarchy;
|
public bool IsOpenPanel() => _panel && _panel.activeInHierarchy;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attribute 기반 자동 바인딩 설정
|
||||||
|
/// </summary>
|
||||||
|
private void SetupAutoBindings()
|
||||||
|
{
|
||||||
|
var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(f => f.GetCustomAttribute<BindToAttribute>() != null);
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
var bindAttribute = field.GetCustomAttribute<BindToAttribute>();
|
||||||
|
SetupBinding(field, bindAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 컬렉션 바인딩 설정
|
||||||
|
var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
||||||
|
.Where(f => f.GetCustomAttribute<BindCollectionAttribute>() != null);
|
||||||
|
|
||||||
|
foreach (var field in collectionFields)
|
||||||
|
{
|
||||||
|
var bindAttribute = field.GetCustomAttribute<BindCollectionAttribute>();
|
||||||
|
SetupCollectionBinding(field, bindAttribute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 개별 필드의 바인딩 설정
|
||||||
|
/// </summary>
|
||||||
|
private void SetupBinding(FieldInfo field, BindToAttribute bindAttribute)
|
||||||
|
{
|
||||||
|
var target = field.GetValue(this);
|
||||||
|
|
||||||
|
IValueConverter converter = null;
|
||||||
|
if (bindAttribute.ConverterType != null)
|
||||||
|
{
|
||||||
|
converter = Activator.CreateInstance(bindAttribute.ConverterType) as IValueConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// UI 컴포넌트 타입별 바인딩 타겟 생성
|
||||||
|
IBindingTarget bindingTarget = target switch
|
||||||
|
{
|
||||||
|
Text text => new TextBindingTarget(text, bindAttribute.PropertyPath),
|
||||||
|
Image image => new ImageBindingTarget(image, bindAttribute.PropertyPath),
|
||||||
|
GameObject go => new ActiveBindingTarget(go, bindAttribute.PropertyPath),
|
||||||
|
Slider slider => new SliderBindingTarget(slider, bindAttribute.PropertyPath),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (bindingTarget != null)
|
||||||
|
{
|
||||||
|
_bindingContext.Bind(bindAttribute.PropertyPath, bindingTarget, converter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 컬렉션 바인딩 설정
|
||||||
|
/// </summary>
|
||||||
|
private void SetupCollectionBinding(FieldInfo field, BindCollectionAttribute bindAttribute)
|
||||||
|
{
|
||||||
|
var target = field.GetValue(this);
|
||||||
|
|
||||||
|
if (target is Transform parent)
|
||||||
|
{
|
||||||
|
// 컬렉션 바인딩 로직 (필요시 확장)
|
||||||
|
Debug.Log($"Collection binding for {bindAttribute.PropertyPath} is set up on {parent.name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 추가 바인딩 설정 - 하위 클래스에서 구현
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void SetupBindings() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel 속성 변경 이벤트 핸들러
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
HandleCustomPropertyChanged(e.PropertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드)
|
||||||
|
/// </summary>
|
||||||
|
protected virtual void HandleCustomPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
// 하위 클래스에서 구현
|
||||||
|
}
|
||||||
|
|
||||||
|
// 수동 바인딩 헬퍼 메서드들
|
||||||
|
protected void BindText(Text text, string propertyPath, IValueConverter converter = null)
|
||||||
|
{
|
||||||
|
var target = new TextBindingTarget(text, propertyPath);
|
||||||
|
_bindingContext?.Bind(propertyPath, target, converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void BindImage(Image image, string propertyPath, IValueConverter converter = null)
|
||||||
|
{
|
||||||
|
var target = new ImageBindingTarget(image, propertyPath);
|
||||||
|
_bindingContext?.Bind(propertyPath, target, converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void BindActive(GameObject gameObject, string propertyPath, IValueConverter converter = null)
|
||||||
|
{
|
||||||
|
var target = new ActiveBindingTarget(gameObject, propertyPath);
|
||||||
|
_bindingContext?.Bind(propertyPath, target, converter);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void BindSlider(Slider slider, string propertyPath, IValueConverter converter = null)
|
||||||
|
{
|
||||||
|
var target = new SliderBindingTarget(slider, propertyPath);
|
||||||
|
_bindingContext?.Bind(propertyPath, target, converter);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
74
Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs
Normal file
74
Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
namespace DDD
|
||||||
|
{
|
||||||
|
public class BaseViewModelUi<TViewModel> : BaseUi where TViewModel : SimpleViewModel
|
||||||
|
{
|
||||||
|
protected TViewModel _viewModel;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
|
||||||
|
_viewModel = GetComponent<TViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnEnable()
|
||||||
|
{
|
||||||
|
base.OnEnable();
|
||||||
|
|
||||||
|
if (_viewModel && _bindingContext != null)
|
||||||
|
{
|
||||||
|
_bindingContext.SetDataContext(_viewModel);
|
||||||
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisable()
|
||||||
|
{
|
||||||
|
base.OnDisable();
|
||||||
|
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void OpenPanel()
|
||||||
|
{
|
||||||
|
base.OpenPanel();
|
||||||
|
|
||||||
|
_viewModel?.Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void ClosePanel()
|
||||||
|
{
|
||||||
|
base.ClosePanel();
|
||||||
|
|
||||||
|
_viewModel?.Cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel 메서드 호출 헬퍼
|
||||||
|
/// </summary>
|
||||||
|
protected void InvokeViewModelMethod(string methodName, params object[] parameters)
|
||||||
|
{
|
||||||
|
if (_viewModel == null) return;
|
||||||
|
|
||||||
|
var method = _viewModel.GetType().GetMethod(methodName);
|
||||||
|
method?.Invoke(_viewModel, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel 속성 설정 헬퍼
|
||||||
|
/// </summary>
|
||||||
|
protected void SetViewModelProperty(string propertyName, object value)
|
||||||
|
{
|
||||||
|
if (_viewModel == null) return;
|
||||||
|
|
||||||
|
var property = _viewModel.GetType().GetProperty(propertyName);
|
||||||
|
if (property != null && property.CanWrite)
|
||||||
|
{
|
||||||
|
property.SetValue(_viewModel, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta
Normal file
3
Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: df6384ea09a44f188d636ca7ee47db13
|
||||||
|
timeCreated: 1755678434
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
namespace DDD
|
namespace DDD
|
||||||
{
|
{
|
||||||
public class ConfirmUi : PopupUi<RestaurantUiActions>
|
public class ConfirmUi : PopupUi<RestaurantUiActions, ConfirmViewModel>
|
||||||
{
|
{
|
||||||
[SerializeField] private TextMeshProUGUI _messageLabel;
|
[SerializeField] private TextMeshProUGUI _messageLabel;
|
||||||
[SerializeField] private LocalizeStringEvent _messageLabelLocalizeStringEvent;
|
[SerializeField] private LocalizeStringEvent _messageLabelLocalizeStringEvent;
|
||||||
|
7
Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs
Normal file
7
Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace DDD
|
||||||
|
{
|
||||||
|
public class ConfirmViewModel : SimpleViewModel
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
3
Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta
Normal file
3
Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7552fc9cc76345e09148a145ed7799a5
|
||||||
|
timeCreated: 1755679705
|
@ -1,321 +0,0 @@
|
|||||||
# 기존 UI 시스템을 MVVM으로 마이그레이션 가이드
|
|
||||||
|
|
||||||
Unity 프로젝트에서 기존 UI 시스템(BaseUi, BasePopupUi, PopupUi)을 MVVM 패턴으로 점진적으로 전환하는 방법을 설명합니다.
|
|
||||||
|
|
||||||
## 호환성 보장
|
|
||||||
|
|
||||||
### 1. 기존 UI 시스템은 그대로 유지됩니다
|
|
||||||
- `BaseUi`, `BasePopupUi`, `PopupUi<T>` 클래스들은 변경되지 않음
|
|
||||||
- 기존 UI들은 계속해서 정상 동작
|
|
||||||
- `UiManager`와 `PopupUiState`도 기존 방식 그대로 지원
|
|
||||||
|
|
||||||
### 2. 새로운 Integrated 클래스들 사용
|
|
||||||
- `IntegratedBaseUi<TViewModel>` : BaseUi + MVVM 기능 통합
|
|
||||||
- `IntegratedBasePopupUi<TViewModel>` : BasePopupUi + MVVM 기능 통합
|
|
||||||
- `IntegratedPopupUi<TInputEnum, TViewModel>` : PopupUi<T> + MVVM 기능 통합
|
|
||||||
|
|
||||||
## 마이그레이션 전략
|
|
||||||
|
|
||||||
### 단계 1: 점진적 전환 계획 수립
|
|
||||||
|
|
||||||
1. **우선순위 결정**
|
|
||||||
```
|
|
||||||
높음: 복잡한 상태 관리가 필요한 UI (InventoryView, ShopView 등)
|
|
||||||
중간: 중간 규모의 팝업 UI
|
|
||||||
낮음: 간단한 확인/알림 다이얼로그
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **전환 범위 설정**
|
|
||||||
- 한 번에 하나의 UI씩 전환
|
|
||||||
- 관련된 ViewModel과 Service 함께 구현
|
|
||||||
- 철저한 테스트 후 다음 UI 진행
|
|
||||||
|
|
||||||
### 단계 2: ViewModel 및 Service 구현
|
|
||||||
|
|
||||||
#### 기존 View 분석
|
|
||||||
```csharp
|
|
||||||
// 기존 InventoryView
|
|
||||||
public class InventoryView : MonoBehaviour
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory;
|
|
||||||
private List<ItemViewModel> _items;
|
|
||||||
|
|
||||||
public void UpdateCategoryView(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
_currentCategory = category;
|
|
||||||
// 복잡한 UI 업데이트 로직
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### ViewModel로 분리
|
|
||||||
```csharp
|
|
||||||
// 새로운 InventoryViewModel
|
|
||||||
public class InventoryViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory;
|
|
||||||
private List<ItemViewModel> _visibleItems;
|
|
||||||
|
|
||||||
public InventoryCategoryType CurrentCategory
|
|
||||||
{
|
|
||||||
get => _currentCategory;
|
|
||||||
set => SetField(ref _currentCategory, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetCategory(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
CurrentCategory = category;
|
|
||||||
UpdateVisibleItems(); // 비즈니스 로직
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Service 계층 분리
|
|
||||||
```csharp
|
|
||||||
// 비즈니스 로직을 Service로 분리
|
|
||||||
public class InventoryService : IUiService
|
|
||||||
{
|
|
||||||
public IEnumerable<ItemViewModel> FilterItems(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
// 복잡한 필터링 로직
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 단계 3: View를 MVVM으로 전환
|
|
||||||
|
|
||||||
#### 기존 View 코드
|
|
||||||
```csharp
|
|
||||||
public class InventoryView : MonoBehaviour, IEventHandler<InventoryChangedEvent>
|
|
||||||
{
|
|
||||||
[SerializeField] private Transform _slotParent;
|
|
||||||
[SerializeField] private Text _categoryLabel;
|
|
||||||
|
|
||||||
// 많은 상태 변수들과 복잡한 로직들...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### MVVM View로 전환
|
|
||||||
```csharp
|
|
||||||
public class MvvmInventoryView : MvvmBasePopupUi<InventoryViewModel>
|
|
||||||
{
|
|
||||||
[SerializeField] private Transform _slotParent;
|
|
||||||
[SerializeField] private Text _categoryLabel;
|
|
||||||
|
|
||||||
protected override void SetupBindings()
|
|
||||||
{
|
|
||||||
BindText(_categoryLabel, nameof(InventoryViewModel.CategoryDisplayText));
|
|
||||||
// 간단한 바인딩 설정만
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnCategoryButtonClicked(int categoryIndex)
|
|
||||||
{
|
|
||||||
ViewModel.SetCategory((InventoryCategoryType)categoryIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 단계 4: 마이그레이션 체크리스트
|
|
||||||
|
|
||||||
#### ViewModel 체크리스트
|
|
||||||
- [ ] SimpleViewModel 상속
|
|
||||||
- [ ] 모든 상태를 속성으로 변환
|
|
||||||
- [ ] SetField 사용한 PropertyChanged 알림
|
|
||||||
- [ ] 계산된 속성 구현
|
|
||||||
- [ ] 비즈니스 로직을 Service로 위임
|
|
||||||
|
|
||||||
#### View 체크리스트
|
|
||||||
- [ ] 적절한 MVVM Base 클래스 상속
|
|
||||||
- [ ] SetupBindings() 구현
|
|
||||||
- [ ] UI 이벤트를 ViewModel 메서드 호출로 변경
|
|
||||||
- [ ] 직접적인 UI 업데이트 코드 제거
|
|
||||||
- [ ] HandleCustomPropertyChanged에서 복잡한 UI 처리
|
|
||||||
|
|
||||||
#### Service 체크리스트
|
|
||||||
- [ ] IService 인터페이스 구현
|
|
||||||
- [ ] 복잡한 비즈니스 로직 포함
|
|
||||||
- [ ] 데이터 접근 로직 캡슐화
|
|
||||||
- [ ] 테스트 가능한 구조
|
|
||||||
|
|
||||||
## 구체적인 마이그레이션 예시
|
|
||||||
|
|
||||||
### 예시 1: 간단한 팝업 UI
|
|
||||||
|
|
||||||
#### Before (기존)
|
|
||||||
```csharp
|
|
||||||
public class SimpleDialogUi : BasePopupUi
|
|
||||||
{
|
|
||||||
[SerializeField] private Text _messageText;
|
|
||||||
[SerializeField] private Button _confirmButton;
|
|
||||||
|
|
||||||
public void ShowMessage(string message)
|
|
||||||
{
|
|
||||||
_messageText.text = message;
|
|
||||||
_confirmButton.onClick.AddListener(Close);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After (MVVM)
|
|
||||||
```csharp
|
|
||||||
// ViewModel
|
|
||||||
public class DialogViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
private string _message;
|
|
||||||
|
|
||||||
public string Message
|
|
||||||
{
|
|
||||||
get => _message;
|
|
||||||
set => SetField(ref _message, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View
|
|
||||||
public class MvvmSimpleDialogUi : MvvmBasePopupUi<DialogViewModel>
|
|
||||||
{
|
|
||||||
[SerializeField] private Text _messageText;
|
|
||||||
[SerializeField] private Button _confirmButton;
|
|
||||||
|
|
||||||
protected override void SetupBindings()
|
|
||||||
{
|
|
||||||
BindText(_messageText, nameof(DialogViewModel.Message));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Awake()
|
|
||||||
{
|
|
||||||
base.Awake();
|
|
||||||
_confirmButton.onClick.AddListener(() => Close());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 예시 2: 복잡한 목록 UI
|
|
||||||
|
|
||||||
#### Before (기존 InventoryView)
|
|
||||||
```csharp
|
|
||||||
public class InventoryView : MonoBehaviour
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory;
|
|
||||||
private Dictionary<string, ItemSlotUi> _slotLookup = new();
|
|
||||||
|
|
||||||
public void UpdateCategoryView(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
_currentCategory = category;
|
|
||||||
|
|
||||||
// 복잡한 필터링 로직
|
|
||||||
foreach (var slot in _slotLookup.Values)
|
|
||||||
{
|
|
||||||
bool shouldShow = MatchesCategory(slot.Model, category);
|
|
||||||
slot.SetActive(shouldShow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 정렬 로직
|
|
||||||
// UI 업데이트 로직
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool MatchesCategory(ItemViewModel model, InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
// 복잡한 카테고리 매칭 로직
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### After (MVVM)
|
|
||||||
```csharp
|
|
||||||
// ViewModel (상태 관리)
|
|
||||||
public class InventoryViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory;
|
|
||||||
private List<ItemViewModel> _visibleItems;
|
|
||||||
|
|
||||||
public InventoryCategoryType CurrentCategory
|
|
||||||
{
|
|
||||||
get => _currentCategory;
|
|
||||||
set => SetField(ref _currentCategory, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<ItemViewModel> VisibleItems
|
|
||||||
{
|
|
||||||
get => _visibleItems;
|
|
||||||
private set => SetField(ref _visibleItems, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CategoryDisplayText => GetCategoryDisplayText();
|
|
||||||
|
|
||||||
public void SetCategory(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
CurrentCategory = category;
|
|
||||||
UpdateVisibleItems();
|
|
||||||
OnPropertyChanged(nameof(CategoryDisplayText));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateVisibleItems()
|
|
||||||
{
|
|
||||||
var filtered = _inventoryService.FilterItems(CurrentCategory, CurrentSortType);
|
|
||||||
VisibleItems = filtered.ToList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Service (비즈니스 로직)
|
|
||||||
public class InventoryService : IUiService
|
|
||||||
{
|
|
||||||
public IEnumerable<ItemViewModel> FilterItems(InventoryCategoryType category, InventorySortType sortType)
|
|
||||||
{
|
|
||||||
// 복잡한 필터링과 정렬 로직을 Service로 이동
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// View (UI 바인딩만)
|
|
||||||
public class MvvmInventoryView : MvvmBasePopupUi<InventoryViewModel>
|
|
||||||
{
|
|
||||||
[SerializeField] private Text _categoryLabel;
|
|
||||||
[SerializeField] private Transform _slotParent;
|
|
||||||
|
|
||||||
protected override void SetupBindings()
|
|
||||||
{
|
|
||||||
BindText(_categoryLabel, nameof(InventoryViewModel.CategoryDisplayText));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void HandleCustomPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
if (propertyName == nameof(InventoryViewModel.VisibleItems))
|
|
||||||
{
|
|
||||||
UpdateItemSlots(); // 복잡한 UI 업데이트는 여전히 필요
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 마이그레이션 주의사항
|
|
||||||
|
|
||||||
### 1. 기존 코드와의 의존성
|
|
||||||
- 기존 UI를 참조하는 다른 코드들 확인
|
|
||||||
- 이벤트 시스템과의 연동 유지
|
|
||||||
- ScriptableObject 설정 파일들과의 호환성
|
|
||||||
|
|
||||||
### 2. 성능 고려사항
|
|
||||||
- PropertyChanged 이벤트의 과도한 발생 방지
|
|
||||||
- UI 업데이트 배칭 활용
|
|
||||||
- 메모리 누수 방지 (이벤트 핸들러 정리)
|
|
||||||
|
|
||||||
### 3. 테스트 전략
|
|
||||||
- ViewModel 단위 테스트 작성
|
|
||||||
- 기존 UI와 새 UI 동시 테스트
|
|
||||||
- 사용자 시나리오 기반 통합 테스트
|
|
||||||
|
|
||||||
## 마이그레이션 우선순위 추천
|
|
||||||
|
|
||||||
### 1차 마이그레이션 (높은 우선순위)
|
|
||||||
- **InventoryView**: 복잡한 상태 관리, 필터링, 정렬
|
|
||||||
- **ShopView**: 상품 목록, 카테고리, 구매 로직
|
|
||||||
- **MenuView**: 메뉴 관리, 레시피 선택
|
|
||||||
|
|
||||||
### 2차 마이그레이션 (중간 우선순위)
|
|
||||||
- **SettingsView**: 다양한 설정 옵션들
|
|
||||||
- **StatisticsView**: 데이터 표시와 계산
|
|
||||||
|
|
||||||
### 3차 마이그레이션 (낮은 우선순위)
|
|
||||||
- **SimpleDialogUi**: 간단한 확인 다이얼로그들
|
|
||||||
- **NotificationUi**: 알림 팝업들
|
|
||||||
|
|
||||||
이 가이드를 따라 점진적으로 UI를 MVVM으로 전환하면, 기존 시스템의 안정성을 유지하면서도 새로운 아키텍처의 이점을 얻을 수 있습니다.
|
|
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 4d3c9e8511cb71c42b53aa3543216518
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,275 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using UnityEngine.EventSystems;
|
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
using DDD.MVVM;
|
|
||||||
|
|
||||||
namespace DDD
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// MVVM 패턴을 적용한 새로운 레스토랑 관리 UI
|
|
||||||
/// 기존 RestaurantManagementUi의 기능을 ViewModel과 분리하여 구현
|
|
||||||
/// </summary>
|
|
||||||
public class NewRestaurantManagementUi : IntegratedBasePopupUi<RestaurantUiActions, RestaurantManagementViewModel>
|
|
||||||
{
|
|
||||||
[Header("Sub Views")]
|
|
||||||
[SerializeField] private ChecklistView _checklistView;
|
|
||||||
[SerializeField] private InventoryView _inventoryView;
|
|
||||||
[SerializeField] private ItemDetailView _itemDetailView;
|
|
||||||
[SerializeField] private TodayMenuView _todayMenuView;
|
|
||||||
[SerializeField] private TodayRestaurantStateView _todayRestaurantStateView;
|
|
||||||
|
|
||||||
[Header("Tab Groups")]
|
|
||||||
[SerializeField] private TabGroupUi _sectionTabs;
|
|
||||||
[SerializeField] private TabGroupUi _menuCategoryTabs;
|
|
||||||
[SerializeField] private TabGroupUi _cookwareCategoryTabs;
|
|
||||||
|
|
||||||
[Header("Hold Progress UI")]
|
|
||||||
[SerializeField, BindTo(nameof(RestaurantManagementViewModel.NormalizedHoldProgress))]
|
|
||||||
private Image _completeBatchFilledImage;
|
|
||||||
|
|
||||||
protected override void Awake()
|
|
||||||
{
|
|
||||||
base.Awake();
|
|
||||||
|
|
||||||
SetupViewModelEvents();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (_viewModel != null && _viewModel.IsHolding)
|
|
||||||
{
|
|
||||||
_viewModel.UpdateHoldProgress();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Open(OpenPopupUiEvent evt)
|
|
||||||
{
|
|
||||||
base.Open(evt);
|
|
||||||
|
|
||||||
InitializeViews();
|
|
||||||
SetupTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override GameObject GetInitialSelected()
|
|
||||||
{
|
|
||||||
// ViewModel의 현재 상태에 따라 초기 선택 UI 결정
|
|
||||||
var inventoryInitialSelected = _inventoryView.GetInitialSelected();
|
|
||||||
if (inventoryInitialSelected) return inventoryInitialSelected;
|
|
||||||
|
|
||||||
var menuCategoryButton = _menuCategoryTabs.GetFirstInteractableButton();
|
|
||||||
if (menuCategoryButton != null && menuCategoryButton.activeInHierarchy)
|
|
||||||
return menuCategoryButton;
|
|
||||||
|
|
||||||
var cookwareCategoryButton = _cookwareCategoryTabs.GetFirstInteractableButton();
|
|
||||||
if (cookwareCategoryButton != null && cookwareCategoryButton.activeInHierarchy)
|
|
||||||
return cookwareCategoryButton;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void SetupBindings()
|
|
||||||
{
|
|
||||||
// Attribute 기반 자동 바인딩이 처리됨
|
|
||||||
// 추가적인 수동 바인딩이 필요한 경우 여기에 구현
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void HandleCustomPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
switch (propertyName)
|
|
||||||
{
|
|
||||||
case nameof(RestaurantManagementViewModel.CurrentSection):
|
|
||||||
UpdateSectionTabs();
|
|
||||||
break;
|
|
||||||
case nameof(RestaurantManagementViewModel.CurrentCategory):
|
|
||||||
UpdateCategoryTabs();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupViewModelEvents()
|
|
||||||
{
|
|
||||||
if (!_viewModel) return;
|
|
||||||
|
|
||||||
_viewModel.OnBatchCompleted = HandleBatchCompleted;
|
|
||||||
_viewModel.OnChecklistFailed = HandleChecklistFailed;
|
|
||||||
_viewModel.OnMenuSectionSelected = HandleMenuSectionSelected;
|
|
||||||
_viewModel.OnCookwareSectionSelected = HandleCookwareSectionSelected;
|
|
||||||
_viewModel.OnCategoryChanged = HandleCategoryChanged;
|
|
||||||
_viewModel.OnTabMoved = HandleTabMoved;
|
|
||||||
_viewModel.OnInteractRequested = HandleInteractRequested;
|
|
||||||
_viewModel.OnCloseRequested = HandleCloseRequested;
|
|
||||||
_viewModel.OnMenuCategorySelected = HandleMenuCategorySelected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeViews()
|
|
||||||
{
|
|
||||||
_checklistView.Initalize();
|
|
||||||
_inventoryView.Initialize();
|
|
||||||
_itemDetailView.Initialize();
|
|
||||||
_todayMenuView.Initialize();
|
|
||||||
_todayRestaurantStateView.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupTabs()
|
|
||||||
{
|
|
||||||
SetupCategoryTabs();
|
|
||||||
InitializeTabGroups();
|
|
||||||
SelectInitialTabs();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetupCategoryTabs()
|
|
||||||
{
|
|
||||||
_menuCategoryTabs.UseDefaultAllowedValues();
|
|
||||||
_cookwareCategoryTabs.UseDefaultAllowedValues();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeTabGroups()
|
|
||||||
{
|
|
||||||
_sectionTabs.Initialize(OnSectionTabSelected);
|
|
||||||
_menuCategoryTabs.Initialize(OnCategoryTabSelected);
|
|
||||||
_cookwareCategoryTabs.Initialize(OnCategoryTabSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SelectInitialTabs()
|
|
||||||
{
|
|
||||||
_sectionTabs.SelectFirstTab();
|
|
||||||
_menuCategoryTabs.SelectFirstTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateSectionTabs()
|
|
||||||
{
|
|
||||||
if (_viewModel == null) return;
|
|
||||||
_sectionTabs.SelectTab((int)_viewModel.CurrentSection);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateCategoryTabs()
|
|
||||||
{
|
|
||||||
if (_viewModel == null) return;
|
|
||||||
|
|
||||||
switch (_viewModel.CurrentSection)
|
|
||||||
{
|
|
||||||
case SectionButtonType.Menu:
|
|
||||||
_menuCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
|
||||||
break;
|
|
||||||
case SectionButtonType.Cookware:
|
|
||||||
_cookwareCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ViewModel 이벤트 핸들러들
|
|
||||||
private void HandleBatchCompleted()
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleChecklistFailed()
|
|
||||||
{
|
|
||||||
var evt = GameEvents.OpenPopupUiEvent;
|
|
||||||
evt.UiType = typeof(ConfirmUi);
|
|
||||||
evt.IsCancelButtonVisible = true;
|
|
||||||
evt.NewMessageKey = "checklist_failed_message";
|
|
||||||
evt.OnConfirm = ClosePanel;
|
|
||||||
EventBus.Broadcast(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMenuSectionSelected()
|
|
||||||
{
|
|
||||||
_menuCategoryTabs.SelectFirstTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCookwareSectionSelected()
|
|
||||||
{
|
|
||||||
_cookwareCategoryTabs.SelectFirstTab();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCategoryChanged(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
_inventoryView.UpdateCategoryView(category);
|
|
||||||
_itemDetailView.UpdateCategory(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleTabMoved(int direction)
|
|
||||||
{
|
|
||||||
_sectionTabs.Move(direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleInteractRequested()
|
|
||||||
{
|
|
||||||
var selected = EventSystem.current.currentSelectedGameObject;
|
|
||||||
var interactable = selected?.GetComponent<IInteractableUi>();
|
|
||||||
interactable?.OnInteract();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCloseRequested()
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleMenuCategorySelected(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
_menuCategoryTabs.SelectTab((int)category);
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI 이벤트 핸들러들 (TabGroupUi 콜백)
|
|
||||||
private void OnSectionTabSelected(int sectionValue)
|
|
||||||
{
|
|
||||||
_viewModel?.SetSection((SectionButtonType)sectionValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnCategoryTabSelected(int categoryValue)
|
|
||||||
{
|
|
||||||
_viewModel?.SetCategory((InventoryCategoryType)categoryValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 입력 처리 - ViewModel로 위임
|
|
||||||
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
|
||||||
{
|
|
||||||
var isHandled = base.OnInputPerformed(actionEnum, context);
|
|
||||||
|
|
||||||
if (isHandled && _viewModel != null)
|
|
||||||
{
|
|
||||||
switch (actionEnum)
|
|
||||||
{
|
|
||||||
case RestaurantUiActions.Cancel:
|
|
||||||
_viewModel.CloseUi();
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.PreviousTab:
|
|
||||||
_viewModel.MoveTab(-1);
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.NextTab:
|
|
||||||
_viewModel.MoveTab(1);
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.Interact1:
|
|
||||||
_viewModel.InteractWithSelected();
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.Interact2:
|
|
||||||
_viewModel.StartHold();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isHandled;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnInputCanceled(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
|
||||||
{
|
|
||||||
var isHandled = base.OnInputCanceled(actionEnum, context);
|
|
||||||
|
|
||||||
if (isHandled && _viewModel != null)
|
|
||||||
{
|
|
||||||
switch (actionEnum)
|
|
||||||
{
|
|
||||||
case RestaurantUiActions.Interact2:
|
|
||||||
_viewModel.CancelHold();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isHandled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,3 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 8a2e0954aa144633aad86e53dc80a46a
|
|
||||||
timeCreated: 1755673386
|
|
@ -1,278 +0,0 @@
|
|||||||
# Unity MVVM 시스템 가이드
|
|
||||||
|
|
||||||
Unity에서 MVVM(Model-View-ViewModel) 패턴을 구현한 시스템입니다. Attribute 기반 데이터 바인딩과 `nameof`를 활용한 타입 안전한 구조를 제공합니다.
|
|
||||||
|
|
||||||
## 폴더 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
GameUi/New/
|
|
||||||
├── ViewModels/ # ViewModel 클래스들
|
|
||||||
│ ├── Base/ # 기본 ViewModel 클래스
|
|
||||||
│ │ └── SimpleViewModel.cs
|
|
||||||
│ └── InventoryViewModel.cs # 예시 ViewModel
|
|
||||||
├── Views/ # View 클래스들
|
|
||||||
│ └── Base/
|
|
||||||
│ └── AutoBindView.cs # 자동 바인딩 View 기본 클래스
|
|
||||||
├── Services/ # 서비스 계층 클래스들
|
|
||||||
│ ├── IService.cs # 서비스 인터페이스
|
|
||||||
│ └── InventoryService.cs # 예시 서비스
|
|
||||||
├── Utils/ # 유틸리티 클래스들
|
|
||||||
│ ├── BindToAttribute.cs # 바인딩 Attribute
|
|
||||||
│ └── BindingContext.cs # 바인딩 컨텍스트
|
|
||||||
└── Converters/ # 값 변환기들
|
|
||||||
├── IValueConverter.cs # 컨버터 인터페이스
|
|
||||||
└── CommonConverters.cs # 공통 컨버터들
|
|
||||||
```
|
|
||||||
|
|
||||||
## 핵심 클래스
|
|
||||||
|
|
||||||
### 1. SimpleViewModel
|
|
||||||
- ViewModel의 기본 클래스
|
|
||||||
- `INotifyPropertyChanged` 구현
|
|
||||||
- 속성 변경 알림 자동 처리
|
|
||||||
- 배치 업데이트 지원
|
|
||||||
|
|
||||||
### 2. IntegratedBaseUi<TViewModel>
|
|
||||||
- UI의 기본 클래스 (BaseUi + MVVM 기능 통합)
|
|
||||||
- Attribute 기반 자동 바인딩
|
|
||||||
- ViewModel과 UI 요소 자동 연결
|
|
||||||
|
|
||||||
### 3. BindToAttribute
|
|
||||||
- UI 요소를 ViewModel 속성에 바인딩
|
|
||||||
- `nameof()` 사용으로 타입 안전성 보장
|
|
||||||
- 컨버터 지원
|
|
||||||
|
|
||||||
## 사용법
|
|
||||||
|
|
||||||
### 1. ViewModel 생성
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace DDD.MVVM
|
|
||||||
{
|
|
||||||
public class MyViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
private string _title = "기본 제목";
|
|
||||||
private int _count = 0;
|
|
||||||
private bool _isVisible = true;
|
|
||||||
|
|
||||||
public string Title
|
|
||||||
{
|
|
||||||
get => _title;
|
|
||||||
set => SetField(ref _title, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int Count
|
|
||||||
{
|
|
||||||
get => _count;
|
|
||||||
set => SetField(ref _count, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsVisible
|
|
||||||
{
|
|
||||||
get => _isVisible;
|
|
||||||
set => SetField(ref _isVisible, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CountText => $"개수: {Count}";
|
|
||||||
|
|
||||||
public void IncrementCount()
|
|
||||||
{
|
|
||||||
Count++;
|
|
||||||
OnPropertyChanged(nameof(CountText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. View 생성
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
namespace DDD
|
|
||||||
{
|
|
||||||
public class MyView : IntegratedBaseUi<MyViewModel>
|
|
||||||
{
|
|
||||||
[SerializeField, BindTo(nameof(MyViewModel.Title))]
|
|
||||||
private Text _titleText;
|
|
||||||
|
|
||||||
[SerializeField, BindTo(nameof(MyViewModel.CountText))]
|
|
||||||
private Text _countText;
|
|
||||||
|
|
||||||
[SerializeField, BindTo(nameof(MyViewModel.IsVisible))]
|
|
||||||
private GameObject _panel;
|
|
||||||
|
|
||||||
// UI 이벤트 핸들러
|
|
||||||
public void OnIncrementButtonClicked()
|
|
||||||
{
|
|
||||||
ViewModel.IncrementCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnToggleVisibilityClicked()
|
|
||||||
{
|
|
||||||
ViewModel.IsVisible = !ViewModel.IsVisible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 컨버터 사용
|
|
||||||
|
|
||||||
```csharp
|
|
||||||
[SerializeField, BindTo(nameof(MyViewModel.IsVisible), typeof(InvertBoolConverter))]
|
|
||||||
private GameObject _hiddenPanel;
|
|
||||||
|
|
||||||
[SerializeField, BindTo(nameof(MyViewModel.Count), typeof(ItemCountConverter))]
|
|
||||||
private Text _formattedCountText;
|
|
||||||
```
|
|
||||||
|
|
||||||
## 기존 InventoryView 변환 예시
|
|
||||||
|
|
||||||
### 기존 코드 (InventoryView)
|
|
||||||
```csharp
|
|
||||||
public class InventoryView : MonoBehaviour
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
|
|
||||||
|
|
||||||
public void UpdateCategoryView(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
_currentCategory = category;
|
|
||||||
// 복잡한 UI 업데이트 로직...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### MVVM 변환 후
|
|
||||||
|
|
||||||
**InventoryViewModel.cs**
|
|
||||||
```csharp
|
|
||||||
namespace DDD.MVVM
|
|
||||||
{
|
|
||||||
public class InventoryViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
|
|
||||||
|
|
||||||
public InventoryCategoryType CurrentCategory
|
|
||||||
{
|
|
||||||
get => _currentCategory;
|
|
||||||
set => SetField(ref _currentCategory, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string CategoryDisplayText => CurrentCategory switch
|
|
||||||
{
|
|
||||||
InventoryCategoryType.Food => "음식",
|
|
||||||
InventoryCategoryType.Drink => "음료",
|
|
||||||
_ => "전체"
|
|
||||||
};
|
|
||||||
|
|
||||||
public void SetCategory(InventoryCategoryType category)
|
|
||||||
{
|
|
||||||
CurrentCategory = category;
|
|
||||||
OnPropertyChanged(nameof(CategoryDisplayText));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**InventoryView.cs**
|
|
||||||
```csharp
|
|
||||||
namespace DDD.MVVM
|
|
||||||
{
|
|
||||||
public class InventoryView : AutoBindView<InventoryViewModel>
|
|
||||||
{
|
|
||||||
[SerializeField, BindTo(nameof(InventoryViewModel.CategoryDisplayText))]
|
|
||||||
private Text _categoryLabel;
|
|
||||||
|
|
||||||
public void OnCategoryButtonClicked(int categoryIndex)
|
|
||||||
{
|
|
||||||
ViewModel.SetCategory((InventoryCategoryType)categoryIndex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 장점
|
|
||||||
|
|
||||||
### 1. 타입 안전성
|
|
||||||
- `nameof()` 사용으로 컴파일 타임 검증
|
|
||||||
- 속성명 변경 시 자동 업데이트
|
|
||||||
- IDE IntelliSense 지원
|
|
||||||
|
|
||||||
### 2. Unity 통합성
|
|
||||||
- Inspector에서 바인딩 확인
|
|
||||||
- MonoBehaviour 패턴 유지
|
|
||||||
- 기존 Unity 워크플로우와 호환
|
|
||||||
|
|
||||||
### 3. 유지보수성
|
|
||||||
- View와 비즈니스 로직 분리
|
|
||||||
- 테스트 가능한 ViewModel
|
|
||||||
- 재사용 가능한 컴포넌트
|
|
||||||
|
|
||||||
### 4. 개발 생산성
|
|
||||||
- 자동 바인딩으로 보일러플레이트 코드 감소
|
|
||||||
- 데이터 변경 시 UI 자동 업데이트
|
|
||||||
- 일관된 개발 패턴
|
|
||||||
|
|
||||||
## 컨버터 목록
|
|
||||||
|
|
||||||
### 기본 컨버터들
|
|
||||||
- `InvertBoolConverter`: 불린 값 반전
|
|
||||||
- `ItemCountConverter`: 숫자를 "아이템 수: N" 형식으로 변환
|
|
||||||
- `InventoryCategoryConverter`: 카테고리 열거형을 한국어로 변환
|
|
||||||
- `CurrencyConverter`: 숫자를 통화 형식으로 변환
|
|
||||||
- `PercentageConverter`: 소수를 백분율로 변환
|
|
||||||
|
|
||||||
### 커스텀 컨버터 생성
|
|
||||||
```csharp
|
|
||||||
public class CustomConverter : IValueConverter
|
|
||||||
{
|
|
||||||
public object Convert(object value)
|
|
||||||
{
|
|
||||||
// 변환 로직 구현
|
|
||||||
return convertedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ConvertBack(object value)
|
|
||||||
{
|
|
||||||
// 역변환 로직 구현 (선택사항)
|
|
||||||
return originalValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 베스트 프랙티스
|
|
||||||
|
|
||||||
### 1. ViewModel 설계
|
|
||||||
- 단일 책임 원칙 준수
|
|
||||||
- UI 관련 Unity API 직접 사용 금지
|
|
||||||
- 계산된 속성 적극 활용
|
|
||||||
- 이벤트를 통한 느슨한 결합
|
|
||||||
|
|
||||||
### 2. 바인딩 설정
|
|
||||||
- `nameof()` 사용 필수
|
|
||||||
- 적절한 컨버터 활용
|
|
||||||
- 복잡한 로직은 ViewModel에서 처리
|
|
||||||
|
|
||||||
### 3. 성능 고려사항
|
|
||||||
- 배치 업데이트 활용
|
|
||||||
- 불필요한 PropertyChanged 이벤트 방지
|
|
||||||
- 컬렉션 변경 시 효율적인 업데이트
|
|
||||||
|
|
||||||
## 마이그레이션 가이드
|
|
||||||
|
|
||||||
### 단계별 적용 방법
|
|
||||||
|
|
||||||
#### 1단계: 기존 View에서 ViewModel 분리
|
|
||||||
1. View의 상태 변수들을 ViewModel로 이동
|
|
||||||
2. 비즈니스 로직을 Service로 분리
|
|
||||||
3. UI 업데이트 로직을 바인딩으로 대체
|
|
||||||
|
|
||||||
#### 2단계: 자동 바인딩 적용
|
|
||||||
1. `AutoBindView<TViewModel>` 상속
|
|
||||||
2. UI 요소에 `BindTo` Attribute 추가
|
|
||||||
3. 수동 UI 업데이트 코드 제거
|
|
||||||
|
|
||||||
#### 3단계: 최적화 및 리팩토링
|
|
||||||
1. 컨버터 활용으로 로직 단순화
|
|
||||||
2. 계산된 속성으로 중복 제거
|
|
||||||
3. 이벤트 시스템과 통합
|
|
||||||
|
|
||||||
이 MVVM 시스템을 통해 Unity UI 개발의 생산성과 유지보수성을 크게 향상시킬 수 있습니다.
|
|
@ -1,7 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 5d5b08d15b2e43f4698c0f24977c2582
|
|
||||||
TextScriptImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
@ -1,252 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.EventSystems;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using Sirenix.OdinInspector;
|
|
||||||
using UnityEngine.InputSystem;
|
|
||||||
using DDD.MVVM;
|
|
||||||
|
|
||||||
namespace DDD
|
|
||||||
{
|
|
||||||
public abstract class IntegratedBasePopupUi<TInputEnum, TViewModel> : BasePopupUi
|
|
||||||
where TInputEnum : Enum
|
|
||||||
where TViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
[SerializeField, Required] protected BaseUiActionsInputBinding<TInputEnum> _uiActionsInputBinding;
|
|
||||||
|
|
||||||
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers =
|
|
||||||
new();
|
|
||||||
|
|
||||||
// MVVM 기능들
|
|
||||||
protected BindingContext _bindingContext;
|
|
||||||
protected TViewModel _viewModel;
|
|
||||||
|
|
||||||
public override InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
|
|
||||||
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
|
|
||||||
|
|
||||||
protected override void Awake()
|
|
||||||
{
|
|
||||||
base.Awake();
|
|
||||||
|
|
||||||
_enableBlockImage = true;
|
|
||||||
_viewModel = GetComponent<TViewModel>();
|
|
||||||
|
|
||||||
_bindingContext = new BindingContext();
|
|
||||||
SetupAutoBindings();
|
|
||||||
SetupBindings();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnEnable()
|
|
||||||
{
|
|
||||||
base.OnEnable();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Update()
|
|
||||||
{
|
|
||||||
base.Update();
|
|
||||||
|
|
||||||
if (IsOpenPanel() == false) return;
|
|
||||||
|
|
||||||
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
|
|
||||||
if (currentSelectedGameObject == null || currentSelectedGameObject.activeInHierarchy == false)
|
|
||||||
{
|
|
||||||
var initialSelected = GetInitialSelected();
|
|
||||||
if (initialSelected != null)
|
|
||||||
{
|
|
||||||
EventSystem.current.SetSelectedGameObject(initialSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Open(OpenPopupUiEvent evt)
|
|
||||||
{
|
|
||||||
base.Open(evt);
|
|
||||||
|
|
||||||
var initialSelected = GetInitialSelected();
|
|
||||||
if (initialSelected != null)
|
|
||||||
{
|
|
||||||
EventSystem.current.SetSelectedGameObject(initialSelected);
|
|
||||||
}
|
|
||||||
|
|
||||||
transform.SetAsLastSibling();
|
|
||||||
|
|
||||||
if (IsTopPopup)
|
|
||||||
{
|
|
||||||
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override abstract GameObject GetInitialSelected();
|
|
||||||
|
|
||||||
protected override void TryRegister()
|
|
||||||
{
|
|
||||||
base.TryRegister();
|
|
||||||
|
|
||||||
// PopupUi의 입력 바인딩 등록
|
|
||||||
foreach (var actionEnum in _uiActionsInputBinding.BindingActions.GetFlags())
|
|
||||||
{
|
|
||||||
if (actionEnum.Equals(default(TInputEnum))) continue;
|
|
||||||
|
|
||||||
var inputAction =
|
|
||||||
InputManager.Instance.GetAction(_uiActionsInputBinding.InputActionMaps, actionEnum.ToString());
|
|
||||||
if (inputAction == null) continue;
|
|
||||||
|
|
||||||
var startedHandler = new Action<InputAction.CallbackContext>(context =>
|
|
||||||
{
|
|
||||||
OnInputStarted(actionEnum, context);
|
|
||||||
});
|
|
||||||
inputAction.started += startedHandler;
|
|
||||||
|
|
||||||
var performedHandler = new Action<InputAction.CallbackContext>(context =>
|
|
||||||
{
|
|
||||||
OnInputPerformed(actionEnum, context);
|
|
||||||
});
|
|
||||||
inputAction.performed += performedHandler;
|
|
||||||
|
|
||||||
var canceledHandler = new Action<InputAction.CallbackContext>(context =>
|
|
||||||
{
|
|
||||||
OnInputCanceled(actionEnum, context);
|
|
||||||
});
|
|
||||||
inputAction.canceled += canceledHandler;
|
|
||||||
|
|
||||||
_registeredHandlers.Add((inputAction, startedHandler));
|
|
||||||
_registeredHandlers.Add((inputAction, performedHandler));
|
|
||||||
_registeredHandlers.Add((inputAction, canceledHandler));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void TryUnregister()
|
|
||||||
{
|
|
||||||
base.TryUnregister();
|
|
||||||
|
|
||||||
// 입력 핸들러 해제
|
|
||||||
foreach (var (action, handler) in _registeredHandlers)
|
|
||||||
{
|
|
||||||
if (action != null)
|
|
||||||
{
|
|
||||||
action.started -= handler;
|
|
||||||
action.performed -= handler;
|
|
||||||
action.canceled -= handler;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_registeredHandlers.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 입력 처리 메서드들
|
|
||||||
protected virtual bool OnInputStarted(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
|
||||||
protected virtual bool OnInputPerformed(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
|
||||||
protected virtual bool OnInputCanceled(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute 기반 자동 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupAutoBindings()
|
|
||||||
{
|
|
||||||
var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
|
||||||
.Where(f => f.GetCustomAttribute<BindToAttribute>() != null);
|
|
||||||
|
|
||||||
foreach (var field in fields)
|
|
||||||
{
|
|
||||||
var bindAttribute = field.GetCustomAttribute<BindToAttribute>();
|
|
||||||
SetupBinding(field, bindAttribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 컬렉션 바인딩 설정
|
|
||||||
var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
|
||||||
.Where(f => f.GetCustomAttribute<BindCollectionAttribute>() != null);
|
|
||||||
|
|
||||||
foreach (var field in collectionFields)
|
|
||||||
{
|
|
||||||
var bindAttribute = field.GetCustomAttribute<BindCollectionAttribute>();
|
|
||||||
SetupCollectionBinding(field, bindAttribute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 개별 필드의 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupBinding(FieldInfo field, BindToAttribute bindAttribute)
|
|
||||||
{
|
|
||||||
var target = field.GetValue(this);
|
|
||||||
|
|
||||||
IValueConverter converter = null;
|
|
||||||
if (bindAttribute.ConverterType != null)
|
|
||||||
{
|
|
||||||
converter = Activator.CreateInstance(bindAttribute.ConverterType) as IValueConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI 컴포넌트 타입별 바인딩 타겟 생성
|
|
||||||
IBindingTarget bindingTarget = target switch
|
|
||||||
{
|
|
||||||
Text text => new TextBindingTarget(text, bindAttribute.PropertyPath),
|
|
||||||
Image image => new ImageBindingTarget(image, bindAttribute.PropertyPath),
|
|
||||||
GameObject gameObject => new ActiveBindingTarget(gameObject, bindAttribute.PropertyPath),
|
|
||||||
Slider slider => new SliderBindingTarget(slider, bindAttribute.PropertyPath),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (bindingTarget != null)
|
|
||||||
{
|
|
||||||
_bindingContext.Bind(bindAttribute.PropertyPath, bindingTarget, converter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 컬렉션 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupCollectionBinding(FieldInfo field, BindCollectionAttribute bindAttribute)
|
|
||||||
{
|
|
||||||
var target = field.GetValue(this);
|
|
||||||
|
|
||||||
if (target is Transform parent)
|
|
||||||
{
|
|
||||||
// 컬렉션 바인딩 로직 (필요시 확장)
|
|
||||||
Debug.Log($"Collection binding for {bindAttribute.PropertyPath} is set up on {parent.name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 추가 바인딩 설정 - 하위 클래스에서 구현
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SetupBindings() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ViewModel 속성 변경 이벤트 핸들러
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
HandleCustomPropertyChanged(e.PropertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드)
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void HandleCustomPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
// 하위 클래스에서 구현
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI 패널 열기 오버라이드 (ViewModel 초기화 추가)
|
|
||||||
/// </summary>
|
|
||||||
public override void OpenPanel()
|
|
||||||
{
|
|
||||||
base.OpenPanel();
|
|
||||||
_viewModel?.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// UI 패널 닫기 오버라이드 (ViewModel 정리 추가)
|
|
||||||
/// </summary>
|
|
||||||
public override void ClosePanel()
|
|
||||||
{
|
|
||||||
_viewModel?.Cleanup();
|
|
||||||
base.ClosePanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 3ef87d5c2f3d82e488302056ac09a287
|
|
@ -1,248 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Reflection;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using DDD.MVVM;
|
|
||||||
|
|
||||||
namespace DDD
|
|
||||||
{
|
|
||||||
public abstract class IntegratedBaseUi<TViewModel> : MonoBehaviour where TViewModel : SimpleViewModel
|
|
||||||
{
|
|
||||||
[SerializeField] protected bool _enableBlockImage;
|
|
||||||
|
|
||||||
protected CanvasGroup _canvasGroup;
|
|
||||||
protected GameObject _blockImage;
|
|
||||||
protected GameObject _panel;
|
|
||||||
protected BindingContext _bindingContext;
|
|
||||||
protected TViewModel _viewModel;
|
|
||||||
|
|
||||||
public virtual bool IsBlockingTime => false;
|
|
||||||
public virtual bool IsOpen => _panel != null && _panel.activeSelf;
|
|
||||||
|
|
||||||
protected virtual void Awake()
|
|
||||||
{
|
|
||||||
_canvasGroup = GetComponent<CanvasGroup>();
|
|
||||||
_panel = transform.Find(CommonConstants.Panel)?.gameObject;
|
|
||||||
_blockImage = transform.Find(CommonConstants.BlockImage)?.gameObject;
|
|
||||||
|
|
||||||
if (_viewModel == null)
|
|
||||||
_viewModel = GetComponent<TViewModel>();
|
|
||||||
|
|
||||||
_bindingContext = new BindingContext();
|
|
||||||
SetupAutoBindings();
|
|
||||||
SetupBindings();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnEnable()
|
|
||||||
{
|
|
||||||
if (_viewModel && _bindingContext != null)
|
|
||||||
{
|
|
||||||
_bindingContext.SetDataContext(_viewModel);
|
|
||||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Start()
|
|
||||||
{
|
|
||||||
TryRegister();
|
|
||||||
ClosePanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void Update()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnDisable()
|
|
||||||
{
|
|
||||||
if (_viewModel != null)
|
|
||||||
{
|
|
||||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void OnDestroy()
|
|
||||||
{
|
|
||||||
TryUnregister();
|
|
||||||
_bindingContext?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual void TryRegister() { }
|
|
||||||
protected virtual void TryUnregister() { }
|
|
||||||
|
|
||||||
// BaseUi 메서드들을 직접 구현
|
|
||||||
public virtual void OpenPanel()
|
|
||||||
{
|
|
||||||
if (_enableBlockImage)
|
|
||||||
{
|
|
||||||
_blockImage.SetActive(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
_panel.SetActive(true);
|
|
||||||
_viewModel?.Initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void ClosePanel()
|
|
||||||
{
|
|
||||||
if (_enableBlockImage)
|
|
||||||
{
|
|
||||||
_blockImage.SetActive(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
_panel.SetActive(false);
|
|
||||||
_viewModel?.Cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
public virtual void SetUiInteractable(bool active)
|
|
||||||
{
|
|
||||||
if (_canvasGroup != null)
|
|
||||||
{
|
|
||||||
_canvasGroup.interactable = active;
|
|
||||||
_canvasGroup.blocksRaycasts = active;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsOpenPanel() => _panel && _panel.activeInHierarchy;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Attribute 기반 자동 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupAutoBindings()
|
|
||||||
{
|
|
||||||
var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
|
||||||
.Where(f => f.GetCustomAttribute<BindToAttribute>() != null);
|
|
||||||
|
|
||||||
foreach (var field in fields)
|
|
||||||
{
|
|
||||||
var bindAttribute = field.GetCustomAttribute<BindToAttribute>();
|
|
||||||
SetupBinding(field, bindAttribute);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 컬렉션 바인딩 설정
|
|
||||||
var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
|
|
||||||
.Where(f => f.GetCustomAttribute<BindCollectionAttribute>() != null);
|
|
||||||
|
|
||||||
foreach (var field in collectionFields)
|
|
||||||
{
|
|
||||||
var bindAttribute = field.GetCustomAttribute<BindCollectionAttribute>();
|
|
||||||
SetupCollectionBinding(field, bindAttribute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 개별 필드의 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupBinding(FieldInfo field, BindToAttribute bindAttribute)
|
|
||||||
{
|
|
||||||
var target = field.GetValue(this);
|
|
||||||
|
|
||||||
IValueConverter converter = null;
|
|
||||||
if (bindAttribute.ConverterType != null)
|
|
||||||
{
|
|
||||||
converter = Activator.CreateInstance(bindAttribute.ConverterType) as IValueConverter;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI 컴포넌트 타입별 바인딩 타겟 생성
|
|
||||||
IBindingTarget bindingTarget = target switch
|
|
||||||
{
|
|
||||||
Text text => new TextBindingTarget(text, bindAttribute.PropertyPath),
|
|
||||||
Image image => new ImageBindingTarget(image, bindAttribute.PropertyPath),
|
|
||||||
GameObject go => new ActiveBindingTarget(go, bindAttribute.PropertyPath),
|
|
||||||
Slider slider => new SliderBindingTarget(slider, bindAttribute.PropertyPath),
|
|
||||||
_ => null
|
|
||||||
};
|
|
||||||
|
|
||||||
if (bindingTarget != null)
|
|
||||||
{
|
|
||||||
_bindingContext.Bind(bindAttribute.PropertyPath, bindingTarget, converter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 컬렉션 바인딩 설정
|
|
||||||
/// </summary>
|
|
||||||
private void SetupCollectionBinding(FieldInfo field, BindCollectionAttribute bindAttribute)
|
|
||||||
{
|
|
||||||
var target = field.GetValue(this);
|
|
||||||
|
|
||||||
if (target is Transform parent)
|
|
||||||
{
|
|
||||||
// 컬렉션 바인딩 로직 (필요시 확장)
|
|
||||||
Debug.Log($"Collection binding for {bindAttribute.PropertyPath} is set up on {parent.name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 추가 바인딩 설정 - 하위 클래스에서 구현
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void SetupBindings() { }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ViewModel 속성 변경 이벤트 핸들러
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
|
||||||
{
|
|
||||||
HandleCustomPropertyChanged(e.PropertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드)
|
|
||||||
/// </summary>
|
|
||||||
protected virtual void HandleCustomPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
// 하위 클래스에서 구현
|
|
||||||
}
|
|
||||||
|
|
||||||
// 수동 바인딩 헬퍼 메서드들
|
|
||||||
protected void BindText(Text text, string propertyPath, IValueConverter converter = null)
|
|
||||||
{
|
|
||||||
var target = new TextBindingTarget(text, propertyPath);
|
|
||||||
_bindingContext?.Bind(propertyPath, target, converter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void BindImage(Image image, string propertyPath, IValueConverter converter = null)
|
|
||||||
{
|
|
||||||
var target = new ImageBindingTarget(image, propertyPath);
|
|
||||||
_bindingContext?.Bind(propertyPath, target, converter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void BindActive(GameObject gameObject, string propertyPath, IValueConverter converter = null)
|
|
||||||
{
|
|
||||||
var target = new ActiveBindingTarget(gameObject, propertyPath);
|
|
||||||
_bindingContext?.Bind(propertyPath, target, converter);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void BindSlider(Slider slider, string propertyPath, IValueConverter converter = null)
|
|
||||||
{
|
|
||||||
var target = new SliderBindingTarget(slider, propertyPath);
|
|
||||||
_bindingContext?.Bind(propertyPath, target, converter);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ViewModel 메서드 호출 헬퍼
|
|
||||||
/// </summary>
|
|
||||||
protected void InvokeViewModelMethod(string methodName, params object[] parameters)
|
|
||||||
{
|
|
||||||
if (_viewModel == null) return;
|
|
||||||
|
|
||||||
var method = _viewModel.GetType().GetMethod(methodName);
|
|
||||||
method?.Invoke(_viewModel, parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ViewModel 속성 설정 헬퍼
|
|
||||||
/// </summary>
|
|
||||||
protected void SetViewModelProperty(string propertyName, object value)
|
|
||||||
{
|
|
||||||
if (_viewModel == null) return;
|
|
||||||
|
|
||||||
var property = _viewModel.GetType().GetProperty(propertyName);
|
|
||||||
if (property != null && property.CanWrite)
|
|
||||||
{
|
|
||||||
property.SetValue(_viewModel, value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 868322e05b33bdd4cbbe3e1495fe359b
|
|
@ -1,221 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UI;
|
|
||||||
using DDD.MVVM;
|
|
||||||
|
|
||||||
namespace DDD
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// IntegratedPopupUi를 사용한 인벤토리 뷰 예시
|
|
||||||
/// 상속 대신 컴포지션 기반으로 모든 기능을 통합하여 구현
|
|
||||||
/// Attribute 기반 자동 바인딩을 적극 활용
|
|
||||||
/// </summary>
|
|
||||||
public class IntegratedInventoryView : IntegratedBasePopupUi<RestaurantUiActions, InventoryViewModel>
|
|
||||||
{
|
|
||||||
[Header("UI References")]
|
|
||||||
// Attribute를 통한 자동 바인딩 설정
|
|
||||||
[SerializeField, BindTo(nameof(InventoryViewModel.CategoryDisplayText))]
|
|
||||||
private Text _categoryLabel;
|
|
||||||
|
|
||||||
[SerializeField, BindTo(nameof(InventoryViewModel.ItemCountText))]
|
|
||||||
private Text _itemCountLabel;
|
|
||||||
|
|
||||||
[SerializeField, BindTo(nameof(InventoryViewModel.ShowEmptyMessage))]
|
|
||||||
private GameObject _emptyMessage;
|
|
||||||
|
|
||||||
// 수동 바인딩이 필요한 복잡한 UI 요소들
|
|
||||||
[SerializeField] private Transform _slotParent;
|
|
||||||
[SerializeField] private Button[] _categoryButtons;
|
|
||||||
[SerializeField] private Button[] _sortButtons;
|
|
||||||
|
|
||||||
[Header("Prefab References")]
|
|
||||||
[SerializeField] private GameObject _itemSlotPrefab;
|
|
||||||
|
|
||||||
protected override GameObject GetInitialSelected()
|
|
||||||
{
|
|
||||||
// ViewModel의 FirstValidItem을 활용하여 초기 선택 UI 결정
|
|
||||||
var firstItem = _viewModel?.FirstValidItem;
|
|
||||||
if (firstItem != null)
|
|
||||||
{
|
|
||||||
return FindSlotGameObject(firstItem);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _categoryButtons?.Length > 0 ? _categoryButtons[0].gameObject : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 수동 바인딩이 필요한 복잡한 UI 요소들 설정
|
|
||||||
/// Attribute로 처리하기 어려운 컬렉션이나 복잡한 로직이 필요한 경우
|
|
||||||
/// </summary>
|
|
||||||
protected override void SetupBindings()
|
|
||||||
{
|
|
||||||
// 복잡한 컬렉션 바인딩은 수동으로 처리
|
|
||||||
// BindCollection은 아직 완전 구현되지 않았으므로 HandleCustomPropertyChanged에서 처리
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void HandleCustomPropertyChanged(string propertyName)
|
|
||||||
{
|
|
||||||
switch (propertyName)
|
|
||||||
{
|
|
||||||
case nameof(InventoryViewModel.VisibleItems):
|
|
||||||
UpdateItemSlots();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(InventoryViewModel.CurrentCategory):
|
|
||||||
UpdateCategoryButtons();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(InventoryViewModel.CurrentSortType):
|
|
||||||
UpdateSortButtons();
|
|
||||||
break;
|
|
||||||
|
|
||||||
case nameof(InventoryViewModel.FirstValidItem):
|
|
||||||
UpdateInitialSelection();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 아이템 슬롯 UI 업데이트
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateItemSlots()
|
|
||||||
{
|
|
||||||
if (_viewModel?.VisibleItems == null) return;
|
|
||||||
|
|
||||||
// 기존 슬롯들 정리
|
|
||||||
ClearSlots();
|
|
||||||
|
|
||||||
int siblingIndex = 0;
|
|
||||||
foreach (var itemViewModel in _viewModel.VisibleItems)
|
|
||||||
{
|
|
||||||
if (!itemViewModel.HasItem) continue;
|
|
||||||
|
|
||||||
// 아이템 슬롯 UI 생성
|
|
||||||
var slotGameObject = Instantiate(_itemSlotPrefab, _slotParent);
|
|
||||||
var slotUi = slotGameObject.GetComponent<ItemSlotUi>();
|
|
||||||
|
|
||||||
// 기존 방식대로 슬롯 초기화
|
|
||||||
slotUi.Initialize(itemViewModel, new InventorySlotUiStrategy());
|
|
||||||
slotGameObject.name = $"ItemSlotUi_{itemViewModel.Id}";
|
|
||||||
|
|
||||||
// 인터랙터 설정
|
|
||||||
var interactor = slotGameObject.GetComponent<ItemSlotInteractor>();
|
|
||||||
if (itemViewModel.ItemType == ItemType.Recipe)
|
|
||||||
{
|
|
||||||
interactor.Initialize(TodayMenuEventType.Add, new TodayMenuInteractorStrategy());
|
|
||||||
}
|
|
||||||
else if (DataManager.Instance.GetDataSo<CookwareDataSo>().TryGetDataById(itemViewModel.Id, out var cookwareData))
|
|
||||||
{
|
|
||||||
interactor.Initialize(TodayMenuEventType.Add, new TodayCookwareInteractorStrategy());
|
|
||||||
}
|
|
||||||
|
|
||||||
slotGameObject.transform.SetSiblingIndex(siblingIndex++);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 카테고리 버튼 상태 업데이트
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateCategoryButtons()
|
|
||||||
{
|
|
||||||
if (_categoryButtons == null || _viewModel == null) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < _categoryButtons.Length; i++)
|
|
||||||
{
|
|
||||||
var button = _categoryButtons[i];
|
|
||||||
var isSelected = (int)_viewModel.CurrentCategory == i;
|
|
||||||
|
|
||||||
button.interactable = !isSelected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 정렬 버튼 상태 업데이트
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateSortButtons()
|
|
||||||
{
|
|
||||||
if (_sortButtons == null || _viewModel == null) return;
|
|
||||||
|
|
||||||
for (int i = 0; i < _sortButtons.Length; i++)
|
|
||||||
{
|
|
||||||
var button = _sortButtons[i];
|
|
||||||
var isSelected = (int)_viewModel.CurrentSortType == i;
|
|
||||||
|
|
||||||
button.interactable = !isSelected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 초기 선택 UI 업데이트
|
|
||||||
/// </summary>
|
|
||||||
private void UpdateInitialSelection()
|
|
||||||
{
|
|
||||||
var initialSelected = GetInitialSelected();
|
|
||||||
if (initialSelected != null)
|
|
||||||
{
|
|
||||||
UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(initialSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 기존 슬롯들 정리
|
|
||||||
/// </summary>
|
|
||||||
private void ClearSlots()
|
|
||||||
{
|
|
||||||
foreach (Transform child in _slotParent)
|
|
||||||
{
|
|
||||||
Destroy(child.gameObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// ItemViewModel에 해당하는 UI GameObject 찾기
|
|
||||||
/// </summary>
|
|
||||||
private GameObject FindSlotGameObject(ItemViewModel itemViewModel)
|
|
||||||
{
|
|
||||||
foreach (Transform child in _slotParent)
|
|
||||||
{
|
|
||||||
if (child.name == $"ItemSlotUi_{itemViewModel.Id}")
|
|
||||||
{
|
|
||||||
return child.gameObject;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI 이벤트 핸들러들 - ViewModel 메서드 호출
|
|
||||||
public void OnCategoryButtonClicked(int categoryIndex)
|
|
||||||
{
|
|
||||||
_viewModel?.SetCategory((InventoryCategoryType)categoryIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnSortButtonClicked(int sortIndex)
|
|
||||||
{
|
|
||||||
_viewModel?.SetSortType((InventorySortType)sortIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnItemSlotClicked(ItemViewModel item)
|
|
||||||
{
|
|
||||||
_viewModel?.SelectItem(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 입력 처리 - ViewModel로 위임
|
|
||||||
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, UnityEngine.InputSystem.InputAction.CallbackContext context)
|
|
||||||
{
|
|
||||||
var isHandled = base.OnInputPerformed(actionEnum, context);
|
|
||||||
|
|
||||||
// 특별한 입력 처리 로직이 필요한 경우 여기에 추가
|
|
||||||
if (isHandled)
|
|
||||||
{
|
|
||||||
switch (actionEnum)
|
|
||||||
{
|
|
||||||
case RestaurantUiActions.Cancel:
|
|
||||||
Close();
|
|
||||||
break;
|
|
||||||
// 기타 액션들은 ViewModel로 위임됨
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return isHandled;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,2 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 12f3141cdd485054e8c73be9d549feb7
|
|
@ -5,36 +5,65 @@ namespace DDD
|
|||||||
{
|
{
|
||||||
public abstract class BasePopupUi : BaseUi
|
public abstract class BasePopupUi : BaseUi
|
||||||
{
|
{
|
||||||
public abstract InputActionMaps InputActionMaps { get; }
|
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
|
||||||
protected abstract GameObject GetInitialSelected();
|
public InputActionMaps InputActionMaps;
|
||||||
|
|
||||||
protected override void Awake()
|
protected override void Awake()
|
||||||
{
|
{
|
||||||
base.Awake();
|
base.Awake();
|
||||||
|
|
||||||
|
// BasePopupUi의 기본값 적용
|
||||||
_enableBlockImage = true;
|
_enableBlockImage = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnEnable()
|
||||||
|
{
|
||||||
|
base.OnEnable();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
|
// BasePopupUi의 Update 로직 구현
|
||||||
if (IsOpenPanel() == false) return;
|
if (IsOpenPanel() == false) return;
|
||||||
|
|
||||||
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
|
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
|
||||||
if (currentSelectedGameObject == null || currentSelectedGameObject.activeInHierarchy == false)
|
if (currentSelectedGameObject == null || currentSelectedGameObject.activeInHierarchy == false)
|
||||||
{
|
{
|
||||||
if (GetInitialSelected() == null) return;
|
var initialSelected = GetInitialSelected();
|
||||||
|
if (initialSelected != null)
|
||||||
EventSystem.current.SetSelectedGameObject(GetInitialSelected());
|
{
|
||||||
|
EventSystem.current.SetSelectedGameObject(initialSelected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void TryRegister()
|
||||||
|
{
|
||||||
|
base.TryRegister();
|
||||||
|
|
||||||
|
UiManager.Instance.PopupUiState?.RegisterPopupUI(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void TryUnregister()
|
||||||
|
{
|
||||||
|
base.TryUnregister();
|
||||||
|
|
||||||
|
UiManager.Instance?.PopupUiState?.UnregisterPopupUI(this);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void Open(OpenPopupUiEvent evt)
|
public virtual void Open(OpenPopupUiEvent evt)
|
||||||
{
|
{
|
||||||
base.OpenPanel();
|
OpenPanel();
|
||||||
|
|
||||||
EventSystem.current.SetSelectedGameObject(GetInitialSelected());
|
var initialSelected = GetInitialSelected();
|
||||||
|
if (initialSelected != null)
|
||||||
|
{
|
||||||
|
EventSystem.current.SetSelectedGameObject(initialSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform.SetAsLastSibling();
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual void Close()
|
public virtual void Close()
|
||||||
@ -43,5 +72,7 @@ public virtual void Close()
|
|||||||
evt.UiType = GetType();
|
evt.UiType = GetType();
|
||||||
EventBus.Broadcast(evt);
|
EventBus.Broadcast(evt);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract GameObject GetInitialSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -19,25 +19,54 @@ public static IEnumerable<T> GetFlags<T>(this T input) where T : Enum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class PopupUi<T> : BasePopupUi where T : Enum
|
public abstract class PopupUi<TInputEnum, TViewModel> : BasePopupUi
|
||||||
|
where TInputEnum : Enum
|
||||||
|
where TViewModel : SimpleViewModel
|
||||||
{
|
{
|
||||||
[SerializeField, Required] protected BaseUiActionsInputBinding<T> _uiActionsInputBinding;
|
[SerializeField, Required] protected BaseUiActionsInputBinding<TInputEnum> _uiActionsInputBinding;
|
||||||
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers = new();
|
|
||||||
public override InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
|
|
||||||
|
|
||||||
private bool _isTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
|
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers = new();
|
||||||
|
protected TViewModel _viewModel;
|
||||||
|
|
||||||
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
|
||||||
|
_viewModel = GetComponent<TViewModel>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnEnable()
|
||||||
|
{
|
||||||
|
base.OnEnable();
|
||||||
|
|
||||||
|
if (_viewModel && _bindingContext != null)
|
||||||
|
{
|
||||||
|
_bindingContext.SetDataContext(_viewModel);
|
||||||
|
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDisable()
|
||||||
|
{
|
||||||
|
base.OnDisable();
|
||||||
|
|
||||||
|
if (_viewModel != null)
|
||||||
|
{
|
||||||
|
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override void TryRegister()
|
protected override void TryRegister()
|
||||||
{
|
{
|
||||||
base.TryRegister();
|
base.TryRegister();
|
||||||
|
|
||||||
UiManager.Instance.PopupUiState.RegisterPopupUI(this);
|
// PopupUi의 입력 바인딩 등록
|
||||||
|
|
||||||
foreach (var actionEnum in _uiActionsInputBinding.BindingActions.GetFlags())
|
foreach (var actionEnum in _uiActionsInputBinding.BindingActions.GetFlags())
|
||||||
{
|
{
|
||||||
if (actionEnum.Equals(default(T))) continue;
|
if (actionEnum.Equals(default(TInputEnum))) continue;
|
||||||
|
|
||||||
var inputAction = InputManager.Instance.GetAction(_uiActionsInputBinding.InputActionMaps, actionEnum.ToString());
|
var inputAction =
|
||||||
|
InputManager.Instance.GetAction(_uiActionsInputBinding.InputActionMaps, actionEnum.ToString());
|
||||||
if (inputAction == null) continue;
|
if (inputAction == null) continue;
|
||||||
|
|
||||||
var startedHandler = new Action<InputAction.CallbackContext>(context =>
|
var startedHandler = new Action<InputAction.CallbackContext>(context =>
|
||||||
@ -62,13 +91,15 @@ protected override void TryRegister()
|
|||||||
_registeredHandlers.Add((inputAction, performedHandler));
|
_registeredHandlers.Add((inputAction, performedHandler));
|
||||||
_registeredHandlers.Add((inputAction, canceledHandler));
|
_registeredHandlers.Add((inputAction, canceledHandler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputActionMaps = _uiActionsInputBinding.InputActionMaps;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void TryUnregister()
|
protected override void TryUnregister()
|
||||||
{
|
{
|
||||||
base.TryUnregister();
|
base.TryUnregister();
|
||||||
UiManager.Instance?.PopupUiState?.UnregisterPopupUI(this);
|
|
||||||
|
|
||||||
|
// 입력 핸들러 해제
|
||||||
foreach (var (action, handler) in _registeredHandlers)
|
foreach (var (action, handler) in _registeredHandlers)
|
||||||
{
|
{
|
||||||
if (action != null)
|
if (action != null)
|
||||||
@ -86,27 +117,24 @@ public override void Open(OpenPopupUiEvent evt)
|
|||||||
{
|
{
|
||||||
base.Open(evt);
|
base.Open(evt);
|
||||||
|
|
||||||
transform.SetAsLastSibling();
|
_viewModel?.Initialize();
|
||||||
|
|
||||||
if (UiManager.Instance.PopupUiState.IsTopPopup(this))
|
if (IsTopPopup)
|
||||||
{
|
{
|
||||||
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
|
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual bool OnInputStarted(T actionEnum, InputAction.CallbackContext context)
|
public override void Close()
|
||||||
{
|
{
|
||||||
return _isTopPopup;
|
base.Close();
|
||||||
|
|
||||||
|
_viewModel?.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual bool OnInputPerformed(T actionEnum, InputAction.CallbackContext context)
|
// 입력 처리 메서드들
|
||||||
{
|
protected virtual bool OnInputStarted(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
||||||
return _isTopPopup;
|
protected virtual bool OnInputPerformed(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
||||||
}
|
protected virtual bool OnInputCanceled(TInputEnum actionEnum, InputAction.CallbackContext context) => IsTopPopup;
|
||||||
|
|
||||||
protected virtual bool OnInputCanceled(T actionEnum, InputAction.CallbackContext context)
|
|
||||||
{
|
|
||||||
return _isTopPopup;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,114 +1,108 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
using UnityEngine.UI;
|
||||||
using UnityEngine.EventSystems;
|
using UnityEngine.EventSystems;
|
||||||
using UnityEngine.InputSystem;
|
using UnityEngine.InputSystem;
|
||||||
using UnityEngine.UI;
|
using DDD.MVVM;
|
||||||
|
|
||||||
namespace DDD
|
namespace DDD
|
||||||
{
|
{
|
||||||
public class RestaurantManagementUi : PopupUi<RestaurantUiActions>, IEventHandler<TodayMenuRemovedEvent>
|
/// <summary>
|
||||||
|
/// MVVM 패턴을 적용한 새로운 레스토랑 관리 UI
|
||||||
|
/// 기존 RestaurantManagementUi의 기능을 ViewModel과 분리하여 구현
|
||||||
|
/// </summary>
|
||||||
|
[RequireComponent(typeof(RestaurantManagementViewModel))]
|
||||||
|
public class RestaurantManagementUi : PopupUi<RestaurantUiActions, RestaurantManagementViewModel>
|
||||||
{
|
{
|
||||||
|
[Header("Sub Views")]
|
||||||
[SerializeField] private ChecklistView _checklistView;
|
[SerializeField] private ChecklistView _checklistView;
|
||||||
[SerializeField] private InventoryView _inventoryView;
|
[SerializeField] private InventoryView _inventoryView;
|
||||||
[SerializeField] private ItemDetailView _itemDetailView;
|
[SerializeField] private ItemDetailView _itemDetailView;
|
||||||
[SerializeField] private TodayMenuView _todayMenuView;
|
[SerializeField] private TodayMenuView _todayMenuView;
|
||||||
[SerializeField] private TodayRestaurantStateView _todayRestaurantStateView;
|
[SerializeField] private TodayRestaurantStateView _todayRestaurantStateView;
|
||||||
|
|
||||||
|
[Header("Tab Groups")]
|
||||||
[SerializeField] private TabGroupUi _sectionTabs;
|
[SerializeField] private TabGroupUi _sectionTabs;
|
||||||
[SerializeField] private TabGroupUi _menuCategoryTabs;
|
[SerializeField] private TabGroupUi _menuCategoryTabs;
|
||||||
[SerializeField] private TabGroupUi _cookwareCategoryTabs;
|
[SerializeField] private TabGroupUi _cookwareCategoryTabs;
|
||||||
[SerializeField] private Image _completeBatchFilledImage;
|
|
||||||
|
|
||||||
[SerializeField] private float _holdCompleteTime = 1f;
|
[Header("Hold Progress UI")]
|
||||||
private float _elapsedTime;
|
[SerializeField, BindTo(nameof(RestaurantManagementViewModel.NormalizedHoldProgress))]
|
||||||
private bool _isHolding;
|
private Image _completeBatchFilledImage;
|
||||||
|
|
||||||
private const string ChecklistFailedMessageKey = "checklist_failed_message";
|
protected override void Awake()
|
||||||
|
{
|
||||||
|
base.Awake();
|
||||||
|
|
||||||
|
SetupViewModelEvents();
|
||||||
|
}
|
||||||
|
|
||||||
protected override void Update()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
base.Update();
|
base.Update();
|
||||||
|
|
||||||
if (_isHolding)
|
if (_viewModel != null && _viewModel.IsHolding)
|
||||||
{
|
{
|
||||||
UpdateHoldProgress();
|
_viewModel.UpdateHoldProgress();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateHoldProgress()
|
|
||||||
{
|
|
||||||
if (_holdCompleteTime <= 0f)
|
|
||||||
{
|
|
||||||
HandleInteract2Canceled();
|
|
||||||
ProcessCompleteBatchAction();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_completeBatchFilledImage.fillAmount = _elapsedTime;
|
|
||||||
|
|
||||||
var multiply = 1f / _holdCompleteTime;
|
|
||||||
if (_elapsedTime >= 1f)
|
|
||||||
{
|
|
||||||
HandleInteract2Canceled();
|
|
||||||
ProcessCompleteBatchAction();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_elapsedTime += Time.deltaTime * multiply;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ProcessCompleteBatchAction()
|
|
||||||
{
|
|
||||||
if (RestaurantState.Instance.ManagementState.GetChecklistStates().Any(state => state == false))
|
|
||||||
{
|
|
||||||
ShowChecklistFailedPopup();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShowChecklistFailedPopup()
|
|
||||||
{
|
|
||||||
var evt = GameEvents.OpenPopupUiEvent;
|
|
||||||
evt.UiType = typeof(ConfirmUi);
|
|
||||||
evt.IsCancelButtonVisible = true;
|
|
||||||
evt.NewMessageKey = ChecklistFailedMessageKey;
|
|
||||||
evt.OnConfirm = ClosePanel;
|
|
||||||
EventBus.Broadcast(evt);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override GameObject GetInitialSelected()
|
|
||||||
{
|
|
||||||
var inventoryViewInitialSelectedObject = _inventoryView.GetInitialSelected();
|
|
||||||
if (inventoryViewInitialSelectedObject) return inventoryViewInitialSelectedObject;
|
|
||||||
|
|
||||||
var menuCategoryFirstButton = _menuCategoryTabs.GetFirstInteractableButton();
|
|
||||||
if (menuCategoryFirstButton != null && menuCategoryFirstButton.activeInHierarchy)
|
|
||||||
{
|
|
||||||
return menuCategoryFirstButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
var cookwareCategoryFirstButton = _cookwareCategoryTabs.GetFirstInteractableButton();
|
|
||||||
if (cookwareCategoryFirstButton != null && cookwareCategoryFirstButton.activeInHierarchy)
|
|
||||||
{
|
|
||||||
return cookwareCategoryFirstButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Open(OpenPopupUiEvent evt)
|
public override void Open(OpenPopupUiEvent evt)
|
||||||
{
|
{
|
||||||
base.Open(evt);
|
base.Open(evt);
|
||||||
|
|
||||||
InitializeViews();
|
InitializeViews();
|
||||||
SetupCategoryTabs();
|
SetupTabs();
|
||||||
InitializeTabGroups();
|
}
|
||||||
SelectInitialTabs();
|
|
||||||
RegisterEventHandlers();
|
protected override GameObject GetInitialSelected()
|
||||||
|
{
|
||||||
|
// ViewModel의 현재 상태에 따라 초기 선택 UI 결정
|
||||||
|
var inventoryInitialSelected = _inventoryView.GetInitialSelected();
|
||||||
|
if (inventoryInitialSelected) return inventoryInitialSelected;
|
||||||
|
|
||||||
|
var menuCategoryButton = _menuCategoryTabs.GetFirstInteractableButton();
|
||||||
|
if (menuCategoryButton != null && menuCategoryButton.activeInHierarchy)
|
||||||
|
return menuCategoryButton;
|
||||||
|
|
||||||
|
var cookwareCategoryButton = _cookwareCategoryTabs.GetFirstInteractableButton();
|
||||||
|
if (cookwareCategoryButton != null && cookwareCategoryButton.activeInHierarchy)
|
||||||
|
return cookwareCategoryButton;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void SetupBindings()
|
||||||
|
{
|
||||||
|
// Attribute 기반 자동 바인딩이 처리됨
|
||||||
|
// 추가적인 수동 바인딩이 필요한 경우 여기에 구현
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void HandleCustomPropertyChanged(string propertyName)
|
||||||
|
{
|
||||||
|
switch (propertyName)
|
||||||
|
{
|
||||||
|
case nameof(RestaurantManagementViewModel.CurrentSection):
|
||||||
|
UpdateSectionTabs();
|
||||||
|
break;
|
||||||
|
case nameof(RestaurantManagementViewModel.CurrentCategory):
|
||||||
|
UpdateCategoryTabs();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupViewModelEvents()
|
||||||
|
{
|
||||||
|
if (!_viewModel) return;
|
||||||
|
|
||||||
|
_viewModel.OnBatchCompleted = HandleBatchCompleted;
|
||||||
|
_viewModel.OnChecklistFailed = HandleChecklistFailed;
|
||||||
|
_viewModel.OnMenuSectionSelected = HandleMenuSectionSelected;
|
||||||
|
_viewModel.OnCookwareSectionSelected = HandleCookwareSectionSelected;
|
||||||
|
_viewModel.OnCategoryChanged = HandleCategoryChanged;
|
||||||
|
_viewModel.OnTabMoved = HandleTabMoved;
|
||||||
|
_viewModel.OnInteractRequested = HandleInteractRequested;
|
||||||
|
_viewModel.OnCloseRequested = HandleCloseRequested;
|
||||||
|
_viewModel.OnMenuCategorySelected = HandleMenuCategorySelected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeViews()
|
private void InitializeViews()
|
||||||
@ -120,12 +114,15 @@ private void InitializeViews()
|
|||||||
_todayRestaurantStateView.Initialize();
|
_todayRestaurantStateView.Initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private void SetupTabs()
|
||||||
/// 각 그룹별로 허용된 카테고리를 설정합니다.
|
{
|
||||||
/// </summary>
|
SetupCategoryTabs();
|
||||||
|
InitializeTabGroups();
|
||||||
|
SelectInitialTabs();
|
||||||
|
}
|
||||||
|
|
||||||
private void SetupCategoryTabs()
|
private void SetupCategoryTabs()
|
||||||
{
|
{
|
||||||
// 각 그룹별로 기본 허용 값 사용 (자동으로 적절한 카테고리 필터링)
|
|
||||||
_menuCategoryTabs.UseDefaultAllowedValues();
|
_menuCategoryTabs.UseDefaultAllowedValues();
|
||||||
_cookwareCategoryTabs.UseDefaultAllowedValues();
|
_cookwareCategoryTabs.UseDefaultAllowedValues();
|
||||||
}
|
}
|
||||||
@ -143,113 +140,137 @@ private void SelectInitialTabs()
|
|||||||
_menuCategoryTabs.SelectFirstTab();
|
_menuCategoryTabs.SelectFirstTab();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RegisterEventHandlers()
|
private void UpdateSectionTabs()
|
||||||
{
|
{
|
||||||
EventBus.Register<TodayMenuRemovedEvent>(this);
|
if (_viewModel == null) return;
|
||||||
|
_sectionTabs.SelectTab((int)_viewModel.CurrentSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Close()
|
private void UpdateCategoryTabs()
|
||||||
{
|
{
|
||||||
base.Close();
|
if (_viewModel == null) return;
|
||||||
|
|
||||||
EventBus.Unregister<TodayMenuRemovedEvent>(this);
|
switch (_viewModel.CurrentSection)
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
|
||||||
{
|
|
||||||
if (base.OnInputPerformed(actionEnum, context) == false) return false;
|
|
||||||
|
|
||||||
switch (actionEnum)
|
|
||||||
{
|
{
|
||||||
case RestaurantUiActions.Cancel:
|
case SectionButtonType.Menu:
|
||||||
HandleCancelPerformed();
|
_menuCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
||||||
break;
|
break;
|
||||||
case RestaurantUiActions.PreviousTab:
|
case SectionButtonType.Cookware:
|
||||||
HandleMoveTabPerformed(-1);
|
_cookwareCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
||||||
break;
|
|
||||||
case RestaurantUiActions.NextTab:
|
|
||||||
HandleMoveTabPerformed(1);
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.Interact1:
|
|
||||||
HandleInteract1Performed();
|
|
||||||
break;
|
|
||||||
case RestaurantUiActions.Interact2:
|
|
||||||
HandleInteract2Performed();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnInputCanceled(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
// ViewModel 이벤트 핸들러들
|
||||||
{
|
private void HandleBatchCompleted()
|
||||||
if (base.OnInputPerformed(actionEnum, context) == false) return false;
|
|
||||||
|
|
||||||
switch (actionEnum)
|
|
||||||
{
|
|
||||||
case RestaurantUiActions.Interact2:
|
|
||||||
HandleInteract2Canceled();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleCancelPerformed()
|
|
||||||
{
|
{
|
||||||
Close();
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleMoveTabPerformed(int direction)
|
private void HandleChecklistFailed()
|
||||||
|
{
|
||||||
|
var evt = GameEvents.OpenPopupUiEvent;
|
||||||
|
evt.UiType = typeof(ConfirmUi);
|
||||||
|
evt.IsCancelButtonVisible = true;
|
||||||
|
evt.NewMessageKey = "checklist_failed_message";
|
||||||
|
evt.OnConfirm = ClosePanel;
|
||||||
|
EventBus.Broadcast(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMenuSectionSelected()
|
||||||
|
{
|
||||||
|
_menuCategoryTabs.SelectFirstTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCookwareSectionSelected()
|
||||||
|
{
|
||||||
|
_cookwareCategoryTabs.SelectFirstTab();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleCategoryChanged(InventoryCategoryType category)
|
||||||
|
{
|
||||||
|
_inventoryView.UpdateCategoryView(category);
|
||||||
|
_itemDetailView.UpdateCategory(category);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleTabMoved(int direction)
|
||||||
{
|
{
|
||||||
_sectionTabs.Move(direction);
|
_sectionTabs.Move(direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleInteract1Performed()
|
private void HandleInteractRequested()
|
||||||
{
|
{
|
||||||
var selected = EventSystem.current.currentSelectedGameObject;
|
var selected = EventSystem.current.currentSelectedGameObject;
|
||||||
var interactable = selected?.GetComponent<IInteractableUi>();
|
var interactable = selected?.GetComponent<IInteractableUi>();
|
||||||
interactable?.OnInteract();
|
interactable?.OnInteract();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleInteract2Performed()
|
private void HandleCloseRequested()
|
||||||
{
|
{
|
||||||
_isHolding = true;
|
Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void HandleInteract2Canceled()
|
private void HandleMenuCategorySelected(InventoryCategoryType category)
|
||||||
{
|
{
|
||||||
_isHolding = false;
|
_menuCategoryTabs.SelectTab((int)category);
|
||||||
_elapsedTime = 0f;
|
|
||||||
_completeBatchFilledImage.fillAmount = 0f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UI 이벤트 핸들러들 (TabGroupUi 콜백)
|
||||||
private void OnSectionTabSelected(int sectionValue)
|
private void OnSectionTabSelected(int sectionValue)
|
||||||
{
|
{
|
||||||
var section = (SectionButtonType)sectionValue;
|
_viewModel?.SetSection((SectionButtonType)sectionValue);
|
||||||
switch (section)
|
|
||||||
{
|
|
||||||
case SectionButtonType.Menu:
|
|
||||||
_menuCategoryTabs.SelectFirstTab();
|
|
||||||
break;
|
|
||||||
case SectionButtonType.Cookware:
|
|
||||||
_cookwareCategoryTabs.SelectFirstTab();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(section), section, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCategoryTabSelected(int categoryValue)
|
private void OnCategoryTabSelected(int categoryValue)
|
||||||
{
|
{
|
||||||
var category = (InventoryCategoryType)categoryValue;
|
_viewModel?.SetCategory((InventoryCategoryType)categoryValue);
|
||||||
_inventoryView.UpdateCategoryView(category);
|
|
||||||
_itemDetailView.UpdateCategory(category);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Invoke(TodayMenuRemovedEvent evt)
|
// 입력 처리 - ViewModel로 위임
|
||||||
|
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
||||||
{
|
{
|
||||||
_menuCategoryTabs.SelectTab((int)evt.InventoryCategoryType);
|
var isHandled = base.OnInputPerformed(actionEnum, context);
|
||||||
|
|
||||||
|
if (isHandled && _viewModel != null)
|
||||||
|
{
|
||||||
|
switch (actionEnum)
|
||||||
|
{
|
||||||
|
case RestaurantUiActions.Cancel:
|
||||||
|
_viewModel.CloseUi();
|
||||||
|
break;
|
||||||
|
case RestaurantUiActions.PreviousTab:
|
||||||
|
_viewModel.MoveTab(-1);
|
||||||
|
break;
|
||||||
|
case RestaurantUiActions.NextTab:
|
||||||
|
_viewModel.MoveTab(1);
|
||||||
|
break;
|
||||||
|
case RestaurantUiActions.Interact1:
|
||||||
|
_viewModel.InteractWithSelected();
|
||||||
|
break;
|
||||||
|
case RestaurantUiActions.Interact2:
|
||||||
|
_viewModel.StartHold();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isHandled;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnInputCanceled(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
||||||
|
{
|
||||||
|
var isHandled = base.OnInputCanceled(actionEnum, context);
|
||||||
|
|
||||||
|
if (isHandled && _viewModel != null)
|
||||||
|
{
|
||||||
|
switch (actionEnum)
|
||||||
|
{
|
||||||
|
case RestaurantUiActions.Interact2:
|
||||||
|
_viewModel.CancelHold();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isHandled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user