using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.Localization; namespace DDD.Restaurant { public class RestaurantManagementViewModel : SimpleViewModel, IEventHandler { // View에서 구독할 이벤트들 public Action OnBatchCompleted; public Action OnChecklistFailed; public Action OnMenuSectionSelected; public Action OnCookwareSectionSelected; public Action OnCategoryChanged; public Action OnTabMoved; public Action OnInteractRequested; public Action OnCloseRequested; public Action OnMenuCategorySelected; private RestaurantManagementData GetRestaurantManagementData() => RestaurantData.Instance.ManagementData; private RestaurantManagementState GetRestaurantManagementState() => RestaurantState.Instance.ManagementState; public override void Initialize() { base.Initialize(); RegisterEvents(); ResetHoldState(); } public override void Cleanup() { base.Cleanup(); UnregisterEvents(); } private void RegisterEvents() { EventBus.Register(this); } private void UnregisterEvents() { EventBus.Unregister(this); } public void InteractWithSelected() { OnInteractRequested?.Invoke(); } public void CloseUi() { OnCloseRequested?.Invoke(); } public void HandleEvent(TodayMenuRemovedEvent evt) { SetCategory(evt.InventoryCategoryType); OnMenuCategorySelected?.Invoke(evt.InventoryCategoryType); } #region RestaurantManagementView private bool _isHolding; private float _holdProgress; public float HoldProgress { get => _holdProgress; private set { if (SetField(ref _holdProgress, value)) { OnPropertyChanged(nameof(NormalizedHoldProgress)); } } } public float NormalizedHoldProgress => GetRestaurantManagementData().HoldCompleteTime <= 0f ? 0f : Mathf.Clamp01(_holdProgress / GetRestaurantManagementData().HoldCompleteTime); public bool CanCompleteBatch => GetRestaurantManagementState().GetChecklistStates().All(state => state); public void UpdateHoldProgress() { if (_isHolding == false) return; if (GetRestaurantManagementData().HoldCompleteTime <= 0f) { ProcessCompleteBatch(); return; } var deltaTime = Time.deltaTime; HoldProgress += deltaTime; if (HoldProgress >= GetRestaurantManagementData().HoldCompleteTime) { ProcessCompleteBatch(); } } public void StartHold() { _isHolding = true; HoldProgress = 0f; } public void CancelHold() { ResetHoldState(); } private void ResetHoldState() { _isHolding = false; HoldProgress = 0f; } private void ProcessCompleteBatch() { ResetHoldState(); if (CanCompleteBatch) { // 배치 완료 - UI 닫기 이벤트 발생 OnBatchCompleted?.Invoke(); } else { // 체크리스트 미완료 - 실패 팝업 표시 이벤트 발생 OnChecklistFailed?.Invoke(); } } #endregion #region TabGroup // 탭 상태 관리 private SectionButtonType _currentSection = SectionButtonType.Menu; public SectionButtonType CurrentSection { get => _currentSection; set => SetField(ref _currentSection, value); } private InventoryCategoryType _currentCategory = InventoryCategoryType.Food; public InventoryCategoryType CurrentCategory { get => _currentCategory; set => SetField(ref _currentCategory, value); } public void SetSection(SectionButtonType section) { CurrentSection = section; // 섹션 변경 시 해당 섹션의 첫 번째 카테고리로 설정 switch (section) { case SectionButtonType.Menu: OnMenuSectionSelected?.Invoke(); break; case SectionButtonType.Cookware: OnCookwareSectionSelected?.Invoke(); break; } } public void SetCategory(InventoryCategoryType category) { if (CurrentCategory == category) return; BeginUpdate(); CurrentCategory = category; RecomputeItemDetail(); EndUpdate(); OnCategoryChanged?.Invoke(category); } public void MoveTab(int direction) { OnTabMoved?.Invoke(direction); } #endregion #region ChecklistView private List _checklistDatas; private Dictionary _checklistLocalizationKeys = new() { {ChecklistLocalizationKey.Checklist1, "checklist_1"}, {ChecklistLocalizationKey.Checklist2, "checklist_2"}, {ChecklistLocalizationKey.Checklist3, "checklist_3"}, }; public void CreateChecklist(Transform parent) { var checklistCount = GetRestaurantManagementData().ChecklistCount; _checklistDatas = new List(checklistCount); for (int i = 0; i < checklistCount; i++) { var instance = Instantiate(GetRestaurantManagementData().ChecklistDataPrefab, parent); instance.Initialize(); _checklistDatas.Add(instance); } } public void UpdateChecklistView() { bool[] states = GetRestaurantManagementState().GetChecklistStates(); int loopCount = Mathf.Min(_checklistDatas.Count, states.Length); for (int i = 0; i < loopCount; i++) { _checklistDatas[i].UpdateData(_checklistLocalizationKeys[(ChecklistLocalizationKey)i], states[i]); } } #endregion #region InventoryView private InventoryCategoryType _currenInventoryCategoryType = InventoryCategoryType.Food; private readonly Dictionary _slotLookup = new(); private InventorySortType _currentSortType = InventorySortType.None; private GameObject _firstSlot; private const string ItemSlotUiName = "ItemSlotUi_"; public GameObject GetInitialSelectedByInventory() => _firstSlot; public void CreateInventoryItemSlot(Transform parent) { Utils.DestroyAllChildren(parent); var models = ItemViewModelFactory.CreateRestaurantManagementInventoryItem(); _slotLookup.Clear(); foreach (var model in models) { var itemSlotUi = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent); var slot = itemSlotUi.GetComponent(); slot.Initialize(model, new InventorySlotUiStrategy()); itemSlotUi.name = ItemSlotUiName + model.Id; var interactor = itemSlotUi.GetComponent(); if (model.ItemType == ItemType.Recipe) { interactor.Initialize(TodayMenuEventType.Add, new TodayMenuInteractorStrategy()); } else { if (DataManager.Instance.GetDataSo().TryGetDataById(model.Id, out var cookwareData)) { interactor.Initialize(TodayMenuEventType.Add, new TodayCookwareInteractorStrategy()); } } _slotLookup[model.Id] = slot; } UpdateCategoryView(); } public void SetSortType(InventorySortType sortType) { _currentSortType = sortType; UpdateCategoryView(); } private IEnumerable SortSlots(IEnumerable slots) { return _currentSortType switch { InventorySortType.NameAscending => slots.OrderByDescending(slot => slot.Model.HasItem).ThenBy(slot => slot.Model.DisplayName), InventorySortType.NameDescending => slots.OrderByDescending(slot => slot.Model.HasItem).ThenByDescending(slot => slot.Model.DisplayName), InventorySortType.QuantityAscending => slots.OrderByDescending(slot => slot.Model.HasItem).ThenBy(slot => slot.Model.Count), InventorySortType.QuantityDescending => slots.OrderByDescending(slot => slot.Model.HasItem).ThenByDescending(slot => slot.Model.Count), InventorySortType.None => slots.OrderBy(slot => slot.Model.Id), _ => slots }; } public void UpdateCategoryView() { _currenInventoryCategoryType = _currenInventoryCategoryType == InventoryCategoryType.None ? InventoryCategoryType.Food : _currenInventoryCategoryType; UpdateCategoryViewByCategory(_currenInventoryCategoryType); } public void UpdateCategoryViewByCategory(InventoryCategoryType category) { _currenInventoryCategoryType = category; GameObject firstValidSlot = null; var filteredSlots = _slotLookup.Values; var sortedSlots = SortSlots(filteredSlots); int siblingIndex = 0; foreach (var slot in sortedSlots) { var model = slot.Model; string id = model.Id; bool isRegisteredTodayMenu = model.ItemType == ItemType.Recipe && GetRestaurantManagementState().IsContainTodayMenu(id); bool matchCategory = MatchesCategory(model, _currenInventoryCategoryType); bool shouldShow = !isRegisteredTodayMenu && matchCategory; slot.SetActive(shouldShow); if (shouldShow) { if (model.HasItem) { slot.transform.SetSiblingIndex(siblingIndex++); } if (firstValidSlot == null) { firstValidSlot = slot.gameObject; } } } _firstSlot = firstValidSlot; } private bool MatchesCategory(ItemModel model, InventoryCategoryType category) { switch (category) { case InventoryCategoryType.Food: if (model.ItemType != ItemType.Recipe) return false; return DataManager.Instance.GetDataSo() .TryGetDataById(model.Id, out var foodRecipe) && foodRecipe.RecipeType == RecipeType.FoodRecipe; case InventoryCategoryType.Drink: if (model.ItemType != ItemType.Recipe) return false; return DataManager.Instance.GetDataSo() .TryGetDataById(model.Id, out var drinkRecipe) && drinkRecipe.RecipeType == RecipeType.DrinkRecipe; case InventoryCategoryType.Ingredient: return model.ItemType == ItemType.Ingredient; case InventoryCategoryType.Cookware: return DataManager.Instance.GetDataSo() .TryGetDataById(model.Id, out var cookwareData); case InventoryCategoryType.Special: return false; default: return false; } } public void OnInventoryChanged() { foreach (var slot in _slotLookup.Values) { if (slot.Strategy is InventorySlotUiStrategy inventorySlotUiStrategy) { inventorySlotUiStrategy.OnInventoryChanged(slot); } } } #endregion #region ItemDetailView private ItemDetailSnapshot _itemDetail = ItemDetailSnapshot.Empty; public ItemDetailSnapshot ItemDetail { get => _itemDetail; private set => SetField(ref _itemDetail, value); } public ItemModel SelectedItem { get; private set; } private const string CookwareDetailPanel = "CookwareDetailPanel"; private const string IngredientDetailPanel = "IngredientDetailPanel"; private const string RecipeDetailPanel = "RecipeDetailPanel"; public void SetSelectedItem(ItemModel item) { if (SelectedItem == item) return; BeginUpdate(); SelectedItem = item; RecomputeItemDetail(); EndUpdate(); } public TasteHashTagSlotUi CreateHashTag(RectTransform parent) { return Instantiate(GetRestaurantManagementData().TasteHashTagSlotUiPrefab, parent, false); } private void RecomputeItemDetail() { var background = CurrentCategory 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 }; bool showTaste = CurrentCategory is InventoryCategoryType.Food or InventoryCategoryType.Drink or InventoryCategoryType.Ingredient; bool showCookware = CurrentCategory is InventoryCategoryType.Food or InventoryCategoryType.Drink; var cookwareSprite = SelectedItem.GetCookwareIcon; string key = SelectedItem.ItemType == ItemType.Recipe ? SelectedItem.GetRecipeResultKey : SelectedItem.Id; var nameLocalizedString = LocalizationManager.Instance.GetLocalizedName(key); var descriptionLocalizedString = LocalizationManager.Instance.GetLocalizedDescription(key); var tastes = SelectedItem.GetTasteDatas; var tasteMat = SelectedItem.RecipeType switch { RecipeType.FoodRecipe => RestaurantData.Instance.ManagementData.FoodTasteMaterial, RecipeType.DrinkRecipe => RestaurantData.Instance.ManagementData.DrinkTasteMaterial, _ => null }; ItemDetail = new ItemDetailSnapshot( backgroundSprite: background, showTastePanel: showTaste, showCookwarePanel: showCookware, cookwareSprite: cookwareSprite, name: nameLocalizedString, description: descriptionLocalizedString, tastes: tastes, tasteMaterial: tasteMat); } public IReadOnlyList 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 }; } #endregion #region TodayMenuView private List _foodSlots; private List _drinkSlots; public void CreateFoodSlot(Transform parent) { Utils.DestroyAllChildren(parent); var foodMaxCount = GetRestaurantManagementData().MaxFoodCount; _foodSlots = new List(foodMaxCount); for (int i = 0; i < foodMaxCount; i++) { var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent); var slot = instance.GetComponent(); slot.Initialize(null, new TodayMenuSlotUiStrategy(RecipeType.FoodRecipe)); var itemSlotInteractor = instance.GetComponent(); itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayMenuInteractorStrategy()); _foodSlots.Add(slot); } } public void CreateDrinkSlot(Transform parent) { Utils.DestroyAllChildren(parent); var drinkMaxCount = GetRestaurantManagementData().MaxDrinkCount; _drinkSlots = new List(drinkMaxCount); for (int i = 0; i < drinkMaxCount; i++) { var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent); var slot = instance.GetComponent(); slot.Initialize(null, new TodayMenuSlotUiStrategy(RecipeType.FoodRecipe)); var itemSlotInteractor = instance.GetComponent(); itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayMenuInteractorStrategy()); _drinkSlots.Add(slot); } } public void UpdateTodayMenuItems() { int foodIndex = 0; foreach (var foodRecipeIdCountPair in GetRestaurantManagementState().TodayFoodRecipeIds) { if (foodIndex >= _foodSlots.Count) break; var model = ItemViewModelFactory.CreateByItemId(foodRecipeIdCountPair.Key); var foodSlot = _foodSlots[foodIndex]; foodSlot.Initialize(model, new TodayMenuSlotUiStrategy(RecipeType.FoodRecipe)); foodSlot.Model.SetCount(foodRecipeIdCountPair.Value); foodIndex++; } for (int i = foodIndex; i < _foodSlots.Count; i++) { _foodSlots[i].Initialize(null, new TodayMenuSlotUiStrategy(RecipeType.FoodRecipe)); } int drinkIndex = 0; foreach (var drinkRecipeIdCountPair in GetRestaurantManagementState().TodayDrinkRecipeIds) { if (drinkIndex >= _drinkSlots.Count) break; var model = ItemViewModelFactory.CreateByItemId(drinkRecipeIdCountPair.Key); var drinkSlot = _drinkSlots[drinkIndex]; drinkSlot.Initialize(model, new TodayMenuSlotUiStrategy(RecipeType.DrinkRecipe)); drinkSlot.Model.SetCount(drinkRecipeIdCountPair.Value); drinkIndex++; } for (int i = drinkIndex; i < _drinkSlots.Count; i++) { _drinkSlots[i].Initialize(null, new TodayMenuSlotUiStrategy(RecipeType.DrinkRecipe)); } } #endregion #region TodayRestaurantStateView private List _workerSlots; private List _cookwareSlots; public void CreateTodayWorkerSlot(Transform parent) { Utils.DestroyAllChildren(parent); int maxWorkerCount = GetRestaurantManagementData().MaxWorkerCount; _workerSlots = new List(maxWorkerCount); for (int i = 0; i < maxWorkerCount; i++) { var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent); var slot = instance.GetComponent(); slot.Initialize(null, new TodayWorkerSlotUiStrategy()); var itemSlotInteractor = instance.GetComponent(); itemSlotInteractor.Initialize(TodayMenuEventType.Remove, new TodayCookwareInteractorStrategy()); _workerSlots.Add(slot); } } public void CreateTodayCookwareSlot(Transform parent) { Utils.DestroyAllChildren(parent); int maxCookwareCount = GetRestaurantManagementData().MaxCookwareCount; _cookwareSlots = new List(maxCookwareCount); for (int i = 0; i < maxCookwareCount; i++) { var instance = Instantiate(GetRestaurantManagementData().ItemSlotUiPrefab, parent); var slot = instance.GetComponent(); slot.Initialize(null, new TodayWorkerSlotUiStrategy()); var itemSlotInteractor = instance.GetComponent(); 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 } }