임시 가이드라인

This commit is contained in:
NTG 2025-08-20 18:16:57 +09:00
parent 6f4417cd98
commit 4a18a239e8
2 changed files with 548 additions and 0 deletions

View 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를 효율적으로 개발할 수 있습니다.

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 31ff6b5920be49feb652a1b614d62c15
timeCreated: 1755681370