ui 로직 개선

This commit is contained in:
NTG 2025-08-24 20:44:32 +09:00
parent b0ffb9df08
commit 4ca10808a9
24 changed files with 495 additions and 958 deletions

View File

@ -119,6 +119,19 @@ TextureImporter:
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []

View File

@ -1,10 +1,5 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace DDD
{
@ -34,54 +29,42 @@ public abstract class BaseUi : MonoBehaviour
protected virtual void Awake()
{
_canvasGroup = GetComponent<CanvasGroup>();
_panel = transform.Find(CommonConstants.Panel)?.gameObject;
_blockImage = transform.Find(CommonConstants.BlockImage)?.gameObject;
_bindingContext = new BindingContext();
SetupBindings();
}
protected virtual void OnEnable()
{
_panel = transform.Find(CommonConstants.Panel).gameObject;
_blockImage = transform.Find(CommonConstants.BlockImage).gameObject;
if (_enableBlockImage)
{
_blockImage.SetActive(false);
}
_panel.SetActive(false);
}
protected virtual void Start()
{
ClosePanel();
}
protected virtual void Update()
{
}
protected virtual void OnDisable()
{
}
protected virtual void Start() { }
protected virtual void Update() { }
protected virtual void OnDestroy()
{
TryUnregister();
_bindingContext?.Dispose();
}
public virtual void CreateInitialize()
{
TryRegister();
}
protected virtual void TryRegister()
{
UiManager.Instance.UiState.RegisterUI(this);
}
protected virtual void TryUnregister()
{
UiManager.Instance.UiState.UnregisterUI(this);
}
public void CreateInitialize()
{
OnCreatedInitialize();
}
protected virtual void OnCreatedInitialize()
{
UiManager.Instance.UiState.RegisterUI(this);
_bindingContext = new BindingContext();
SetupBindings();
}
protected virtual void OnOpenedEvents() { }
protected virtual void OnClosedEvents() { }
// BaseUi 메서드들을 직접 구현
public virtual void OpenPanel()
{
@ -91,6 +74,8 @@ public virtual void OpenPanel()
}
_panel.SetActive(true);
OnOpenedEvents();
}
public virtual void ClosePanel()
@ -101,12 +86,14 @@ public virtual void ClosePanel()
}
_panel.SetActive(false);
OnClosedEvents();
IsInitialized = false;
}
public virtual void SetUiInteractable(bool active)
{
if (_canvasGroup != null)
if (_canvasGroup)
{
_canvasGroup.interactable = active;
_canvasGroup.blocksRaycasts = active;

View File

@ -11,9 +11,9 @@ protected override void Awake()
_viewModel = GetComponent<TViewModel>();
}
protected override void OnEnable()
protected override void OnOpenedEvents()
{
base.OnEnable();
base.OnOpenedEvents();
if (_viewModel && _bindingContext != null)
{
@ -22,9 +22,9 @@ protected override void OnEnable()
}
}
protected override void OnDisable()
protected override void OnClosedEvents()
{
base.OnDisable();
base.OnClosedEvents();
if (_viewModel != null)
{

View File

@ -5,27 +5,24 @@ namespace DDD
{
public class FadeUi : BaseUi, IEventHandler<FadeInEvent>, IEventHandler<FadeOutEvent>
{
protected override void Awake()
protected override void OnDestroy()
{
base.Awake();
base.OnDestroy();
_canvasGroup.alpha = 0f;
EventBus.Unregister<FadeInEvent>(this);
EventBus.Unregister<FadeOutEvent>(this);
}
protected override void TryRegister()
protected override void OnCreatedInitialize()
{
base.TryRegister();
base.OnCreatedInitialize();
_canvasGroup.alpha = 0f;
EventBus.Register<FadeInEvent>(this);
EventBus.Register<FadeOutEvent>(this);
}
protected override void TryUnregister()
{
EventBus.Unregister<FadeInEvent>(this);
EventBus.Unregister<FadeOutEvent>(this);
}
public void Invoke(FadeInEvent evt)
{
_ = FadeInAsync(evt);

View File

@ -13,29 +13,24 @@ public class GlobalMessageUi : BaseUi, IEventHandler<ShowGlobalMessageEvent>
private readonly Queue<ShowGlobalMessageEvent> _messageQueue = new();
private bool _isDisplayingMessage = false;
protected override void Awake()
protected override void OnDestroy()
{
base.Awake();
_canvasGroup.alpha = 0;
_messageText.text = null;
}
protected override void TryRegister()
{
base.TryRegister();
EventBus.Register(this);
}
protected override void TryUnregister()
{
base.TryUnregister();
base.OnDestroy();
EventBus.Unregister(this);
_fadeTween?.Kill();
}
protected override void OnCreatedInitialize()
{
base.OnCreatedInitialize();
_canvasGroup.alpha = 0;
_messageText.text = null;
EventBus.Register(this);
}
public void Invoke(ShowGlobalMessageEvent evt)
{
_messageQueue.Enqueue(evt);

View File

@ -2,6 +2,11 @@ namespace DDD
{
public class RestaurantHud : BaseUi
{
protected override void OnCreatedInitialize()
{
base.OnCreatedInitialize();
OpenPanel();
}
}
}

View File

@ -25,17 +25,17 @@ protected override void Awake()
_filledImage.fillAmount = 0f;
}
protected override void TryRegister()
protected override void OnOpenedEvents()
{
base.TryRegister();
base.OnOpenedEvents();
EventBus.Register<ShowInteractionUiEvent>(this);
EventBus.Register<HideInteractionUiEvent>(this);
}
protected override void TryUnregister()
protected override void OnClosedEvents()
{
base.TryUnregister();
base.OnClosedEvents();
EventBus.Unregister<ShowInteractionUiEvent>(this);
EventBus.Unregister<HideInteractionUiEvent>(this);

View File

@ -10,22 +10,15 @@ public abstract class BasePopupUi : BaseUi
protected override void Awake()
{
base.Awake();
// BasePopupUi의 기본값 적용
_enableBlockImage = true;
}
protected override void OnEnable()
{
base.OnEnable();
base.Awake();
}
protected override void Update()
{
base.Update();
// BasePopupUi의 Update 로직 구현
if (IsOpenPanel() == false || IsInitialized == false) return;
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
@ -39,20 +32,20 @@ protected override void Update()
}
}
protected override void TryRegister()
protected override void OnDestroy()
{
base.TryRegister();
UiManager.Instance.UiState.RegisterPopupUI(this);
}
protected override void TryUnregister()
{
base.TryUnregister();
base.OnDestroy();
UiManager.Instance?.UiState?.UnregisterPopupUI(this);
}
protected override void OnCreatedInitialize()
{
base.OnCreatedInitialize();
UiManager.Instance.UiState.RegisterPopupUI(this);
}
public virtual void Open(OpenPopupUiEvent evt)
{
OpenPanel();

View File

@ -35,9 +35,9 @@ protected override void Awake()
_viewModel = GetComponent<TViewModel>();
}
protected override void TryRegister()
protected override void OnOpenedEvents()
{
base.TryRegister();
base.OnOpenedEvents();
if (_viewModel && _bindingContext != null)
{
@ -80,9 +80,9 @@ protected override void TryRegister()
InputActionMaps = _uiActionsInputBinding.InputActionMaps;
}
protected override void TryUnregister()
protected override void OnClosedEvents()
{
base.TryUnregister();
base.OnClosedEvents();
if (_viewModel != null)
{

View File

@ -14,30 +14,34 @@ public enum ChecklistLocalizationKey
Checklist3,
}
public class ChecklistView : MonoBehaviour, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
public class ChecklistView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
{
[SerializeField] private Transform _parent;
private RestaurantManagementViewModel _viewModel;
private void OnDestroy()
{
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Initialize(RestaurantManagementViewModel viewModel)
{
_viewModel = viewModel;
ClearObject(_parent);
Utils.DestroyAllChildren(_parent);
_viewModel.CreateChecklist(_parent);
}
public void OnOpenedEvents()
{
UpdateView();
EventBus.Register<TodayMenuAddedEvent>(this);
EventBus.Register<TodayMenuRemovedEvent>(this);
}
public void OnClosedEvents()
{
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void UpdateView()
{
_viewModel.UpdateChecklistView();
@ -45,13 +49,5 @@ public void UpdateView()
public void Invoke(TodayMenuRemovedEvent evt) => UpdateView();
public void Invoke(TodayMenuAddedEvent evt) => UpdateView();
private void ClearObject(Transform parent)
{
foreach (Transform child in _parent)
{
Destroy(child.gameObject);
}
}
}
}

View File

@ -2,7 +2,7 @@
namespace DDD
{
public class InventoryView : MonoBehaviour, IEventHandler<InventoryChangedEvent>,
public class InventoryView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<InventoryChangedEvent>,
IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
{
private RestaurantManagementViewModel _viewModel;
@ -11,25 +11,36 @@ public class InventoryView : MonoBehaviour, IEventHandler<InventoryChangedEvent>
public GameObject GetInitialSelected() => _viewModel.GetInitialSelectedByInventory();
private void OnDestroy()
{
EventBus.Unregister<InventoryChangedEvent>(this);
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Initialize(RestaurantManagementViewModel viewModel)
{
_viewModel = viewModel;
}
ClearObject(_slotParent);
public void OnOpenedEvents()
{
_viewModel.CreateInventoryItemSlot(_slotParent);
UpdateCategoryView(InventoryCategoryType.Food);
_viewModel.OnCategoryChanged += UpdateCategoryView;
EventBus.Register<InventoryChangedEvent>(this);
EventBus.Register<TodayMenuAddedEvent>(this);
EventBus.Register<TodayMenuRemovedEvent>(this);
}
public void OnClosedEvents()
{
if (_viewModel)
{
_viewModel.OnCategoryChanged -= UpdateCategoryView;
}
EventBus.Unregister<InventoryChangedEvent>(this);
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void UpdateView()
{
_viewModel.UpdateCategoryView();
@ -37,14 +48,6 @@ public void UpdateView()
public void UpdateCategoryView(InventoryCategoryType category) => _viewModel.UpdateCategoryViewByCategory(category);
private void ClearObject(Transform root)
{
foreach (Transform child in root)
{
Destroy(child.gameObject);
}
}
public void Invoke(TodayMenuAddedEvent evt)
{
UpdateView();

View File

@ -1,5 +1,3 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.Localization.Components;
@ -7,7 +5,7 @@
namespace DDD
{
public class ItemDetailView : MonoBehaviour, IEventHandler<ItemSlotSelectedEvent>
public class ItemDetailView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<ItemSlotSelectedEvent>
{
[SerializeField] private Image _viewImage;
[SerializeField] private TextMeshProUGUI _nameLabel;
@ -20,151 +18,111 @@ public class ItemDetailView : MonoBehaviour, IEventHandler<ItemSlotSelectedEvent
[SerializeField] private RectTransform _tasteHashTagContent1;
[SerializeField] private HorizontalLayoutGroup _tasteHashTagContentLayoutGroup;
[SerializeField] private RectTransform _tasteHashTagContent2;
private RestaurantManagementData restaurantManagementDataSo;
private List<TasteHashTagSlotUi> _tasteHashTagSlotUis = new();
private ItemViewModel _currentItemViewModel;
private RestaurantManagementViewModel _viewModel;
private const string CookwareDetailPanel = "CookwareDetailPanel";
private const string IngredientDetailPanel = "IngredientDetailPanel";
private const string RecipeDetailPanel = "RecipeDetailPanel";
private void Start()
public void Initialize(RestaurantManagementViewModel viewModel)
{
_viewModel = viewModel;
_nameLabel.text = string.Empty;
_descriptionLabel.text = string.Empty;
_cookwareImage.sprite = null;
}
private void OnEnable()
public void OnOpenedEvents()
{
UpdateView();
_viewModel.OnCategoryChanged += UpdateCategory;
EventBus.Register<ItemSlotSelectedEvent>(this);
}
private void OnDisable()
public void OnClosedEvents()
{
if (_viewModel)
{
_viewModel.OnCategoryChanged -= UpdateCategory;
}
EventBus.Unregister<ItemSlotSelectedEvent>(this);
}
public void Initialize()
public void UpdateView()
{
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
}
public void Invoke(ItemSlotSelectedEvent evt)
{
Show(evt.Model);
}
public void Show(ItemViewModel model)
{
_currentItemViewModel = model;
if (_currentItemViewModel == null) return;
string viewItemKey = null;
if (_currentItemViewModel.ItemType == ItemType.Recipe)
UpdateCategory(_viewModel.CurrentCategory);
if (_viewModel.SelectedItem == null)
{
viewItemKey = _currentItemViewModel.GetRecipeResultKey;
_labelLocalizer.StringReference = null;
_descriptionLocalizer.StringReference = null;
_cookwareImage.sprite = null;
ClearHashTags();
return;
}
else
{
viewItemKey = _currentItemViewModel.Id;
}
_labelLocalizer.StringReference = LocalizationManager.Instance.GetLocalizedName(viewItemKey);
_descriptionLocalizer.StringReference = LocalizationManager.Instance.GetLocalizedDescription(viewItemKey);
_cookwareImage.sprite = _currentItemViewModel.GetCookwareIcon;
UpdateTasteHashTags(_currentItemViewModel);
_labelLocalizer.StringReference = _viewModel.GetItemName();
_descriptionLocalizer.StringReference = _viewModel.GetItemDescription();
_cookwareImage.sprite = _viewModel.GetCookwareSprite();
UpdateTasteHashTags();
}
private void UpdateTasteHashTags(ItemViewModel model)
private void UpdateTasteHashTags()
{
ClearHashTags();
var tastes = _viewModel.GetTastes();
if (tastes == null || tastes.Count == 0) return;
if (model == null) return;
_tasteHashTagSlotUis.Clear();
List<TasteData> tasteDatas = model.GetTasteDatas;
if (tasteDatas == null || tasteDatas.Count <= 0) return;
var backgroundMaterial = model.RecipeType switch
{
RecipeType.FoodRecipe => restaurantManagementDataSo.FoodTasteMaterial,
RecipeType.DrinkRecipe => restaurantManagementDataSo.DrinkTasteMaterial,
_ => throw new ArgumentOutOfRangeException()
};
var material = _viewModel.GetTasteMaterial();
float maxWidth = _tasteHashTagContent1.rect.width;
float currentLineWidth = 0f;
foreach (var tasteData in tasteDatas)
foreach (var taste in tastes)
{
var newTasteHashTag = Instantiate(restaurantManagementDataSo.TasteHashTagSlotUiPrefab, _tasteHashTagContent1, false);
newTasteHashTag.Initialize(backgroundMaterial, tasteData);
LayoutRebuilder.ForceRebuildLayoutImmediate(newTasteHashTag.RectTransform);
float slotWidth = newTasteHashTag.RectTransform.rect.width;
if (currentLineWidth + slotWidth > maxWidth)
var instance = _viewModel.CreateHashTag(_tasteHashTagContent1);
instance.Initialize(material, taste);
LayoutRebuilder.ForceRebuildLayoutImmediate(instance.RectTransform);
float w = instance.RectTransform.rect.width;
if (currentLineWidth + w > maxWidth)
{
newTasteHashTag.transform.SetParent(_tasteHashTagContent2, false);
currentLineWidth = slotWidth + _tasteHashTagContentLayoutGroup.spacing;
instance.transform.SetParent(_tasteHashTagContent2, false);
currentLineWidth = w + _tasteHashTagContentLayoutGroup.spacing;
}
else
{
currentLineWidth += slotWidth + _tasteHashTagContentLayoutGroup.spacing;
currentLineWidth += w + _tasteHashTagContentLayoutGroup.spacing;
}
_tasteHashTagSlotUis.Add(newTasteHashTag);
}
LayoutRebuilder.ForceRebuildLayoutImmediate(_tasteHashTagContent1);
LayoutRebuilder.ForceRebuildLayoutImmediate(_tasteHashTagContent2);
}
public void UpdateCategory(InventoryCategoryType category)
{
switch (category)
{
case InventoryCategoryType.Food:
case InventoryCategoryType.Drink:
_viewImage.sprite = DataManager.Instance.GetSprite(RecipeDetailPanel);
_tasteHashTagPanel.gameObject.SetActive(true);
_cookWarePanel.gameObject.SetActive(true);
break;
case InventoryCategoryType.Ingredient:
_viewImage.sprite = DataManager.Instance.GetSprite(IngredientDetailPanel);
_tasteHashTagPanel.gameObject.SetActive(true);
_cookWarePanel.gameObject.SetActive(false);
break;
case InventoryCategoryType.Cookware:
case InventoryCategoryType.Special:
_viewImage.sprite = DataManager.Instance.GetSprite(CookwareDetailPanel);
_tasteHashTagPanel.gameObject.SetActive(false);
_cookWarePanel.gameObject.SetActive(false);
break;
default:
throw new ArgumentOutOfRangeException(nameof(category), category, null);
}
_viewImage.sprite = _viewModel.GetDetailBackground(category);
bool showTaste = _viewModel.ShouldShowTaste(category);
bool showCookware = _viewModel.ShouldShowCookware(category);
_tasteHashTagPanel.gameObject.SetActive(showTaste);
_cookWarePanel.gameObject.SetActive(showCookware);
if (!_tasteHashTagPanel.gameObject.activeInHierarchy) return;
Canvas.ForceUpdateCanvases();
UpdateTasteHashTags(_currentItemViewModel);
}
private void ClearHashTags()
{
foreach (Transform content in _tasteHashTagContent1)
{
Destroy(content?.gameObject);
}
foreach (Transform content in _tasteHashTagContent2)
{
Destroy(content?.gameObject);
}
Utils.DestroyAllChildren(_tasteHashTagContent1);
Utils.DestroyAllChildren(_tasteHashTagContent2);
}
public void Invoke(ItemSlotSelectedEvent evt)
{
_viewModel.SetSelectedItem(evt.Model);
UpdateView();
}
}
}

View File

@ -1,3 +1,4 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
@ -22,6 +23,8 @@ public class RestaurantManagementUi : PopupUi<RestaurantUiActions, RestaurantMan
[Header("Hold Progress UI")]
[SerializeField] private Image _completeBatchFilledImage;
private List<IUiView<RestaurantManagementViewModel>> _subViews;
protected override void Update()
{
@ -33,23 +36,64 @@ protected override void Update()
}
}
protected override void TryRegister()
protected override void OnCreatedInitialize()
{
base.TryRegister();
SetupViewModelEvents();
}
public override void Open(OpenPopupUiEvent evt)
{
base.Open(evt);
base.OnCreatedInitialize();
InitializeViews();
SetupTabs();
InitializeTabGroups();
SetupCategoryTabs();
}
protected override void OnOpenedEvents()
{
base.OnOpenedEvents();
_sectionTabs.SelectFirstTab();
_menuCategoryTabs.SelectFirstTab();
if (_viewModel)
{
_viewModel.OnBatchCompleted += HandleBatchCompleted;
_viewModel.OnChecklistFailed += HandleChecklistFailed;
_viewModel.OnMenuSectionSelected += HandleMenuSectionSelected;
_viewModel.OnCookwareSectionSelected += HandleCookwareSectionSelected;
_viewModel.OnTabMoved += HandleTabMoved;
_viewModel.OnInteractRequested += HandleInteractRequested;
_viewModel.OnCloseRequested += HandleCloseRequested;
_viewModel.OnMenuCategorySelected += HandleMenuCategorySelected;
}
foreach (var view in _subViews)
{
view.OnOpenedEvents();
}
IsInitialized = true;
}
protected override void OnClosedEvents()
{
base.OnClosedEvents();
if (_viewModel)
{
_viewModel.OnBatchCompleted -= HandleBatchCompleted;
_viewModel.OnChecklistFailed -= HandleChecklistFailed;
_viewModel.OnMenuSectionSelected -= HandleMenuSectionSelected;
_viewModel.OnCookwareSectionSelected -= HandleCookwareSectionSelected;
_viewModel.OnTabMoved -= HandleTabMoved;
_viewModel.OnInteractRequested -= HandleInteractRequested;
_viewModel.OnCloseRequested -= HandleCloseRequested;
_viewModel.OnMenuCategorySelected -= HandleMenuCategorySelected;
}
foreach (var view in _subViews)
{
view.OnClosedEvents();
}
}
protected override GameObject GetInitialSelected()
{
if (IsInitialized == false) return null;
@ -81,48 +125,21 @@ protected override void SetupBindings()
BindingHelper.BindImageFilled(_bindingContext, _completeBatchFilledImage, nameof(RestaurantManagementViewModel.NormalizedHoldProgress));
}
protected override void HandleCustomPropertyChanged(string propertyName)
{
switch (propertyName)
{
case nameof(RestaurantManagementViewModel.CurrentSection):
UpdateSectionTabs();
break;
case nameof(RestaurantManagementViewModel.CurrentCategory):
UpdateCategoryTabs();
break;
}
}
private void SetupViewModelEvents()
{
if (!_viewModel) return;
_viewModel.OnBatchCompleted = HandleBatchCompleted;
_viewModel.OnChecklistFailed = HandleChecklistFailed;
_viewModel.OnMenuSectionSelected = HandleMenuSectionSelected;
_viewModel.OnCookwareSectionSelected = HandleCookwareSectionSelected;
_viewModel.OnCategoryChanged = HandleCategoryChanged;
_viewModel.OnTabMoved = HandleTabMoved;
_viewModel.OnInteractRequested = HandleInteractRequested;
_viewModel.OnCloseRequested = HandleCloseRequested;
_viewModel.OnMenuCategorySelected = HandleMenuCategorySelected;
}
private void InitializeViews()
{
_checklistView.Initialize(_viewModel);
_inventoryView.Initialize(_viewModel);
_itemDetailView.Initialize();
_todayMenuView.Initialize(_viewModel);
_todayRestaurantStateView.Initialize();
}
_subViews = new List<IUiView<RestaurantManagementViewModel>>
{
_checklistView,
_inventoryView,
_itemDetailView,
_todayMenuView,
_todayRestaurantStateView
};
private void SetupTabs()
{
SetupCategoryTabs();
InitializeTabGroups();
SelectInitialTabs();
foreach (var uiView in _subViews)
{
uiView.Initialize(_viewModel);
}
}
private void SetupCategoryTabs()
@ -138,12 +155,6 @@ private void InitializeTabGroups()
_cookwareCategoryTabs.Initialize(OnCategoryTabSelected);
}
private void SelectInitialTabs()
{
_sectionTabs.SelectFirstTab();
_menuCategoryTabs.SelectFirstTab();
}
private void UpdateSectionTabs()
{
if (_viewModel == null) return;
@ -164,6 +175,19 @@ private void UpdateCategoryTabs()
break;
}
}
protected override void HandleCustomPropertyChanged(string propertyName)
{
switch (propertyName)
{
case nameof(RestaurantManagementViewModel.CurrentSection):
UpdateSectionTabs();
break;
case nameof(RestaurantManagementViewModel.CurrentCategory):
UpdateCategoryTabs();
break;
}
}
// ViewModel 이벤트 핸들러들
private void HandleBatchCompleted()
@ -191,12 +215,6 @@ private void HandleCookwareSectionSelected()
_cookwareCategoryTabs.SelectFirstTab();
}
private void HandleCategoryChanged(InventoryCategoryType category)
{
_inventoryView.UpdateCategoryView(category);
_itemDetailView.UpdateCategory(category);
}
private void HandleTabMoved(int direction)
{
_sectionTabs.Move(direction);

View File

@ -1,21 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Localization;
namespace DDD
{
public class RestaurantManagementViewModel : SimpleViewModel, IEventHandler<TodayMenuRemovedEvent>
{
// View에서 구독할 이벤트들
public System.Action OnBatchCompleted;
public System.Action OnChecklistFailed;
public System.Action OnMenuSectionSelected;
public System.Action OnCookwareSectionSelected;
public System.Action<InventoryCategoryType> OnCategoryChanged;
public System.Action<int> OnTabMoved;
public System.Action OnInteractRequested;
public System.Action OnCloseRequested;
public System.Action<InventoryCategoryType> OnMenuCategorySelected;
public Action OnBatchCompleted;
public Action OnChecklistFailed;
public Action OnMenuSectionSelected;
public Action OnCookwareSectionSelected;
public Action<InventoryCategoryType> OnCategoryChanged;
public Action<int> OnTabMoved;
public Action OnInteractRequested;
public Action OnCloseRequested;
public Action<InventoryCategoryType> OnMenuCategorySelected;
public Action<ItemViewModel> OnSelectedItemChanged;
private RestaurantManagementData GetRestaurantManagementData() => RestaurantData.Instance.ManagementData;
private RestaurantManagementState GetRestaurantManagementState() => RestaurantState.Instance.ManagementState;
@ -169,6 +172,8 @@ public void SetSection(SectionButtonType section)
public void SetCategory(InventoryCategoryType category)
{
if (CurrentCategory == category) return;
CurrentCategory = category;
OnCategoryChanged?.Invoke(category);
}
@ -227,6 +232,7 @@ public void UpdateChecklistView()
public void CreateInventoryItemSlot(Transform parent)
{
Utils.DestroyAllChildren(parent);
var models = ItemViewModelFactory.CreateRestaurantManagementInventoryItem();
_slotLookup.Clear();
@ -274,8 +280,12 @@ private IEnumerable<ItemSlotUi> SortSlots(IEnumerable<ItemSlotUi> slots)
_ => slots
};
}
public void UpdateCategoryView() => UpdateCategoryViewByCategory(_currenInventoryCategoryType);
public void UpdateCategoryView()
{
_currenInventoryCategoryType = _currenInventoryCategoryType == InventoryCategoryType.None ? InventoryCategoryType.Food : _currenInventoryCategoryType;
UpdateCategoryViewByCategory(_currenInventoryCategoryType);
}
public void UpdateCategoryViewByCategory(InventoryCategoryType category)
{
@ -298,9 +308,12 @@ public void UpdateCategoryViewByCategory(InventoryCategoryType category)
slot.SetActive(shouldShow);
if (shouldShow && model.HasItem)
if (shouldShow)
{
slot.transform.SetSiblingIndex(siblingIndex++);
if (model.HasItem)
{
slot.transform.SetSiblingIndex(siblingIndex++);
}
if (firstValidSlot == null)
{
@ -351,6 +364,80 @@ public void OnInventoryChanged()
}
#endregion
#region ItemDetailView
private const string CookwareDetailPanel = "CookwareDetailPanel";
private const string IngredientDetailPanel = "IngredientDetailPanel";
private const string RecipeDetailPanel = "RecipeDetailPanel";
public ItemViewModel SelectedItem { get; private set; }
public void SetSelectedItem(ItemViewModel item)
{
if (SelectedItem == item) return;
SelectedItem = item;
}
public TasteHashTagSlotUi CreateHashTag(RectTransform parent)
{
return Instantiate(GetRestaurantManagementData().TasteHashTagSlotUiPrefab, parent, false);
}
private string GetViewItemKey()
{
return SelectedItem.ItemType == ItemType.Recipe ? SelectedItem.GetRecipeResultKey : SelectedItem.Id;
}
public LocalizedString GetItemName()
{
var key = GetViewItemKey();
return LocalizationManager.Instance.GetLocalizedName(key);
}
public LocalizedString GetItemDescription()
{
var key = GetViewItemKey();
return LocalizationManager.Instance.GetLocalizedDescription(key);
}
public Sprite GetDetailBackground(InventoryCategoryType category)
{
return category switch
{
InventoryCategoryType.Food or InventoryCategoryType.Drink
=> DataManager.Instance.GetSprite(RecipeDetailPanel),
InventoryCategoryType.Ingredient
=> DataManager.Instance.GetSprite(IngredientDetailPanel),
InventoryCategoryType.Cookware or InventoryCategoryType.Special
=> DataManager.Instance.GetSprite(CookwareDetailPanel),
_ => null
};
}
public Sprite GetCookwareSprite() => SelectedItem.GetCookwareIcon;
public IReadOnlyList<TasteData> GetTastes() => SelectedItem?.GetTasteDatas;
public Material GetTasteMaterial()
{
if (SelectedItem == null) return null;
var restaurantManagementData = RestaurantData.Instance.ManagementData;
return SelectedItem.RecipeType switch
{
RecipeType.FoodRecipe => restaurantManagementData.FoodTasteMaterial,
RecipeType.DrinkRecipe => restaurantManagementData.DrinkTasteMaterial,
_ => null
};
}
public bool ShouldShowTaste(InventoryCategoryType category)
=> category is InventoryCategoryType.Food or InventoryCategoryType.Drink or InventoryCategoryType.Ingredient;
public bool ShouldShowCookware(InventoryCategoryType category)
=> category is InventoryCategoryType.Food or InventoryCategoryType.Drink;
#endregion
#region TodayMenuView
@ -359,6 +446,7 @@ public void OnInventoryChanged()
public void CreateFoodSlot(Transform parent)
{
Utils.DestroyAllChildren(parent);
var foodMaxCount = GetRestaurantManagementData().MaxFoodCount;
_foodSlots = new List<ItemSlotUi>(foodMaxCount);
for (int i = 0; i < foodMaxCount; i++)
@ -375,6 +463,7 @@ public void CreateFoodSlot(Transform parent)
public void CreateDrinkSlot(Transform parent)
{
Utils.DestroyAllChildren(parent);
var drinkMaxCount = GetRestaurantManagementData().MaxDrinkCount;
_drinkSlots = new List<ItemSlotUi>(drinkMaxCount);
for (int i = 0; i < drinkMaxCount; i++)
@ -427,5 +516,85 @@ public void UpdateTodayMenuItems()
}
#endregion
#region TodayRestaurantStateView
private List<ItemSlotUi> _workerSlots;
private List<ItemSlotUi> _cookwareSlots;
public void CreateTodayWorkerSlot(Transform parent)
{
Utils.DestroyAllChildren(parent);
int maxWorkerCount = GetRestaurantManagementData().MaxWorkerCount;
_workerSlots = new List<ItemSlotUi>(maxWorkerCount);
for (int i = 0; i < maxWorkerCount; i++)
{
var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent);
var slot = instance.GetComponent<ItemSlotUi>();
slot.Initialize(null, new TodayWorkerSlotUiStrategy());
var itemSlotInteractor = instance.GetComponent<ItemSlotInteractor>();
itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayCookwareInteractorStrategy());
_workerSlots.Add(slot);
}
}
public void CreateTodayCookwareSlot(Transform parent)
{
Utils.DestroyAllChildren(parent);
int maxCookwareCount = GetRestaurantManagementData().MaxCookwareCount;
_cookwareSlots = new List<ItemSlotUi>(maxCookwareCount);
for (int i = 0; i < maxCookwareCount; i++)
{
var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent);
var slot = instance.GetComponent<ItemSlotUi>();
slot.Initialize(null, new TodayWorkerSlotUiStrategy());
var itemSlotInteractor = instance.GetComponent<ItemSlotInteractor>();
itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayCookwareInteractorStrategy());
_cookwareSlots.Add(slot);
}
}
public void UpdateTodayRestaurantStateView()
{
int workerIndex = 0;
foreach (var workerKey in GetRestaurantManagementState().TodayWorkerIds)
{
if (workerIndex >= _workerSlots.Count) break;
var model = ItemViewModelFactory.CreateByItemId(workerKey);
var newWorkerSlot = _workerSlots[workerIndex];
newWorkerSlot.Initialize(model, new TodayWorkerSlotUiStrategy());
newWorkerSlot.Model.SetCount(1);
workerIndex++;
}
for (int i = workerIndex; i < _workerSlots.Count; i++)
{
_workerSlots[i].Initialize(null, new TodayWorkerSlotUiStrategy());
}
int cookwareIndex = 0;
foreach (var cookwareKey in GetRestaurantManagementState().CookwareToRecipeIds.Keys)
{
if (cookwareIndex >= _cookwareSlots.Count) break;
var model = ItemViewModelFactory.CreateByItemId(cookwareKey);
var newCookwareSlot = _cookwareSlots[cookwareIndex];
newCookwareSlot.Initialize(model, new TodayCookwareSlotUiStrategy());
newCookwareSlot.Model.SetCount(1);
cookwareIndex++;
}
for (int i = cookwareIndex; i < _cookwareSlots.Count; i++)
{
_cookwareSlots[i].Initialize(null, new TodayCookwareSlotUiStrategy());
}
}
#endregion
}
}

View File

@ -2,26 +2,20 @@
namespace DDD
{
public class TodayMenuView : MonoBehaviour, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
public class TodayMenuView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
{
[SerializeField] private Transform _todayFoodContent;
[SerializeField] private Transform _todayDrinkContent;
private RestaurantManagementViewModel _viewModel;
private void OnDestroy()
{
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Initialize(RestaurantManagementViewModel viewModel)
{
_viewModel = viewModel;
}
ClearObject(_todayFoodContent);
ClearObject(_todayDrinkContent);
public void OnOpenedEvents()
{
_viewModel.CreateFoodSlot(_todayFoodContent);
_viewModel.CreateDrinkSlot(_todayDrinkContent);
@ -31,12 +25,10 @@ public void Initialize(RestaurantManagementViewModel viewModel)
EventBus.Register<TodayMenuRemovedEvent>(this);
}
private void ClearObject(Transform root)
public void OnClosedEvents()
{
foreach (Transform child in root)
{
Destroy(child.gameObject);
}
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Invoke(TodayMenuAddedEvent evt)

View File

@ -3,69 +3,38 @@
namespace DDD
{
public class TodayRestaurantStateView : MonoBehaviour, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
public class TodayRestaurantStateView : MonoBehaviour, IUiView<RestaurantManagementViewModel>, IEventHandler<TodayMenuAddedEvent>, IEventHandler<TodayMenuRemovedEvent>
{
[SerializeField] private Transform _todayWorkerContent;
[SerializeField] private Transform _todayCookwareContent;
private List<ItemSlotUi> _workerSlots;
private List<ItemSlotUi> _cookwareSlots;
private RestaurantManagementState restaurantManagementStateSo;
private RestaurantManagementData restaurantManagementDataSo;
private void OnDestroy()
{
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Initialize()
private RestaurantManagementViewModel _viewModel;
public void Initialize(RestaurantManagementViewModel viewModel)
{
restaurantManagementStateSo = RestaurantState.Instance.ManagementState;
restaurantManagementDataSo = RestaurantData.Instance.ManagementData;
_viewModel = viewModel;
foreach (Transform child in _todayWorkerContent)
{
Destroy(child.gameObject);
}
int maxCookwareCount = restaurantManagementDataSo!.MaxCookwareCount;
_workerSlots = new List<ItemSlotUi>(maxCookwareCount);
for (int i = 0; i < restaurantManagementDataSo.MaxCookwareCount; i++)
{
var go = Instantiate(restaurantManagementDataSo.ItemSlotUiPrefab, _todayWorkerContent);
var slot = go.GetComponent<ItemSlotUi>();
slot.Initialize(null, new TodayWorkerSlotUiStrategy());
var itemSlotInteractor = go.GetComponent<ItemSlotInteractor>();
itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayCookwareInteractorStrategy());
_workerSlots.Add(slot);
}
foreach (Transform child in _todayCookwareContent)
{
Destroy(child.gameObject);
}
_cookwareSlots = new List<ItemSlotUi>(maxCookwareCount);
for (int i = 0; i < restaurantManagementDataSo.MaxCookwareCount; i++)
{
var go = Instantiate(restaurantManagementDataSo.ItemSlotUiPrefab, _todayCookwareContent);
var slot = go.GetComponent<ItemSlotUi>();
slot.Initialize(null, new TodayCookwareSlotUiStrategy());
var itemSlotInteractor = go.GetComponent<ItemSlotInteractor>();
itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayCookwareInteractorStrategy());
_cookwareSlots.Add(slot);
}
_viewModel.CreateTodayWorkerSlot(_todayWorkerContent);
_viewModel.CreateTodayCookwareSlot(_todayCookwareContent);
}
public void OnOpenedEvents()
{
UpdateView();
EventBus.Register<TodayMenuAddedEvent>(this);
EventBus.Register<TodayMenuRemovedEvent>(this);
}
public void OnClosedEvents()
{
EventBus.Unregister<TodayMenuAddedEvent>(this);
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
public void Invoke(TodayMenuAddedEvent evt)
{
UpdateView();
@ -78,39 +47,7 @@ public void Invoke(TodayMenuRemovedEvent evt)
private void UpdateView()
{
int workerIndex = 0;
foreach (var workerKey in restaurantManagementStateSo.TodayWorkerIds)
{
if (workerIndex >= _workerSlots.Count) break;
var model = ItemViewModelFactory.CreateByItemId(workerKey);
var newWorkerSlot = _workerSlots[workerIndex];
newWorkerSlot.Initialize(model, new TodayWorkerSlotUiStrategy());
newWorkerSlot.Model.SetCount(1);
workerIndex++;
}
for (int i = workerIndex; i < _workerSlots.Count; i++)
{
_workerSlots[i].Initialize(null, new TodayWorkerSlotUiStrategy());
}
int cookwareIndex = 0;
foreach (var cookwareKey in restaurantManagementStateSo.CookwareToRecipeIds.Keys)
{
if (cookwareIndex >= _cookwareSlots.Count) break;
var model = ItemViewModelFactory.CreateByItemId(cookwareKey);
var newCookwareSlot = _cookwareSlots[cookwareIndex];
newCookwareSlot.Initialize(model, new TodayCookwareSlotUiStrategy());
newCookwareSlot.Model.SetCount(1);
cookwareIndex++;
}
for (int i = cookwareIndex; i < _cookwareSlots.Count; i++)
{
_cookwareSlots[i].Initialize(null, new TodayCookwareSlotUiStrategy());
}
_viewModel.UpdateTodayRestaurantStateView();
}
}
}

View File

@ -1,545 +0,0 @@
# 새로운 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

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

View File

@ -0,0 +1,9 @@
namespace DDD
{
public interface IUiView<T> where T : SimpleViewModel
{
public void Initialize(T viewModel);
public void OnOpenedEvents();
public void OnClosedEvents();
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ae6782400fed433399774db2670ab219
timeCreated: 1756027826

View File

@ -12,6 +12,7 @@ public class RestaurantManagementData : ScriptableObject
public int MaxFoodCount = 8;
public int MaxDrinkCount = 6;
public int MaxCookwareCount = 6;
public int MaxWorkerCount = 6;
[Title("체크리스트 조건")]
public int ChecklistCount = 3;

View File

@ -2,6 +2,7 @@
using System.Collections;
using System.IO;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
@ -58,6 +59,14 @@ public static void MakeFolderFromFilePath(string filePath)
}
}
}
public static void DestroyAllChildren(Transform parent)
{
foreach (Transform child in parent)
{
Object.Destroy(child.gameObject);
}
}
public static IEnumerator CoolDownCoroutine(float waitTime, Action onCooldownComplete = null)
{