임시 가이드라인
This commit is contained in:
parent
6f4417cd98
commit
4a18a239e8
545
Assets/_DDD/_Scripts/GameUi/UiGuideline
Normal file
545
Assets/_DDD/_Scripts/GameUi/UiGuideline
Normal file
@ -0,0 +1,545 @@
|
||||
# 새로운 UI 개발을 위한 단계별 가이드라인
|
||||
|
||||
Unity 프로젝트에서 RestaurantManagementUi를 기반으로 새로운 UI를 만들 때 따라야 할 구체적인 단계별 가이드라인을 제공합니다.
|
||||
|
||||
## 개발 순서 및 클래스 생성 가이드
|
||||
|
||||
### 1단계: 요구사항 분석 및 설계
|
||||
|
||||
#### 먼저 결정해야 할 사항들
|
||||
- **UI 타입**: 일반 UI인가? 팝업 UI인가?
|
||||
- **입력 처리**: 키보드/게임패드 입력이 필요한가?
|
||||
- **데이터 복잡도**: 단순한 표시용인가? 복잡한 상태 관리가 필요한가?
|
||||
- **재사용성**: 다른 곳에서도 사용될 가능성이 있는가?
|
||||
|
||||
#### 예시: ShopUi를 만든다고 가정
|
||||
```
|
||||
요구사항:
|
||||
- 상품 목록 표시 (카테고리별 필터링)
|
||||
- 상품 구매 기능
|
||||
- 소지금 표시
|
||||
- 키보드 입력 지원
|
||||
- 팝업 형태로 동작
|
||||
```
|
||||
|
||||
### 2단계: Service 클래스 생성 (비즈니스 로직)
|
||||
|
||||
**가장 먼저 Service부터 시작하는 이유:**
|
||||
- 비즈니스 로직이 명확해야 UI 설계가 가능
|
||||
- 테스트 가능한 코드 작성
|
||||
- ViewModel에서 사용할 데이터와 기능 정의
|
||||
|
||||
#### ShopService.cs
|
||||
```csharp
|
||||
// Assets/_DDD/_Scripts/GameUi/New/Services/ShopService.cs
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
public class ShopService : IUiService
|
||||
{
|
||||
private ShopData _shopData;
|
||||
private PlayerInventoryData _playerData;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_shopData = DataManager.Instance.GetDataSo<ShopData>();
|
||||
_playerData = PlayerState.Instance.InventoryData;
|
||||
}
|
||||
|
||||
public void Cleanup() { }
|
||||
public void UpdateUiState() { }
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리별 상품 목록 가져오기
|
||||
/// </summary>
|
||||
public IEnumerable<ShopItemViewModel> GetItemsByCategory(ShopCategoryType category)
|
||||
{
|
||||
return _shopData.Items
|
||||
.Where(item => MatchesCategory(item, category))
|
||||
.Select(item => new ShopItemViewModel(item))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상품 구매 가능 여부 확인
|
||||
/// </summary>
|
||||
public bool CanPurchaseItem(string itemId, int quantity = 1)
|
||||
{
|
||||
var item = _shopData.GetItemById(itemId);
|
||||
return _playerData.Money >= (item.Price * quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상품 구매 처리
|
||||
/// </summary>
|
||||
public bool PurchaseItem(string itemId, int quantity = 1)
|
||||
{
|
||||
if (!CanPurchaseItem(itemId, quantity)) return false;
|
||||
|
||||
var item = _shopData.GetItemById(itemId);
|
||||
var totalCost = item.Price * quantity;
|
||||
|
||||
_playerData.Money -= totalCost;
|
||||
_playerData.AddItem(itemId, quantity);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool MatchesCategory(ShopItemData item, ShopCategoryType category)
|
||||
{
|
||||
// 카테고리 매칭 로직
|
||||
return category switch
|
||||
{
|
||||
ShopCategoryType.Food => item.Type == ItemType.Food,
|
||||
ShopCategoryType.Equipment => item.Type == ItemType.Equipment,
|
||||
_ => true
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3단계: ViewModel 클래스 생성 (상태 관리)
|
||||
|
||||
Service가 준비되었으니 이를 활용하는 ViewModel을 생성합니다.
|
||||
|
||||
#### ShopViewModel.cs
|
||||
```csharp
|
||||
// Assets/_DDD/_Scripts/GameUi/New/ViewModels/ShopViewModel.cs
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
public class ShopViewModel : SimpleViewModel, IEventHandler<MoneyChangedEvent>
|
||||
{
|
||||
[Header("Services")]
|
||||
[SerializeField] private ShopService _shopService;
|
||||
|
||||
// Private fields for properties
|
||||
private ShopCategoryType _currentCategory = ShopCategoryType.All;
|
||||
private List<ShopItemViewModel> _visibleItems = new();
|
||||
private ShopItemViewModel _selectedItem;
|
||||
private int _playerMoney;
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 카테고리
|
||||
/// </summary>
|
||||
public ShopCategoryType CurrentCategory
|
||||
{
|
||||
get => _currentCategory;
|
||||
set
|
||||
{
|
||||
if (SetField(ref _currentCategory, value))
|
||||
{
|
||||
UpdateVisibleItems();
|
||||
OnPropertyChanged(nameof(CategoryDisplayText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 보이는 상품 목록
|
||||
/// </summary>
|
||||
public List<ShopItemViewModel> VisibleItems
|
||||
{
|
||||
get => _visibleItems;
|
||||
private set => SetField(ref _visibleItems, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 현재 선택된 상품
|
||||
/// </summary>
|
||||
public ShopItemViewModel SelectedItem
|
||||
{
|
||||
get => _selectedItem;
|
||||
set => SetField(ref _selectedItem, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 플레이어 소지금
|
||||
/// </summary>
|
||||
public int PlayerMoney
|
||||
{
|
||||
get => _playerMoney;
|
||||
set => SetField(ref _playerMoney, value);
|
||||
}
|
||||
|
||||
// 계산된 속성들
|
||||
public string CategoryDisplayText => CurrentCategory switch
|
||||
{
|
||||
ShopCategoryType.Food => "음식",
|
||||
ShopCategoryType.Equipment => "장비",
|
||||
_ => "전체"
|
||||
};
|
||||
|
||||
public string MoneyDisplayText => $"{PlayerMoney:N0} G";
|
||||
public bool HasVisibleItems => VisibleItems.Any();
|
||||
public bool CanPurchaseSelected =>
|
||||
SelectedItem != null && _shopService.CanPurchaseItem(SelectedItem.Id);
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
if (_shopService == null)
|
||||
_shopService = new ShopService();
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_shopService.Initialize();
|
||||
LoadShopData();
|
||||
RegisterEvents();
|
||||
}
|
||||
|
||||
public override void Cleanup()
|
||||
{
|
||||
base.Cleanup();
|
||||
|
||||
UnregisterEvents();
|
||||
_shopService?.Cleanup();
|
||||
}
|
||||
|
||||
private void RegisterEvents()
|
||||
{
|
||||
EventBus.Register<MoneyChangedEvent>(this);
|
||||
}
|
||||
|
||||
private void UnregisterEvents()
|
||||
{
|
||||
EventBus.Unregister<MoneyChangedEvent>(this);
|
||||
}
|
||||
|
||||
private void LoadShopData()
|
||||
{
|
||||
PlayerMoney = PlayerState.Instance.InventoryData.Money;
|
||||
UpdateVisibleItems();
|
||||
}
|
||||
|
||||
private void UpdateVisibleItems()
|
||||
{
|
||||
BeginUpdate();
|
||||
|
||||
var items = _shopService.GetItemsByCategory(CurrentCategory);
|
||||
VisibleItems = items.ToList();
|
||||
|
||||
OnPropertyChanged(nameof(HasVisibleItems));
|
||||
OnPropertyChanged(nameof(CanPurchaseSelected));
|
||||
|
||||
EndUpdate();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 카테고리 변경
|
||||
/// </summary>
|
||||
public void SetCategory(ShopCategoryType category)
|
||||
{
|
||||
CurrentCategory = category;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상품 선택
|
||||
/// </summary>
|
||||
public void SelectItem(ShopItemViewModel item)
|
||||
{
|
||||
SelectedItem = item;
|
||||
OnPropertyChanged(nameof(CanPurchaseSelected));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 상품 구매
|
||||
/// </summary>
|
||||
public bool PurchaseSelectedItem(int quantity = 1)
|
||||
{
|
||||
if (SelectedItem == null || !CanPurchaseSelected) return false;
|
||||
|
||||
var success = _shopService.PurchaseItem(SelectedItem.Id, quantity);
|
||||
if (success)
|
||||
{
|
||||
// 구매 성공 시 UI 업데이트
|
||||
LoadShopData();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
// 이벤트 핸들러
|
||||
public void Invoke(MoneyChangedEvent evt)
|
||||
{
|
||||
PlayerMoney = evt.NewAmount;
|
||||
OnPropertyChanged(nameof(MoneyDisplayText));
|
||||
OnPropertyChanged(nameof(CanPurchaseSelected));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4단계: ItemViewModel 클래스 생성 (필요시)
|
||||
|
||||
복잡한 아이템이 있다면 별도의 ItemViewModel을 생성합니다.
|
||||
|
||||
#### ShopItemViewModel.cs
|
||||
```csharp
|
||||
// Assets/_DDD/_Scripts/GameUi/New/ViewModels/ShopItemViewModel.cs
|
||||
namespace DDD.MVVM
|
||||
{
|
||||
public class ShopItemViewModel : SimpleViewModel
|
||||
{
|
||||
private ShopItemData _itemData;
|
||||
private bool _isSelected;
|
||||
|
||||
public string Id => _itemData?.Id ?? "";
|
||||
public string Name => _itemData?.Name ?? "";
|
||||
public string Description => _itemData?.Description ?? "";
|
||||
public int Price => _itemData?.Price ?? 0;
|
||||
public Sprite Icon => _itemData?.Icon;
|
||||
|
||||
public bool IsSelected
|
||||
{
|
||||
get => _isSelected;
|
||||
set => SetField(ref _isSelected, value);
|
||||
}
|
||||
|
||||
public string PriceDisplayText => $"{Price:N0} G";
|
||||
|
||||
public ShopItemViewModel(ShopItemData itemData)
|
||||
{
|
||||
_itemData = itemData;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5단계: View 클래스 생성
|
||||
|
||||
마지막으로 UI View를 생성합니다. RestaurantManagementUi를 참고하여 구조를 만듭니다.
|
||||
|
||||
#### ShopUi.cs
|
||||
```csharp
|
||||
// Assets/_DDD/_Scripts/GameUi/PopupUi/ShopUi/ShopUi.cs
|
||||
namespace DDD
|
||||
{
|
||||
/// <summary>
|
||||
/// 상점 UI - RestaurantManagementUi 구조를 참고하여 구현
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(ShopViewModel))]
|
||||
public class ShopUi : IntegratedBasePopupUi<ShopUiActions, ShopViewModel>
|
||||
{
|
||||
[Header("UI References")]
|
||||
// Attribute 기반 자동 바인딩
|
||||
[SerializeField, BindTo(nameof(ShopViewModel.CategoryDisplayText))]
|
||||
private Text _categoryLabel;
|
||||
|
||||
[SerializeField, BindTo(nameof(ShopViewModel.MoneyDisplayText))]
|
||||
private Text _moneyLabel;
|
||||
|
||||
[SerializeField, BindTo(nameof(ShopViewModel.HasVisibleItems), typeof(InvertBoolConverter))]
|
||||
private GameObject _emptyMessage;
|
||||
|
||||
// 수동 처리가 필요한 UI 요소들
|
||||
[SerializeField] private Transform _itemSlotParent;
|
||||
[SerializeField] private GameObject _itemSlotPrefab;
|
||||
[SerializeField] private Button[] _categoryButtons;
|
||||
[SerializeField] private Button _purchaseButton;
|
||||
|
||||
public override InputActionMaps InputActionMaps => InputActionMaps.Shop;
|
||||
|
||||
protected override GameObject GetInitialSelected()
|
||||
{
|
||||
// 첫 번째 상품 슬롯 또는 카테고리 버튼 반환
|
||||
var firstSlot = _itemSlotParent.childCount > 0 ?
|
||||
_itemSlotParent.GetChild(0).gameObject : null;
|
||||
|
||||
return firstSlot ?? (_categoryButtons.Length > 0 ? _categoryButtons[0].gameObject : null);
|
||||
}
|
||||
|
||||
protected override void SetupBindings()
|
||||
{
|
||||
// Attribute로 처리하기 어려운 복잡한 바인딩들은 여기서 수동 설정
|
||||
}
|
||||
|
||||
protected override void HandleCustomPropertyChanged(string propertyName)
|
||||
{
|
||||
switch (propertyName)
|
||||
{
|
||||
case nameof(ShopViewModel.VisibleItems):
|
||||
UpdateItemSlots();
|
||||
break;
|
||||
|
||||
case nameof(ShopViewModel.CurrentCategory):
|
||||
UpdateCategoryButtons();
|
||||
break;
|
||||
|
||||
case nameof(ShopViewModel.CanPurchaseSelected):
|
||||
_purchaseButton.interactable = ViewModel.CanPurchaseSelected;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateItemSlots()
|
||||
{
|
||||
// 기존 슬롯 정리
|
||||
foreach (Transform child in _itemSlotParent)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 새 슬롯 생성
|
||||
if (ViewModel?.VisibleItems != null)
|
||||
{
|
||||
foreach (var item in ViewModel.VisibleItems)
|
||||
{
|
||||
var slotGO = Instantiate(_itemSlotPrefab, _itemSlotParent);
|
||||
var slotComponent = slotGO.GetComponent<ShopItemSlot>();
|
||||
slotComponent.Initialize(item, OnItemClicked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCategoryButtons()
|
||||
{
|
||||
if (_categoryButtons == null || ViewModel == null) return;
|
||||
|
||||
for (int i = 0; i < _categoryButtons.Length; i++)
|
||||
{
|
||||
var isSelected = (int)ViewModel.CurrentCategory == i;
|
||||
_categoryButtons[i].interactable = !isSelected;
|
||||
}
|
||||
}
|
||||
|
||||
// UI 이벤트 핸들러들
|
||||
public void OnCategoryButtonClicked(int categoryIndex)
|
||||
{
|
||||
ViewModel?.SetCategory((ShopCategoryType)categoryIndex);
|
||||
}
|
||||
|
||||
public void OnPurchaseButtonClicked()
|
||||
{
|
||||
if (ViewModel?.PurchaseSelectedItem() == true)
|
||||
{
|
||||
// 구매 성공 피드백
|
||||
ShowPurchaseSuccessEffect();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnItemClicked(ShopItemViewModel item)
|
||||
{
|
||||
ViewModel?.SelectItem(item);
|
||||
}
|
||||
|
||||
private void ShowPurchaseSuccessEffect()
|
||||
{
|
||||
// 구매 성공 시 시각적 피드백
|
||||
}
|
||||
|
||||
// 입력 처리
|
||||
protected override bool OnInputPerformed(ShopUiActions actionEnum, UnityEngine.InputSystem.InputAction.CallbackContext context)
|
||||
{
|
||||
var isHandled = base.OnInputPerformed(actionEnum, context);
|
||||
|
||||
if (isHandled && ViewModel != null)
|
||||
{
|
||||
switch (actionEnum)
|
||||
{
|
||||
case ShopUiActions.Purchase:
|
||||
OnPurchaseButtonClicked();
|
||||
break;
|
||||
case ShopUiActions.Cancel:
|
||||
Close();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return isHandled;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6단계: 입력 액션 정의 (필요시)
|
||||
|
||||
새로운 UI에 특별한 입력이 필요하다면 입력 액션을 정의합니다.
|
||||
|
||||
#### ShopUiActions.cs
|
||||
```csharp
|
||||
// Assets/_DDD/_Scripts/Input/ShopUiActions.cs
|
||||
namespace DDD
|
||||
{
|
||||
[System.Flags]
|
||||
public enum ShopUiActions
|
||||
{
|
||||
None = 0,
|
||||
Navigate = 1 << 0,
|
||||
Submit = 1 << 1,
|
||||
Cancel = 1 << 2,
|
||||
Purchase = 1 << 3,
|
||||
PreviousCategory = 1 << 4,
|
||||
NextCategory = 1 << 5,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 개발 체크리스트
|
||||
|
||||
### Service 개발 체크리스트
|
||||
- [ ] IService 인터페이스 구현
|
||||
- [ ] 데이터 접근 로직 캡슐화
|
||||
- [ ] 비즈니스 로직 구현
|
||||
- [ ] 에러 처리 및 검증 로직
|
||||
- [ ] 단위 테스트 가능한 구조
|
||||
|
||||
### ViewModel 개발 체크리스트
|
||||
- [ ] SimpleViewModel 상속
|
||||
- [ ] 모든 상태를 속성으로 정의
|
||||
- [ ] SetField를 통한 PropertyChanged 알림
|
||||
- [ ] 계산된 속성 구현
|
||||
- [ ] Service 의존성 주입
|
||||
- [ ] 이벤트 등록/해제
|
||||
- [ ] 생명주기 메서드 구현
|
||||
|
||||
### View 개발 체크리스트
|
||||
- [ ] 적절한 Base 클래스 상속 선택
|
||||
- 일반 UI: IntegratedBaseUi<TViewModel>
|
||||
- 팝업 UI: IntegratedBasePopupUi<TViewModel>
|
||||
- 입력 필요 팝업: IntegratedBasePopupUi<TInputEnum, TViewModel>
|
||||
- [ ] BindTo Attribute 적용
|
||||
- [ ] GetInitialSelected() 구현
|
||||
- [ ] SetupBindings() 구현 (필요시)
|
||||
- [ ] HandleCustomPropertyChanged() 구현
|
||||
- [ ] UI 이벤트 핸들러 ViewModel로 연결
|
||||
|
||||
## 폴더 구조 권장사항
|
||||
|
||||
```
|
||||
Assets/_DDD/_Scripts/GameUi/PopupUi/ShopUi/
|
||||
├── ShopUi.cs # View 클래스
|
||||
├── ShopItemSlot.cs # 하위 UI 컴포넌트
|
||||
└── ShopUiData.cs # UI 설정 데이터 (필요시)
|
||||
|
||||
Assets/_DDD/_Scripts/GameUi/New/
|
||||
├── Services/
|
||||
│ └── ShopService.cs # Service 클래스
|
||||
├── ViewModels/
|
||||
│ ├── ShopViewModel.cs # 메인 ViewModel
|
||||
│ └── ShopItemViewModel.cs # 아이템 ViewModel
|
||||
└── Converters/
|
||||
└── PriceConverter.cs # 커스텀 컨버터 (필요시)
|
||||
```
|
||||
|
||||
## 개발 팁
|
||||
|
||||
### 1. 기존 코드 활용
|
||||
- RestaurantManagementUi의 패턴을 최대한 재활용
|
||||
- InventoryView의 필터링/정렬 로직 참고
|
||||
- 기존 Service 클래스들의 구조 패턴 따라하기
|
||||
|
||||
### 2. 점진적 개발
|
||||
1. 먼저 기본 기능만 구현 (Service → ViewModel → View)
|
||||
2. UI 바인딩은 단순한 것부터 시작
|
||||
3. 복잡한 기능은 나중에 추가
|
||||
|
||||
### 3. 테스트 우선
|
||||
- Service 로직은 반드시 단위 테스트
|
||||
- ViewModel은 Mock Service로 테스트
|
||||
- View는 수동 테스트로 검증
|
||||
|
||||
이 가이드라인을 따라하면 일관성 있고 유지보수가 쉬운 UI를 효율적으로 개발할 수 있습니다.
|
3
Assets/_DDD/_Scripts/GameUi/UiGuideline.meta
Normal file
3
Assets/_DDD/_Scripts/GameUi/UiGuideline.meta
Normal file
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31ff6b5920be49feb652a1b614d62c15
|
||||
timeCreated: 1755681370
|
Loading…
Reference in New Issue
Block a user