feat: 데이터/그래프 설정에 따라 설정 데이터 변경

hhsung_work
HyungJune Kim 10 months ago
parent d6391fbbc5
commit d2987be358

@ -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();
}
}
}

@ -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);
}
}
}

@ -132,33 +132,173 @@
</Grid.ColumnDefinitions>
<Border Margin="20"
Background="#384659" BorderBrush="#404F63" BorderThickness="1" CornerRadius="10">
<Grid>
<StackPanel>
<Grid Margin="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="50" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="15 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="50" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding XFieldCandidates}"
SelectedItem="{Binding SelectedXField}"
DisplayMemberPath="Display"/>
</Grid>
<Grid Margin="15 5">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<!-- 보이기 토글용 스타일 -->
<Style x:Key="VisibleWhenTrue" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STEP">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenScatter" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="SCATTER">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenBox" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="BOX">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<!-- LINE/STEP: 다중 선택 -->
<Grid Style="{StaticResource VisibleWhenTrue}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="40"/>
<RowDefinition/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Text="Y축 (복수 선택)" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 -->
<ListBox ItemsSource="{Binding YFieldCandidates}"
DisplayMemberPath="Display"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
Height="150" Grid.Row="1"
FontSize="16" FontWeight="Bold"/>
<!-- 옵션 -->
<StackPanel Orientation="Horizontal" Margin="0 15 0 0" Grid.Row="2">
<CheckBox Content="마커 표시" IsChecked="{Binding ShowMarkers}" Margin="0 0 15 0"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"/>
<CheckBox Content="스무딩" IsChecked="{Binding UseSmoothing}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
VerticalContentAlignment="Center"/>
</StackPanel>
</Grid>
<TextBlock Text="그래프" FontSize="24" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="15 15 15 5"/>
<TextBlock Grid.Row="2" Text="X축" FontSize="24" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="15 15 15 5"/>
<TextBlock Grid.Row="4" Text="Y축" FontSize="24" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="15 15 15 5"/>
<!-- SCATTER: 단일 Y + 옵션 -->
<Grid Style="{StaticResource VisibleWhenScatter}">
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="70"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="50"
Grid.Column="1" Grid.ColumnSpan="3" Margin="15 0 0 0"
Style="{StaticResource ComboBoxStyle}"/>
<ComboBox Grid.Row="1" Margin="15 0" Height="50"
Style="{StaticResource ComboBoxStyle}"
ItemsSource="{Binding GraphTypes}"
SelectedItem="{Binding SelectedGraphType}"/>
<TextBlock Text="마커 크기" Grid.Row="1" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Slider Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Margin="15 0 0 0"
Minimum="1" Maximum="15" Value="{Binding ScatterMarkerSize}" Width="280" IsSnapToTickEnabled="True" TickFrequency="1"/>
<TextBlock Text="{Binding ScatterMarkerSize}" Margin="15 0"
Grid.Row="1" Grid.Column="2" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<CheckBox Content="회귀선" IsChecked="{Binding ShowRegression}"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"
Margin="0" Grid.Row="1" Grid.Column="3"
VerticalContentAlignment="Center"/>
</Grid>
<ComboBox Grid.Row="3" Margin="15 0" Height="50"
Style="{StaticResource ComboBoxStyle}"></ComboBox>
<ComboBox Grid.Row="5" Margin="15 0" Height="50"
Style="{StaticResource ComboBoxStyle}"></ComboBox>
<!-- BOX: 값 필드 + 그룹 필드 + 옵션 -->
<Grid Style="{StaticResource VisibleWhenBox}">
<Grid.RowDefinitions>
<RowDefinition Height="70"/>
<RowDefinition Height="70"/>
<RowDefinition Height="70"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="값 필드" VerticalAlignment="Center"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="50"
Grid.Column="1" Margin="15 0 0 0"
Style="{StaticResource ComboBoxStyle}"/>
<TextBlock VerticalAlignment="Center" Grid.Row="1"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White">
<Run Text="그룹"/>
<LineBreak/>
<Run Text="(박스 기준)" FontSize="14"/>
</TextBlock>
<ComboBox ItemsSource="{Binding GroupFieldCandidates}"
SelectedItem="{Binding SelectedGroupField}"
DisplayMemberPath="Display" Height="50"
Grid.Row="1" Grid.Column="1" Margin="15 0 0 0"
Style="{StaticResource ComboBoxStyle}"/>
<TextBlock Text="박스 너비" VerticalAlignment="Center" Grid.Row="2"
FontSize="20" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
<Slider Margin="15 0 0 0" Grid.Row="2" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Left"
Minimum="0.1" Maximum="1.0" TickFrequency="0.05" IsSnapToTickEnabled="True"
Value="{Binding BoxWidth}" Width="310"/>
<TextBlock Text="{Binding BoxWidth, StringFormat=F2}"
Margin="15 0" Grid.Row="2" Grid.Column="2"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="18" FontFamily="{StaticResource SCDream4}" Foreground="White"/>
</Grid>
</Grid>
</StackPanel>
</Border>
<Border Grid.Column="1" Margin="0 20 20 20"

@ -9,6 +9,7 @@ using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
@ -16,11 +17,15 @@ using static SmartAquaViewer.Model.Enums;
namespace SmartAquaViewer.ViewModel
{
public class MonitoringViewModel : INotifyPropertyChanged
public sealed class FieldItem
{
public delegate void SystemChangedEventHandler(MonitorTab selectedTab);
public event SystemChangedEventHandler OnSystemChanged;
public string? Name { get; init; } // 바인딩 경로 키 (예: "Tank.DOValue")
public string? Display { get; init; } // UI 표시명 (예: "DO (mg/L)")
public Type? DataType { get; init; } // typeof(double), typeof(DateTime) 등
}
public class MonitoringViewModel : INotifyPropertyChanged
{
public ObservableCollection<GraphType> GraphTypes { get; }
public List<WaterQualityVO> 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<FieldItem> AvailableFields { get; } = new();
// [X축 후보/선택]
public ObservableCollection<FieldItem> 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<FieldItem> YFieldCandidates { get; } = new();
// 다중 선택(Y)용
public ObservableCollection<FieldItem> SelectedYFields { get; } = new();
// 단일 선택(Y)용
private FieldItem? _selectedYField;
public FieldItem? SelectedYField
{
get => _selectedYField;
set { if (_selectedYField != value) { _selectedYField = value; OnPropertyChanged(); } }
}
// BoxPlot 전용 그룹 기준(예: 수조/시간 등)
public ObservableCollection<FieldItem> 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<GraphType>();
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) });
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);
Debug.WriteLine(graphTypes);
// 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;

Loading…
Cancel
Save