ui 구조 변경 임시저장
This commit is contained in:
parent
0cce2efe62
commit
6f4417cd98
@ -6311,7 +6311,10 @@ PrefabInstance:
|
||||
m_AddedComponents:
|
||||
- targetCorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||
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}
|
||||
--- !u!224 &4720669467062659157 stripped
|
||||
RectTransform:
|
||||
@ -6323,7 +6326,7 @@ GameObject:
|
||||
m_CorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
|
||||
m_PrefabInstance: {fileID: 4463400116329503023}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
--- !u!114 &7197937761328488698
|
||||
--- !u!114 &3190533777077691364
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
@ -6332,10 +6335,11 @@ MonoBehaviour:
|
||||
m_GameObject: {fileID: 6740783381500491556}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 8a2e0954aa144633aad86e53dc80a46a, type: 3}
|
||||
m_Script: {fileID: 11500000, guid: 46c8396c996c804449b383960b44e812, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier:
|
||||
_enableBlockImage: 0
|
||||
InputActionMaps: 3
|
||||
_uiActionsInputBinding: {fileID: 11400000, guid: 8073fcaf56fc7c34e996d0d47044f146, type: 2}
|
||||
_checklistView: {fileID: 7075966153492927588}
|
||||
_inventoryView: {fileID: 3570087040626823091}
|
||||
@ -6346,6 +6350,18 @@ MonoBehaviour:
|
||||
_menuCategoryTabs: {fileID: 6805049896193344908}
|
||||
_cookwareCategoryTabs: {fileID: 6628923975427483430}
|
||||
_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
|
||||
PrefabInstance:
|
||||
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,28 +1,39 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using DDD.MVVM;
|
||||
|
||||
namespace DDD
|
||||
{
|
||||
public abstract class BaseUi : MonoBehaviour
|
||||
{
|
||||
[SerializeField] protected bool _enableBlockImage;
|
||||
|
||||
protected CanvasGroup _canvasGroup;
|
||||
protected GameObject _blockImage;
|
||||
protected GameObject _panel;
|
||||
protected BindingContext _bindingContext;
|
||||
|
||||
public virtual bool IsBlockingTime => false;
|
||||
public virtual bool IsOpen => _panel.activeSelf;
|
||||
|
||||
[SerializeField] protected bool _enableBlockImage;
|
||||
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;
|
||||
_panel = transform.Find(CommonConstants.Panel)?.gameObject;
|
||||
_blockImage = transform.Find(CommonConstants.BlockImage)?.gameObject;
|
||||
|
||||
_bindingContext = new BindingContext();
|
||||
SetupAutoBindings();
|
||||
SetupBindings();
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected virtual void Start()
|
||||
@ -33,22 +44,24 @@ protected virtual void Start()
|
||||
|
||||
protected virtual void Update()
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
TryUnregister();
|
||||
_bindingContext?.Dispose();
|
||||
}
|
||||
|
||||
protected virtual void TryRegister() { }
|
||||
protected virtual void TryUnregister() { }
|
||||
|
||||
// BaseUi 메서드들을 직접 구현
|
||||
public virtual void OpenPanel()
|
||||
{
|
||||
if (_enableBlockImage)
|
||||
@ -71,10 +84,127 @@ public virtual void ClosePanel()
|
||||
|
||||
public virtual void SetUiInteractable(bool active)
|
||||
{
|
||||
_canvasGroup.interactable = active;
|
||||
_canvasGroup.blocksRaycasts = active;
|
||||
if (_canvasGroup != null)
|
||||
{
|
||||
_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
|
||||
{
|
||||
public class ConfirmUi : PopupUi<RestaurantUiActions>
|
||||
public class ConfirmUi : PopupUi<RestaurantUiActions, ConfirmViewModel>
|
||||
{
|
||||
[SerializeField] private TextMeshProUGUI _messageLabel;
|
||||
[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 InputActionMaps InputActionMaps { get; }
|
||||
protected abstract GameObject GetInitialSelected();
|
||||
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
|
||||
public InputActionMaps InputActionMaps;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
// BasePopupUi의 기본값 적용
|
||||
_enableBlockImage = true;
|
||||
}
|
||||
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
// BasePopupUi의 Update 로직 구현
|
||||
if (IsOpenPanel() == false) return;
|
||||
|
||||
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
|
||||
if (currentSelectedGameObject == null || currentSelectedGameObject.activeInHierarchy == false)
|
||||
{
|
||||
if (GetInitialSelected() == null) return;
|
||||
|
||||
EventSystem.current.SetSelectedGameObject(GetInitialSelected());
|
||||
var initialSelected = GetInitialSelected();
|
||||
if (initialSelected != null)
|
||||
{
|
||||
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)
|
||||
{
|
||||
base.OpenPanel();
|
||||
|
||||
EventSystem.current.SetSelectedGameObject(GetInitialSelected());
|
||||
OpenPanel();
|
||||
|
||||
var initialSelected = GetInitialSelected();
|
||||
if (initialSelected != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(initialSelected);
|
||||
}
|
||||
|
||||
transform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
public virtual void Close()
|
||||
@ -43,5 +72,7 @@ public virtual void Close()
|
||||
evt.UiType = GetType();
|
||||
EventBus.Broadcast(evt);
|
||||
}
|
||||
|
||||
protected abstract GameObject GetInitialSelected();
|
||||
}
|
||||
}
|
@ -18,26 +18,55 @@ 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;
|
||||
protected TViewModel _viewModel;
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
_viewModel = GetComponent<TViewModel>();
|
||||
}
|
||||
|
||||
private bool _isTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
|
||||
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()
|
||||
{
|
||||
base.TryRegister();
|
||||
|
||||
UiManager.Instance.PopupUiState.RegisterPopupUI(this);
|
||||
|
||||
// PopupUi의 입력 바인딩 등록
|
||||
foreach (var actionEnum in _uiActionsInputBinding.BindingActions.GetFlags())
|
||||
{
|
||||
if (actionEnum.Equals(default(T))) continue;
|
||||
|
||||
var inputAction = InputManager.Instance.GetAction(_uiActionsInputBinding.InputActionMaps, actionEnum.ToString());
|
||||
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 =>
|
||||
@ -62,13 +91,15 @@ protected override void TryRegister()
|
||||
_registeredHandlers.Add((inputAction, performedHandler));
|
||||
_registeredHandlers.Add((inputAction, canceledHandler));
|
||||
}
|
||||
|
||||
InputActionMaps = _uiActionsInputBinding.InputActionMaps;
|
||||
}
|
||||
|
||||
|
||||
protected override void TryUnregister()
|
||||
{
|
||||
base.TryUnregister();
|
||||
UiManager.Instance?.PopupUiState?.UnregisterPopupUI(this);
|
||||
|
||||
|
||||
// 입력 핸들러 해제
|
||||
foreach (var (action, handler) in _registeredHandlers)
|
||||
{
|
||||
if (action != null)
|
||||
@ -81,32 +112,29 @@ protected override void TryUnregister()
|
||||
|
||||
_registeredHandlers.Clear();
|
||||
}
|
||||
|
||||
|
||||
public override void Open(OpenPopupUiEvent evt)
|
||||
{
|
||||
base.Open(evt);
|
||||
|
||||
transform.SetAsLastSibling();
|
||||
_viewModel?.Initialize();
|
||||
|
||||
if (UiManager.Instance.PopupUiState.IsTopPopup(this))
|
||||
if (IsTopPopup)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return _isTopPopup;
|
||||
}
|
||||
|
||||
protected virtual bool OnInputCanceled(T actionEnum, InputAction.CallbackContext context)
|
||||
{
|
||||
return _isTopPopup;
|
||||
}
|
||||
// 입력 처리 메서드들
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,114 +1,108 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.InputSystem;
|
||||
using UnityEngine.UI;
|
||||
using DDD.MVVM;
|
||||
|
||||
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 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;
|
||||
[SerializeField] private Image _completeBatchFilledImage;
|
||||
|
||||
[SerializeField] private float _holdCompleteTime = 1f;
|
||||
private float _elapsedTime;
|
||||
private bool _isHolding;
|
||||
[Header("Hold Progress UI")]
|
||||
[SerializeField, BindTo(nameof(RestaurantManagementViewModel.NormalizedHoldProgress))]
|
||||
private Image _completeBatchFilledImage;
|
||||
|
||||
private const string ChecklistFailedMessageKey = "checklist_failed_message";
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
SetupViewModelEvents();
|
||||
}
|
||||
|
||||
protected override void 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)
|
||||
{
|
||||
base.Open(evt);
|
||||
|
||||
|
||||
InitializeViews();
|
||||
SetupCategoryTabs();
|
||||
InitializeTabGroups();
|
||||
SelectInitialTabs();
|
||||
RegisterEventHandlers();
|
||||
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()
|
||||
@ -120,12 +114,15 @@ private void InitializeViews()
|
||||
_todayRestaurantStateView.Initialize();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 각 그룹별로 허용된 카테고리를 설정합니다.
|
||||
/// </summary>
|
||||
private void SetupTabs()
|
||||
{
|
||||
SetupCategoryTabs();
|
||||
InitializeTabGroups();
|
||||
SelectInitialTabs();
|
||||
}
|
||||
|
||||
private void SetupCategoryTabs()
|
||||
{
|
||||
// 각 그룹별로 기본 허용 값 사용 (자동으로 적절한 카테고리 필터링)
|
||||
_menuCategoryTabs.UseDefaultAllowedValues();
|
||||
_cookwareCategoryTabs.UseDefaultAllowedValues();
|
||||
}
|
||||
@ -143,113 +140,137 @@ private void SelectInitialTabs()
|
||||
_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();
|
||||
|
||||
EventBus.Unregister<TodayMenuRemovedEvent>(this);
|
||||
}
|
||||
|
||||
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
||||
{
|
||||
if (base.OnInputPerformed(actionEnum, context) == false) return false;
|
||||
|
||||
switch (actionEnum)
|
||||
if (_viewModel == null) return;
|
||||
|
||||
switch (_viewModel.CurrentSection)
|
||||
{
|
||||
case RestaurantUiActions.Cancel:
|
||||
HandleCancelPerformed();
|
||||
case SectionButtonType.Menu:
|
||||
_menuCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
||||
break;
|
||||
case RestaurantUiActions.PreviousTab:
|
||||
HandleMoveTabPerformed(-1);
|
||||
break;
|
||||
case RestaurantUiActions.NextTab:
|
||||
HandleMoveTabPerformed(1);
|
||||
break;
|
||||
case RestaurantUiActions.Interact1:
|
||||
HandleInteract1Performed();
|
||||
break;
|
||||
case RestaurantUiActions.Interact2:
|
||||
HandleInteract2Performed();
|
||||
case SectionButtonType.Cookware:
|
||||
_cookwareCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override bool OnInputCanceled(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
|
||||
{
|
||||
if (base.OnInputPerformed(actionEnum, context) == false) return false;
|
||||
|
||||
switch (actionEnum)
|
||||
{
|
||||
case RestaurantUiActions.Interact2:
|
||||
HandleInteract2Canceled();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void HandleCancelPerformed()
|
||||
// ViewModel 이벤트 핸들러들
|
||||
private void HandleBatchCompleted()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
private void HandleInteract1Performed()
|
||||
private void HandleInteractRequested()
|
||||
{
|
||||
var selected = EventSystem.current.currentSelectedGameObject;
|
||||
var interactable = selected?.GetComponent<IInteractableUi>();
|
||||
interactable?.OnInteract();
|
||||
}
|
||||
|
||||
private void HandleInteract2Performed()
|
||||
private void HandleCloseRequested()
|
||||
{
|
||||
_isHolding = true;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void HandleInteract2Canceled()
|
||||
private void HandleMenuCategorySelected(InventoryCategoryType category)
|
||||
{
|
||||
_isHolding = false;
|
||||
_elapsedTime = 0f;
|
||||
_completeBatchFilledImage.fillAmount = 0f;
|
||||
_menuCategoryTabs.SelectTab((int)category);
|
||||
}
|
||||
|
||||
// UI 이벤트 핸들러들 (TabGroupUi 콜백)
|
||||
private void OnSectionTabSelected(int sectionValue)
|
||||
{
|
||||
var section = (SectionButtonType)sectionValue;
|
||||
switch (section)
|
||||
{
|
||||
case SectionButtonType.Menu:
|
||||
_menuCategoryTabs.SelectFirstTab();
|
||||
break;
|
||||
case SectionButtonType.Cookware:
|
||||
_cookwareCategoryTabs.SelectFirstTab();
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(section), section, null);
|
||||
}
|
||||
_viewModel?.SetSection((SectionButtonType)sectionValue);
|
||||
}
|
||||
|
||||
private void OnCategoryTabSelected(int categoryValue)
|
||||
{
|
||||
var category = (InventoryCategoryType)categoryValue;
|
||||
_inventoryView.UpdateCategoryView(category);
|
||||
_itemDetailView.UpdateCategory(category);
|
||||
_viewModel?.SetCategory((InventoryCategoryType)categoryValue);
|
||||
}
|
||||
|
||||
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