From 6f4417cd981e8e0cbdb8f091b2b3f4a3c367c407 Mon Sep 17 00:00:00 2001 From: NTG Date: Wed, 20 Aug 2025 18:08:41 +0900 Subject: [PATCH] =?UTF-8?q?ui=20=EA=B5=AC=EC=A1=B0=20=EB=B3=80=EA=B2=BD=20?= =?UTF-8?q?=EC=9E=84=EC=8B=9C=EC=A0=80=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopupUis/RestaurantManagementUi.prefab | 22 +- .../_DDD/_Addressables/So/PopupUiState.asset | 2 +- Assets/_DDD/_Scripts/GameUi/BaseUi.cs | 152 +++++++- .../_DDD/_Scripts/GameUi/BaseViewModelUi.cs | 74 ++++ .../_Scripts/GameUi/BaseViewModelUi.cs.meta | 3 + Assets/_DDD/_Scripts/GameUi/ConfirmUi.cs | 2 +- .../_DDD/_Scripts/GameUi/ConfirmViewModel.cs | 7 + .../_Scripts/GameUi/ConfirmViewModel.cs.meta | 3 + .../_Scripts/GameUi/New/MIGRATION_GUIDE.md | 321 ----------------- .../GameUi/New/MIGRATION_GUIDE.md.meta | 7 - .../GameUi/New/NewRestaurantManagementUi.cs | 275 --------------- .../New/NewRestaurantManagementUi.cs.meta | 3 - Assets/_DDD/_Scripts/GameUi/New/README.md | 278 --------------- .../_DDD/_Scripts/GameUi/New/README.md.meta | 7 - .../New/Views/Base/IntegratedBasePopupUi.cs | 252 -------------- .../Views/Base/IntegratedBasePopupUi.cs.meta | 2 - .../GameUi/New/Views/Base/IntegratedBaseUi.cs | 248 ------------- .../New/Views/Base/IntegratedBaseUi.cs.meta | 2 - .../Views/Examples/IntegratedInventoryView.cs | 221 ------------ .../Examples/IntegratedInventoryView.cs.meta | 2 - .../_Scripts/GameUi/PopupUi/BasePopupUi.cs | 49 ++- .../_DDD/_Scripts/GameUi/PopupUi/PopupUi.cs | 82 +++-- .../RestaurantManagementUi.cs | 329 ++++++++++-------- 23 files changed, 519 insertions(+), 1824 deletions(-) create mode 100644 Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs create mode 100644 Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta create mode 100644 Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs create mode 100644 Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/README.md delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/README.md.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs.meta delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs delete mode 100644 Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs.meta diff --git a/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab b/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab index 1cb4e065e..f33167b14 100644 --- a/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab +++ b/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab @@ -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 diff --git a/Assets/_DDD/_Addressables/So/PopupUiState.asset b/Assets/_DDD/_Addressables/So/PopupUiState.asset index 2283d5b74..f792667ef 100644 --- a/Assets/_DDD/_Addressables/So/PopupUiState.asset +++ b/Assets/_DDD/_Addressables/So/PopupUiState.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce0f58b16cd51536806732f34bb1c1c46f59b6ee19b1d85e982978d11c370cee +oid sha256:a2ffcd5421ab4902f8d7df18de785daba51fd284682cec5bc8b83e3f87a1debb size 1962 diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi.cs index 448195596..05871ee2a 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi.cs @@ -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(); - _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; + + /// + /// Attribute 기반 자동 바인딩 설정 + /// + private void SetupAutoBindings() + { + var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) + .Where(f => f.GetCustomAttribute() != null); + + foreach (var field in fields) + { + var bindAttribute = field.GetCustomAttribute(); + SetupBinding(field, bindAttribute); + } + + // 컬렉션 바인딩 설정 + var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) + .Where(f => f.GetCustomAttribute() != null); + + foreach (var field in collectionFields) + { + var bindAttribute = field.GetCustomAttribute(); + SetupCollectionBinding(field, bindAttribute); + } + } + + /// + /// 개별 필드의 바인딩 설정 + /// + 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); + } + } + + /// + /// 컬렉션 바인딩 설정 + /// + 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}"); + } + } + + /// + /// 추가 바인딩 설정 - 하위 클래스에서 구현 + /// + protected virtual void SetupBindings() { } + + /// + /// ViewModel 속성 변경 이벤트 핸들러 + /// + protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) + { + HandleCustomPropertyChanged(e.PropertyName); + } + + /// + /// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드) + /// + 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); + } } } \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs b/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs new file mode 100644 index 000000000..6a3a6b243 --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs @@ -0,0 +1,74 @@ +namespace DDD +{ + public class BaseViewModelUi : BaseUi where TViewModel : SimpleViewModel + { + protected TViewModel _viewModel; + + protected override void Awake() + { + base.Awake(); + + _viewModel = GetComponent(); + } + + 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(); + } + + /// + /// ViewModel 메서드 호출 헬퍼 + /// + protected void InvokeViewModelMethod(string methodName, params object[] parameters) + { + if (_viewModel == null) return; + + var method = _viewModel.GetType().GetMethod(methodName); + method?.Invoke(_viewModel, parameters); + } + + /// + /// ViewModel 속성 설정 헬퍼 + /// + 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); + } + } + } +} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta b/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta new file mode 100644 index 000000000..395a2f0b8 --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/BaseViewModelUi.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: df6384ea09a44f188d636ca7ee47db13 +timeCreated: 1755678434 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/ConfirmUi.cs b/Assets/_DDD/_Scripts/GameUi/ConfirmUi.cs index 0d6015b27..c0f8ec30f 100644 --- a/Assets/_DDD/_Scripts/GameUi/ConfirmUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/ConfirmUi.cs @@ -8,7 +8,7 @@ namespace DDD { - public class ConfirmUi : PopupUi + public class ConfirmUi : PopupUi { [SerializeField] private TextMeshProUGUI _messageLabel; [SerializeField] private LocalizeStringEvent _messageLabelLocalizeStringEvent; diff --git a/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs b/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs new file mode 100644 index 000000000..c64f2fdcc --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs @@ -0,0 +1,7 @@ +namespace DDD +{ + public class ConfirmViewModel : SimpleViewModel + { + + } +} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta b/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta new file mode 100644 index 000000000..6c174b2b8 --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/ConfirmViewModel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 7552fc9cc76345e09148a145ed7799a5 +timeCreated: 1755679705 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md b/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md deleted file mode 100644 index 5c57932cb..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md +++ /dev/null @@ -1,321 +0,0 @@ -# 기존 UI 시스템을 MVVM으로 마이그레이션 가이드 - -Unity 프로젝트에서 기존 UI 시스템(BaseUi, BasePopupUi, PopupUi)을 MVVM 패턴으로 점진적으로 전환하는 방법을 설명합니다. - -## 호환성 보장 - -### 1. 기존 UI 시스템은 그대로 유지됩니다 -- `BaseUi`, `BasePopupUi`, `PopupUi` 클래스들은 변경되지 않음 -- 기존 UI들은 계속해서 정상 동작 -- `UiManager`와 `PopupUiState`도 기존 방식 그대로 지원 - -### 2. 새로운 Integrated 클래스들 사용 -- `IntegratedBaseUi` : BaseUi + MVVM 기능 통합 -- `IntegratedBasePopupUi` : BasePopupUi + MVVM 기능 통합 -- `IntegratedPopupUi` : PopupUi + 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 _items; - - public void UpdateCategoryView(InventoryCategoryType category) - { - _currentCategory = category; - // 복잡한 UI 업데이트 로직 - } -} -``` - -#### ViewModel로 분리 -```csharp -// 새로운 InventoryViewModel -public class InventoryViewModel : SimpleViewModel -{ - private InventoryCategoryType _currentCategory; - private List _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 FilterItems(InventoryCategoryType category) - { - // 복잡한 필터링 로직 - } -} -``` - -### 단계 3: View를 MVVM으로 전환 - -#### 기존 View 코드 -```csharp -public class InventoryView : MonoBehaviour, IEventHandler -{ - [SerializeField] private Transform _slotParent; - [SerializeField] private Text _categoryLabel; - - // 많은 상태 변수들과 복잡한 로직들... -} -``` - -#### MVVM View로 전환 -```csharp -public class MvvmInventoryView : MvvmBasePopupUi -{ - [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 -{ - [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 _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 _visibleItems; - - public InventoryCategoryType CurrentCategory - { - get => _currentCategory; - set => SetField(ref _currentCategory, value); - } - - public List 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 FilterItems(InventoryCategoryType category, InventorySortType sortType) - { - // 복잡한 필터링과 정렬 로직을 Service로 이동 - } -} - -// View (UI 바인딩만) -public class MvvmInventoryView : MvvmBasePopupUi -{ - [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으로 전환하면, 기존 시스템의 안정성을 유지하면서도 새로운 아키텍처의 이점을 얻을 수 있습니다. \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta b/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta deleted file mode 100644 index 423f0dde4..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 4d3c9e8511cb71c42b53aa3543216518 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs b/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs deleted file mode 100644 index 6f7404022..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs +++ /dev/null @@ -1,275 +0,0 @@ -using UnityEngine; -using UnityEngine.UI; -using UnityEngine.EventSystems; -using UnityEngine.InputSystem; -using DDD.MVVM; - -namespace DDD -{ - /// - /// MVVM 패턴을 적용한 새로운 레스토랑 관리 UI - /// 기존 RestaurantManagementUi의 기능을 ViewModel과 분리하여 구현 - /// - public class NewRestaurantManagementUi : IntegratedBasePopupUi - { - [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(); - 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; - } - } -} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs.meta b/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs.meta deleted file mode 100644 index fb8636ee8..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/NewRestaurantManagementUi.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 8a2e0954aa144633aad86e53dc80a46a -timeCreated: 1755673386 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/README.md b/Assets/_DDD/_Scripts/GameUi/New/README.md deleted file mode 100644 index 1b09e6a8e..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/README.md +++ /dev/null @@ -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 -- 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 - { - [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 - { - [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` 상속 -2. UI 요소에 `BindTo` Attribute 추가 -3. 수동 UI 업데이트 코드 제거 - -#### 3단계: 최적화 및 리팩토링 -1. 컨버터 활용으로 로직 단순화 -2. 계산된 속성으로 중복 제거 -3. 이벤트 시스템과 통합 - -이 MVVM 시스템을 통해 Unity UI 개발의 생산성과 유지보수성을 크게 향상시킬 수 있습니다. \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/README.md.meta b/Assets/_DDD/_Scripts/GameUi/New/README.md.meta deleted file mode 100644 index 392b933fe..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/README.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 5d5b08d15b2e43f4698c0f24977c2582 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs b/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs deleted file mode 100644 index 6805b6df9..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs +++ /dev/null @@ -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 : BasePopupUi - where TInputEnum : Enum - where TViewModel : SimpleViewModel - { - [SerializeField, Required] protected BaseUiActionsInputBinding _uiActionsInputBinding; - - protected readonly List<(InputAction action, Action 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(); - - _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(context => - { - OnInputStarted(actionEnum, context); - }); - inputAction.started += startedHandler; - - var performedHandler = new Action(context => - { - OnInputPerformed(actionEnum, context); - }); - inputAction.performed += performedHandler; - - var canceledHandler = new Action(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; - - /// - /// Attribute 기반 자동 바인딩 설정 - /// - private void SetupAutoBindings() - { - var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) - .Where(f => f.GetCustomAttribute() != null); - - foreach (var field in fields) - { - var bindAttribute = field.GetCustomAttribute(); - SetupBinding(field, bindAttribute); - } - - // 컬렉션 바인딩 설정 - var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) - .Where(f => f.GetCustomAttribute() != null); - - foreach (var field in collectionFields) - { - var bindAttribute = field.GetCustomAttribute(); - SetupCollectionBinding(field, bindAttribute); - } - } - - /// - /// 개별 필드의 바인딩 설정 - /// - 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); - } - } - - /// - /// 컬렉션 바인딩 설정 - /// - 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}"); - } - } - - /// - /// 추가 바인딩 설정 - 하위 클래스에서 구현 - /// - protected virtual void SetupBindings() { } - - /// - /// ViewModel 속성 변경 이벤트 핸들러 - /// - protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - HandleCustomPropertyChanged(e.PropertyName); - } - - /// - /// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드) - /// - protected virtual void HandleCustomPropertyChanged(string propertyName) - { - // 하위 클래스에서 구현 - } - - /// - /// UI 패널 열기 오버라이드 (ViewModel 초기화 추가) - /// - public override void OpenPanel() - { - base.OpenPanel(); - _viewModel?.Initialize(); - } - - /// - /// UI 패널 닫기 오버라이드 (ViewModel 정리 추가) - /// - public override void ClosePanel() - { - _viewModel?.Cleanup(); - base.ClosePanel(); - } - } -} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs.meta b/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs.meta deleted file mode 100644 index 23e1d5306..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBasePopupUi.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3ef87d5c2f3d82e488302056ac09a287 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs b/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs deleted file mode 100644 index 0f2195045..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs +++ /dev/null @@ -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 : 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(); - _panel = transform.Find(CommonConstants.Panel)?.gameObject; - _blockImage = transform.Find(CommonConstants.BlockImage)?.gameObject; - - if (_viewModel == null) - _viewModel = GetComponent(); - - _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; - - /// - /// Attribute 기반 자동 바인딩 설정 - /// - private void SetupAutoBindings() - { - var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) - .Where(f => f.GetCustomAttribute() != null); - - foreach (var field in fields) - { - var bindAttribute = field.GetCustomAttribute(); - SetupBinding(field, bindAttribute); - } - - // 컬렉션 바인딩 설정 - var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public) - .Where(f => f.GetCustomAttribute() != null); - - foreach (var field in collectionFields) - { - var bindAttribute = field.GetCustomAttribute(); - SetupCollectionBinding(field, bindAttribute); - } - } - - /// - /// 개별 필드의 바인딩 설정 - /// - 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); - } - } - - /// - /// 컬렉션 바인딩 설정 - /// - 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}"); - } - } - - /// - /// 추가 바인딩 설정 - 하위 클래스에서 구현 - /// - protected virtual void SetupBindings() { } - - /// - /// ViewModel 속성 변경 이벤트 핸들러 - /// - protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e) - { - HandleCustomPropertyChanged(e.PropertyName); - } - - /// - /// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드) - /// - 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); - } - - /// - /// ViewModel 메서드 호출 헬퍼 - /// - protected void InvokeViewModelMethod(string methodName, params object[] parameters) - { - if (_viewModel == null) return; - - var method = _viewModel.GetType().GetMethod(methodName); - method?.Invoke(_viewModel, parameters); - } - - /// - /// ViewModel 속성 설정 헬퍼 - /// - 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); - } - } - } -} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs.meta b/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs.meta deleted file mode 100644 index ed069a47f..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 868322e05b33bdd4cbbe3e1495fe359b \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs b/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs deleted file mode 100644 index 344ad881d..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs +++ /dev/null @@ -1,221 +0,0 @@ -using UnityEngine; -using UnityEngine.UI; -using DDD.MVVM; - -namespace DDD -{ - /// - /// IntegratedPopupUi를 사용한 인벤토리 뷰 예시 - /// 상속 대신 컴포지션 기반으로 모든 기능을 통합하여 구현 - /// Attribute 기반 자동 바인딩을 적극 활용 - /// - public class IntegratedInventoryView : IntegratedBasePopupUi - { - [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; - } - - /// - /// 수동 바인딩이 필요한 복잡한 UI 요소들 설정 - /// Attribute로 처리하기 어려운 컬렉션이나 복잡한 로직이 필요한 경우 - /// - 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; - } - } - - /// - /// 아이템 슬롯 UI 업데이트 - /// - 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(); - - // 기존 방식대로 슬롯 초기화 - slotUi.Initialize(itemViewModel, new InventorySlotUiStrategy()); - slotGameObject.name = $"ItemSlotUi_{itemViewModel.Id}"; - - // 인터랙터 설정 - var interactor = slotGameObject.GetComponent(); - if (itemViewModel.ItemType == ItemType.Recipe) - { - interactor.Initialize(TodayMenuEventType.Add, new TodayMenuInteractorStrategy()); - } - else if (DataManager.Instance.GetDataSo().TryGetDataById(itemViewModel.Id, out var cookwareData)) - { - interactor.Initialize(TodayMenuEventType.Add, new TodayCookwareInteractorStrategy()); - } - - slotGameObject.transform.SetSiblingIndex(siblingIndex++); - } - } - - /// - /// 카테고리 버튼 상태 업데이트 - /// - 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; - } - } - - /// - /// 정렬 버튼 상태 업데이트 - /// - 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; - } - } - - /// - /// 초기 선택 UI 업데이트 - /// - private void UpdateInitialSelection() - { - var initialSelected = GetInitialSelected(); - if (initialSelected != null) - { - UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(initialSelected); - } - } - - /// - /// 기존 슬롯들 정리 - /// - private void ClearSlots() - { - foreach (Transform child in _slotParent) - { - Destroy(child.gameObject); - } - } - - /// - /// ItemViewModel에 해당하는 UI GameObject 찾기 - /// - 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; - } - } -} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs.meta b/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs.meta deleted file mode 100644 index 4c0ae5b0a..000000000 --- a/Assets/_DDD/_Scripts/GameUi/New/Views/Examples/IntegratedInventoryView.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 12f3141cdd485054e8c73be9d549feb7 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/PopupUi/BasePopupUi.cs b/Assets/_DDD/_Scripts/GameUi/PopupUi/BasePopupUi.cs index 28901bfe2..c13bc96bf 100644 --- a/Assets/_DDD/_Scripts/GameUi/PopupUi/BasePopupUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/PopupUi/BasePopupUi.cs @@ -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(); } } \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/PopupUi/PopupUi.cs b/Assets/_DDD/_Scripts/GameUi/PopupUi/PopupUi.cs index 2e2d7fffc..38be27a27 100644 --- a/Assets/_DDD/_Scripts/GameUi/PopupUi/PopupUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/PopupUi/PopupUi.cs @@ -18,26 +18,55 @@ public static IEnumerable GetFlags(this T input) where T : Enum } } } - - public abstract class PopupUi : BasePopupUi where T : Enum + + public abstract class PopupUi : BasePopupUi + where TInputEnum : Enum + where TViewModel : SimpleViewModel { - [SerializeField, Required] protected BaseUiActionsInputBinding _uiActionsInputBinding; + [SerializeField, Required] protected BaseUiActionsInputBinding _uiActionsInputBinding; + protected readonly List<(InputAction action, Action handler)> _registeredHandlers = new(); - public override InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps; + protected TViewModel _viewModel; + + protected override void Awake() + { + base.Awake(); + + _viewModel = GetComponent(); + } - 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(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; } } \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/PopupUi/RestaurantManagementUi/RestaurantManagementUi.cs b/Assets/_DDD/_Scripts/GameUi/PopupUi/RestaurantManagementUi/RestaurantManagementUi.cs index 573a78fe6..c65d6b376 100644 --- a/Assets/_DDD/_Scripts/GameUi/PopupUi/RestaurantManagementUi/RestaurantManagementUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/PopupUi/RestaurantManagementUi/RestaurantManagementUi.cs @@ -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, IEventHandler + /// + /// MVVM 패턴을 적용한 새로운 레스토랑 관리 UI + /// 기존 RestaurantManagementUi의 기능을 ViewModel과 분리하여 구현 + /// + [RequireComponent(typeof(RestaurantManagementViewModel))] + public class RestaurantManagementUi : PopupUi { + [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(); } - /// - /// 각 그룹별로 허용된 카테고리를 설정합니다. - /// + 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(this); + if (_viewModel == null) return; + _sectionTabs.SelectTab((int)_viewModel.CurrentSection); } - public override void Close() + private void UpdateCategoryTabs() { - base.Close(); - - EventBus.Unregister(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(); 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; } } } \ No newline at end of file