ui 테스트

This commit is contained in:
NTG 2025-08-20 15:22:08 +09:00
parent db0388b171
commit 832c369073
39 changed files with 2785 additions and 0 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b710f3e6683e1e649a6d44dc476b5083
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 75317739247907e4099f44de6f643a56
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7c7031b52d02cf3479d48a4cab3ca66e

View File

@ -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);
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a0229b137966b684f99c93cb7a488e48

View 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으로 전환하면, 기존 시스템의 안정성을 유지하면서도 새로운 아키텍처의 이점을 얻을 수 있습니다.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4d3c9e8511cb71c42b53aa3543216518
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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 개발의 생산성과 유지보수성을 크게 향상시킬 수 있습니다.

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5d5b08d15b2e43f4698c0f24977c2582
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7196d7444953d844c9b0e893fb3e958a
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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();
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7f55f2be524632444aeebeb8b8965efc

View 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();
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 682535d5577b54c499b1f52b4177d202

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 0d5bef5ee11a4064e84fb4e318be2bee
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 279b3238907a3564f842594af646eab7

View 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];
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bff0e2748b37ec54a982f4bc8568d2fd

View File

@ -0,0 +1,24 @@
namespace DDD.MVVM
{
/// <summary>
/// 입력 처리 단계를 나타내는 열거형
/// 매직 스트링을 제거하고 타입 안전성을 제공
/// </summary>
public enum InputPhaseType
{
/// <summary>
/// 입력이 시작됨
/// </summary>
Started,
/// <summary>
/// 입력이 수행됨
/// </summary>
Performed,
/// <summary>
/// 입력이 취소됨
/// </summary>
Canceled
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c9b66b101f99e1458e01b9e0653935f

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a394c2737bee3d645a8c74d1449d7176
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c5e7000232c822247a01c4b7c288a6f4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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);
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2996e8c7ea7282e4685a79943083c29a

View 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
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 321a552f0b0773b4db1ab3dc95217719

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 36adabeb3767cf64684116798ff0ef30
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 76b62bf64be94ab4bb1e4b610da29fa4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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);
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 738101122cf3fb74e99b244165797ab8

View File

@ -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);
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ef87d5c2f3d82e488302056ac09a287

View 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);
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 868322e05b33bdd4cbbe3e1495fe359b

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: abb1dd67b48daeb4f968a2641cf7b4a3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -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;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 12f3141cdd485054e8c73be9d549feb7