From d2987be3589f2b18788f0370cb43f478e1662959 Mon Sep 17 00:00:00 2001 From: hj615 Date: Thu, 14 Aug 2025 13:35:56 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EB=8D=B0=EC=9D=B4=ED=84=B0/=EA=B7=B8?= =?UTF-8?q?=EB=9E=98=ED=94=84=20=EC=84=A4=EC=A0=95=EC=97=90=20=EB=94=B0?= =?UTF-8?q?=EB=9D=BC=20=EC=84=A4=EC=A0=95=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SmartAquaViewer/Helper/ComboBoxHelper.cs | 51 +++++ SmartAquaViewer/Helper/MultiSelectBehavior.cs | 48 +++++ SmartAquaViewer/View/MonitoringView.xaml | 194 ++++++++++++++--- .../ViewModel/MonitoringViewModel.cs | 202 ++++++++++++++++-- 4 files changed, 455 insertions(+), 40 deletions(-) create mode 100644 SmartAquaViewer/Helper/ComboBoxHelper.cs create mode 100644 SmartAquaViewer/Helper/MultiSelectBehavior.cs diff --git a/SmartAquaViewer/Helper/ComboBoxHelper.cs b/SmartAquaViewer/Helper/ComboBoxHelper.cs new file mode 100644 index 0000000..ea10a35 --- /dev/null +++ b/SmartAquaViewer/Helper/ComboBoxHelper.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; + +namespace SmartAquaViewer.Helper +{ + public static class ComboBoxHelper + { + public static readonly DependencyProperty SelectFirstOnItemsChangeProperty = + DependencyProperty.RegisterAttached( + "SelectFirstOnItemsChange", typeof(bool), typeof(ComboBoxHelper), + new PropertyMetadata(false, OnSelectFirstChanged)); + + public static void SetSelectFirstOnItemsChange(DependencyObject obj, bool value) + => obj.SetValue(SelectFirstOnItemsChangeProperty, value); + public static bool GetSelectFirstOnItemsChange(DependencyObject obj) + => (bool)obj.GetValue(SelectFirstOnItemsChangeProperty); + + private static void OnSelectFirstChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ComboBox cb) return; + + void TrySelectFirst() + { + if (cb.Items.Count > 0 && cb.SelectedIndex < 0) + cb.SelectedIndex = 0; + } + + cb.Loaded += (_, __) => TrySelectFirst(); + + void Hook() + { + if (cb.ItemsSource is INotifyCollectionChanged incc) + { + incc.CollectionChanged -= OnItemsChanged; + incc.CollectionChanged += OnItemsChanged; + } + } + void OnItemsChanged(object? s, NotifyCollectionChangedEventArgs args) => TrySelectFirst(); + + // ItemsSource가 바뀌어도 다시 훅 + cb.DataContextChanged += (_, __) => Hook(); + Hook(); + } + } +} diff --git a/SmartAquaViewer/Helper/MultiSelectBehavior.cs b/SmartAquaViewer/Helper/MultiSelectBehavior.cs new file mode 100644 index 0000000..b68e3a5 --- /dev/null +++ b/SmartAquaViewer/Helper/MultiSelectBehavior.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Controls; +using System.Windows; + +namespace SmartAquaViewer.Helper +{ + public static class MultiSelectBehavior + { + public static readonly DependencyProperty SelectedItemsProperty = + DependencyProperty.RegisterAttached( + "SelectedItems", typeof(IList), typeof(MultiSelectBehavior), + new PropertyMetadata(null, OnSelectedItemsChanged)); + + public static void SetSelectedItems(DependencyObject element, IList? value) + => element.SetValue(SelectedItemsProperty, value); + public static IList? GetSelectedItems(DependencyObject element) + => (IList?)element.GetValue(SelectedItemsProperty); + + private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is not ListBox lb) return; + + lb.SelectionChanged -= Lb_SelectionChanged; + lb.SelectionChanged += Lb_SelectionChanged; + + if (e.NewValue is IList target) + { + target.Clear(); + foreach (var item in lb.SelectedItems) target.Add(item); + } + } + + private static void Lb_SelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (sender is not ListBox lb) return; + var list = GetSelectedItems(lb); + if (list == null) return; + + foreach (var removed in e.RemovedItems) list.Remove(removed); + foreach (var added in e.AddedItems) list.Add(added); + } + } +} diff --git a/SmartAquaViewer/View/MonitoringView.xaml b/SmartAquaViewer/View/MonitoringView.xaml index 77983c7..61878f4 100644 --- a/SmartAquaViewer/View/MonitoringView.xaml +++ b/SmartAquaViewer/View/MonitoringView.xaml @@ -132,33 +132,173 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GraphTypes { get; } public List WaterQualityList { get; } @@ -37,14 +42,36 @@ namespace SmartAquaViewer.ViewModel _selectedTab = value; OnPropertyChanged(); - Debug.WriteLine($"CurrentTab changed to: {_selectedTab}"); + RebuildAvailableFields(); // 탭에 맞춰 필드 목록 재구성 SetGraphType(); - SelectedGraphType = GraphTypes.FirstOrDefault(); + + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + RebuildFieldCandidates(); + }), DispatcherPriority.Background); + OnSystemChanged?.Invoke(SelectedTab); } } } + private int _selectedGraphIndex = -1; + public int SelectedGraphIndex + { + get => _selectedGraphIndex; + set + { + if (_selectedGraphIndex != value) + { + _selectedGraphIndex = value; + OnPropertyChanged(); + // 인덱스가 바뀌면 enum도 맞춰준다 + if (value >= 0 && value < GraphTypes.Count) + SelectedGraphType = GraphTypes[value]; + } + } + } + private GraphType _selectedGraphType; public GraphType SelectedGraphType { @@ -55,6 +82,14 @@ namespace SmartAquaViewer.ViewModel { _selectedGraphType = value; OnPropertyChanged(); + RebuildFieldCandidates(); + + var idx = GraphTypes.IndexOf(value); + if (idx != -1 && idx != _selectedGraphIndex) + { + _selectedGraphIndex = idx; + OnPropertyChanged(nameof(SelectedGraphIndex)); + } } } } @@ -104,8 +139,64 @@ namespace SmartAquaViewer.ViewModel } } + // [필드 후보 목록] 탭/시스템에 따라 달라짐 + public ObservableCollection AvailableFields { get; } = new(); + + // [X축 후보/선택] + public ObservableCollection XFieldCandidates { get; } = new(); + private FieldItem? _selectedXField; + public FieldItem? SelectedXField + { + get => _selectedXField; + set { if (_selectedXField != value) { _selectedXField = value; OnPropertyChanged(); } } + } + + // [Y축 후보/선택] — Line/Step: 다중, Scatter/Box: 단일 + public ObservableCollection YFieldCandidates { get; } = new(); + + // 다중 선택(Y)용 + public ObservableCollection SelectedYFields { get; } = new(); + + // 단일 선택(Y)용 + private FieldItem? _selectedYField; + public FieldItem? SelectedYField + { + get => _selectedYField; + set { if (_selectedYField != value) { _selectedYField = value; OnPropertyChanged(); } } + } + + // BoxPlot 전용 그룹 기준(예: 수조/시간 등) + public ObservableCollection GroupFieldCandidates { get; } = new(); + private FieldItem? _selectedGroupField; + public FieldItem? SelectedGroupField + { + get => _selectedGroupField; + set { if (_selectedGroupField != value) { _selectedGroupField = value; OnPropertyChanged(); } } + } + + // [옵션] 예시 — 필요하면 추가 + private bool _showMarkers; // Line + public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } } + + private bool _useSmoothing; // Line + public bool UseSmoothing { get => _useSmoothing; set { _useSmoothing = value; OnPropertyChanged(); } } + + private double _scatterMarkerSize = 3; // Scatter + public double ScatterMarkerSize { get => _scatterMarkerSize; set { _scatterMarkerSize = value; OnPropertyChanged(); } } + + private bool _showRegression; // Scatter + public bool ShowRegression { get => _showRegression; set { _showRegression = value; OnPropertyChanged(); } } + + private double _boxWidth = 0.3; // Box + public double BoxWidth { get => _boxWidth; set { _boxWidth = value; OnPropertyChanged(); } } + public ICommand ChangeDrawerStatusCommand { get; } + public delegate void SystemChangedEventHandler(MonitorTab selectedTab); + public event SystemChangedEventHandler OnSystemChanged; + + + public MonitoringViewModel() { IsOpenMode = true; @@ -126,9 +217,11 @@ namespace SmartAquaViewer.ViewModel GraphTypes = new ObservableCollection(); SelectedTab = MonitorTab.Tank; // Default system SetGraphType(); - SelectedGraphType = GraphTypes.FirstOrDefault(); ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode); + + RebuildAvailableFields(); + RebuildFieldCandidates(); } private void SetGraphType() @@ -143,9 +236,6 @@ namespace SmartAquaViewer.ViewModel GraphTypes.Add(GraphType.SCATTER); break; case MonitorTab.Filter: - GraphTypes.Add(GraphType.LINE); - GraphTypes.Add(GraphType.STEP); - break; case MonitorTab.Sterilizer: GraphTypes.Add(GraphType.LINE); GraphTypes.Add(GraphType.STEP); @@ -154,11 +244,97 @@ namespace SmartAquaViewer.ViewModel break; } - string graphTypes = string.Empty; - foreach (GraphType graphType in GraphTypes) - graphTypes += (graphType.ToString() + ", "); + Application.Current.Dispatcher.BeginInvoke(new Action(() => + { + SelectedGraphIndex = -1; + SelectedGraphIndex = GraphTypes.Count > 0 ? 0 : -1; + }), System.Windows.Threading.DispatcherPriority.Background); + } + + private void RebuildAvailableFields() + { + AvailableFields.Clear(); + + // 공통 시간 + AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) }); - Debug.WriteLine(graphTypes); + if (SelectedTab == MonitorTab.Tank) + { + AvailableFields.Add(new FieldItem { Name = "Tank.Number", Display = "수조", DataType = typeof(int) }); + AvailableFields.Add(new FieldItem { Name = "Tank.DOValue", Display = "DO (mg/L)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Tank.PH", Display = "pH", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Tank.ORP", Display = "ORP (mV)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Tank.Temperature", Display = "온도 (℃)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Tank.FlowRate", Display = "유량 (m³/s)", DataType = typeof(double) }); + } + else if (SelectedTab == MonitorTab.Filter) + { + AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double) }); + AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double) }); + } + else // Sterilizer + { + AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double) }); + // 필요한 다른 수치 필드들 추가 + } + } + + // 그래프 타입이 바뀔 때 후보/기본 선택 재구성 + private void RebuildFieldCandidates() + { + // 후보 초기화 + XFieldCandidates.Clear(); + YFieldCandidates.Clear(); + GroupFieldCandidates.Clear(); + + // X축: 시간 우선 + foreach (var f in AvailableFields) XFieldCandidates.Add(f); + SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime)) + ?? AvailableFields.FirstOrDefault(); + + // Y축 후보: 수치형 + foreach (var f in AvailableFields.Where(f => f.DataType == typeof(double))) + YFieldCandidates.Add(f); + + // BoxPlot 그룹 후보: 시간/수조/카테고리 등 + foreach (var f in AvailableFields) + GroupFieldCandidates.Add(f); + + // 기본 선택 세팅 (타입별) + SelectedYFields.Clear(); + SelectedYField = null; + SelectedGroupField = null; + + switch (SelectedGraphType) + { + case GraphType.LINE: + case GraphType.STEP: + var def = YFieldCandidates.FirstOrDefault(); + if (def != null) SelectedYFields.Add(def); + ShowMarkers = false; + UseSmoothing = false; + break; + + case GraphType.SCATTER: + SelectedYField = YFieldCandidates.FirstOrDefault(); + ScatterMarkerSize = 3; + ShowRegression = false; + break; + + case GraphType.BOX: + SelectedYField = YFieldCandidates.FirstOrDefault(); + // 그룹은 시간으로 기본 + SelectedGroupField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime)) + ?? AvailableFields.FirstOrDefault(); + BoxWidth = 0.3; + break; + } + OnPropertyChanged(nameof(SelectedYFields)); } public event PropertyChangedEventHandler? PropertyChanged;