From 3e93ad0b39690242d488b6a2acd6f91950d67b7e Mon Sep 17 00:00:00 2001 From: NTG Date: Mon, 25 Aug 2025 05:12:05 +0900 Subject: [PATCH] =?UTF-8?q?ui=20=EB=B0=94=EC=9D=B8=EB=94=A9=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../PopupUis/RestaurantManagementUi.prefab | 28 +----- .../RestaurantManagementUi/ItemDetailView.cs | 98 +++++++++++++------ .../RestaurantManagementUi.cs | 2 + .../RestaurantManagementViewModel.cs | 81 ++++++++------- .../TodayRestaurantStateView.cs | 3 - .../_Scripts/GameUi/BaseUi/SimpleViewModel.cs | 60 +++++++----- .../GameUi/Utils/Binding/BindingContext.cs | 18 +++- .../GameUi/Utils/Binding/BindingHelper.cs | 45 +++++++++ .../GameUi/Utils/Binding/BindingPath.cs | 56 +++++++++++ .../GameUi/Utils/Binding/BindingPath.cs.meta | 3 + .../GameUi/Utils/Binding/IBindingTarget.cs | 35 ++++++- 11 files changed, 303 insertions(+), 126 deletions(-) create mode 100644 Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs create mode 100644 Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs.meta diff --git a/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab b/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab index a7df0fad0..663265cda 100644 --- a/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab +++ b/Assets/_DDD/_Addressables/Prefabs/Uis/GameUi/PopupUis/RestaurantManagementUi.prefab @@ -53,10 +53,8 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _viewImage: {fileID: 5168542205125039251} - _nameLabel: {fileID: 3228882035781370616} - _labelLocalizer: {fileID: 8756294767538431005} - _descriptionLabel: {fileID: 4113829672213848922} - _descriptionLocalizer: {fileID: 7808277355010082239} + _nameLocalizeStringEvent: {fileID: 8756294767538431005} + _descriptionLocalizeStringEvent: {fileID: 7808277355010082239} _cookWarePanel: {fileID: 4302691437602401827} _cookwareImage: {fileID: 3915032697630876799} _tasteHashTagPanel: {fileID: 5646014643746221419} @@ -9938,17 +9936,6 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_PrefabInstance: {fileID: 7850329751525360396} m_PrefabAsset: {fileID: 0} ---- !u!114 &4113829672213848922 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6189840460486090838, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} - m_PrefabInstance: {fileID: 7850329751525360396} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &7808277355010082239 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} @@ -11320,17 +11307,6 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 9048682655274794071, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} m_PrefabInstance: {fileID: 8730773236163191470} m_PrefabAsset: {fileID: 0} ---- !u!114 &3228882035781370616 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6189840460486090838, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} - m_PrefabInstance: {fileID: 8730773236163191470} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &8756294767538431005 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 48813585706763955, guid: c7ae409f7430c9a408d36ccf54ef4164, type: 3} diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/ItemDetailView.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/ItemDetailView.cs index f91219213..35a316600 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/ItemDetailView.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/ItemDetailView.cs @@ -1,17 +1,60 @@ +using System; +using System.Collections.Generic; using TMPro; using UnityEngine; +using UnityEngine.Localization; using UnityEngine.Localization.Components; using UnityEngine.UI; namespace DDD { + public sealed class ItemDetailSnapshot + { + public Sprite BackgroundSprite { get; } + public bool ShowTastePanel { get; } + public bool ShowCookwarePanel { get; } + public Sprite CookwareSprite { get; } + public LocalizedString Name { get; } + public LocalizedString Description { get; } + public IReadOnlyList Tastes { get; } + public Material TasteMaterial { get; } + + public ItemDetailSnapshot( + Sprite backgroundSprite, + bool showTastePanel, + bool showCookwarePanel, + Sprite cookwareSprite, + LocalizedString name, + LocalizedString description, + IReadOnlyList tastes, + Material tasteMaterial) + { + BackgroundSprite = backgroundSprite; + ShowTastePanel = showTastePanel; + ShowCookwarePanel = showCookwarePanel; + CookwareSprite = cookwareSprite; + Name = name; + Description = description; + Tastes = tastes; + TasteMaterial = tasteMaterial; + } + + public static readonly ItemDetailSnapshot Empty = new( + backgroundSprite: null, + showTastePanel: false, + showCookwarePanel: false, + cookwareSprite: null, + name: null, + description: null, + tastes: System.Array.Empty(), + tasteMaterial: null); + } + public class ItemDetailView : MonoBehaviour, IUiView, IEventHandler { [SerializeField] private Image _viewImage; - [SerializeField] private TextMeshProUGUI _nameLabel; - [SerializeField] private LocalizeStringEvent _labelLocalizer; - [SerializeField] private TextMeshProUGUI _descriptionLabel; - [SerializeField] private LocalizeStringEvent _descriptionLocalizer; + [SerializeField] private LocalizeStringEvent _nameLocalizeStringEvent; + [SerializeField] private LocalizeStringEvent _descriptionLocalizeStringEvent; [SerializeField] private Transform _cookWarePanel; [SerializeField] private Image _cookwareImage; [SerializeField] private Transform _tasteHashTagPanel; @@ -21,51 +64,53 @@ public class ItemDetailView : MonoBehaviour, IUiView(this); + } + public void Initialize(RestaurantManagementViewModel viewModel) { _viewModel = viewModel; - - _nameLabel.text = string.Empty; - _descriptionLabel.text = string.Empty; + _cookwareImage.sprite = null; + + EventBus.Register(this); + } + + public void SetupBindings(BindingContext bindingContext) + { + BindingHelper.BindImage(bindingContext, _viewImage, viewModel => viewModel.ItemDetail.BackgroundSprite); + BindingHelper.BindImage(bindingContext, _cookwareImage, viewModel => viewModel.ItemDetail.CookwareSprite); + BindingHelper.BindActive(bindingContext, _tasteHashTagPanel.gameObject, viewModel => viewModel.ItemDetail.ShowTastePanel); + BindingHelper.BindActive(bindingContext, _cookWarePanel.gameObject, viewModel => viewModel.ItemDetail.ShowCookwarePanel); + BindingHelper.BindLocalizedStringEvent(bindingContext, _nameLocalizeStringEvent, viewModel => viewModel.ItemDetail.Name); + BindingHelper.BindLocalizedStringEvent(bindingContext, _descriptionLocalizeStringEvent, viewModel => viewModel.ItemDetail.Description); } public void OnOpenedEvents() { UpdateView(); - _viewModel.OnCategoryChanged += UpdateCategory; - - EventBus.Register(this); + _viewModel.OnCategoryChanged += HandleCategoryChanged; } public void OnClosedEvents() { if (_viewModel) { - _viewModel.OnCategoryChanged -= UpdateCategory; + _viewModel.OnCategoryChanged -= HandleCategoryChanged; } - - EventBus.Unregister(this); } public void UpdateView() { - UpdateCategory(_viewModel.CurrentCategory); - if (_viewModel.SelectedItem == null) { - _labelLocalizer.StringReference = null; - _descriptionLocalizer.StringReference = null; - _cookwareImage.sprite = null; ClearHashTags(); return; } - _labelLocalizer.StringReference = _viewModel.GetItemName(); - _descriptionLocalizer.StringReference = _viewModel.GetItemDescription(); - _cookwareImage.sprite = _viewModel.GetCookwareSprite(); - UpdateTasteHashTags(); } @@ -101,15 +146,8 @@ private void UpdateTasteHashTags() LayoutRebuilder.ForceRebuildLayoutImmediate(_tasteHashTagContent2); } - public void UpdateCategory(InventoryCategoryType category) + public void HandleCategoryChanged(InventoryCategoryType category) { - _viewImage.sprite = _viewModel.GetDetailBackground(category); - - bool showTaste = _viewModel.ShouldShowTaste(category); - bool showCookware = _viewModel.ShouldShowCookware(category); - _tasteHashTagPanel.gameObject.SetActive(showTaste); - _cookWarePanel.gameObject.SetActive(showCookware); - Canvas.ForceUpdateCanvases(); } diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementUi.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementUi.cs index 64fef8e37..641887329 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementUi.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementUi.cs @@ -123,6 +123,8 @@ protected override GameObject GetInitialSelected() protected override void SetupBindings() { BindingHelper.BindImageFilled(_bindingContext, _completeBatchFilledImage, nameof(RestaurantManagementViewModel.NormalizedHoldProgress)); + + _itemDetailView.SetupBindings(_bindingContext); } private void InitializeViews() diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementViewModel.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementViewModel.cs index 981c4194f..ad849615d 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementViewModel.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/RestaurantManagementViewModel.cs @@ -173,7 +173,10 @@ public void SetCategory(InventoryCategoryType category) { if (CurrentCategory == category) return; + BeginUpdate(); CurrentCategory = category; + RecomputeItemDetail(); + EndUpdate(); OnCategoryChanged?.Invoke(category); } @@ -365,56 +368,68 @@ public void OnInventoryChanged() #endregion #region ItemDetailView + + private ItemDetailSnapshot _itemDetail = ItemDetailSnapshot.Empty; + public ItemDetailSnapshot ItemDetail + { + get => _itemDetail; + private set => SetField(ref _itemDetail, value); + } + + public ItemViewModel SelectedItem { get; private set; } 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; + + BeginUpdate(); SelectedItem = item; + RecomputeItemDetail(); + EndUpdate(); } public TasteHashTagSlotUi CreateHashTag(RectTransform parent) { return Instantiate(GetRestaurantManagementData().TasteHashTagSlotUiPrefab, parent, false); } - - private string GetViewItemKey() + + private void RecomputeItemDetail() { - 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 + 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), + 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 }; - } - public Sprite GetCookwareSprite() => SelectedItem.GetCookwareIcon; + ItemDetail = new ItemDetailSnapshot( + backgroundSprite: background, + showTastePanel: showTaste, + showCookwarePanel: showCookware, + cookwareSprite: cookwareSprite, + name: nameLocalizedString, + description: descriptionLocalizedString, + tastes: tastes, + tasteMaterial: tasteMat); + } public IReadOnlyList GetTastes() => SelectedItem?.GetTasteDatas; @@ -430,12 +445,6 @@ public Material GetTasteMaterial() }; } - 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 diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/TodayRestaurantStateUi/TodayRestaurantStateView.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/TodayRestaurantStateUi/TodayRestaurantStateView.cs index fc5ad5c0d..73d37cbf6 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/TodayRestaurantStateUi/TodayRestaurantStateView.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi/PopupUis/RestaurantManagementUi/TodayRestaurantStateUi/TodayRestaurantStateView.cs @@ -8,9 +8,6 @@ public class TodayRestaurantStateView : MonoBehaviour, IUiView _workerSlots; - private List _cookwareSlots; - private RestaurantManagementViewModel _viewModel; public void Initialize(RestaurantManagementViewModel viewModel) diff --git a/Assets/_DDD/_Scripts/GameUi/BaseUi/SimpleViewModel.cs b/Assets/_DDD/_Scripts/GameUi/BaseUi/SimpleViewModel.cs index 50267af30..77b43a5e9 100644 --- a/Assets/_DDD/_Scripts/GameUi/BaseUi/SimpleViewModel.cs +++ b/Assets/_DDD/_Scripts/GameUi/BaseUi/SimpleViewModel.cs @@ -9,12 +9,6 @@ public abstract class SimpleViewModel : MonoBehaviour, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; - protected virtual void Awake() { } - protected virtual void OnEnable() { } - protected virtual void Start() { } - protected virtual void OnDisable() { } - protected virtual void OnDestroy() { } - public virtual void Initialize() { } public virtual void Cleanup() { } @@ -24,6 +18,15 @@ public virtual void Cleanup() { } /// 변경된 속성 이름 (자동으로 설정됨) protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { + if (string.IsNullOrEmpty(propertyName)) return; + + if (_updateDepth > 0) + { + // 배치 업데이트 중: 나중에 일괄 발행 + _pendingNotifications.Add(propertyName); + return; + } + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } @@ -43,35 +46,38 @@ protected bool SetField(ref T field, T value, [CallerMemberName] string prope OnPropertyChanged(propertyName); return true; } - - /// - /// 배치 업데이트를 위한 플래그 - /// - private bool _isUpdating; - /// - /// 배치 업데이트 중 보류된 알림들 - /// + private int _updateDepth; private readonly HashSet _pendingNotifications = new(); + + protected void BeginUpdate() + { + _updateDepth++; + } - /// - /// 배치 업데이트 시작 - 여러 속성 변경을 한 번에 처리 - /// - protected void BeginUpdate() => _isUpdating = true; - - /// - /// 배치 업데이트 종료 - 보류된 모든 알림을 처리 - /// protected void EndUpdate() { - _isUpdating = false; + if (_updateDepth == 0) + { + Debug.LogWarning("EndUpdate called without matching BeginUpdate."); + return; + } + + _updateDepth--; + + // 아직 상위 배치가 진행 중이면 플러시하지 않음 + if (_updateDepth > 0) return; + if (_pendingNotifications.Count > 0) { - foreach (var prop in _pendingNotifications) - { - OnPropertyChanged(prop); - } + // 복사 후 클리어(핸들러 내부에서 BeginUpdate가 호출되어도 안전) + var toNotify = new List(_pendingNotifications); _pendingNotifications.Clear(); + + foreach (var prop in toNotify) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop)); + } } } } diff --git a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingContext.cs b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingContext.cs index b9e5ea4dc..fb4faf61f 100644 --- a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingContext.cs +++ b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingContext.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq.Expressions; using System.Reflection; -using TMPro; -using UnityEngine; -using UnityEngine.UI; namespace DDD { @@ -16,6 +14,12 @@ public class BindingContext private readonly Dictionary> _bindings = new(); private readonly Dictionary _converters = new(); private INotifyPropertyChanged _dataContext; + + public void Bind(Expression> property, IBindingTarget target, IValueConverter converter = null) + { + var path = BindingPath.For(property); + Bind(path, target, converter); + } /// /// 데이터 컨텍스트 (ViewModel) 설정 @@ -61,6 +65,14 @@ public void Bind(string propertyPath, IBindingTarget target, IValueConverter con private void OnPropertyChanged(object sender, PropertyChangedEventArgs e) { UpdateBinding(e.PropertyName); + var prefix = e.PropertyName + "."; + foreach (var key in _bindings.Keys) + { + if (key.StartsWith(prefix, StringComparison.Ordinal)) + { + UpdateBinding(key); + } + } } /// diff --git a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingHelper.cs b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingHelper.cs index 1b55af711..7361f8582 100644 --- a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingHelper.cs +++ b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingHelper.cs @@ -1,5 +1,8 @@ +using System; +using System.Linq.Expressions; using TMPro; using UnityEngine; +using UnityEngine.Localization.Components; using UnityEngine.UI; namespace DDD @@ -11,6 +14,12 @@ public static void BindText(BindingContext context, TextMeshProUGUI text, string var target = new TextBindingTarget(text, propertyPath); context?.Bind(propertyPath, target, converter); } + + public static void BindLocalizedStringEvent(BindingContext context, LocalizeStringEvent localizeEvent, string propertyPath, IValueConverter converter = null) + { + var target = new LocalizeStringEventBindingTarget(localizeEvent, propertyPath); + context?.Bind(propertyPath, target, converter); + } public static void BindImage(BindingContext context, Image image, string propertyPath, IValueConverter converter = null) { @@ -35,5 +44,41 @@ public static void BindSlider(BindingContext context, Slider slider, string prop var target = new SliderBindingTarget(slider, propertyPath); context?.Bind(propertyPath, target, converter); } + + public static void BindText(BindingContext ctx, TextMeshProUGUI text, + Expression> prop, IValueConverter conv = null) + => BindText(ctx, text, BindingPath.For(prop), conv); + + public static void BindLocalizedStringEvent(BindingContext ctx, LocalizeStringEvent loc, + Expression> prop, IValueConverter conv = null) + => BindLocalizedStringEvent(ctx, loc, BindingPath.For(prop), conv); + + public static void BindImage(BindingContext ctx, Image img, + Expression> prop, IValueConverter conv = null) + => BindImage(ctx, img, BindingPath.For(prop), conv); + + public static void BindImageFilled(BindingContext ctx, Image img, + Expression> prop, IValueConverter conv = null) + => BindImageFilled(ctx, img, BindingPath.For(prop), conv); + + public static void BindActive(BindingContext ctx, GameObject go, + Expression> prop, IValueConverter conv = null) + => BindActive(ctx, go, BindingPath.For(prop), conv); + + public static void BindSlider(BindingContext ctx, Slider slider, + Expression> prop, IValueConverter conv = null) + => BindSlider(ctx, slider, BindingPath.For(prop), conv); + + public static void BindImage(BindingContext ctx, Image img, + Expression> prop, IValueConverter conv = null) + => BindImage(ctx, img, BindingPath.For(prop), conv); + + public static void BindActive(BindingContext ctx, GameObject go, + Expression> prop, IValueConverter conv = null) + => BindActive(ctx, go, BindingPath.For(prop), conv); + + public static void BindLocalizedStringEvent(BindingContext ctx, LocalizeStringEvent loc, + Expression> prop, IValueConverter conv = null) + => BindLocalizedStringEvent(ctx, loc, BindingPath.For(prop), conv); } } \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs new file mode 100644 index 000000000..3aa1944ae --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; + +namespace DDD +{ + public static class BindingPath + { + public static string For(Expression> expression) + { + if (expression == null) throw new ArgumentNullException(nameof(expression)); + + Expression expr = expression.Body; + + // 값형식 → object 캐스팅 제거 + if (expr is UnaryExpression unary && unary.NodeType == ExpressionType.Convert) + { + expr = unary.Operand; + } + + var members = new Stack(); + while (expr is MemberExpression me) + { + members.Push(me.Member.Name); + expr = me.Expression; + + if (expr is ParameterExpression) break; // 루트 도달 + + if (expr is UnaryExpression innerUnary && innerUnary.NodeType == ExpressionType.Convert) + expr = innerUnary.Operand; + } + + return string.Join(".", members); + } + + // 편의용: object 리턴 시그니처도 지원 + public static string For(Expression> expression) + { + if (expression == null) throw new ArgumentNullException(nameof(expression)); + Expression expr = expression.Body; + if (expr is UnaryExpression unary && unary.NodeType == ExpressionType.Convert) + expr = unary.Operand; + + var members = new Stack(); + while (expr is MemberExpression me) + { + members.Push(me.Member.Name); + expr = me.Expression; + if (expr is ParameterExpression) break; + if (expr is UnaryExpression innerUnary && innerUnary.NodeType == ExpressionType.Convert) + expr = innerUnary.Operand; + } + return string.Join(".", members); + } + } +} \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs.meta b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs.meta new file mode 100644 index 000000000..c9f363bf6 --- /dev/null +++ b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/BindingPath.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 35fee64a12cf410e8758bef3294aa1ee +timeCreated: 1756065063 \ No newline at end of file diff --git a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/IBindingTarget.cs b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/IBindingTarget.cs index 37702732e..652169349 100644 --- a/Assets/_DDD/_Scripts/GameUi/Utils/Binding/IBindingTarget.cs +++ b/Assets/_DDD/_Scripts/GameUi/Utils/Binding/IBindingTarget.cs @@ -1,5 +1,7 @@ using TMPro; using UnityEngine; +using UnityEngine.Localization; +using UnityEngine.Localization.Components; using UnityEngine.UI; namespace DDD @@ -38,6 +40,33 @@ public void UpdateValue(object value) } } + public class LocalizeStringEventBindingTarget : IBindingTarget + { + private readonly LocalizeStringEvent _localizeEvent; + public string PropertyPath { get; } + + public LocalizeStringEventBindingTarget(LocalizeStringEvent localizeEvent, string propertyPath) + { + _localizeEvent = localizeEvent; + PropertyPath = propertyPath; + } + + public void UpdateValue(object value) + { + if (_localizeEvent == null) return; + + if (value is LocalizedString localized) + { + _localizeEvent.StringReference = localized; + } + else + { + // null 또는 다른 타입인 경우 초기화 + _localizeEvent.StringReference = null; + } + } + } + public class ImageBindingTarget : IBindingTarget { private readonly Image _image; @@ -51,10 +80,14 @@ public ImageBindingTarget(Image image, string propertyPath) public void UpdateValue(object value) { - if (_image != null && value is Sprite sprite) + if (value is Sprite sprite) { _image.sprite = sprite; } + else + { + _image.sprite = null; + } } }