ui 테스트
This commit is contained in:
parent
db0388b171
commit
832c369073
8
Assets/_DDD/_Scripts/GameUi/New.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b710f3e6683e1e649a6d44dc476b5083
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/_DDD/_Scripts/GameUi/New/Converters.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Converters.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75317739247907e4099f44de6f643a56
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
223
Assets/_DDD/_Scripts/GameUi/New/Converters/CommonConverters.cs
Normal file
223
Assets/_DDD/_Scripts/GameUi/New/Converters/CommonConverters.cs
Normal file
@ -0,0 +1,223 @@
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 불린 값을 반전시키는 컨버터
|
||||
/// 예: true → false, false → true
|
||||
/// </summary>
|
||||
public class InvertBoolConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
return value is bool boolValue ? !boolValue : false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
return Convert(value); // 반전은 양방향 동일
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 개수를 텍스트로 변환하는 컨버터
|
||||
/// 예: 5 → "아이템 수: 5"
|
||||
/// </summary>
|
||||
public class ItemCountConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is int count)
|
||||
return $"아이템 수: {count}";
|
||||
return "아이템 수: 0";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
if (value is string str && str.StartsWith("아이템 수: "))
|
||||
{
|
||||
var countStr = str.Substring("아이템 수: ".Length);
|
||||
return int.TryParse(countStr, out var count) ? count : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// null 또는 빈 값을 불린으로 변환하는 컨버터
|
||||
/// 예: null → false, "" → false, "text" → true
|
||||
/// </summary>
|
||||
public class IsNullOrEmptyConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value == null) return true;
|
||||
if (value is string str) return string.IsNullOrEmpty(str);
|
||||
return false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
return value is bool boolValue && !boolValue ? "" : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 숫자를 백분율 텍스트로 변환하는 컨버터
|
||||
/// 예: 0.75f → "75%"
|
||||
/// </summary>
|
||||
public class PercentageConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is float floatValue)
|
||||
return $"{(floatValue * 100):F0}%";
|
||||
if (value is double doubleValue)
|
||||
return $"{(doubleValue * 100):F0}%";
|
||||
return "0%";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
if (value is string str && str.EndsWith("%"))
|
||||
{
|
||||
var percentStr = str.Substring(0, str.Length - 1);
|
||||
if (float.TryParse(percentStr, out var percent))
|
||||
return percent / 100f;
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 열거형을 문자열로 변환하는 컨버터
|
||||
/// </summary>
|
||||
public class EnumToStringConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
return value?.ToString() ?? string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
// 역변환은 타입 정보가 필요하므로 기본 구현만 제공
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 컬렉션의 개수를 확인하는 컨버터
|
||||
/// 예: List<T>(5개) → true, List<T>(0개) → false
|
||||
/// </summary>
|
||||
public class CollectionHasItemsConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is System.Collections.ICollection collection)
|
||||
return collection.Count > 0;
|
||||
if (value is System.Collections.IEnumerable enumerable)
|
||||
return enumerable.Cast<object>().Any();
|
||||
return false;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
return value; // 역변환 불가
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 색상 투명도 조절 컨버터
|
||||
/// 불린 값에 따라 알파값을 조절
|
||||
/// </summary>
|
||||
public class AlphaConverter : IValueConverter
|
||||
{
|
||||
public float EnabledAlpha { get; set; } = 1.0f;
|
||||
public float DisabledAlpha { get; set; } = 0.5f;
|
||||
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is bool boolValue)
|
||||
return boolValue ? EnabledAlpha : DisabledAlpha;
|
||||
return EnabledAlpha;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
if (value is float alpha)
|
||||
return Mathf.Approximately(alpha, EnabledAlpha);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InventoryCategoryType을 한국어로 변환하는 컨버터
|
||||
/// </summary>
|
||||
public class InventoryCategoryConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is InventoryCategoryType category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
InventoryCategoryType.Food => "음식",
|
||||
InventoryCategoryType.Drink => "음료",
|
||||
InventoryCategoryType.Ingredient => "재료",
|
||||
InventoryCategoryType.Cookware => "조리도구",
|
||||
InventoryCategoryType.Special => "특수",
|
||||
_ => "전체"
|
||||
};
|
||||
}
|
||||
return "전체";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
if (value is string str)
|
||||
{
|
||||
return str switch
|
||||
{
|
||||
"음식" => InventoryCategoryType.Food,
|
||||
"음료" => InventoryCategoryType.Drink,
|
||||
"재료" => InventoryCategoryType.Ingredient,
|
||||
"조리도구" => InventoryCategoryType.Cookware,
|
||||
"특수" => InventoryCategoryType.Special,
|
||||
_ => InventoryCategoryType.Food
|
||||
};
|
||||
}
|
||||
return InventoryCategoryType.Food;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 가격을 통화 형식으로 변환하는 컨버터
|
||||
/// 예: 1000 → "1,000원"
|
||||
/// </summary>
|
||||
public class CurrencyConverter : IValueConverter
|
||||
{
|
||||
public string CurrencySymbol { get; set; } = "원";
|
||||
|
||||
public object Convert(object value)
|
||||
{
|
||||
if (value is int intValue)
|
||||
return $"{intValue:N0}{CurrencySymbol}";
|
||||
if (value is float floatValue)
|
||||
return $"{floatValue:N0}{CurrencySymbol}";
|
||||
return $"0{CurrencySymbol}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value)
|
||||
{
|
||||
if (value is string str && str.EndsWith(CurrencySymbol))
|
||||
{
|
||||
var numberStr = str.Substring(0, str.Length - CurrencySymbol.Length).Replace(",", "");
|
||||
if (int.TryParse(numberStr, out var number))
|
||||
return number;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c7031b52d02cf3479d48a4cab3ca66e
|
@ -0,0 +1,39 @@
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 값 변환기 인터페이스
|
||||
/// ViewModel의 데이터를 View에서 표시하기 적합한 형태로 변환
|
||||
/// </summary>
|
||||
public interface IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel 값을 View 표시용으로 변환
|
||||
/// </summary>
|
||||
/// <param name="value">변환할 값</param>
|
||||
/// <returns>변환된 값</returns>
|
||||
object Convert(object value);
|
||||
|
||||
/// <summary>
|
||||
/// View 값을 ViewModel용으로 역변환 (선택적 구현)
|
||||
/// </summary>
|
||||
/// <param name="value">역변환할 값</param>
|
||||
/// <returns>역변환된 값</returns>
|
||||
object ConvertBack(object value)
|
||||
{
|
||||
return value; // 기본 구현: 그대로 반환
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 간단한 값 변환기 인터페이스 (단방향 전용)
|
||||
/// </summary>
|
||||
public interface ISimpleConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel 값을 View 표시용으로 변환
|
||||
/// </summary>
|
||||
/// <param name="value">변환할 값</param>
|
||||
/// <returns>변환된 값</returns>
|
||||
object Convert(object value);
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a0229b137966b684f99c93cb7a488e48
|
321
Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md
Normal file
321
Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md
Normal file
@ -0,0 +1,321 @@
|
||||
# 기존 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으로 전환하면, 기존 시스템의 안정성을 유지하면서도 새로운 아키텍처의 이점을 얻을 수 있습니다.
|
7
Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta
Normal file
7
Assets/_DDD/_Scripts/GameUi/New/MIGRATION_GUIDE.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d3c9e8511cb71c42b53aa3543216518
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
278
Assets/_DDD/_Scripts/GameUi/New/README.md
Normal file
278
Assets/_DDD/_Scripts/GameUi/New/README.md
Normal file
@ -0,0 +1,278 @@
|
||||
# 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 개발의 생산성과 유지보수성을 크게 향상시킬 수 있습니다.
|
7
Assets/_DDD/_Scripts/GameUi/New/README.md.meta
Normal file
7
Assets/_DDD/_Scripts/GameUi/New/README.md.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d5b08d15b2e43f4698c0f24977c2582
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/_DDD/_Scripts/GameUi/New/Services.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Services.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7196d7444953d844c9b0e893fb3e958a
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
62
Assets/_DDD/_Scripts/GameUi/New/Services/IService.cs
Normal file
62
Assets/_DDD/_Scripts/GameUi/New/Services/IService.cs
Normal file
@ -0,0 +1,62 @@
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 서비스 계층의 기본 인터페이스
|
||||
/// MVVM 패턴에서 비즈니스 로직을 담당하는 서비스들의 공통 인터페이스
|
||||
/// </summary>
|
||||
public interface IService
|
||||
{
|
||||
/// <summary>
|
||||
/// 서비스 초기화
|
||||
/// </summary>
|
||||
void Initialize();
|
||||
|
||||
/// <summary>
|
||||
/// 서비스 종료 시 리소스 정리
|
||||
/// </summary>
|
||||
void Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 서비스 인터페이스
|
||||
/// 데이터 CRUD 작업을 담당하는 서비스들의 기본 인터페이스
|
||||
/// </summary>
|
||||
/// <typeparam name="TData">관리할 데이터 타입</typeparam>
|
||||
public interface IDataService<TData> : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// 데이터 로드
|
||||
/// </summary>
|
||||
void LoadData();
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 저장
|
||||
/// </summary>
|
||||
void SaveData();
|
||||
|
||||
/// <summary>
|
||||
/// 특정 ID의 데이터 가져오기
|
||||
/// </summary>
|
||||
/// <param name="id">데이터 ID</param>
|
||||
/// <returns>데이터 객체 또는 null</returns>
|
||||
TData GetData(string id);
|
||||
|
||||
/// <summary>
|
||||
/// 모든 데이터 가져오기
|
||||
/// </summary>
|
||||
/// <returns>모든 데이터 컬렉션</returns>
|
||||
System.Collections.Generic.IEnumerable<TData> GetAllData();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 서비스 인터페이스
|
||||
/// UI 관련 비즈니스 로직을 담당하는 서비스들의 기본 인터페이스
|
||||
/// </summary>
|
||||
public interface IUiService : IService
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 상태 업데이트
|
||||
/// </summary>
|
||||
void UpdateUiState();
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7f55f2be524632444aeebeb8b8965efc
|
167
Assets/_DDD/_Scripts/GameUi/New/Services/InventoryService.cs
Normal file
167
Assets/_DDD/_Scripts/GameUi/New/Services/InventoryService.cs
Normal file
@ -0,0 +1,167 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 인벤토리 관련 비즈니스 로직을 담당하는 서비스
|
||||
/// 기존 InventoryView의 로직을 서비스 계층으로 분리
|
||||
/// </summary>
|
||||
public class InventoryService : IUiService
|
||||
{
|
||||
private RestaurantManagementData _restaurantManagementData;
|
||||
private RestaurantManagementState _restaurantManagementState;
|
||||
|
||||
/// <summary>
|
||||
/// 서비스 초기화
|
||||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
_restaurantManagementState = RestaurantState.Instance.ManagementState;
|
||||
_restaurantManagementData = RestaurantData.Instance.ManagementData;
|
||||
|
||||
Debug.Assert(_restaurantManagementData != null, "RestaurantManagementData is null");
|
||||
Debug.Assert(_restaurantManagementState != null, "RestaurantManagementState is null");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서비스 정리
|
||||
/// </summary>
|
||||
public void Cleanup()
|
||||
{
|
||||
// 필요한 경우 리소스 정리
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// UI 상태 업데이트
|
||||
/// </summary>
|
||||
public void UpdateUiState()
|
||||
{
|
||||
// UI 상태 업데이트 로직
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인벤토리 아이템 목록 가져오기
|
||||
/// </summary>
|
||||
/// <returns>인벤토리 아이템 ViewModel 목록</returns>
|
||||
public List<ItemViewModel> GetInventoryItems()
|
||||
{
|
||||
return ItemViewModelFactory.CreateRestaurantManagementInventoryItem();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리와 정렬 타입에 따라 필터링된 아이템 목록 가져오기
|
||||
/// </summary>
|
||||
/// <param name="category">카테고리 필터</param>
|
||||
/// <param name="sortType">정렬 타입</param>
|
||||
/// <returns>필터링 및 정렬된 아이템 목록</returns>
|
||||
public IEnumerable<ItemViewModel> FilterItems(InventoryCategoryType category, InventorySortType sortType)
|
||||
{
|
||||
var items = GetInventoryItems();
|
||||
|
||||
// 카테고리 필터링
|
||||
var filtered = items.Where(item => MatchesCategory(item, category));
|
||||
|
||||
// 정렬
|
||||
return ApplySort(filtered, sortType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 오늘의 메뉴에 등록되지 않은 아이템들만 필터링
|
||||
/// </summary>
|
||||
/// <param name="items">원본 아이템 목록</param>
|
||||
/// <returns>오늘의 메뉴에 등록되지 않은 아이템들</returns>
|
||||
public IEnumerable<ItemViewModel> FilterOutTodayMenuItems(IEnumerable<ItemViewModel> items)
|
||||
{
|
||||
return items.Where(item =>
|
||||
!(item.ItemType == ItemType.Recipe && _restaurantManagementState.IsContainTodayMenu(item.Id)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 특정 카테고리와 일치하는지 확인
|
||||
/// </summary>
|
||||
/// <param name="item">확인할 아이템</param>
|
||||
/// <param name="category">카테고리</param>
|
||||
/// <returns>일치 여부</returns>
|
||||
public bool MatchesCategory(ItemViewModel item, InventoryCategoryType category)
|
||||
{
|
||||
switch (category)
|
||||
{
|
||||
case InventoryCategoryType.Food:
|
||||
if (item.ItemType != ItemType.Recipe) return false;
|
||||
return DataManager.Instance.GetDataSo<RecipeDataSo>()
|
||||
.TryGetDataById(item.Id, out var foodRecipe) && foodRecipe.RecipeType == RecipeType.FoodRecipe;
|
||||
|
||||
case InventoryCategoryType.Drink:
|
||||
if (item.ItemType != ItemType.Recipe) return false;
|
||||
return DataManager.Instance.GetDataSo<RecipeDataSo>()
|
||||
.TryGetDataById(item.Id, out var drinkRecipe) && drinkRecipe.RecipeType == RecipeType.DrinkRecipe;
|
||||
|
||||
case InventoryCategoryType.Ingredient:
|
||||
return item.ItemType == ItemType.Ingredient;
|
||||
|
||||
case InventoryCategoryType.Cookware:
|
||||
return DataManager.Instance.GetDataSo<CookwareDataSo>()
|
||||
.TryGetDataById(item.Id, out var cookwareData);
|
||||
|
||||
case InventoryCategoryType.Special:
|
||||
return false; // 특수 아이템 로직 추가 필요
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정렬 타입에 따라 아이템 목록 정렬
|
||||
/// </summary>
|
||||
/// <param name="items">정렬할 아이템 목록</param>
|
||||
/// <param name="sortType">정렬 타입</param>
|
||||
/// <returns>정렬된 아이템 목록</returns>
|
||||
public IEnumerable<ItemViewModel> ApplySort(IEnumerable<ItemViewModel> items, InventorySortType sortType)
|
||||
{
|
||||
return sortType switch
|
||||
{
|
||||
InventorySortType.NameAscending => items.OrderByDescending(item => item.HasItem).ThenBy(item => item.DisplayName),
|
||||
InventorySortType.NameDescending => items.OrderByDescending(item => item.HasItem).ThenByDescending(item => item.DisplayName),
|
||||
InventorySortType.QuantityAscending => items.OrderByDescending(item => item.HasItem).ThenBy(item => item.Count),
|
||||
InventorySortType.QuantityDescending => items.OrderByDescending(item => item.HasItem).ThenByDescending(item => item.Count),
|
||||
InventorySortType.None => items.OrderBy(item => item.Id),
|
||||
_ => items
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템이 아이템을 보유하고 있는지 확인
|
||||
/// </summary>
|
||||
/// <param name="items">확인할 아이템 목록</param>
|
||||
/// <returns>아이템을 보유한 목록</returns>
|
||||
public IEnumerable<ItemViewModel> GetItemsWithStock(IEnumerable<ItemViewModel> items)
|
||||
{
|
||||
return items.Where(item => item.HasItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리별 아이템 개수 가져오기
|
||||
/// </summary>
|
||||
/// <param name="category">카테고리</param>
|
||||
/// <returns>해당 카테고리의 보유 아이템 수</returns>
|
||||
public int GetItemCountByCategory(InventoryCategoryType category)
|
||||
{
|
||||
var filteredItems = FilterItems(category, InventorySortType.None);
|
||||
var itemsWithStock = GetItemsWithStock(filteredItems);
|
||||
return itemsWithStock.Count();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 전체 인벤토리에서 보유한 아이템 총 개수
|
||||
/// </summary>
|
||||
/// <returns>보유 아이템 총 개수</returns>
|
||||
public int GetTotalItemCount()
|
||||
{
|
||||
var allItems = GetInventoryItems();
|
||||
return GetItemsWithStock(allItems).Count();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 682535d5577b54c499b1f52b4177d202
|
8
Assets/_DDD/_Scripts/GameUi/New/Utils.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Utils.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d5bef5ee11a4064e84fb4e318be2bee
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
114
Assets/_DDD/_Scripts/GameUi/New/Utils/BindToAttribute.cs
Normal file
114
Assets/_DDD/_Scripts/GameUi/New/Utils/BindToAttribute.cs
Normal file
@ -0,0 +1,114 @@
|
||||
using System;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// UI 요소를 ViewModel 속성에 바인딩하기 위한 Attribute
|
||||
/// Inspector에서 바인딩 정보를 시각적으로 확인할 수 있도록 지원
|
||||
/// </summary>
|
||||
[System.AttributeUsage(System.AttributeTargets.Field)]
|
||||
public class BindToAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 바인딩할 ViewModel 속성의 경로 (nameof 사용 권장)
|
||||
/// </summary>
|
||||
public string PropertyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 값 변환기 타입 (선택사항)
|
||||
/// </summary>
|
||||
public System.Type ConverterType { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 바인딩 Attribute 생성자
|
||||
/// </summary>
|
||||
/// <param name="propertyPath">바인딩할 속성 경로 (nameof 사용 권장)</param>
|
||||
/// <param name="converterType">값 변환기 타입 (선택사항)</param>
|
||||
public BindToAttribute(string propertyPath, System.Type converterType = null)
|
||||
{
|
||||
PropertyPath = propertyPath;
|
||||
ConverterType = converterType;
|
||||
}
|
||||
}
|
||||
|
||||
// /// <summary>
|
||||
// /// 타입 안전한 바인딩 Attribute (제네릭 버전)
|
||||
// /// 특정 ViewModel 타입에 대한 바인딩을 명시적으로 지정
|
||||
// /// </summary>
|
||||
// /// <typeparam name="TViewModel">바인딩할 ViewModel 타입</typeparam>
|
||||
// [System.AttributeUsage(System.AttributeTargets.Field)]
|
||||
// public class BindToAttribute<TViewModel> : System.Attribute where TViewModel : class
|
||||
// {
|
||||
// /// <summary>
|
||||
// /// 바인딩할 ViewModel 속성의 경로
|
||||
// /// </summary>
|
||||
// public string PropertyPath { get; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 값 변환기 타입 (선택사항)
|
||||
// /// </summary>
|
||||
// public System.Type ConverterType { get; }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 타입 안전한 바인딩 Attribute 생성자
|
||||
// /// </summary>
|
||||
// /// <param name="propertyPath">바인딩할 속성 경로</param>
|
||||
// /// <param name="converterType">값 변환기 타입 (선택사항)</param>
|
||||
// public BindToAttribute(string propertyPath, System.Type converterType = null)
|
||||
// {
|
||||
// PropertyPath = propertyPath;
|
||||
// ConverterType = converterType;
|
||||
// }
|
||||
// }
|
||||
|
||||
/// <summary>
|
||||
/// 컬렉션 바인딩을 위한 Attribute
|
||||
/// 동적으로 생성되는 UI 요소들을 컬렉션에 바인딩
|
||||
/// </summary>
|
||||
[System.AttributeUsage(System.AttributeTargets.Field)]
|
||||
public class BindCollectionAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 바인딩할 컬렉션 속성의 경로
|
||||
/// </summary>
|
||||
public string PropertyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 프리팹의 필드명 또는 속성명
|
||||
/// </summary>
|
||||
public string ItemPrefabReference { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 컬렉션 바인딩 Attribute 생성자
|
||||
/// </summary>
|
||||
/// <param name="propertyPath">바인딩할 컬렉션 속성 경로</param>
|
||||
/// <param name="itemPrefabReference">아이템 프리팹 참조</param>
|
||||
public BindCollectionAttribute(string propertyPath, string itemPrefabReference = null)
|
||||
{
|
||||
PropertyPath = propertyPath;
|
||||
ItemPrefabReference = itemPrefabReference;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 커맨드 바인딩을 위한 Attribute
|
||||
/// 버튼 클릭 등의 이벤트를 ViewModel 메서드에 바인딩
|
||||
/// </summary>
|
||||
[System.AttributeUsage(System.AttributeTargets.Field)]
|
||||
public class BindCommandAttribute : System.Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 바인딩할 ViewModel 메서드 이름
|
||||
/// </summary>
|
||||
public string MethodName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 커맨드 바인딩 Attribute 생성자
|
||||
/// </summary>
|
||||
/// <param name="methodName">바인딩할 메서드 이름</param>
|
||||
public BindCommandAttribute(string methodName)
|
||||
{
|
||||
MethodName = methodName;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 279b3238907a3564f842594af646eab7
|
259
Assets/_DDD/_Scripts/GameUi/New/Utils/BindingContext.cs
Normal file
259
Assets/_DDD/_Scripts/GameUi/New/Utils/BindingContext.cs
Normal file
@ -0,0 +1,259 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 바인딩 타겟 인터페이스
|
||||
/// UI 요소와 ViewModel 속성을 연결하는 역할
|
||||
/// </summary>
|
||||
public interface IBindingTarget
|
||||
{
|
||||
/// <summary>
|
||||
/// 바인딩된 속성의 경로
|
||||
/// </summary>
|
||||
string PropertyPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// UI 요소의 값을 업데이트
|
||||
/// </summary>
|
||||
/// <param name="value">새로운 값</param>
|
||||
void UpdateValue(object value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Text 컴포넌트에 대한 바인딩 타겟
|
||||
/// </summary>
|
||||
public class TextBindingTarget : IBindingTarget
|
||||
{
|
||||
private readonly Text _text;
|
||||
public string PropertyPath { get; }
|
||||
|
||||
public TextBindingTarget(Text text, string propertyPath)
|
||||
{
|
||||
_text = text;
|
||||
PropertyPath = propertyPath;
|
||||
}
|
||||
|
||||
public void UpdateValue(object value)
|
||||
{
|
||||
if (_text != null)
|
||||
_text.text = value?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Image 컴포넌트에 대한 바인딩 타겟
|
||||
/// </summary>
|
||||
public class ImageBindingTarget : IBindingTarget
|
||||
{
|
||||
private readonly Image _image;
|
||||
public string PropertyPath { get; }
|
||||
|
||||
public ImageBindingTarget(Image image, string propertyPath)
|
||||
{
|
||||
_image = image;
|
||||
PropertyPath = propertyPath;
|
||||
}
|
||||
|
||||
public void UpdateValue(object value)
|
||||
{
|
||||
if (_image != null && value is Sprite sprite)
|
||||
_image.sprite = sprite;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// GameObject의 활성화 상태에 대한 바인딩 타겟
|
||||
/// </summary>
|
||||
public class ActiveBindingTarget : IBindingTarget
|
||||
{
|
||||
private readonly GameObject _gameObject;
|
||||
public string PropertyPath { get; }
|
||||
|
||||
public ActiveBindingTarget(GameObject gameObject, string propertyPath)
|
||||
{
|
||||
_gameObject = gameObject;
|
||||
PropertyPath = propertyPath;
|
||||
}
|
||||
|
||||
public void UpdateValue(object value)
|
||||
{
|
||||
if (_gameObject != null)
|
||||
_gameObject.SetActive(value is bool active && active);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Slider 컴포넌트에 대한 바인딩 타겟
|
||||
/// </summary>
|
||||
public class SliderBindingTarget : IBindingTarget
|
||||
{
|
||||
private readonly Slider _slider;
|
||||
public string PropertyPath { get; }
|
||||
|
||||
public SliderBindingTarget(Slider slider, string propertyPath)
|
||||
{
|
||||
_slider = slider;
|
||||
PropertyPath = propertyPath;
|
||||
}
|
||||
|
||||
public void UpdateValue(object value)
|
||||
{
|
||||
if (_slider != null && value is float floatValue)
|
||||
_slider.value = floatValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 바인딩 컨텍스트 - ViewModel과 View 간의 데이터 바인딩을 관리
|
||||
/// </summary>
|
||||
public class BindingContext
|
||||
{
|
||||
private readonly Dictionary<string, List<IBindingTarget>> _bindings = new();
|
||||
private readonly Dictionary<string, IValueConverter> _converters = new();
|
||||
private INotifyPropertyChanged _dataContext;
|
||||
|
||||
/// <summary>
|
||||
/// 데이터 컨텍스트 (ViewModel) 설정
|
||||
/// </summary>
|
||||
/// <param name="dataContext">바인딩할 ViewModel</param>
|
||||
public void SetDataContext(INotifyPropertyChanged dataContext)
|
||||
{
|
||||
if (_dataContext != null)
|
||||
_dataContext.PropertyChanged -= OnPropertyChanged;
|
||||
|
||||
_dataContext = dataContext;
|
||||
|
||||
if (_dataContext != null)
|
||||
{
|
||||
_dataContext.PropertyChanged += OnPropertyChanged;
|
||||
RefreshAllBindings();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 바인딩 추가
|
||||
/// </summary>
|
||||
/// <param name="propertyPath">바인딩할 속성 경로</param>
|
||||
/// <param name="target">바인딩 타겟</param>
|
||||
/// <param name="converter">값 변환기 (선택사항)</param>
|
||||
public void Bind(string propertyPath, IBindingTarget target, IValueConverter converter = null)
|
||||
{
|
||||
if (!_bindings.ContainsKey(propertyPath))
|
||||
_bindings[propertyPath] = new List<IBindingTarget>();
|
||||
|
||||
_bindings[propertyPath].Add(target);
|
||||
|
||||
if (converter != null)
|
||||
_converters[propertyPath] = converter;
|
||||
|
||||
// 즉시 초기값 설정
|
||||
UpdateBinding(propertyPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 변경 이벤트 핸들러
|
||||
/// </summary>
|
||||
private void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
UpdateBinding(e.PropertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 속성의 바인딩 업데이트
|
||||
/// </summary>
|
||||
/// <param name="propertyPath">업데이트할 속성 경로</param>
|
||||
private void UpdateBinding(string propertyPath)
|
||||
{
|
||||
if (!_bindings.ContainsKey(propertyPath)) return;
|
||||
|
||||
var value = GetPropertyValue(propertyPath);
|
||||
|
||||
// 컨버터 적용
|
||||
if (_converters.TryGetValue(propertyPath, out var converter))
|
||||
value = converter.Convert(value);
|
||||
|
||||
foreach (var target in _bindings[propertyPath])
|
||||
{
|
||||
target.UpdateValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 값 가져오기 (리플렉션 사용)
|
||||
/// </summary>
|
||||
/// <param name="propertyPath">속성 경로</param>
|
||||
/// <returns>속성 값</returns>
|
||||
private object GetPropertyValue(string propertyPath)
|
||||
{
|
||||
if (_dataContext == null) return null;
|
||||
|
||||
// 중첩 속성 지원 (예: "ItemData.Name")
|
||||
var properties = propertyPath.Split('.');
|
||||
object current = _dataContext;
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
if (current == null) return null;
|
||||
|
||||
var property = current.GetType().GetProperty(prop);
|
||||
current = property?.GetValue(current);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 바인딩 새로고침
|
||||
/// </summary>
|
||||
private void RefreshAllBindings()
|
||||
{
|
||||
foreach (var propertyPath in _bindings.Keys)
|
||||
{
|
||||
UpdateBinding(propertyPath);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 리소스 정리
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_dataContext != null)
|
||||
_dataContext.PropertyChanged -= OnPropertyChanged;
|
||||
|
||||
_bindings.Clear();
|
||||
_converters.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 속성 경로 캐시 - 성능 최적화를 위한 리플렉션 결과 캐싱
|
||||
/// </summary>
|
||||
public static class PropertyPathCache
|
||||
{
|
||||
private static readonly Dictionary<Type, Dictionary<string, PropertyInfo>> _cache = new();
|
||||
|
||||
/// <summary>
|
||||
/// 캐시된 PropertyInfo 가져오기
|
||||
/// </summary>
|
||||
/// <param name="type">타입</param>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <returns>PropertyInfo</returns>
|
||||
public static PropertyInfo GetProperty(Type type, string propertyName)
|
||||
{
|
||||
if (!_cache.ContainsKey(type))
|
||||
_cache[type] = new Dictionary<string, PropertyInfo>();
|
||||
|
||||
if (!_cache[type].ContainsKey(propertyName))
|
||||
_cache[type][propertyName] = type.GetProperty(propertyName);
|
||||
|
||||
return _cache[type][propertyName];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bff0e2748b37ec54a982f4bc8568d2fd
|
24
Assets/_DDD/_Scripts/GameUi/New/Utils/InputPhase.cs
Normal file
24
Assets/_DDD/_Scripts/GameUi/New/Utils/InputPhase.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 입력 처리 단계를 나타내는 열거형
|
||||
/// 매직 스트링을 제거하고 타입 안전성을 제공
|
||||
/// </summary>
|
||||
public enum InputPhaseType
|
||||
{
|
||||
/// <summary>
|
||||
/// 입력이 시작됨
|
||||
/// </summary>
|
||||
Started,
|
||||
|
||||
/// <summary>
|
||||
/// 입력이 수행됨
|
||||
/// </summary>
|
||||
Performed,
|
||||
|
||||
/// <summary>
|
||||
/// 입력이 취소됨
|
||||
/// </summary>
|
||||
Canceled
|
||||
}
|
||||
}
|
2
Assets/_DDD/_Scripts/GameUi/New/Utils/InputPhase.cs.meta
Normal file
2
Assets/_DDD/_Scripts/GameUi/New/Utils/InputPhase.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c9b66b101f99e1458e01b9e0653935f
|
8
Assets/_DDD/_Scripts/GameUi/New/ViewModels.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/ViewModels.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a394c2737bee3d645a8c74d1449d7176
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/_DDD/_Scripts/GameUi/New/ViewModels/Base.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/ViewModels/Base.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5e7000232c822247a01c4b7c288a6f4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,87 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
public abstract class SimpleViewModel : MonoBehaviour, INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected virtual void Awake() { }
|
||||
protected virtual void Start() { }
|
||||
protected virtual void OnDestroy() { }
|
||||
public virtual void Initialize() { }
|
||||
public virtual void Cleanup() { }
|
||||
|
||||
/// <summary>
|
||||
/// PropertyChanged 이벤트 발생
|
||||
/// </summary>
|
||||
/// <param name="propertyName">변경된 속성 이름 (자동으로 설정됨)</param>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 필드 값 변경 및 PropertyChanged 이벤트 발생
|
||||
/// </summary>
|
||||
/// <typeparam name="T">필드 타입</typeparam>
|
||||
/// <param name="field">변경할 필드 참조</param>
|
||||
/// <param name="value">새로운 값</param>
|
||||
/// <param name="propertyName">속성 이름 (자동으로 설정됨)</param>
|
||||
/// <returns>값이 실제로 변경되었는지 여부</returns>
|
||||
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
|
||||
field = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 배치 업데이트를 위한 플래그
|
||||
/// </summary>
|
||||
private bool _isUpdating;
|
||||
|
||||
/// <summary>
|
||||
/// 배치 업데이트 중 보류된 알림들
|
||||
/// </summary>
|
||||
private readonly HashSet<string> _pendingNotifications = new();
|
||||
|
||||
/// <summary>
|
||||
/// 배치 업데이트 시작 - 여러 속성 변경을 한 번에 처리
|
||||
/// </summary>
|
||||
protected void BeginUpdate() => _isUpdating = true;
|
||||
|
||||
/// <summary>
|
||||
/// 배치 업데이트 종료 - 보류된 모든 알림을 처리
|
||||
/// </summary>
|
||||
protected void EndUpdate()
|
||||
{
|
||||
_isUpdating = false;
|
||||
if (_pendingNotifications.Count > 0)
|
||||
{
|
||||
foreach (var prop in _pendingNotifications)
|
||||
OnPropertyChanged(prop);
|
||||
_pendingNotifications.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PropertyChanged 이벤트 발생 (배치 업데이트 고려)
|
||||
/// </summary>
|
||||
protected virtual void OnPropertyChangedInternal([CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (_isUpdating)
|
||||
{
|
||||
_pendingNotifications.Add(propertyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2996e8c7ea7282e4685a79943083c29a
|
274
Assets/_DDD/_Scripts/GameUi/New/ViewModels/InventoryViewModel.cs
Normal file
274
Assets/_DDD/_Scripts/GameUi/New/ViewModels/InventoryViewModel.cs
Normal file
@ -0,0 +1,274 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 인벤토리 UI의 ViewModel
|
||||
/// 기존 InventoryView의 상태와 로직을 MVVM 패턴으로 분리
|
||||
/// </summary>
|
||||
public class InventoryViewModel : SimpleViewModel, IEventHandler<InventoryChangedEvent>,
|
||||
IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
|
||||
{
|
||||
[Header("Services")]
|
||||
[SerializeField] private InventoryService _inventoryService;
|
||||
|
||||
// Private fields for properties
|
||||
private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
|
||||
private InventorySortType _currentSortType = InventorySortType.None;
|
||||
private List<ItemViewModel> _allItems = new();
|
||||
private List<ItemViewModel> _visibleItems = new();
|
||||
private ItemViewModel _selectedItem;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 카테고리
|
||||
/// </summary>
|
||||
public InventoryCategoryType CurrentCategory
|
||||
{
|
||||
get => _currentCategory;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _currentCategory, value))
|
||||
{
|
||||
UpdateVisibleItems();
|
||||
// 연관된 계산된 속성들도 알림
|
||||
OnPropertyChanged(nameof(CategoryDisplayText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 정렬 타입
|
||||
/// </summary>
|
||||
public InventorySortType CurrentSortType
|
||||
{
|
||||
get => _currentSortType;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _currentSortType, value))
|
||||
{
|
||||
UpdateVisibleItems();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 인벤토리 아이템 목록
|
||||
/// </summary>
|
||||
public List<ItemViewModel> AllItems
|
||||
{
|
||||
get => _allItems;
|
||||
private set => SetField(ref _allItems, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 필터링된 보이는 아이템 목록
|
||||
/// </summary>
|
||||
public List<ItemViewModel> VisibleItems
|
||||
{
|
||||
get => _visibleItems;
|
||||
private set => SetField(ref _visibleItems, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 아이템
|
||||
/// </summary>
|
||||
public ItemViewModel SelectedItem
|
||||
{
|
||||
get => _selectedItem;
|
||||
set => SetField(ref _selectedItem, value);
|
||||
}
|
||||
|
||||
// Computed Properties (계산된 속성들)
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리 표시 텍스트 (한국어)
|
||||
/// </summary>
|
||||
public string CategoryDisplayText => CurrentCategory switch
|
||||
{
|
||||
InventoryCategoryType.Food => "음식",
|
||||
InventoryCategoryType.Drink => "음료",
|
||||
InventoryCategoryType.Ingredient => "재료",
|
||||
InventoryCategoryType.Cookware => "조리도구",
|
||||
InventoryCategoryType.Special => "특수",
|
||||
_ => "전체"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 보이는 아이템들 중 실제 보유한 아이템이 있는지 확인
|
||||
/// </summary>
|
||||
public bool HasVisibleItems => VisibleItems.Any(item => item.HasItem);
|
||||
|
||||
/// <summary>
|
||||
/// 보이는 아이템들 중 실제 보유한 아이템 개수
|
||||
/// </summary>
|
||||
public int VisibleItemCount => VisibleItems.Count(item => item.HasItem);
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 개수 표시 텍스트
|
||||
/// </summary>
|
||||
public string ItemCountText => $"아이템 수: {VisibleItemCount}";
|
||||
|
||||
/// <summary>
|
||||
/// 빈 목록 메시지 표시 여부
|
||||
/// </summary>
|
||||
public bool ShowEmptyMessage => !HasVisibleItems;
|
||||
|
||||
/// <summary>
|
||||
/// 첫 번째 유효한 아이템 (UI 포커스용)
|
||||
/// </summary>
|
||||
public ItemViewModel FirstValidItem => VisibleItems.FirstOrDefault(item => item.HasItem);
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (_inventoryService == null)
|
||||
_inventoryService = new InventoryService();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_inventoryService.Initialize();
|
||||
LoadInventoryData();
|
||||
RegisterEvents();
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
base.Cleanup();
|
||||
|
||||
UnregisterEvents();
|
||||
_inventoryService?.Cleanup();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트 등록
|
||||
/// </summary>
|
||||
private void RegisterEvents()
|
||||
{
|
||||
EventBus.Register<InventoryChangedEvent>(this);
|
||||
EventBus.Register<TodayMenuAddedEvent>(this);
|
||||
EventBus.Register<TodayMenuRemovedEvent>(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 이벤트 등록 해제
|
||||
/// </summary>
|
||||
private void UnregisterEvents()
|
||||
{
|
||||
EventBus.Unregister<InventoryChangedEvent>(this);
|
||||
EventBus.Unregister<TodayMenuAddedEvent>(this);
|
||||
EventBus.Unregister<TodayMenuRemovedEvent>(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 인벤토리 데이터 로드
|
||||
/// </summary>
|
||||
private void LoadInventoryData()
|
||||
{
|
||||
AllItems = _inventoryService.GetInventoryItems();
|
||||
UpdateVisibleItems();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리 설정 (UI에서 호출)
|
||||
/// </summary>
|
||||
/// <param name="category">새 카테고리</param>
|
||||
public void SetCategory(InventoryCategoryType category)
|
||||
{
|
||||
CurrentCategory = category;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 정렬 타입 설정 (UI에서 호출)
|
||||
/// </summary>
|
||||
/// <param name="sortType">새 정렬 타입</param>
|
||||
public void SetSortType(InventorySortType sortType)
|
||||
{
|
||||
CurrentSortType = sortType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보이는 아이템 목록 업데이트
|
||||
/// </summary>
|
||||
private void UpdateVisibleItems()
|
||||
{
|
||||
BeginUpdate(); // 배치 업데이트 시작
|
||||
|
||||
// 서비스에서 필터링된 아이템 가져오기
|
||||
var filteredItems = _inventoryService.FilterItems(CurrentCategory, CurrentSortType);
|
||||
|
||||
// 오늘의 메뉴에 등록된 아이템 제외
|
||||
var finalItems = _inventoryService.FilterOutTodayMenuItems(filteredItems);
|
||||
|
||||
VisibleItems = finalItems.ToList();
|
||||
|
||||
// 관련된 계산된 속성들 알림
|
||||
OnPropertyChanged(nameof(HasVisibleItems));
|
||||
OnPropertyChanged(nameof(VisibleItemCount));
|
||||
OnPropertyChanged(nameof(ItemCountText));
|
||||
OnPropertyChanged(nameof(ShowEmptyMessage));
|
||||
OnPropertyChanged(nameof(FirstValidItem));
|
||||
|
||||
EndUpdate(); // 배치 업데이트 종료
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 아이템 선택
|
||||
/// </summary>
|
||||
/// <param name="item">선택할 아이템</param>
|
||||
public void SelectItem(ItemViewModel item)
|
||||
{
|
||||
SelectedItem = item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 특정 카테고리의 아이템 개수 가져오기
|
||||
/// </summary>
|
||||
/// <param name="category">카테고리</param>
|
||||
/// <returns>아이템 개수</returns>
|
||||
public int GetItemCountForCategory(InventoryCategoryType category)
|
||||
{
|
||||
return _inventoryService.GetItemCountByCategory(category);
|
||||
}
|
||||
|
||||
// Event Handlers
|
||||
|
||||
public void Invoke(InventoryChangedEvent evt)
|
||||
{
|
||||
LoadInventoryData();
|
||||
}
|
||||
|
||||
public void Invoke(TodayMenuAddedEvent evt)
|
||||
{
|
||||
UpdateVisibleItems();
|
||||
}
|
||||
|
||||
public void Invoke(TodayMenuRemovedEvent evt)
|
||||
{
|
||||
UpdateVisibleItems();
|
||||
}
|
||||
|
||||
// Unity Editor에서 테스트용 메서드들
|
||||
#if UNITY_EDITOR
|
||||
[ContextMenu("Test Category Change")]
|
||||
private void TestCategoryChange()
|
||||
{
|
||||
var nextCategory = (InventoryCategoryType)(((int)CurrentCategory + 1) % 5);
|
||||
SetCategory(nextCategory);
|
||||
}
|
||||
|
||||
[ContextMenu("Test Sort Change")]
|
||||
private void TestSortChange()
|
||||
{
|
||||
var nextSort = (InventorySortType)(((int)CurrentSortType + 1) % 5);
|
||||
SetSortType(nextSort);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 321a552f0b0773b4db1ab3dc95217719
|
8
Assets/_DDD/_Scripts/GameUi/New/Views.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Views.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36adabeb3767cf64684116798ff0ef30
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Assets/_DDD/_Scripts/GameUi/New/Views/Base.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Views/Base.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76b62bf64be94ab4bb1e4b610da29fa4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
210
Assets/_DDD/_Scripts/GameUi/New/Views/Base/AutoBindView.cs
Normal file
210
Assets/_DDD/_Scripts/GameUi/New/Views/Base/AutoBindView.cs
Normal file
@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
/// <summary>
|
||||
/// 자동 바인딩을 지원하는 View 기본 클래스
|
||||
/// Attribute를 통해 설정된 바인딩을 자동으로 처리
|
||||
/// </summary>
|
||||
/// <typeparam name="TViewModel">바인딩할 ViewModel 타입</typeparam>
|
||||
public abstract class AutoBindView<TViewModel> : MonoBehaviour where TViewModel : SimpleViewModel
|
||||
{
|
||||
[SerializeField] protected TViewModel _viewModel;
|
||||
protected BindingContext _bindingContext;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel 인스턴스
|
||||
/// </summary>
|
||||
public TViewModel ViewModel => _viewModel;
|
||||
|
||||
protected virtual void Awake()
|
||||
{
|
||||
if (_viewModel == null)
|
||||
_viewModel = GetComponent<TViewModel>();
|
||||
|
||||
_bindingContext = new BindingContext();
|
||||
|
||||
SetupAutoBindings();
|
||||
}
|
||||
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
if (_viewModel != null && _bindingContext != null)
|
||||
{
|
||||
_bindingContext.SetDataContext(_viewModel);
|
||||
_viewModel.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDisable()
|
||||
{
|
||||
if (_viewModel != null)
|
||||
{
|
||||
_viewModel.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnDestroy()
|
||||
{
|
||||
_bindingContext?.Dispose();
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <param name="field">바인딩할 필드</param>
|
||||
/// <param name="bindAttribute">바인딩 Attribute</param>
|
||||
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>
|
||||
/// <param name="field">바인딩할 필드</param>
|
||||
/// <param name="bindAttribute">바인딩 Attribute</param>
|
||||
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>
|
||||
/// ViewModel 속성 변경 이벤트 핸들러
|
||||
/// 추가적인 커스텀 로직이 필요한 경우 오버라이드
|
||||
/// </summary>
|
||||
/// <param name="sender">이벤트 발신자</param>
|
||||
/// <param name="e">속성 변경 정보</param>
|
||||
protected virtual void OnViewModelPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
// 자동 바인딩으로 처리되지 않는 특별한 속성들의 커스텀 처리
|
||||
HandleCustomPropertyChanged(e.PropertyName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 커스텀 속성 변경 처리 (하위 클래스에서 오버라이드)
|
||||
/// </summary>
|
||||
/// <param name="propertyName">변경된 속성 이름</param>
|
||||
protected virtual void HandleCustomPropertyChanged(string propertyName)
|
||||
{
|
||||
// 하위 클래스에서 구현
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 수동 바인딩 헬퍼 메서드들
|
||||
/// Attribute 사용이 어려운 경우 코드로 바인딩 설정
|
||||
/// </summary>
|
||||
|
||||
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 메서드 호출 헬퍼
|
||||
/// UI 이벤트에서 ViewModel 메서드를 쉽게 호출
|
||||
/// </summary>
|
||||
/// <param name="methodName">호출할 메서드 이름</param>
|
||||
/// <param name="parameters">메서드 매개변수</param>
|
||||
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>
|
||||
/// <param name="propertyName">속성 이름</param>
|
||||
/// <param name="value">설정할 값</param>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 738101122cf3fb74e99b244165797ab8
|
@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using DDD.MVVM;
|
||||
using Sirenix.OdinInspector;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace DDD
|
||||
{
|
||||
public abstract class IntegratedBasePopupUi<TInputEnum, TViewModel> : IntegratedBaseUi<TViewModel>
|
||||
where TInputEnum : Enum
|
||||
where TViewModel : SimpleViewModel
|
||||
{
|
||||
[SerializeField, Required] protected BaseUiActionsInputBinding<TInputEnum> _uiActionsInputBinding;
|
||||
|
||||
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers =
|
||||
new();
|
||||
|
||||
public InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
|
||||
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this as BasePopupUi);
|
||||
|
||||
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)
|
||||
{
|
||||
var initialSelected = GetInitialSelected();
|
||||
if (initialSelected != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(initialSelected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected 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;
|
||||
|
||||
|
||||
public virtual void Open(OpenPopupUiEvent evt)
|
||||
{
|
||||
OpenPanel();
|
||||
|
||||
var initialSelected = GetInitialSelected();
|
||||
if (initialSelected != null)
|
||||
{
|
||||
EventSystem.current.SetSelectedGameObject(initialSelected);
|
||||
}
|
||||
|
||||
transform.SetAsLastSibling();
|
||||
|
||||
if (IsTopPopup)
|
||||
{
|
||||
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Close()
|
||||
{
|
||||
var evt = GameEvents.ClosePopupUiEvent;
|
||||
evt.UiType = GetType();
|
||||
EventBus.Broadcast(evt);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3ef87d5c2f3d82e488302056ac09a287
|
248
Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs
Normal file
248
Assets/_DDD/_Scripts/GameUi/New/Views/Base/IntegratedBaseUi.cs
Normal file
@ -0,0 +1,248 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 868322e05b33bdd4cbbe3e1495fe359b
|
8
Assets/_DDD/_Scripts/GameUi/New/Views/Examples.meta
Normal file
8
Assets/_DDD/_Scripts/GameUi/New/Views/Examples.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: abb1dd67b48daeb4f968a2641cf7b4a3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,221 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 12f3141cdd485054e8c73be9d549feb7
|
Loading…
Reference in New Issue
Block a user