테스트 내용 저장

This commit is contained in:
NTG 2025-08-20 17:04:51 +09:00
parent 832c369073
commit 0cce2efe62
7 changed files with 662 additions and 35 deletions

View File

@ -6311,7 +6311,7 @@ PrefabInstance:
m_AddedComponents:
- targetCorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
insertIndex: -1
addedObject: {fileID: 2438716745211137680}
addedObject: {fileID: 7197937761328488698}
m_SourcePrefab: {fileID: 100100000, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
--- !u!224 &4720669467062659157 stripped
RectTransform:
@ -6323,7 +6323,7 @@ GameObject:
m_CorrespondingSourceObject: {fileID: 6952779389930089995, guid: 4f2bf029cb06b084ba41defc8fc76731, type: 3}
m_PrefabInstance: {fileID: 4463400116329503023}
m_PrefabAsset: {fileID: 0}
--- !u!114 &2438716745211137680
--- !u!114 &7197937761328488698
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
@ -6332,10 +6332,10 @@ MonoBehaviour:
m_GameObject: {fileID: 6740783381500491556}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 46c8396c996c804449b383960b44e812, type: 3}
m_Script: {fileID: 11500000, guid: 8a2e0954aa144633aad86e53dc80a46a, type: 3}
m_Name:
m_EditorClassIdentifier:
_enableBlockImage: 1
_enableBlockImage: 0
_uiActionsInputBinding: {fileID: 11400000, guid: 8073fcaf56fc7c34e996d0d47044f146, type: 2}
_checklistView: {fileID: 7075966153492927588}
_inventoryView: {fileID: 3570087040626823091}
@ -6346,7 +6346,6 @@ MonoBehaviour:
_menuCategoryTabs: {fileID: 6805049896193344908}
_cookwareCategoryTabs: {fileID: 6628923975427483430}
_completeBatchFilledImage: {fileID: 2965326806322860544}
_holdCompleteTime: 0.5
--- !u!1001 &4530765275021007961
PrefabInstance:
m_ObjectHideFlags: 0

View File

@ -0,0 +1,275 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.InputSystem;
using DDD.MVVM;
namespace DDD
{
/// <summary>
/// MVVM 패턴을 적용한 새로운 레스토랑 관리 UI
/// 기존 RestaurantManagementUi의 기능을 ViewModel과 분리하여 구현
/// </summary>
public class NewRestaurantManagementUi : IntegratedBasePopupUi<RestaurantUiActions, RestaurantManagementViewModel>
{
[Header("Sub Views")]
[SerializeField] private ChecklistView _checklistView;
[SerializeField] private InventoryView _inventoryView;
[SerializeField] private ItemDetailView _itemDetailView;
[SerializeField] private TodayMenuView _todayMenuView;
[SerializeField] private TodayRestaurantStateView _todayRestaurantStateView;
[Header("Tab Groups")]
[SerializeField] private TabGroupUi _sectionTabs;
[SerializeField] private TabGroupUi _menuCategoryTabs;
[SerializeField] private TabGroupUi _cookwareCategoryTabs;
[Header("Hold Progress UI")]
[SerializeField, BindTo(nameof(RestaurantManagementViewModel.NormalizedHoldProgress))]
private Image _completeBatchFilledImage;
protected override void Awake()
{
base.Awake();
SetupViewModelEvents();
}
protected override void Update()
{
base.Update();
if (_viewModel != null && _viewModel.IsHolding)
{
_viewModel.UpdateHoldProgress();
}
}
public override void Open(OpenPopupUiEvent evt)
{
base.Open(evt);
InitializeViews();
SetupTabs();
}
protected override GameObject GetInitialSelected()
{
// ViewModel의 현재 상태에 따라 초기 선택 UI 결정
var inventoryInitialSelected = _inventoryView.GetInitialSelected();
if (inventoryInitialSelected) return inventoryInitialSelected;
var menuCategoryButton = _menuCategoryTabs.GetFirstInteractableButton();
if (menuCategoryButton != null && menuCategoryButton.activeInHierarchy)
return menuCategoryButton;
var cookwareCategoryButton = _cookwareCategoryTabs.GetFirstInteractableButton();
if (cookwareCategoryButton != null && cookwareCategoryButton.activeInHierarchy)
return cookwareCategoryButton;
return null;
}
protected override void SetupBindings()
{
// Attribute 기반 자동 바인딩이 처리됨
// 추가적인 수동 바인딩이 필요한 경우 여기에 구현
}
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.Initalize();
_inventoryView.Initialize();
_itemDetailView.Initialize();
_todayMenuView.Initialize();
_todayRestaurantStateView.Initialize();
}
private void SetupTabs()
{
SetupCategoryTabs();
InitializeTabGroups();
SelectInitialTabs();
}
private void SetupCategoryTabs()
{
_menuCategoryTabs.UseDefaultAllowedValues();
_cookwareCategoryTabs.UseDefaultAllowedValues();
}
private void InitializeTabGroups()
{
_sectionTabs.Initialize(OnSectionTabSelected);
_menuCategoryTabs.Initialize(OnCategoryTabSelected);
_cookwareCategoryTabs.Initialize(OnCategoryTabSelected);
}
private void SelectInitialTabs()
{
_sectionTabs.SelectFirstTab();
_menuCategoryTabs.SelectFirstTab();
}
private void UpdateSectionTabs()
{
if (_viewModel == null) return;
_sectionTabs.SelectTab((int)_viewModel.CurrentSection);
}
private void UpdateCategoryTabs()
{
if (_viewModel == null) return;
switch (_viewModel.CurrentSection)
{
case SectionButtonType.Menu:
_menuCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
break;
case SectionButtonType.Cookware:
_cookwareCategoryTabs.SelectTab((int)_viewModel.CurrentCategory);
break;
}
}
// ViewModel 이벤트 핸들러들
private void HandleBatchCompleted()
{
Close();
}
private void HandleChecklistFailed()
{
var evt = GameEvents.OpenPopupUiEvent;
evt.UiType = typeof(ConfirmUi);
evt.IsCancelButtonVisible = true;
evt.NewMessageKey = "checklist_failed_message";
evt.OnConfirm = ClosePanel;
EventBus.Broadcast(evt);
}
private void HandleMenuSectionSelected()
{
_menuCategoryTabs.SelectFirstTab();
}
private void HandleCookwareSectionSelected()
{
_cookwareCategoryTabs.SelectFirstTab();
}
private void HandleCategoryChanged(InventoryCategoryType category)
{
_inventoryView.UpdateCategoryView(category);
_itemDetailView.UpdateCategory(category);
}
private void HandleTabMoved(int direction)
{
_sectionTabs.Move(direction);
}
private void HandleInteractRequested()
{
var selected = EventSystem.current.currentSelectedGameObject;
var interactable = selected?.GetComponent<IInteractableUi>();
interactable?.OnInteract();
}
private void HandleCloseRequested()
{
Close();
}
private void HandleMenuCategorySelected(InventoryCategoryType category)
{
_menuCategoryTabs.SelectTab((int)category);
}
// UI 이벤트 핸들러들 (TabGroupUi 콜백)
private void OnSectionTabSelected(int sectionValue)
{
_viewModel?.SetSection((SectionButtonType)sectionValue);
}
private void OnCategoryTabSelected(int categoryValue)
{
_viewModel?.SetCategory((InventoryCategoryType)categoryValue);
}
// 입력 처리 - ViewModel로 위임
protected override bool OnInputPerformed(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
{
var isHandled = base.OnInputPerformed(actionEnum, context);
if (isHandled && _viewModel != null)
{
switch (actionEnum)
{
case RestaurantUiActions.Cancel:
_viewModel.CloseUi();
break;
case RestaurantUiActions.PreviousTab:
_viewModel.MoveTab(-1);
break;
case RestaurantUiActions.NextTab:
_viewModel.MoveTab(1);
break;
case RestaurantUiActions.Interact1:
_viewModel.InteractWithSelected();
break;
case RestaurantUiActions.Interact2:
_viewModel.StartHold();
break;
}
}
return isHandled;
}
protected override bool OnInputCanceled(RestaurantUiActions actionEnum, InputAction.CallbackContext context)
{
var isHandled = base.OnInputCanceled(actionEnum, context);
if (isHandled && _viewModel != null)
{
switch (actionEnum)
{
case RestaurantUiActions.Interact2:
_viewModel.CancelHold();
break;
}
}
return isHandled;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 8a2e0954aa144633aad86e53dc80a46a
timeCreated: 1755673386

View File

@ -0,0 +1,241 @@
using System.Linq;
using DDD.MVVM;
using UnityEngine;
namespace DDD
{
/// <summary>
/// 레스토랑 관리 UI의 ViewModel
/// 기존 RestaurantManagementUi의 상태 관리와 비즈니스 로직을 담당
/// </summary>
public class RestaurantManagementViewModel : SimpleViewModel, IEventHandler<TodayMenuRemovedEvent>
{
// 홀드 진행 상태 관리
private bool _isHolding;
private float _elapsedTime;
private float _holdCompleteTime = 1f;
// 탭 상태 관리
private SectionButtonType _currentSection = SectionButtonType.Menu;
private InventoryCategoryType _currentCategory = InventoryCategoryType.Food;
/// <summary>
/// 현재 홀드 상태
/// </summary>
public bool IsHolding
{
get => _isHolding;
private set => SetField(ref _isHolding, value);
}
/// <summary>
/// 홀드 진행 시간 (0.0 ~ 1.0)
/// </summary>
public float HoldProgress
{
get => _elapsedTime;
private set => SetField(ref _elapsedTime, value);
}
/// <summary>
/// 홀드 완료에 필요한 시간
/// </summary>
public float HoldCompleteTime
{
get => _holdCompleteTime;
set => SetField(ref _holdCompleteTime, value);
}
/// <summary>
/// 현재 선택된 섹션
/// </summary>
public SectionButtonType CurrentSection
{
get => _currentSection;
set => SetField(ref _currentSection, value);
}
/// <summary>
/// 현재 선택된 카테고리
/// </summary>
public InventoryCategoryType CurrentCategory
{
get => _currentCategory;
set => SetField(ref _currentCategory, value);
}
/// <summary>
/// 배치 완료 가능 여부 (체크리스트 완료 상태)
/// </summary>
public bool CanCompleteBatch =>
RestaurantState.Instance.ManagementState.GetChecklistStates().All(state => state);
/// <summary>
/// 홀드 진행률을 0~1 범위로 변환한 값
/// </summary>
public float NormalizedHoldProgress => HoldCompleteTime <= 0f ? 1f : Mathf.Clamp01(HoldProgress / HoldCompleteTime);
public override void Initialize()
{
base.Initialize();
RegisterEvents();
ResetHoldState();
}
public override void Cleanup()
{
base.Cleanup();
UnregisterEvents();
}
private void RegisterEvents()
{
EventBus.Register<TodayMenuRemovedEvent>(this);
}
private void UnregisterEvents()
{
EventBus.Unregister<TodayMenuRemovedEvent>(this);
}
/// <summary>
/// 홀드 진행 업데이트 (View에서 Update마다 호출)
/// </summary>
public void UpdateHoldProgress()
{
if (!IsHolding) return;
if (HoldCompleteTime <= 0f)
{
ProcessCompleteBatch();
return;
}
var deltaTime = Time.deltaTime;
HoldProgress += deltaTime;
if (HoldProgress >= HoldCompleteTime)
{
ProcessCompleteBatch();
}
// UI 업데이트를 위한 정규화된 진행률 알림
OnPropertyChanged(nameof(NormalizedHoldProgress));
}
/// <summary>
/// 홀드 시작
/// </summary>
public void StartHold()
{
IsHolding = true;
HoldProgress = 0f;
OnPropertyChanged(nameof(NormalizedHoldProgress));
}
/// <summary>
/// 홀드 취소
/// </summary>
public void CancelHold()
{
ResetHoldState();
}
private void ResetHoldState()
{
IsHolding = false;
HoldProgress = 0f;
OnPropertyChanged(nameof(NormalizedHoldProgress));
}
/// <summary>
/// 배치 완료 처리
/// </summary>
private void ProcessCompleteBatch()
{
ResetHoldState();
if (CanCompleteBatch)
{
// 배치 완료 - UI 닫기 이벤트 발생
OnBatchCompleted?.Invoke();
}
else
{
// 체크리스트 미완료 - 실패 팝업 표시 이벤트 발생
OnChecklistFailed?.Invoke();
}
}
/// <summary>
/// 섹션 탭 선택 처리
/// </summary>
public void SetSection(SectionButtonType section)
{
CurrentSection = section;
// 섹션 변경 시 해당 섹션의 첫 번째 카테고리로 설정
switch (section)
{
case SectionButtonType.Menu:
OnMenuSectionSelected?.Invoke();
break;
case SectionButtonType.Cookware:
OnCookwareSectionSelected?.Invoke();
break;
}
}
/// <summary>
/// 카테고리 탭 선택 처리
/// </summary>
public void SetCategory(InventoryCategoryType category)
{
CurrentCategory = category;
OnCategoryChanged?.Invoke(category);
}
/// <summary>
/// 탭 이동 (이전/다음)
/// </summary>
public void MoveTab(int direction)
{
OnTabMoved?.Invoke(direction);
}
/// <summary>
/// 현재 선택된 UI 요소와 상호작용
/// </summary>
public void InteractWithSelected()
{
OnInteractRequested?.Invoke();
}
/// <summary>
/// UI 닫기
/// </summary>
public void CloseUi()
{
OnCloseRequested?.Invoke();
}
// 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 void Invoke(TodayMenuRemovedEvent evt)
{
SetCategory(evt.InventoryCategoryType);
OnMenuCategorySelected?.Invoke(evt.InventoryCategoryType);
}
public System.Action<InventoryCategoryType> OnMenuCategorySelected;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80dee5e1862248aab26236036049e5fc
timeCreated: 1755673405

View File

@ -3,7 +3,7 @@
using System.Runtime.CompilerServices;
using UnityEngine;
namespace DDD.MVVM
namespace DDD
{
public abstract class SimpleViewModel : MonoBehaviour, INotifyPropertyChanged
{

View File

@ -1,31 +1,43 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.EventSystems;
using DDD.MVVM;
using UnityEngine.UI;
using Sirenix.OdinInspector;
using UnityEngine.InputSystem;
using DDD.MVVM;
namespace DDD
{
public abstract class IntegratedBasePopupUi<TInputEnum, TViewModel> : IntegratedBaseUi<TViewModel>
public abstract class IntegratedBasePopupUi<TInputEnum, TViewModel> : BasePopupUi
where TInputEnum : Enum
where TViewModel : SimpleViewModel
{
[SerializeField, Required] protected BaseUiActionsInputBinding<TInputEnum> _uiActionsInputBinding;
protected readonly List<(InputAction action, Action<InputAction.CallbackContext> handler)> _registeredHandlers =
new();
// MVVM 기능들
protected BindingContext _bindingContext;
protected TViewModel _viewModel;
public InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this as BasePopupUi);
public override InputActionMaps InputActionMaps => _uiActionsInputBinding.InputActionMaps;
public bool IsTopPopup => UiManager.Instance.PopupUiState.IsTopPopup(this);
protected override void Awake()
{
base.Awake();
// BasePopupUi의 기본값 적용
_enableBlockImage = true;
_viewModel = GetComponent<TViewModel>();
_bindingContext = new BindingContext();
SetupAutoBindings();
SetupBindings();
}
protected override void OnEnable()
@ -36,8 +48,7 @@ protected override void OnEnable()
protected override void Update()
{
base.Update();
// BasePopupUi의 Update 로직 구현
if (IsOpenPanel() == false) return;
var currentSelectedGameObject = EventSystem.current.currentSelectedGameObject;
@ -50,8 +61,26 @@ protected override void Update()
}
}
}
public override void Open(OpenPopupUiEvent evt)
{
base.Open(evt);
protected abstract GameObject GetInitialSelected();
var initialSelected = GetInitialSelected();
if (initialSelected != null)
{
EventSystem.current.SetSelectedGameObject(initialSelected);
}
transform.SetAsLastSibling();
if (IsTopPopup)
{
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
}
}
protected override abstract GameObject GetInitialSelected();
protected override void TryRegister()
{
@ -110,37 +139,114 @@ protected override void TryUnregister()
// 입력 처리 메서드들
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;
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)
/// <summary>
/// Attribute 기반 자동 바인딩 설정
/// </summary>
private void SetupAutoBindings()
{
OpenPanel();
var fields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
.Where(f => f.GetCustomAttribute<BindToAttribute>() != null);
var initialSelected = GetInitialSelected();
if (initialSelected != null)
foreach (var field in fields)
{
EventSystem.current.SetSelectedGameObject(initialSelected);
var bindAttribute = field.GetCustomAttribute<BindToAttribute>();
SetupBinding(field, bindAttribute);
}
transform.SetAsLastSibling();
// 컬렉션 바인딩 설정
var collectionFields = GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public)
.Where(f => f.GetCustomAttribute<BindCollectionAttribute>() != null);
if (IsTopPopup)
foreach (var field in collectionFields)
{
InputManager.Instance.SwitchCurrentActionMap(_uiActionsInputBinding.InputActionMaps);
var bindAttribute = field.GetCustomAttribute<BindCollectionAttribute>();
SetupCollectionBinding(field, bindAttribute);
}
}
public virtual void Close()
/// <summary>
/// 개별 필드의 바인딩 설정
/// </summary>
private void SetupBinding(FieldInfo field, BindToAttribute bindAttribute)
{
var evt = GameEvents.ClosePopupUiEvent;
evt.UiType = GetType();
EventBus.Broadcast(evt);
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>
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)
{
// 하위 클래스에서 구현
}
/// <summary>
/// UI 패널 열기 오버라이드 (ViewModel 초기화 추가)
/// </summary>
public override void OpenPanel()
{
base.OpenPanel();
_viewModel?.Initialize();
}
/// <summary>
/// UI 패널 닫기 오버라이드 (ViewModel 정리 추가)
/// </summary>
public override void ClosePanel()
{
_viewModel?.Cleanup();
base.ClosePanel();
}
}
}