Compare commits

..

77 Commits

Author SHA1 Message Date
HyungJune Kim ea0006530d design: 폰트 변경 및 일부 디자인 변경
6 months ago
HyungJune Kim d4588feb41 design: 그래프 색상, 크기 등 조정
6 months ago
HyungJune Kim b6af317feb design: CCTV 관제화면 디자인 적용
6 months ago
HyungJune Kim 90c8820f39 design: CCTV 플레이어 디자인 적용
6 months ago
HyungJune Kim 1bb3465e79 design: CCTV 목록 디자인 적용
6 months ago
HyungJune Kim aef0f87168 design: 온실가스 화면 디자인 적용
6 months ago
HyungJune Kim 59865bc040 fix: 그래프 스타일 적용
7 months ago
HyungJune Kim 18b7a31ee7 feat: 에너지 화면 그래프 섹션 여닫기 기능 추가
7 months ago
HyungJune Kim ad4cbf4fea design: 에너지 화면 디자인 적용
7 months ago
HyungJune Kim e8fd344c10 fix: 그래프 설정 버그 수정
7 months ago
HyungJune Kim a63172f66c design: 파일 목록 디자인 적용
7 months ago
HyungJune Kim 6197df6be2 design: 슬라이더 스타일 정의 및 적용
7 months ago
HyungJune Kim c880f493f0 design: DataGrid 스타일 적용
7 months ago
HyungJune Kim 53b9fd3883 design: Line 그래프 & 설정칸 디자인 적용
7 months ago
HyungJune Kim 4d4bacd3b7 design: 콤보박스 디자인 정의 및 적용
7 months ago
HyungJune Kim e308fcbf34 design: 리스트 박스 스타일 변경
7 months ago
HyungJune Kim edb92f3191 design: Segmented 컨트롤 디자인 변경
7 months ago
HyungJune Kim 5b71d4e297 design: 헤더, 파일뷰, 탭 스타일 변경
7 months ago
HyungJune Kim 289316a71f feat: 페이지 버튼 추가
7 months ago
HyungJune Kim 90bcf08256 feat: CCTV 리스트 Json 파일에서 불러오기로 추가
7 months ago
HyungJune Kim f4304373b1 feat: Utils 클래스 추가
7 months ago
HyungJune Kim af88b6e5a3 feat: DataGrid 페이징 로직 추가
9 months ago
HyungJune Kim e4dc4a2784 design: 폰트 통합 및 적용
10 months ago
HyungJune Kim 45d20bdfe3 design: 파일 목록 문구 추가
10 months ago
HyungJune Kim 356f3b9c03 feat: CCTV 목록 출력 및 시작/중지 기능 추가
10 months ago
HyungJune Kim a47bf94ee7 fix: CCTV 재생 기능 방식 수정
10 months ago
HyungJune Kim 0396389ff9 design: DataGrid 헤더 값 줄바꿈
10 months ago
HyungJune Kim 89f9383e7a feat: 파일 목록 순차적 표시
10 months ago
HyungJune Kim 55e22ddcc3 feat: CCTV 재생 기능 추가
10 months ago
HyungJune Kim d0254a590e design: 그래프 스타일링
10 months ago
HyungJune Kim df682e6600 design: 레이아웃 및 배경 조정
10 months ago
HyungJune Kim 60349a5723 feat: 온실가스 요약 데이터 출력 추가
10 months ago
HyungJune Kim 6354aa82f7 fix: 그래프 생성 기능 수정
10 months ago
HyungJune Kim 9a3ae0cda2 fix: 상자그림 생성 기능 수정
10 months ago
HyungJune Kim 8858bafd8b fix: 수조 라인 그래프 수정
10 months ago
HyungJune Kim 9cf43930c0 feat: 더미 데이터 파일 읽어드리기 및 믈릭 시 데이터 반영
10 months ago
HyungJune Kim 0a4900689e feat: CCTV 기능 개발 진행중
10 months ago
HyungJune Kim 1be29e82a0 fix: 온실가스 그래프 버그 수정
10 months ago
HyungJune Kim 2e56475725 feat: 에너지 -> 온실가스 변환식 및 데이터 출력
10 months ago
HyungJune Kim 6bc2fc30fb feat: 파이차트 생성 기능
10 months ago
HyungJune Kim ff95feeafc feat: 스택 영역 그래프 생성
10 months ago
HyungJune Kim 9a2bcb47bd feat: 에너지 화면 요약 데이터 출력
10 months ago
HyungJune Kim 11382dcea6 fix: 일부 디자인 수정
10 months ago
HyungJune Kim 9effacb0a5 feat: 에너지 데이터 추가 및 출력
10 months ago
HyungJune Kim e794514592 feat: Step 그래프 생성 기능 추가
10 months ago
HyungJune Kim fecca36925 feat: 산점도 그래프 생성 기능 추가
10 months ago
HyungJune Kim e4f72f965f feat: BoxPlot 생성 기능 추가
10 months ago
HyungJune Kim 41bff54c4f feat: 일반 라인 그래프 생성 추가
10 months ago
HyungJune Kim 4cbcc658b8 feat: 수조 라인차트 생성 기능 추가
10 months ago
HyungJune Kim 630570e506 feat: 그래프 설정 값 가져오기
10 months ago
HyungJune Kim 0edb2c9864 desgin: DataGrid 스타일
10 months ago
HyungJune Kim d2987be358 feat: 데이터/그래프 설정에 따라 설정 데이터 변경
10 months ago
HyungJune Kim d6391fbbc5 design: 에너지, 온실가스 화면 레이아웃
10 months ago
HyungJune Kim 3ac0255008 refactor: 그래프 종류 string -> enum
10 months ago
HyungJune Kim 11bbf7922f feat: 필터 탭 변경 시 DataGrid 변경
10 months ago
HyungJune Kim 92800c3171 feat: 수조 DataGrid 표시
10 months ago
HyungJune Kim b61a0024fc Merge "hhsun_work" to prototype
10 months ago
HyungJune Kim f3c5b5cd2c feat: DataGrid 동적 생성 기능 추가중
10 months ago
hhsung 48f2cd67e6 주석 복구
10 months ago
hhsung a94808b9ca data vo filed changed
10 months ago
HyungJune Kim aa65046922 feat: 필터 변경 시 그래프 종류 업데이트
10 months ago
HyungJune Kim ae31da2035 merge hhsung_work to prototype
10 months ago
HyungJune Kim fbcf495dce fix: field 수정
10 months ago
HyungJune Kim 49a0d14b6c design: 그래프 화면
10 months ago
hhsung 82d36c2b63 ef setting
10 months ago
HyungJune Kim 38458d2c30 feat: SegmentedControl 형태로 모니터링 필터버튼 추가
10 months ago
HyungJune Kim ea88b86186 staging
10 months ago
hhsung c007128dee EF framework 추가
10 months ago
hhsung 14a46fbf90 파일에서 임의의 vo 생성 로직 추가
10 months ago
hhsung 8a9310397b file 처리 클래스 작성
10 months ago
HyungJune Kim 2986bde22a Merge branch 'hhsung_work' into prototype
10 months ago
hhsung c52758fc50 임의의 데이터 클래스 및 샘플자료 생성함수 추가
10 months ago
HyungJune Kim acdfb8db7c fix: 그래프 화면 열고닫기 기능 MaterialDesign Drawer 방식 적용
10 months ago
HyungJune Kim 48cfad129e feat: 그래프 화면 열고닫기 기능 추가
10 months ago
HyungJune Kim 5b60525279 design: 라디오 버튼 스타일 변경
10 months ago
HyungJune Kim 99eb505d72 feat: view swap 기능 추가
11 months ago
HyungJune Kim 7daa875b05 feat: 파일 목록 시각화
11 months ago

@ -1,20 +1,26 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35931.197 d17.13
VisualStudioVersion = 17.13.35931.197
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartAquaViewer", "SmartAquaViewer\SmartAquaViewer.csproj", "{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|x64.ActiveCfg = Debug|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Debug|x64.Build.0 = Debug|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|Any CPU.Build.0 = Release|Any CPU
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.ActiveCfg = Release|x64
{B1AF5CCA-731E-42E1-8ECD-9B8FC7237A95}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

@ -2,8 +2,48 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SmartAquaViewer"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:view="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign2.Defaults.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.DeepPurple.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Secondary/MaterialDesignColor.Lime.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Button.xaml"/>
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Badged.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Card.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Chip.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Clock.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Flipper.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.FlipperClassic.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ColorPicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.RatingBar.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.TimePicker.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Shadows.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.SmartHint.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Snackbar.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.AutoSuggestBox.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.SplitButton.xaml" />
<ResourceDictionary Source="/Resources/Generic.xaml"/>
</ResourceDictionary.MergedDictionaries>
<DataTemplate DataType="{x:Type vm:MonitoringViewModel}">
<view:MonitoringView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:EnergyViewModel}">
<view:EnegyView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:GreenHouseGasViewModel}">
<view:GreenHouseView/>
</DataTemplate>
<DataTemplate DataType="{x:Type vm:CCTVViewModel}">
<view:CCTVView/>
</DataTemplate>
</ResourceDictionary>
</Application.Resources>
</Application>

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Classes
{
class Constants
{
public const string CONFIG_INI = "config.ini";
public class Config
{
public const string CONFIG = "CONFIG";
public const string TITLE = "TITLE";
public const string DATA_FILE_PATH = "DATA-FILE-PATH";
public const string REC_PATH = "REC-PATH";
}
public class Directories
{
public const string DATA_FOLDER = "data_folder";
public const string REC_FOLDER = "REC";
}
public class DataFiles
{
public const string CCTV_LIST = "cctvlist.json";
}
}
}

@ -0,0 +1,110 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace SmartAquaViewer.Classes
{
public class EnumEqualsConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> value != null && parameter != null && value.Equals(parameter);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> (value is bool b && b) ? parameter! : Binding.DoNothing;
}
public class BoolToPowerConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is bool b && b
? "On"
: "Off";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value as string;
}
}
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
=> !(value is bool b && b);
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
=> !(value is bool b && b);
}
public class OneBasedConverter : IValueConverter
{
public object Convert(object value, Type t, object p, CultureInfo c) =>
value is int i ? (i + 1).ToString() : "1";
public object ConvertBack(object value, Type t, object p, CultureInfo c) =>
int.TryParse(value?.ToString(), out var v) ? Math.Max(1, v) - 1 : 0;
}
public class PageIndexToDisplayConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is int idx)
return (idx + 1).ToString(); // 0 -> 1, 1 -> 2 ...
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (int.TryParse(value?.ToString(), out int display))
return display - 1; // 1 -> 0
return 0;
}
}
public class CurrentPageEqualsConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 && values[0] is int buttonIndex && values[1] is int currentIndex)
return buttonIndex == currentIndex;
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
=> throw new NotImplementedException();
}
public class BoolToVisibilityConverter : IValueConverter
{
public bool Inverse { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool flag = false;
if (value is bool b)
flag = b;
if (Inverse)
flag = !flag;
return flag ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is Visibility visibility)
{
bool flag = visibility == Visibility.Visible;
return Inverse ? !flag : flag;
}
return false;
}
}
}

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.INI;
namespace SmartAquaViewer.Classes
{
class Utils
{
public static Utils Instance { get; } = new Utils();
public readonly string CurrentDirectory = Environment.CurrentDirectory;
private static INIManager? iniManager;
public static INIManager IniManager
{
get
{
if (iniManager == null)
{
string iniPath = Path.Combine(Instance.CurrentDirectory, Constants.CONFIG_INI);
iniManager = new INIManager(iniPath);
}
return iniManager;
}
}
private string GetGeneralSection(String strKey, String? defaultValue)
{
return IniManager.ReadValue(Constants.Config.CONFIG, strKey, defaultValue);
}
private void WriteValue(String strKey, String value)
{
IniManager.WriteValue(Constants.Config.CONFIG, strKey, value);
}
private string? title;
private string Title
{
get
{
if (title == null)
{
title = GetGeneralSection(Constants.Config.TITLE, null);
}
return title;
}
}
public string GetTitle()
{
return Title;
}
private string? dataFilePath;
private string DataFilePath
{
get
{
if (dataFilePath == null)
{
dataFilePath = GetGeneralSection(Constants.Config.DATA_FILE_PATH, null);
}
return dataFilePath;
}
}
public string GetDataFilePath()
{
return DataFilePath;
}
public string GetDataFileContentPath(string filePath)
{
return Path.Combine(CurrentDirectory, DataFilePath, filePath);
}
public void CreateDirectory(string folderName)
{
string directoryPath = Path.Combine(Environment.CurrentDirectory, folderName);
if (Directory.Exists(directoryPath))
return;
Directory.CreateDirectory(directoryPath);
Console.WriteLine($"Directory created at: {directoryPath}");
}
public void DebugWriteLine(string value, params object[] args)
{
if (Debugger.IsAttached)
{
Debug.WriteLine(value, args);
FileLog(value);
}
}
public void WriteFileLog(object value)
{
File.AppendAllText(Path.Combine(CurrentDirectory, "log.txt"), String.Format("[{0}]\n{1}\n", DateTime.Now.ToString("yyyy-MM-dd HH:MM:ss"), value), Encoding.Default);
}
public void FileLog(object value)
{
WriteFileLog(value);
}
}
}

@ -0,0 +1,46 @@
<UserControl x:Class="SmartAquaViewer.Controls.FFPlayerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border >
<Grid Background="#1D3649">
<Grid.RowDefinitions>
<RowDefinition Height="32"/>
<RowDefinition Height="*"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<Border BorderThickness="0 0 0 1" BorderBrush="#325C80" Background="#264A60">
<TextBlock x:Name="lblCCTVID" Margin="12 0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Style="{StaticResource CCTVTextBlockStyle}"
Text="{Binding CCTVInfo.DeviceId}"/>
</Border>
<Border Grid.Row="1" x:Name="bdrNoSignalContainer"
Visibility="{Binding IsPlayingVIsibility}">
<TextBlock Name="lblVideoStatus" Text="연결 없음"
VerticalAlignment="Center" HorizontalAlignment="Center"
FontSize="16" Foreground="#999999" FontFamily="{StaticResource Pretendard_Medium}"/>
</Border>
<Image x:Name="imgPlayer" Grid.Row="1"
Stretch="Fill" Source="{Binding CurrentFrame}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<!--<StackPanel Name="spTopMenuBar" Visibility="Hidden" Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
<Image Name="imgMaximize" Source="../Images/maximize.png" Width="24" Margin="1, 5, 0, 5" MouseLeftButtonUp="imgMaximize_MouseUp" Opacity="0.7" MouseEnter="imgTopmenu_MouseEnter" MouseLeave="imgTopmenu_MouseLeave"/>
</StackPanel>-->
<Border Grid.Row="2" Background="#1D3649"
BorderThickness="0 1 0 0" BorderBrush="#325C80">
<TextBlock x:Name="lblCCTVName" Text="{Binding CCTVInfo.DeviceName}"
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="12 0"
Style="{StaticResource CCTVTextBlockStyle}"/>
</Border>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using SmartAquaViewer.Model;
using SmartAquaViewer.ViewModel;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// FFPlayerControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class FFPlayerControl : UserControl
{
public FFPlayerControl()
{
InitializeComponent();
}
}
}

@ -0,0 +1,17 @@
<UserControl x:Class="SmartAquaViewer.Controls.GraphControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
xmlns:oxy="http://oxyplot.org/wpf"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="1080">
<Border>
<Grid>
<oxy:PlotView Name="pvChart" Model="{Binding Model}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="0"
Background="Transparent"/>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// GraphControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class GraphControl : UserControl
{
public GraphControl()
{
InitializeComponent();
}
private void ComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
}
}
}

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace SmartAquaViewer.Controls
{
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
public class RelayCommand<T> : ICommand
{
private readonly Action<T> _execute;
private readonly Func<T, bool>? _canExecute;
public RelayCommand(Action<T> execute, Func<T, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke((T)parameter!) ?? true;
}
public void Execute(object? parameter)
{
_execute((T)parameter!);
}
public event EventHandler? CanExecuteChanged
{
add => CommandManager.RequerySuggested += value;
remove => CommandManager.RequerySuggested -= value;
}
}
}

@ -0,0 +1,23 @@
<UserControl x:Class="SmartAquaViewer.Controls.SegmentedControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border Background="#1D3649" CornerRadius="35" Width="759">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<RadioButton Name="rdbtnA" Content="수조" GroupName="SignalType" Tag="Tank"
Checked="RadioButton_Checked"
Style="{StaticResource RadioButtonSegmentedStyle}" IsChecked="True"/>
<RadioButton Name="rdbtnB" Content="여과시스템" GroupName="SignalType" Tag="Filter"
Checked="RadioButton_Checked"
Style="{StaticResource RadioButtonSegmentedStyle}"/>
<RadioButton Name="rdbtnC" Content="살균시스템" GroupName="SignalType" Tag="Sterilizer"
Checked="RadioButton_Checked"
Style="{StaticResource RadioButtonSegmentedStyle}"/>
</StackPanel>
</Border>
</UserControl>

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.Controls
{
/// <summary>
/// SegmentedControl.xaml에 대한 상호 작용 논리
/// </summary>
public partial class SegmentedControl : UserControl
{
public SegmentedControl()
{
InitializeComponent();
}
public string SelectedValue
{
get { return (string)GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register(nameof(SelectedValue), typeof(string), typeof(SegmentedControl),
new PropertyMetadata(null));
public MonitorTab SelectedTab
{
get { return (MonitorTab)GetValue(SelectedTabProperty); }
set { SetValue(SelectedTabProperty, value); }
}
public static readonly DependencyProperty SelectedTabProperty =
DependencyProperty.Register(nameof(SelectedTab), typeof(MonitorTab), typeof(SegmentedControl),
new PropertyMetadata(null));
private void RadioButton_Checked(object sender, RoutedEventArgs e)
{
if (sender is RadioButton rb && rb.Tag != null)
{
SelectedTab = (MonitorTab)Enum.Parse(typeof(MonitorTab), rb.Tag.ToString());
}
}
}
}

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore;
using SmartAquaViewer.DataAnalysis;
using System.Collections.Generic;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory;
namespace SmartAquaViewer.DataAnalisys
{
public class AppDbContext : DbContext
{
//dotnet tool install --global dotnet-ef
//dotnet ef migrations add InitialCreate --project SmartAquaViewer
//dotnet ef database update --project SmartAquaViewer
//dotnet ef migrations add AddMultipleTables --project SmartAquaViewer
//dotnet ef database update --project SmartAquaViewer
public DbSet<WaterQualityVO> WaterQualities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// MySQL 연결 문자열 (일반적인 형태)
var connectionString = "Server=192.168.10.143;Port=3306;Database=smart_aqua;User=root;Password=znqk123!;";
// MySQL Server Version 설정 (버전에 맞춰 변경)
var serverVersion = new MySqlServerVersion(new Version(8, 0, 36));
optionsBuilder.UseMySql(connectionString, serverVersion);
}
}
}

@ -0,0 +1,106 @@
using SmartAquaViewer.DataAnalysis;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.DataAnalisys
{
internal class AquarDataControl
{
private AQUA_DATA_CONTROL_STATE state = AQUA_DATA_CONTROL_STATE.IDLE;
private readonly IWaterQuality iwaterQuality;
private CancellationTokenSource? cts;
AppDbContext db = new AppDbContext();
public AquarDataControl(IWaterQuality iwaterQuality)
{
this.iwaterQuality = iwaterQuality;
}
public AQUA_DATA_CONTROL_STATE GetState() => state;
/// <summary>
/// 파일 파싱 시작
/// </summary>
public void ParseFile(string filePath)
{
// --- 동기 처리 구간 ---
if (state != AQUA_DATA_CONTROL_STATE.IDLE &&
state != AQUA_DATA_CONTROL_STATE.PROCESS_COMPLETED)
{
iwaterQuality.OnError("illegal state error : " + state);
return;
}
state = AQUA_DATA_CONTROL_STATE.PROCESSING;
cts = new CancellationTokenSource();
// --- 비동기 처리 구간 ---
Task.Run(async () =>
{
try
{
var fileInfo = new FileInfo(filePath);
long totalBytes = fileInfo.Length;
long processedBytes = 0;
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
using (var reader = new StreamReader(fs))
{
string? line;
while ((line = await reader.ReadLineAsync()) != null)
{
cts.Token.ThrowIfCancellationRequested();
processedBytes += Encoding.UTF8.GetByteCount(line + Environment.NewLine);
double percent = (double)processedBytes / totalBytes * 100.0;
// TODO: 라인 파싱 로직
WaterQualityVO vo = new();
//vo.PH = 10.5;
vo.RecordedTime = DateTime.Now;
iwaterQuality.OnParsed(vo);
db.Add(vo);
iwaterQuality.OnProgress(percent);
}
}
state = AQUA_DATA_CONTROL_STATE.PROCESS_COMPLETED;
iwaterQuality.OnCompleted();
}
catch (OperationCanceledException)
{
state = AQUA_DATA_CONTROL_STATE.CANCELLED;
iwaterQuality.OnCancelled();
}
catch (Exception ex)
{
state = AQUA_DATA_CONTROL_STATE.ERROR;
iwaterQuality.OnError("파일 처리 중 오류 발생: " + ex.Message);
}
});
}
/// <summary>
/// 처리 중단 요청
/// </summary>
public void Cancel()
{
if (state == AQUA_DATA_CONTROL_STATE.PROCESSING && cts != null)
{
cts.Cancel();
}
}
}
}

@ -0,0 +1,29 @@
using SmartAquaViewer.DataAnalysis;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.DataAnalisys
{
internal enum AQUA_DATA_CONTROL_STATE
{
IDLE,
PROCESSING,
PROCESS_COMPLETED,
CANCELLED,
ERROR
}
internal interface IWaterQuality
{
void OnParsed(WaterQualityVO vo);
void OnProgress(double percent);
void OnError(string message);
void OnCompleted();
void OnCancelled();
}
}

@ -0,0 +1,522 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;
namespace SmartAquaViewer.DataAnalysis
{
[Table("water_quality")]
public class WaterQualityVO
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// 측정 시각
/// </summary>
[Column("recorded_time")]
public DateTime RecordedTime { get; set; }
/// <summary>
/// 저수조
/// </summary>
public WaterTank Tank { get; set; } = new();
public List<WaterTank> Tanks { get; set; } = new();
/// <summary>
/// 여과 시스템
/// </summary>
public FilteringSystem Filtering { get; set; } = new();
/// <summary>
/// 살균 시스템
/// </summary>
public SterilizingSystem Sterilizing { get; set; } = new();
/// <summary>
/// 총 전력 소비 (킬로와트, kW)
/// </summary>
public double TotalEnergy { get; set; }
/// <summary>
/// 총 온실가스 배출량 (톤, tCO₂)
/// </summary>
public double TotalGreenhouseGas { get; set; }
public WaterQualityVO() { }
public WaterQualityVO(DateTime RecordedTime, WaterTank tank, FilteringSystem filtering, SterilizingSystem sterilizing)
{
this.RecordedTime = RecordedTime;
Tank = tank;
Filtering = filtering;
Sterilizing = sterilizing;
}
/// <summary>
/// 샘플 데이터 리스트 생성
/// </summary>
public static List<WaterQualityVO> GetSampleData(DateTime start, DateTime end, int totalRowsCount)
{
var list = new List<WaterQualityVO>();
var rand = new Random();
double CalculateTotalEnergy(WaterQualityVO vo)
{
return vo.Filtering.SandFilterEnergy +
vo.Filtering.CirculationPumpEnergy +
vo.Filtering.HeatPumpEnergy +
vo.Filtering.AirBlowerEnergy +
vo.Sterilizing.OzoneGeneratorEnergy +
vo.Sterilizing.UVSterilizerEnergy +
vo.Sterilizing.OzoneDissolverEnergy +
vo.Sterilizing.ExcessOzoneDestroyerEnergy;
}
double ConvertEnergyToGHG(double energy)
{
// 2024년 배출계수 (kgCO₂/kWh)
const double EmissionFactor = 0.4747;
return (energy * EmissionFactor); // / 1000.0; // tCO₂
}
double CalculateTotalGreenhouseGas(WaterQualityVO vo)
{
return vo.Filtering.AirBlowerGreenhouseGas +
vo.Filtering.CirculationPumpGreenhouseGas +
vo.Filtering.HeatPumpGreenhouseGas +
vo.Filtering.SandFilterGreenhouseGas +
vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas +
vo.Sterilizing.OzoneDissolverGreenhouseGas +
vo.Sterilizing.OzoneGeneratorGreenhouseGas +
vo.Sterilizing.UVSterilizerGreenhouseGas;
}
if (totalRowsCount <= 0) return list;
double totalSeconds;
if (start == end)
{
totalSeconds = 24 * 60 * 60;
}
else
{
totalSeconds = (end - start).TotalSeconds;
if (totalSeconds < 0) throw new ArgumentException("end 날짜는 start 날짜보다 같거나 커야 합니다.");
}
double stepSeconds = totalSeconds / totalRowsCount;
for (int i = 0; i < totalRowsCount; i++)
{
DateTime ts = start.AddSeconds(stepSeconds * i);
var vo = new WaterQualityVO
{
RecordedTime = ts,
Tank = new WaterTank(
number: rand.Next(1, 4),
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)
),
Tanks = new List<WaterTank>()
{
new WaterTank(
number: 1,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 2,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
new WaterTank(
number: 3,
doValue: Math.Round(rand.NextDouble() * 5 + 5, 2),
ph: Math.Round(rand.NextDouble() * 2 + 6, 2),
orp: Math.Round(rand.NextDouble() * 200 + 100, 2),
temperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2)),
},
Filtering = new FilteringSystem(
sandFilterPower: rand.Next(0, 2) == 1,
sandFilterEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
sandFilterGreenhouseGas: 0, // 추후 계산
sumpPH: Math.Round(rand.NextDouble() * 2 + 6, 2),
sumpORP: Math.Round(rand.NextDouble() * 200 + 100, 2),
sumpWaterLevel: Math.Round(rand.NextDouble() * 2 + 1, 2),
sumpFlowRate: Math.Round(rand.NextDouble() * 5 + 1, 2),
sumpTemperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
circulationPumpPower: rand.Next(0, 2) == 1,
circulationPumpEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
circulationPumpGreenhouseGas: 0, // 추후 계산
inverterControllerStatus: rand.Next(0, 2) == 1 ? "Normal" : "Error",
flowRate: Math.Round(rand.NextDouble() * 5 + 1, 2),
heatPumpPower: rand.Next(0, 2) == 1,
heatPumpTemperature: Math.Round(rand.NextDouble() * 10 + 15, 2),
heatPumpEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
heatPumpGreenhouseGas: 0, // 추후 계산
airBlowerPower: rand.Next(0, 2) == 1,
airBlowerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
airBlowerGreenhouseGas: 0 // 추후 계산
),
Sterilizing = new SterilizingSystem(
ozoneGeneratorPower: rand.Next(0, 2) == 1,
ozoneGeneratorEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
ozoneGeneratorGreenhouseGas: 0, // 추후 계산
uvSterilizerId: "UV" + rand.Next(1, 100),
uvSterilizerPower: rand.Next(0, 2) == 1,
uvSterilizerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
uvSterilizerGreenhouseGas: 0, // 추후 계산
ozoneDissolverPower: rand.Next(0, 2) == 1,
ozoneDissolverPressure: Math.Round(rand.NextDouble() * 100 + 50, 2),
ozoneDissolverEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
ozoneDissolverGreenhouseGas: 0, // 추후 계산
excessOzoneDestroyerPower: rand.Next(0, 2) == 1,
excessOzoneDestroyerEnergy: Math.Round(rand.NextDouble() * 2 + 0.5, 2),
excessOzoneDestroyerGreenhouseGas: 0 // 추후 계산
),
};
vo.TotalEnergy = CalculateTotalEnergy(vo);
vo.Filtering.SandFilterGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.SandFilterEnergy);
vo.Filtering.CirculationPumpGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.CirculationPumpEnergy);
vo.Filtering.HeatPumpGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.HeatPumpEnergy);
vo.Filtering.AirBlowerGreenhouseGas = ConvertEnergyToGHG(vo.Filtering.AirBlowerEnergy);
vo.Sterilizing.OzoneGeneratorGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.OzoneGeneratorEnergy);
vo.Sterilizing.UVSterilizerGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.UVSterilizerEnergy);
vo.Sterilizing.OzoneDissolverGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.OzoneDissolverEnergy);
vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas = ConvertEnergyToGHG(vo.Sterilizing.ExcessOzoneDestroyerEnergy);
vo.TotalGreenhouseGas = CalculateTotalGreenhouseGas(vo);
list.Add(vo);
}
return list;
}
}
[Owned]
public class WaterTank
{
/// <summary>
/// 저수조 번호
/// </summary>
[Column("tank_number")]
public int Number { get; set; }
/// <summary>
/// Dissolved Oxygen (mg/L)
/// </summary>
[Column("tank_do_value")]
public double DOValue { get; set; }
/// <summary>
/// pH (산도)
/// </summary>
[Column("tank_ph")]
public double PH { get; set; }
/// <summary>
/// 산화환원전위 (mV)
/// </summary>
[Column("tank_orp")]
public double ORP { get; set; }
/// <summary>
/// 수온 (°C)
/// </summary>
[Column("tank_temperature")]
public double Temperature { get; set; }
/// <summary>
/// 유량 (m³/s)
/// </summary>
[Column("tank_flow_rate")]
public double FlowRate { get; set; }
public WaterTank() { }
public WaterTank(int number, double doValue, double ph, double orp, double temperature, double flowRate)
{
Number = number;
DOValue = doValue;
PH = ph;
ORP = orp;
Temperature = temperature;
FlowRate = flowRate;
}
}
[Owned]
public class FilteringSystem
{
/// <summary>
/// 모래여과기 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_sand_filter_power")]
public bool SandFilterPower { get; set; }
/// <summary>
/// 모래여과기 전력 (kW)
/// </summary>
[Column("filter_sand_filter_energy")]
public double SandFilterEnergy { get; set; }
/// <summary>
/// 모래여과기 온실가스 (tCO₂)
/// </summary>
[Column("filter_sand_filter_greenhouse_gas")]
public double SandFilterGreenhouseGas { get; set; }
/// <summary>
/// 섬프탱크 pH (산도)
/// </summary>
[Column("filter_sump_ph")]
public double SumpPH { get; set; }
/// <summary>
/// 섬프탱크 산화환원전위 (mV)
/// </summary>
[Column("filter_sump_orp")]
public double SumpORP { get; set; }
/// <summary>
/// 섬프탱크 수위 (m)
/// </summary>
[Column("filter_sump_water_level")]
public double SumpWaterLevel { get; set; }
/// <summary>
/// 섬프탱크 유량 (m³/s)
/// </summary>
[Column("filter_sump_flow_rate")]
public double SumpFlowRate { get; set; }
/// <summary>
/// 섬프탱크 수온 (°C)
/// </summary>
[Column("filter_sump_temperature")]
public double SumpTemperature { get; set; }
/// <summary>
/// 순환펌프 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_circulation_pump_power")]
public bool CirculationPumpPower { get; set; }
/// <summary>
/// 순환펌프 전력 (kW)
/// </summary>
[Column("filter_circulation_pump_energy")]
public double CirculationPumpEnergy { get; set; }
/// <summary>
/// 순환펌프 온실가스 (tCO₂)
/// </summary>
[Column("filter_circulation_pump_greenhouse_gas")]
public double CirculationPumpGreenhouseGas { get; set; }
/// <summary>
/// 인버터 제어기 상태
/// </summary>
[Column("filter_inverter_controller_status")]
public string? InverterControllerStatus { get; set; }
/// <summary>
/// 순환펌프 유량 (m³/s)
/// </summary>
[Column("filter_flow_rate")]
public double FlowRate { get; set; }
/// <summary>
/// 히트펌프 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_heat_pump_power")]
public bool HeatPumpPower { get; set; }
/// <summary>
/// 히트펌프 온도 (°C)
/// </summary>
[Column("filter_heat_pump_temperature")]
public double HeatPumpTemperature { get; set; }
/// <summary>
/// 히트펌프 전력 (kW)
/// </summary>
[Column("filter_heat_pump_energy")]
public double HeatPumpEnergy { get; set; }
/// <summary>
/// 히트펌프 온실가스 (tCO₂)
/// </summary>
[Column("filter_heat_pump_greenhouse_gas")]
public double HeatPumpGreenhouseGas { get; set; }
/// <summary>
/// 에어브로와 전원 (true: ON, false: OFF)
/// </summary>
[Column("filter_air_blower_power")]
public bool AirBlowerPower { get; set; }
/// <summary>
/// 에어브로와 전력 (kW)
/// </summary>
[Column("filter_air_blower_energy")]
public double AirBlowerEnergy { get; set; }
/// <summary>
/// 에어브로와 온실가스 (tCO₂)
/// </summary>
[Column("filter_air_blower_greenhouse_gas")]
public double AirBlowerGreenhouseGas { get; set; }
public FilteringSystem() { }
public FilteringSystem(
bool sandFilterPower, double sandFilterEnergy, double sandFilterGreenhouseGas,
double sumpPH, double sumpORP, double sumpWaterLevel, double sumpFlowRate, double sumpTemperature,
bool circulationPumpPower, double circulationPumpEnergy, double circulationPumpGreenhouseGas, string? inverterControllerStatus, double flowRate,
bool heatPumpPower, double heatPumpTemperature, double heatPumpEnergy, double heatPumpGreenhouseGas,
bool airBlowerPower, double airBlowerEnergy, double airBlowerGreenhouseGas)
{
SandFilterPower = sandFilterPower;
SandFilterEnergy = sandFilterEnergy;
SandFilterGreenhouseGas = sandFilterGreenhouseGas;
SumpPH = sumpPH;
SumpORP = sumpORP;
SumpWaterLevel = sumpWaterLevel;
SumpFlowRate = sumpFlowRate;
SumpTemperature = sumpTemperature;
CirculationPumpPower = circulationPumpPower;
CirculationPumpEnergy = circulationPumpEnergy;
CirculationPumpGreenhouseGas = circulationPumpGreenhouseGas;
InverterControllerStatus = inverterControllerStatus;
FlowRate = flowRate;
HeatPumpPower = heatPumpPower;
HeatPumpTemperature = heatPumpTemperature;
HeatPumpEnergy = heatPumpEnergy;
HeatPumpGreenhouseGas = heatPumpGreenhouseGas;
AirBlowerPower = airBlowerPower;
AirBlowerEnergy = airBlowerEnergy;
AirBlowerGreenhouseGas = airBlowerGreenhouseGas;
}
}
[Owned]
public class SterilizingSystem
{
/// <summary>
/// 오존 발생기 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_ozone_generator_power")]
public bool OzoneGeneratorPower { get; set; }
/// <summary>
/// 오존 발생기 전력 (kW)
/// </summary>
[Column("ster_ozone_generator_energy")]
public double OzoneGeneratorEnergy { get; set; }
/// <summary>
/// 오존 발생기 온실가스 (tCO₂)
/// </summary>
[Column("ster_ozone_generator_greenhouse_gas")]
public double OzoneGeneratorGreenhouseGas { get; set; }
/// <summary>
/// 자외선 살균기 ID
/// </summary>
[Column("ster_uv_sterilizer_id")]
public string UVSterilizerId { get; set; }
/// <summary>
/// 자외선 살균기 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_uv_sterilizer_power")]
public bool UVSterilizerPower { get; set; }
/// <summary>
/// 자외선 살균기 전력 (kW)
/// </summary>
[Column("ster_uv_sterilizer_energy")]
public double UVSterilizerEnergy { get; set; }
/// <summary>
/// 자외선 살균기 온실가스 (tCO₂)
/// </summary>
[Column("ster_uv_sterilizer_greenhouse_gas")]
public double UVSterilizerGreenhouseGas { get; set; }
/// <summary>
/// 오존용해장치 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_ozone_dissolver_power")]
public bool OzoneDissolverPower { get; set; }
/// <summary>
/// 오존용해장치 압력 (kPa)
/// </summary>
[Column("ster_ozone_dissolver_pressure")]
public double OzoneDissolverPressure { get; set; }
/// <summary>
/// 오존용해장치 전력 (kW)
/// </summary>
[Column("ster_ozone_dissolver_energy")]
public double OzoneDissolverEnergy { get; set; }
/// <summary>
/// 오존용해장치 온실가스 (tCO₂)
/// </summary>
[Column("ster_ozone_dissolver_greenhouse_gas")]
public double OzoneDissolverGreenhouseGas { get; set; }
/// <summary>
/// 배오존장치 전원 (true: ON, false: OFF)
/// </summary>
[Column("ster_excess_ozone_destroyer_power")]
public bool ExcessOzoneDestroyerPower { get; set; }
/// <summary>
/// 배오존장치 전력 (kW)
/// </summary>
[Column("ster_excess_ozone_destroyer_energy")]
public double ExcessOzoneDestroyerEnergy { get; set; }
/// <summary>
/// 배오존장치 온실가스 (tCO₂)
/// </summary>
[Column("ster_excess_ozone_destroyer_greenhouse_gas")]
public double ExcessOzoneDestroyerGreenhouseGas { get; set; }
public SterilizingSystem() { }
public SterilizingSystem(
bool ozoneGeneratorPower, double ozoneGeneratorEnergy, double ozoneGeneratorGreenhouseGas,
string uvSterilizerId, bool uvSterilizerPower, double uvSterilizerEnergy, double uvSterilizerGreenhouseGas,
bool ozoneDissolverPower, double ozoneDissolverPressure, double ozoneDissolverEnergy, double ozoneDissolverGreenhouseGas,
bool excessOzoneDestroyerPower, double excessOzoneDestroyerEnergy, double excessOzoneDestroyerGreenhouseGas)
{
OzoneGeneratorPower = ozoneGeneratorPower;
OzoneGeneratorEnergy = ozoneGeneratorEnergy;
OzoneGeneratorGreenhouseGas = ozoneGeneratorGreenhouseGas;
UVSterilizerId = uvSterilizerId;
UVSterilizerPower = uvSterilizerPower;
UVSterilizerEnergy = uvSterilizerEnergy;
UVSterilizerGreenhouseGas = uvSterilizerGreenhouseGas;
OzoneDissolverPower = ozoneDissolverPower;
OzoneDissolverPressure = ozoneDissolverPressure;
OzoneDissolverEnergy = ozoneDissolverEnergy;
OzoneDissolverGreenhouseGas = ozoneDissolverGreenhouseGas;
ExcessOzoneDestroyerPower = excessOzoneDestroyerPower;
ExcessOzoneDestroyerEnergy = excessOzoneDestroyerEnergy;
ExcessOzoneDestroyerGreenhouseGas = excessOzoneDestroyerGreenhouseGas;
}
}
}

@ -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,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using SmartAquaViewer.ViewModel;
namespace SmartAquaViewer.Helper
{
public static class DataGridAutoPageSizeBehavior
{
public static readonly DependencyProperty EnableProperty =
DependencyProperty.RegisterAttached(
"Enable", typeof(bool), typeof(DataGridAutoPageSizeBehavior),
new PropertyMetadata(false, OnEnableChanged));
public static void SetEnable(DependencyObject d, bool v) => d.SetValue(EnableProperty, v);
public static bool GetEnable(DependencyObject d) => (bool)d.GetValue(EnableProperty);
public static readonly DependencyProperty SuspendProperty =
DependencyProperty.RegisterAttached("Suspend", typeof(bool), typeof(DataGridAutoPageSizeBehavior),
new PropertyMetadata(false));
public static void SetSuspend(DependencyObject d, bool v) => d.SetValue(SuspendProperty, v);
public static bool GetSuspend(DependencyObject d) => (bool)d.GetValue(SuspendProperty);
// 🔹 여기! 어느 페이저를 갱신할지 바인딩으로 지정
public static readonly DependencyProperty PagerProperty =
DependencyProperty.RegisterAttached(
"Pager", typeof(IPager), typeof(DataGridAutoPageSizeBehavior),
new PropertyMetadata(null));
public static void SetPager(DependencyObject d, IPager? v) => d.SetValue(PagerProperty, v);
public static IPager? GetPager(DependencyObject d) => (IPager?)d.GetValue(PagerProperty);
public static readonly DependencyProperty ThrottleMsProperty =
DependencyProperty.RegisterAttached("ThrottleMs", typeof(int), typeof(DataGridAutoPageSizeBehavior),
new PropertyMetadata(120));
public static void SetThrottleMs(DependencyObject d, int v) => d.SetValue(ThrottleMsProperty, v);
public static int GetThrottleMs(DependencyObject d) => (int)d.GetValue(ThrottleMsProperty);
private static readonly ConditionalWeakTable<DataGrid, DispatcherTimer> _timers = new();
private static void OnEnableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not DataGrid dg) return;
if ((bool)e.NewValue)
{
dg.SizeChanged += DataGrid_SizeChanged;
EnsureTimer(dg);
}
else
{
dg.SizeChanged -= DataGrid_SizeChanged;
if (_timers.TryGetValue(dg, out var t))
{
t.Stop();
_timers.Remove(dg);
}
}
}
private static void EnsureTimer(DataGrid dg)
{
if (_timers.TryGetValue(dg, out _)) return;
var timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(GetThrottleMs(dg)) };
timer.Tick += (_, __) =>
{
timer.Stop();
ApplyAutoPageSize(dg);
};
_timers.Add(dg, timer);
}
private static void DataGrid_SizeChanged(object sender, SizeChangedEventArgs e)
{
if (sender is not DataGrid dg) return;
// 1) 우선 명시된 Pager를 사용
var pager = GetPager(dg);
// 2) 없으면 DataContext에서 찾아봄 (메인 페이저 케이스)
pager ??= dg.DataContext as IPager;
if (pager == null || dg.RowHeight <= 0 || dg.ActualHeight <= 0) return;
double header = dg.ColumnHeaderHeight > 0 ? dg.ColumnHeaderHeight : 45;
double available = Math.Max(0, dg.ActualHeight - header);
int rows = Math.Max(1, (int)(available / dg.RowHeight));
if (pager.PageSize != rows)
pager.PageSize = rows; // 페이저 쪽에서 RebuildPage() 호출됨
}
private static void ApplyAutoPageSize(DataGrid dg)
{
if (GetSuspend(dg)) return; // 여기도 방어
if (dg.RowHeight <= 0 || dg.ActualHeight <= 0) return;
double header = dg.ColumnHeaderHeight > 0 ? dg.ColumnHeaderHeight : 30;
double available = Math.Max(0, dg.ActualHeight - header - 2);
int rows = Math.Max(1, (int)(available / dg.RowHeight));
IPager? pager = GetPager(dg) ?? dg.DataContext as IPager;
if (pager is null) return;
if (pager.PageSize != rows)
pager.PageSize = rows; // 내부에서 RebuildPage() 호출
}
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
internal static class Helper
{
public static unsafe string av_strerror(int error)
{
var bufferSize = 1024;
var buffer = stackalloc byte[bufferSize];
ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
return message;
}
public static int ThrowExceptionIfError(this int error)
{
if (error < 0) throw new ApplicationException(av_strerror(error));
return error;
}
}
}

@ -0,0 +1,396 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
using System.Windows;
using System.IO;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class StreamDecoder : IDisposable
{
public delegate void ThreadStoppedHandler(object obj);
public static event ThreadStoppedHandler hThreadStoppedHandler;
private readonly AVFormatContext* _pFormatContext;
public readonly AVFrame* _pFrame;
private readonly AVPacket* _pPacket;
AVCodec* codec;
public string CodecName { get; }
public Size FrameSize { get; }
public AVPixelFormat PixelFormat { get; }
public int videoStreamIndex { get; }
public AVCodecContext* _vcodecContext { get; }
public bool _isContextNotInitialized = false;
public double _fps;
private static bool _ffInited;
private static void EnsureFFmpegLoaded()
{
if (_ffInited) return;
// 실행폴더\ffmpeg 강제 지정 (RootPath는 어떤 ffmpeg.* 호출보다 먼저!)
var root = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ffmpeg");
ffmpeg.RootPath = root;
// 네트워크 초기화 (중복 호출 무해)
ffmpeg.avformat_network_init();
// 여기서 한번 강제로 심볼 로딩 검증
var ver = ffmpeg.av_version_info();
_ffInited = true;
}
private static void SetLogLevelSafe(int level)
{
try { ffmpeg.av_log_set_level(level); }
catch (NotSupportedException) { /* 일부 조합에서만 실패 → 그냥 무시 */ }
}
public StreamDecoder(string url)
{
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Show();
//});
lock (this)
{
AVFormatContext* pFormatContext = null;
AVCodecContext* vcodecContext = null;
AVDictionary* options = null;
AVStream* videoStream = null;
try
{
EnsureFFmpegLoaded(); // ★ 가장 첫 줄
SetLogLevelSafe(ffmpeg.AV_LOG_ERROR);
_pFormatContext = ffmpeg.avformat_alloc_context();
if (_pFormatContext == null)
{
throw new InvalidOperationException("Failed to allocate format context.");
}
pFormatContext = _pFormatContext;
// Get the codec context
_vcodecContext = ffmpeg.avcodec_alloc_context3(codec);
if (_vcodecContext == null)
{
// Out of memory
ffmpeg.avformat_close_input(&pFormatContext);
}
ffmpeg.av_dict_set(&options, "stimeout", "600000000", 0); // 60 seconds timeout
ffmpeg.av_dict_set(&options, "reconnect", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_at_eof", "1", 0);
ffmpeg.av_dict_set(&options, "reconnect_streamed", "1", 0);
ffmpeg.av_dict_set(&options, "rtsp_transport", "tcp", 0);
int ret = ffmpeg.avformat_open_input(&pFormatContext, url, null, &options).ThrowExceptionIfError();
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
ret = ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();
for (int i = 0; i < pFormatContext->nb_streams; i++)
{
if (pFormatContext->streams[i]->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
{
videoStream = pFormatContext->streams[i];
break;
}
}
if (videoStream == null)
{
Console.WriteLine("Failed to find video stream");
return;
}
AVRational frameRate = videoStream->avg_frame_rate;
_fps = (double)frameRate.num / frameRate.den;
videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
int context = ffmpeg.avcodec_parameters_to_context(_vcodecContext, _pFormatContext->streams[videoStreamIndex]->codecpar).ThrowExceptionIfError();
if (context < 0)
{
//Log4NetManager.GetLog().Error("StreamDecoder() : avcodec_parameters_to_context Error");
return;
}
if (videoStreamIndex >= 0)
{
AVCodecContext* avctx = OpenStream(_vcodecContext);
if (avctx == null)
{
throw new InvalidOperationException("Failed to open codec context.");
}
FrameSize = new Size(avctx->width, avctx->height);
PixelFormat = avctx->pix_fmt;
}
else
{
throw new InvalidOperationException("No video stream found.");
}
_pPacket = ffmpeg.av_packet_alloc();
if (_pPacket == null)
{
throw new InvalidOperationException("Failed to allocate packet.");
}
_pFrame = ffmpeg.av_frame_alloc();
if (_pFrame == null)
{
throw new InvalidOperationException("Failed to allocate frame.");
}
}
catch (Exception ex)
{
_isContextNotInitialized = true;
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
MessageBox.Show("StreamDecoder() : " + ex.Message);
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null)
{
vcodecContext = _vcodecContext;
ffmpeg.avcodec_close(vcodecContext);
ffmpeg.avcodec_free_context(&vcodecContext);
}
if (_pFormatContext != null)
{
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
//Log4NetManager.GetLog().Error("StreamDecoder() : " + ex.Message);
}
finally
{
if (options != null)
{
ffmpeg.av_dict_free(&options);
}
//Application.Current.Dispatcher.Invoke(() =>
//{
// ProgressWindow.GetInstance().Close();
//});
}
}
}
private AVCodecContext* OpenStream(AVCodecContext* avctx)
{
AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
if (codec == null) throw new InvalidOperationException("No codec could be found.");
avctx->codec_id = codec->id;
avctx->lowres = 0;
if (avctx->lowres > codec->max_lowres)
avctx->lowres = codec->max_lowres;
avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
avctx->error_concealment = 3;
ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();
return avctx;
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public void Dispose()
//{
// ffmpeg.av_frame_unref(_pFrame);
// ffmpeg.av_free(_pFrame);
// ffmpeg.av_packet_unref(_pPacket);
// ffmpeg.av_free(_pPacket);
// ffmpeg.avcodec_close(_vcodecContext);
// //ffmpeg.avcodec_close(acodecContext);
// var pFormatContext = _pFormatContext;
// ffmpeg.avformat_close_input(&pFormatContext);
//}
public void Dispose()
{
lock (this)
{
if (_pFrame != null)
{
ffmpeg.av_frame_unref(_pFrame);
ffmpeg.av_free(_pFrame);
}
if (_pPacket != null)
{
ffmpeg.av_packet_unref(_pPacket);
ffmpeg.av_free(_pPacket);
}
if (_vcodecContext != null && !_isContextNotInitialized)
{
ffmpeg.avcodec_close(_vcodecContext);
}
//ffmpeg.avcodec_close(acodecContext);
if (_pFormatContext != null && !_isContextNotInitialized)
{
var pFormatContext = _pFormatContext;
ffmpeg.avformat_close_input(&pFormatContext);
ffmpeg.avformat_free_context(pFormatContext);
}
}
}
//20240326 LHB - 원본. 안될경우 이 주석을 해제하고 아래의 함수와 교체하세요
//public bool TryDecodeNextFrame(out AVFrame frame)
//{
// ffmpeg.av_frame_unref(_pFrame);
// int error = 0;
// do
// {
// try
// {
// do
// {
// error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
// if (error == ffmpeg.AVERROR_EOF)
// {
// frame = *_pFrame;
// return false;
// }
// error.ThrowExceptionIfError();
// } while (_pPacket->stream_index != videoStreamIndex);
// ffmpeg.avcodec_send_packet(_vcodecContext, _pPacket).ThrowExceptionIfError();
// }
// catch (Exception ex)
// {
// Log4NetManager.GetLog().Error("TryDecodeNextFrame() : " + ex.Message);
// MessageBox.Show("TryDecodeNextFrame() : " + ex.Message);
// }
// finally
// {
// ffmpeg.av_packet_unref(_pPacket);
// }
// error = ffmpeg.avcodec_receive_frame(_vcodecContext, _pFrame);
// } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));
// error.ThrowExceptionIfError();
// frame = *_pFrame;
// return true;
//}
public bool TryDecodeNextFrame(out AVFrame frame)
{
ffmpeg.av_frame_unref(this._pFrame);
int error;
// repeated-try avcodec_receive_frame until enough packets haven been send
while (true)
{
// try read frame; maybe last avcodec_send_packet resulted in multiple frames to be read; will respond with EAGAIN if more packets are needed
error = ffmpeg.avcodec_receive_frame(this._vcodecContext, this._pFrame);
if (error != ffmpeg.AVERROR(ffmpeg.EAGAIN))
{
frame = *this._pFrame;
if (error == ffmpeg.AVERROR_EOF)
return false;
error.ThrowExceptionIfError();
return true;
}
try
{
// feed all stream-matching packets to decoder
while (true)
{
error = ffmpeg.av_read_frame(this._pFormatContext, this._pPacket);
if (error == ffmpeg.AVERROR_EOF)
{
// no more packets to read -> trigger draining/flushing of remaining frames
ffmpeg.avcodec_send_packet(this._vcodecContext, null).ThrowExceptionIfError();
break; // don't throw error, just exit packet-feed-loop
}
error.ThrowExceptionIfError();
if (this._pPacket->stream_index == this.videoStreamIndex)
{
var sendPacketResult = ffmpeg.avcodec_send_packet(this._vcodecContext, this._pPacket);
if (sendPacketResult == 0)
break;
// no reason to abort/crash (it was just an invalid packet from demuxer) -> just retrieve and feed next packet; most of time the codec will resume without problems
var errorMsg = Helper.av_strerror(sendPacketResult);
Trace.TraceError(errorMsg);
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : av_strerror 오류 " + errorMsg);
}
// unref all packets, no matter if sent to avcodec or ignored
ffmpeg.av_packet_unref(this._pPacket);
}
}
catch (Exception ex)
{
Debug.WriteLine("TryDecodeNextFrame()" + ex.Message);
if (ex.Message.Equals("End of file"))
{
//Log4NetManager.GetLog().Error("TryDecodeNextFrame()" + " : " + ex.Message);
}
//쓰레드가 망함,
}
finally
{
ffmpeg.av_packet_unref(this._pPacket);
}
}
}
public IReadOnlyDictionary<string, string> GetContextInfo()
{
AVDictionaryEntry* tag = null;
var result = new Dictionary<string, string>();
while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
{
var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
result.Add(key, value);
}
return result;
}
}
}

@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Imaging;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Media;
using FFmpeg.AutoGen;
namespace SmartAquaViewer.Helper.FFHelper
{
public sealed unsafe class VideoFrameConverter : IDisposable
{
private readonly IntPtr _convertedFrameBufferPtr;
private readonly System.Windows.Size _destinationSize;
private readonly byte_ptrArray4 _dstData;
private readonly int_array4 _dstLinesize;
private readonly SwsContext* _pConvertContext;
private bool _disposed = false;
public VideoFrameConverter(System.Windows.Size sourceSize, AVPixelFormat sourcePixelFormat,
System.Windows.Size destinationSize, AVPixelFormat destinationPixelFormat)
{
try
{
_destinationSize = destinationSize;
_pConvertContext = ffmpeg.sws_getContext(
(int)sourceSize.Width,
(int)sourceSize.Height,
sourcePixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height,
destinationPixelFormat,
ffmpeg.SWS_FAST_BILINEAR, null, null, null);
if (_pConvertContext == null)
throw new ApplicationException("Could not initialize the conversion context.");
var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixelFormat,
(int)destinationSize.Width, (int)destinationSize.Height, 1);
_convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
_dstData = new byte_ptrArray4();
_dstLinesize = new int_array4();
ffmpeg.av_image_fill_arrays(
ref _dstData,
ref _dstLinesize,
(byte*)_convertedFrameBufferPtr,
destinationPixelFormat,
(int)destinationSize.Width,
(int)destinationSize.Height, 1);
}
catch (Exception ex)
{
Debug.WriteLine("VideoFrameConverter() : " + ex.Message);
}
}
public void Dispose()
{
if (!_disposed)
{
if (_convertedFrameBufferPtr != IntPtr.Zero)
Marshal.FreeHGlobal(_convertedFrameBufferPtr);
if (_pConvertContext != null)
ffmpeg.sws_freeContext(_pConvertContext);
_disposed = true;
}
}
public AVFrame Convert(AVFrame sourceFrame)
{
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, _dstData, _dstLinesize);
var data = new byte_ptrArray8();
data.UpdateFrom(_dstData);
var linesize = new int_array8();
linesize.UpdateFrom(_dstLinesize);
return new AVFrame
{
data = data,
linesize = linesize,
width = (int)_destinationSize.Width,
height = (int)_destinationSize.Height
};
}
public unsafe Bitmap DeepCopyFrame(AVFrame sourceFrame)
{
if (_disposed)
throw new ObjectDisposedException(nameof(VideoFrameConverter));
AVFrame* dstFramePtr = ffmpeg.av_frame_alloc();
if (dstFramePtr == null)
{
throw new Exception("Failed to allocate destination frame");
}
try
{
dstFramePtr->format = (int)AVPixelFormat.AV_PIX_FMT_BGR24;
dstFramePtr->width = sourceFrame.width;
dstFramePtr->height = sourceFrame.height;
int ret = ffmpeg.av_frame_get_buffer(dstFramePtr, 32);
if (ret < 0)
{
throw new Exception($"Failed to allocate buffer for destination frame: {Helper.av_strerror(ret)}");
}
ret = ffmpeg.av_frame_make_writable(dstFramePtr);
if (ret < 0)
{
throw new Exception($"Failed to make frame writable: {Helper.av_strerror(ret)}");
}
ffmpeg.sws_scale(_pConvertContext,
sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, dstFramePtr->data, dstFramePtr->linesize);
ret = ffmpeg.av_frame_copy_props(dstFramePtr, &sourceFrame);
if (ret < 0)
{
throw new Exception($"Failed to copy frame properties: {Helper.av_strerror(ret)}");
}
dstFramePtr->pts = sourceFrame.pts;
dstFramePtr->pkt_dts = sourceFrame.pkt_dts;
dstFramePtr->best_effort_timestamp = sourceFrame.best_effort_timestamp;
var dstDataArr = new byte_ptrArray8();
dstDataArr.UpdateFrom(dstFramePtr->data);
var dstLinesizeArr = new int_array8();
dstLinesizeArr.UpdateFrom(dstFramePtr->linesize);
AVFrame frame = new AVFrame
{
data = dstDataArr,
linesize = dstLinesizeArr,
width = dstFramePtr->width,
height = dstFramePtr->height,
format = (int)AVPixelFormat.AV_PIX_FMT_BGR24,
pts = dstFramePtr->pts,
pkt_dts = dstFramePtr->pkt_dts,
best_effort_timestamp = dstFramePtr->best_effort_timestamp
};
Bitmap bitmap = new Bitmap(
dstFramePtr->width,
dstFramePtr->height,
dstFramePtr->linesize[0],
System.Drawing.Imaging.PixelFormat.Format24bppRgb,
(IntPtr)dstFramePtr->data[0]
);
// Bitmap의 데이터를 따로 복사
Bitmap finalBitmap = new Bitmap(bitmap);
bitmap.Dispose(); // 중간 비트맵 해제
return finalBitmap; // 데이터 복사된 최종 비트맵 리턴
}
catch (Exception ex)
{
//Log4NetManager.GetLog().Error($"Exception during frame copy: {ex.Message}");
throw;
}
finally
{
// 포인터 유효성 검사 후 안전하게 해제
if (dstFramePtr != null)
{
ffmpeg.av_frame_unref(dstFramePtr); // 참조 해제
ffmpeg.av_frame_free(&dstFramePtr); // 메모리 해제
}
}
}
}
}

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows;
namespace SmartAquaViewer.Helper
{
public static class ImageButtonHelper
{
public static readonly DependencyProperty ImageSourceProperty =
DependencyProperty.RegisterAttached(
"ImageSource",
typeof(ImageSource),
typeof(ImageButtonHelper),
new PropertyMetadata(null));
public static void SetImageSource(UIElement element, ImageSource value)
{
element.SetValue(ImageSourceProperty, value);
}
public static ImageSource GetImageSource(UIElement element)
{
return (ImageSource)element.GetValue(ImageSourceProperty);
}
}
}

@ -0,0 +1,150 @@
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;
using System.ComponentModel;
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)
{
foreach (var removed in e.RemovedItems) list.Remove(removed);
foreach (var added in e.AddedItems) list.Add(added);
}
// Dictionary 동기화도 같이 수행
var dict = GetSelectedDictionary(lb);
if (dict != null)
{
string? keyPath = GetKeyPath(lb);
string? valuePath = GetValuePath(lb);
foreach (var removed in e.RemovedItems)
{
var key = ResolvePath(removed, keyPath);
if (key != null && dict.Contains(key)) dict.Remove(key);
}
foreach (var added in e.AddedItems)
{
var key = ResolvePath(added, keyPath);
var value = ResolvePath(added, valuePath);
if (key != null) dict[key] = value;
}
}
}
// ===== IDictionary 버전 =====
public static readonly DependencyProperty SelectedDictionaryProperty =
DependencyProperty.RegisterAttached(
"SelectedDictionary", typeof(IDictionary), typeof(MultiSelectBehavior),
new PropertyMetadata(null, OnSelectedDictionaryChanged));
public static void SetSelectedDictionary(DependencyObject element, IDictionary? value)
=> element.SetValue(SelectedDictionaryProperty, value);
public static IDictionary? GetSelectedDictionary(DependencyObject element)
=> (IDictionary?)element.GetValue(SelectedDictionaryProperty);
// 선택된 항목에서 key/value를 뽑아낼 경로 (예: "Id", "Name", "User.Profile.Id")
public static readonly DependencyProperty KeyPathProperty =
DependencyProperty.RegisterAttached(
"KeyPath", typeof(string), typeof(MultiSelectBehavior), new PropertyMetadata(null));
public static void SetKeyPath(DependencyObject element, string? value)
=> element.SetValue(KeyPathProperty, value);
public static string? GetKeyPath(DependencyObject element)
=> (string?)element.GetValue(KeyPathProperty);
// 값 경로. 비우거나 "." 이면 아이템 자체를 값으로 사용
public static readonly DependencyProperty ValuePathProperty =
DependencyProperty.RegisterAttached(
"ValuePath", typeof(string), typeof(MultiSelectBehavior), new PropertyMetadata("."));
public static void SetValuePath(DependencyObject element, string? value)
=> element.SetValue(ValuePathProperty, value);
public static string? GetValuePath(DependencyObject element)
=> (string?)element.GetValue(ValuePathProperty);
private static void OnSelectedDictionaryChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not ListBox lb) return;
lb.SelectionChanged -= Lb_SelectionChanged;
lb.SelectionChanged += Lb_SelectionChanged;
if (e.NewValue is IDictionary dict)
{
dict.Clear();
string? keyPath = GetKeyPath(lb);
string? valuePath = GetValuePath(lb);
foreach (var item in lb.SelectedItems)
{
var key = ResolvePath(item, keyPath);
var value = ResolvePath(item, valuePath);
if (key != null) dict[key] = value;
}
}
}
// ===== 유틸 =====
private static object? ResolvePath(object? instance, string? path)
{
if (instance == null) return null;
if (string.IsNullOrWhiteSpace(path) || path == ".") return instance;
object? current = instance;
foreach (var segment in path.Split('.'))
{
if (current == null) return null;
// IDictionary인 중간 단계도 지원 (예: dict["Key"])
if (current is IDictionary dict)
{
if (dict.Contains(segment)) { current = dict[segment]; continue; }
return null;
}
var pd = TypeDescriptor.GetProperties(current)[segment];
if (pd == null) return null;
current = pd.GetValue(current);
}
return current;
}
}
}

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace SmartAquaViewer.Helper
{
public static class RadioButtonHelper
{
public static readonly DependencyProperty PressedImageProperty =
DependencyProperty.RegisterAttached(
"PressedImage",
typeof(ImageSource),
typeof(RadioButtonHelper),
new PropertyMetadata(null));
public static void SetPressedImage(DependencyObject element, ImageSource value)
=> element.SetValue(PressedImageProperty, value);
public static ImageSource GetPressedImage(DependencyObject element)
=> (ImageSource)element.GetValue(PressedImageProperty);
public static readonly DependencyProperty UnPressedImageProperty =
DependencyProperty.RegisterAttached(
"UnPressedImage",
typeof(ImageSource),
typeof(RadioButtonHelper),
new PropertyMetadata(null));
public static void SetUnPressedImage(DependencyObject element, ImageSource value)
=> element.SetValue(UnPressedImageProperty, value);
public static ImageSource GetUnPressedImage(DependencyObject element)
=> (ImageSource)element.GetValue(UnPressedImageProperty);
}
}

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.Classes;
namespace SmartAquaViewer.INI
{
class EmbeddedAssembly
{
static Dictionary<string, Assembly> dic = null;
/// <summary>
/// Load Assembly, DLL from Embedded Resources into memory.
/// </summary>
/// <param name="embeddedResource">Embedded Resource string. Example: WindowsFormsApplication1.SomeTools.dll</param>
/// <param name="fileName">File Name. Example: SomeTools.dll</param>
public static void Load(string embeddedResource, string fileName)
{
if (dic == null)
dic = new Dictionary<string, Assembly>();
byte[] ba = null;
Assembly asm = null;
Assembly curAsm = Assembly.GetExecutingAssembly();
using (Stream stm = curAsm.GetManifestResourceStream(embeddedResource))
{
// Either the file is not existed or it is not mark as embedded resource
if (stm == null)
throw new Exception(embeddedResource + " is not found in Embedded Resources.");
// Get byte[] from the file from embedded resource
ba = new byte[(int)stm.Length];
stm.Read(ba, 0, (int)stm.Length);
try
{
asm = Assembly.Load(ba);
// Add the assembly/dll into dictionary
dic.Add(asm.FullName, asm);
return;
}
catch
{
// Purposely do nothing
// Unmanaged dll or assembly cannot be loaded directly from byte[]
// Let the process fall through for next part
}
}
bool fileOk = false;
string tempFile = "";
using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
{
// Get the hash value from embedded DLL/assembly
string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);
// Define the temporary storage location of the DLL/assembly
tempFile = Path.GetTempPath() + fileName;
// Determines whether the DLL/assembly is existed or not
if (File.Exists(tempFile))
{
// Get the hash value of the existed file
byte[] bb = File.ReadAllBytes(tempFile);
string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);
// Compare the existed DLL/assembly with the Embedded DLL/assembly
if (fileHash == fileHash2)
{
// Same file
fileOk = true;
}
else
{
// Not same
fileOk = false;
}
}
else
{
// The DLL/assembly is not existed yet
fileOk = false;
}
}
// Create the file on disk
if (!fileOk)
{
File.WriteAllBytes(tempFile, ba);
}
// Load it into memory
asm = Assembly.LoadFile(tempFile);
// Add the loaded DLL/assembly into dictionary
dic.Add(asm.FullName, asm);
}
/// <summary>
/// Retrieve specific loaded DLL/assembly from memory
/// </summary>
/// <param name="assemblyFullName"></param>
/// <returns></returns>
public static Assembly Get(string assemblyFullName)
{
if (dic == null || dic.Count == 0)
return null;
if (dic.ContainsKey(assemblyFullName))
return dic[assemblyFullName];
return null;
// Don't throw Exception if the dictionary does not contain the requested assembly.
// This is because the event of AssemblyResolve will be raised for every
// Embedded Resources (such as pictures) of the projects.
// Those resources wil not be loaded by this class and will not exist in dictionary.
}
internal static void Copy(string embeddedResource, string fileName)
{
byte[] ba = null;
Assembly curAsm = Assembly.GetExecutingAssembly();
using (Stream stm = curAsm.GetManifestResourceStream(embeddedResource))
{
// Either the file is not existed or it is not mark as embedded resource
if (stm == null)
throw new Exception(embeddedResource + " is not found in Embedded Resources.");
// Get byte[] from the file from embedded resource
ba = new byte[(int)stm.Length];
stm.Read(ba, 0, (int)stm.Length);
}
bool fileOk = false;
using (SHA1CryptoServiceProvider sha1 = new SHA1CryptoServiceProvider())
{
// Get the hash value from embedded DLL/assembly
string fileHash = BitConverter.ToString(sha1.ComputeHash(ba)).Replace("-", string.Empty);
// Determines whether the DLL/assembly is existed or not
if (File.Exists(fileName))
{
// Get the hash value of the existed file
byte[] bb = File.ReadAllBytes(fileName);
string fileHash2 = BitConverter.ToString(sha1.ComputeHash(bb)).Replace("-", string.Empty);
// Compare the existed DLL/assembly with the Embedded DLL/assembly
if (fileHash == fileHash2)
{
// Same file
fileOk = true;
}
else
{
// Not same
fileOk = false;
}
}
else
{
// The DLL/assembly is not existed yet
fileOk = false;
}
}
// Create the file on disk
if (!fileOk)
{
try
{
File.WriteAllBytes(fileName, ba);
}
catch (Exception ex)
{
Utils.Instance.DebugWriteLine(ex.ToString());
}
}
}
}
}

@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.Classes;
namespace SmartAquaViewer.INI
{
class INIManager
{
private readonly string strINIPath;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern long WritePrivateProfileString(
string section, string key, string val, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
private static extern int GetPrivateProfileString(
string section, string key, string def, StringBuilder retVal, int size, string filePath);
public INIManager(String INIPath)
{
strINIPath = INIPath;
if (!ExistINI())
{
CreateIni();
}
else
{
CheckAndInsertIni();
}
}
public void CreateIni()
{
WriteValue("CONFIG", Constants.Config.TITLE, "SmartAquaViwer");
WriteValue("CONFIG", Constants.Config.DATA_FILE_PATH, Constants.Directories.DATA_FOLDER);
}
public bool ExistINI()
{
return File.Exists(strINIPath);
}
public void CheckAndInsertIni()
{
CheckAndInsert("CONFIG", Constants.Config.TITLE, "SmartAquaViwer");
CheckAndInsert("CONFIG", Constants.Config.DATA_FILE_PATH, Constants.Directories.DATA_FOLDER);
}
public void CheckAndInsert(String strSection, String strKey, String strValue)
{
string key = ReadValue(strSection, strKey);
if (key == null || key.Trim() == "")
{
WriteValue(strSection, strKey, strValue);
}
}
public void WriteValue(String strSection, String strKey, String strValue)
{
WritePrivateProfileString(strSection, strKey, strValue, strINIPath);
}
public void DeleteSection(String strSection)
{
WritePrivateProfileString(strSection, null, null, strINIPath);
}
public string ReadValue(String strSection, String Key)
{
return ReadValue(strSection, Key, "");
}
public string ReadValue(String strSection, String Key, String def)
{
var strValue = new StringBuilder(255);
int i = GetPrivateProfileString(strSection, Key, def, strValue, 255, strINIPath);
return strValue.ToString();
}
}
}

@ -4,9 +4,86 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SmartAquaViewer"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:view="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
Title="MainWindow" Height="1080" Width="1920" WindowStartupLocation="CenterScreen" Style="{StaticResource WindowChromeStyle}">
<Window.DataContext>
<vm:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.Background>
<ImageBrush ImageSource="./Resources/Images/background.png"></ImageBrush>
</Grid.Background>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="260"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="92"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Grid.ColumnSpan="2" Background="#1D3649"
BorderBrush="#325C80" BorderThickness="0 0 0 1">
<Grid>
<TextBlock x:Name="appTitle" Text="{Binding AppTitle}" Margin="20 0 0 0"
Style="{StaticResource TitleTextBlockStyle}"/>
<Image Source="/Resources/Images/NIFS_logo_w.png"
HorizontalAlignment="Center" VerticalAlignment="Stretch"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center" WindowChrome.IsHitTestVisibleInChrome="True">
<Button Style="{StaticResource WindowChromeButtonStyle}" x:Name="windowMin" Content="━" Click="WindowMin_Click" FontSize="14" FontWeight="Bold"></Button>
<Button Style="{StaticResource WindowChromeButtonStyle}" x:Name="windowMax" Click="WindowMax_Click">
<StackPanel Orientation="Vertical">
<Image Source="./Resources/Images/max.png" Margin="12 10"></Image>
</StackPanel>
</Button>
<Button Style="{StaticResource WindowChromeButtonStyle}" x:Name="windowNormal" Click="WindowNormal_Click" Visibility="Collapsed">
<StackPanel Orientation="Vertical">
<Image Source="./Resources/Images/normal.png" Margin="12 10 12 5"></Image>
</StackPanel>
</Button>
<Button Style="{StaticResource WindowChromeButtonStyle}" x:Name="windowClose" Content="X" Click="WindowClose_Click" FontSize="14"></Button>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1" Grid.RowSpan="2" Grid.ColumnSpan="2"
Background="#152935"/>
<view:FileListView Grid.Row="1" Grid.RowSpan="2" x:Name="fileListView" Loaded="fileListView_Loaded"/>
<Border Grid.Row="1" Grid.Column="1" Margin="24 12 24 0" CornerRadius="4">
<UniformGrid Columns="4">
<RadioButton x:Name="rdbtnMonitoing" GroupName="contentSwap" Content="모니터링" Tag="monitoring"
Style="{StaticResource RadioButtonTabStyle}" IsChecked="True" FontSize="30"
helper:RadioButtonHelper.UnPressedImage="/Resources/Images/tab_bg_off.png"
helper:RadioButtonHelper.PressedImage="/Resources/Images/tab_bg.png"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdbtnEnergy" GroupName="contentSwap" Content="에너지" Tag="energy"
Style="{StaticResource RadioButtonTabStyle}" Grid.Column="1" FontSize="30"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdBtnGreenHouseGas" GroupName="contentSwap" Content="온실가스" Tag="greenHouseGas"
Style="{StaticResource RadioButtonTabStyle}" Grid.Column="2" FontSize="30"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
<RadioButton x:Name="rdbtnCCTV" GroupName="contentSwap" Content="CCTV" Tag="cctv"
Style="{StaticResource RadioButtonTabStyle}" Grid.Column="3" FontSize="30"
Command="{Binding SwapViewCommand}"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource Self}}"/>
</UniformGrid>
</Border>
<ContentControl x:Name="contentControl" Content="{Binding SelectedViewModel}"
Grid.Row="2" Grid.Column="1" Margin="24 16 24 12"/>
</Grid>
</Window>

@ -8,6 +8,9 @@ using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using SmartAquaViewer.Classes;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer
{
@ -19,6 +22,63 @@ namespace SmartAquaViewer
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
StateChanged += MainWindow_StateChanged;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
//ConfigData.Instance.LoadConfig();
CreateDirectories();
Datas.Instance.GetCCTVInfoListFromJson();
}
private void CreateDirectories()
{
Utils.Instance.CreateDirectory(Constants.Directories.DATA_FOLDER);
}
private void WindowNormal_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Normal;
appTitle.Margin = new Thickness(20, 0, 0, 0);
}
private void WindowMax_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Maximized;
appTitle.Margin = new Thickness(20, 5, 0, 0);
}
private void WindowMin_Click(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void WindowClose_Click(object sender, RoutedEventArgs e)
{
Close();
}
private void MainWindow_StateChanged(object sender, EventArgs e)
{
if (WindowState == WindowState.Maximized)
{
windowNormal.Visibility = Visibility.Visible;
windowMax.Visibility = Visibility.Collapsed;
}
else if (WindowState == WindowState.Normal)
{
windowNormal.Visibility = Visibility.Collapsed;
windowMax.Visibility = Visibility.Visible;
}
}
private void fileListView_Loaded(object sender, RoutedEventArgs e)
{
}
}
}

@ -0,0 +1,80 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250811050443_InitialCreate")]
partial class InitialCreate
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalisys.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<double>("DO")
.HasColumnType("double");
b.Property<double>("ElectricalConductivity")
.HasColumnType("double");
b.Property<double>("FlowRate")
.HasColumnType("double");
b.Property<double>("ORP")
.HasColumnType("double");
b.Property<double>("PH")
.HasColumnType("double");
b.Property<double>("Salinity")
.HasColumnType("double");
b.Property<double>("TSS")
.HasColumnType("double");
b.Property<double>("Temperature")
.HasColumnType("double");
b.Property<DateTime>("Timestamp")
.HasColumnType("datetime(6)");
b.Property<double>("TotalNitrogen")
.HasColumnType("double");
b.Property<double>("TotalPhosphorus")
.HasColumnType("double");
b.Property<double>("Turbidity")
.HasColumnType("double");
b.Property<double>("WaterLevel")
.HasColumnType("double");
b.Property<int>("WaterTankNum")
.HasColumnType("int");
b.HasKey("Id");
b.ToTable("WaterQuality");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,53 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SmartAquaViewer.Migrations
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "WaterQuality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
WaterTankNum = table.Column<int>(type: "int", nullable: false),
DO = table.Column<double>(type: "double", nullable: false),
PH = table.Column<double>(type: "double", nullable: false),
Temperature = table.Column<double>(type: "double", nullable: false),
WaterLevel = table.Column<double>(type: "double", nullable: false),
FlowRate = table.Column<double>(type: "double", nullable: false),
ElectricalConductivity = table.Column<double>(type: "double", nullable: false),
Turbidity = table.Column<double>(type: "double", nullable: false),
Salinity = table.Column<double>(type: "double", nullable: false),
ORP = table.Column<double>(type: "double", nullable: false),
TSS = table.Column<double>(type: "double", nullable: false),
TotalNitrogen = table.Column<double>(type: "double", nullable: false),
TotalPhosphorus = table.Column<double>(type: "double", nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WaterQuality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WaterQuality");
}
}
}

@ -0,0 +1,190 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20250812043735_AddWaterQualityTable")]
partial class AddWaterQualityTable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("RecordedTime")
.HasColumnType("datetime(6)")
.HasColumnName("recorded_time");
b.HasKey("Id");
b.ToTable("water_quality");
});
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.OwnsOne("SmartAquaViewer.DataAnalysis.FilteringSystem", "Filtering", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("AirBlowerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_air_blower_power");
b1.Property<bool>("CirculationPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_circulation_pump_power");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("filter_flow_rate");
b1.Property<bool>("HeatPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_heat_pump_power");
b1.Property<double>("HeatPumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_heat_pump_temperature");
b1.Property<string>("InverterControllerStatus")
.HasColumnType("longtext")
.HasColumnName("filter_inverter_status");
b1.Property<bool>("SandFilterPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_sand_filter_power");
b1.Property<double>("SumpFlowRate")
.HasColumnType("double")
.HasColumnName("filter_sump_flow_rate");
b1.Property<double>("SumpORP")
.HasColumnType("double")
.HasColumnName("filter_sump_orp");
b1.Property<double>("SumpPH")
.HasColumnType("double")
.HasColumnName("filter_sump_ph");
b1.Property<double>("SumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_sump_temperature");
b1.Property<double>("SumpWaterLevel")
.HasColumnType("double")
.HasColumnName("filter_sump_water_level");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.SterilizingSystem", "Sterilizing", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("ExcessOzoneDestroyerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_excess_ozone_destroyer_power");
b1.Property<bool>("OzoneDissolverPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_dissolver_power");
b1.Property<double>("OzoneDissolverPressure")
.HasColumnType("double")
.HasColumnName("ster_ozone_dissolver_pressure");
b1.Property<bool>("OzoneGeneratorPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_generator_power");
b1.Property<string>("UVSterilizerId")
.IsRequired()
.HasColumnType("longtext")
.HasColumnName("ster_uv_sterilizer_id");
b1.Property<bool>("UVSterilizerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_uv_sterilizer_power");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.WaterTank", "Tank", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<double>("DOValue")
.HasColumnType("double")
.HasColumnName("tank_do_value");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("tank_flow_rate");
b1.Property<int>("Number")
.HasColumnType("int")
.HasColumnName("tank_number");
b1.Property<double>("ORP")
.HasColumnType("double")
.HasColumnName("tank_orp");
b1.Property<double>("PH")
.HasColumnType("double")
.HasColumnName("tank_ph");
b1.Property<double>("Temperature")
.HasColumnType("double")
.HasColumnName("tank_temperature");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.Navigation("Filtering")
.IsRequired();
b.Navigation("Sterilizing")
.IsRequired();
b.Navigation("Tank")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,93 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace SmartAquaViewer.Migrations
{
/// <inheritdoc />
public partial class AddWaterQualityTable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "WaterQuality");
migrationBuilder.CreateTable(
name: "water_quality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
recorded_time = table.Column<DateTime>(type: "datetime(6)", nullable: false),
tank_number = table.Column<int>(type: "int", nullable: false),
tank_do_value = table.Column<double>(type: "double", nullable: false),
tank_ph = table.Column<double>(type: "double", nullable: false),
tank_orp = table.Column<double>(type: "double", nullable: false),
tank_temperature = table.Column<double>(type: "double", nullable: false),
tank_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_sand_filter_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_sump_ph = table.Column<double>(type: "double", nullable: false),
filter_sump_orp = table.Column<double>(type: "double", nullable: false),
filter_sump_water_level = table.Column<double>(type: "double", nullable: false),
filter_sump_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_sump_temperature = table.Column<double>(type: "double", nullable: false),
filter_circulation_pump_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_inverter_status = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
filter_flow_rate = table.Column<double>(type: "double", nullable: false),
filter_heat_pump_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
filter_heat_pump_temperature = table.Column<double>(type: "double", nullable: false),
filter_air_blower_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_generator_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_uv_sterilizer_id = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
ster_uv_sterilizer_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_dissolver_power = table.Column<bool>(type: "tinyint(1)", nullable: false),
ster_ozone_dissolver_pressure = table.Column<double>(type: "double", nullable: false),
ster_excess_ozone_destroyer_power = table.Column<bool>(type: "tinyint(1)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_water_quality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "water_quality");
migrationBuilder.CreateTable(
name: "WaterQuality",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
DO = table.Column<double>(type: "double", nullable: false),
ElectricalConductivity = table.Column<double>(type: "double", nullable: false),
FlowRate = table.Column<double>(type: "double", nullable: false),
ORP = table.Column<double>(type: "double", nullable: false),
PH = table.Column<double>(type: "double", nullable: false),
Salinity = table.Column<double>(type: "double", nullable: false),
TSS = table.Column<double>(type: "double", nullable: false),
Temperature = table.Column<double>(type: "double", nullable: false),
Timestamp = table.Column<DateTime>(type: "datetime(6)", nullable: false),
TotalNitrogen = table.Column<double>(type: "double", nullable: false),
TotalPhosphorus = table.Column<double>(type: "double", nullable: false),
Turbidity = table.Column<double>(type: "double", nullable: false),
WaterLevel = table.Column<double>(type: "double", nullable: false),
WaterTankNum = table.Column<int>(type: "int", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_WaterQuality", x => x.Id);
})
.Annotation("MySql:CharSet", "utf8mb4");
}
}
}

@ -0,0 +1,187 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SmartAquaViewer.DataAnalisys;
#nullable disable
namespace SmartAquaViewer.Migrations
{
[DbContext(typeof(AppDbContext))]
partial class AppDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "8.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("RecordedTime")
.HasColumnType("datetime(6)")
.HasColumnName("recorded_time");
b.HasKey("Id");
b.ToTable("water_quality");
});
modelBuilder.Entity("SmartAquaViewer.DataAnalysis.WaterQualityVO", b =>
{
b.OwnsOne("SmartAquaViewer.DataAnalysis.FilteringSystem", "Filtering", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("AirBlowerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_air_blower_power");
b1.Property<bool>("CirculationPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_circulation_pump_power");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("filter_flow_rate");
b1.Property<bool>("HeatPumpPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_heat_pump_power");
b1.Property<double>("HeatPumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_heat_pump_temperature");
b1.Property<string>("InverterControllerStatus")
.HasColumnType("longtext")
.HasColumnName("filter_inverter_status");
b1.Property<bool>("SandFilterPower")
.HasColumnType("tinyint(1)")
.HasColumnName("filter_sand_filter_power");
b1.Property<double>("SumpFlowRate")
.HasColumnType("double")
.HasColumnName("filter_sump_flow_rate");
b1.Property<double>("SumpORP")
.HasColumnType("double")
.HasColumnName("filter_sump_orp");
b1.Property<double>("SumpPH")
.HasColumnType("double")
.HasColumnName("filter_sump_ph");
b1.Property<double>("SumpTemperature")
.HasColumnType("double")
.HasColumnName("filter_sump_temperature");
b1.Property<double>("SumpWaterLevel")
.HasColumnType("double")
.HasColumnName("filter_sump_water_level");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.SterilizingSystem", "Sterilizing", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<bool>("ExcessOzoneDestroyerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_excess_ozone_destroyer_power");
b1.Property<bool>("OzoneDissolverPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_dissolver_power");
b1.Property<double>("OzoneDissolverPressure")
.HasColumnType("double")
.HasColumnName("ster_ozone_dissolver_pressure");
b1.Property<bool>("OzoneGeneratorPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_ozone_generator_power");
b1.Property<string>("UVSterilizerId")
.IsRequired()
.HasColumnType("longtext")
.HasColumnName("ster_uv_sterilizer_id");
b1.Property<bool>("UVSterilizerPower")
.HasColumnType("tinyint(1)")
.HasColumnName("ster_uv_sterilizer_power");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.OwnsOne("SmartAquaViewer.DataAnalysis.WaterTank", "Tank", b1 =>
{
b1.Property<int>("WaterQualityVOId")
.HasColumnType("int");
b1.Property<double>("DOValue")
.HasColumnType("double")
.HasColumnName("tank_do_value");
b1.Property<double>("FlowRate")
.HasColumnType("double")
.HasColumnName("tank_flow_rate");
b1.Property<int>("Number")
.HasColumnType("int")
.HasColumnName("tank_number");
b1.Property<double>("ORP")
.HasColumnType("double")
.HasColumnName("tank_orp");
b1.Property<double>("PH")
.HasColumnType("double")
.HasColumnName("tank_ph");
b1.Property<double>("Temperature")
.HasColumnType("double")
.HasColumnName("tank_temperature");
b1.HasKey("WaterQualityVOId");
b1.ToTable("water_quality");
b1.WithOwner()
.HasForeignKey("WaterQualityVOId");
});
b.Navigation("Filtering")
.IsRequired();
b.Navigation("Sterilizing")
.IsRequired();
b.Navigation("Tank")
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public class CCTVInfo : INotifyPropertyChanged
{
public string? DeviceId { get; set; }
public string? DeviceName { get; set; }
public string? RtspUrl { get; set; }
private CCTVStatus? _status;
public CCTVStatus? Status
{
get => _status;
set
{
if (_status != value)
{
_status = value;
OnPropertyChanged();
}
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SmartAquaViewer.Classes;
namespace SmartAquaViewer.Model
{
public class ConfigData
{
public static ConfigData Instance { get; } = new ConfigData();
public string Title { get; set; }
public string DataFilePath { get; set; }
public ConfigData()
{
}
public void LoadConfig()
{
Title = Utils.Instance.GetTitle();
DataFilePath = Utils.Instance.GetDataFilePath();
}
}
}

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media.Media3D;
using Newtonsoft.Json;
using SmartAquaViewer.Classes;
using SmartAquaViewer.DataAnalysis;
using static MaterialDesignThemes.Wpf.Theme.ToolBar;
namespace SmartAquaViewer.Model
{
public class Datas : INotifyPropertyChanged
{
public static Datas Instance { get; } = new Datas();
public ObservableCollection<WaterQualityVO> WaterQualityList { get; set; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityView { get; }
public ObservableCollection<CCTVInfo> CctvInfoList { get; set; }
private Datas()
{
WaterQualityList = new ObservableCollection<WaterQualityVO>();
WaterQualityView = new ReadOnlyObservableCollection<WaterQualityVO>(WaterQualityList);
CctvInfoList = new ObservableCollection<CCTVInfo>();
}
public ObservableCollection<WaterQualityVO> GetWaterQualityVO()
{
return WaterQualityList;
}
public void SetWaterQualityVO(List<WaterQualityVO> sampleData)
{
WaterQualityList.Clear();
if (sampleData == null) return;
foreach (var data in sampleData)
WaterQualityList.Add(data);
OnPropertyChanged(nameof(WaterQualityList));
}
public void GetCCTVInfoListFromJson()
{
string cctvListPath = Utils.Instance.GetDataFileContentPath(Constants.DataFiles.CCTV_LIST);
if (!File.Exists(cctvListPath))
{
CreateAndSetMockUpCCTVInfoList(cctvListPath);
return;
}
string jsonString = File.ReadAllText(cctvListPath);
var cctvInfoList = JsonConvert.DeserializeObject<List<CCTVInfo>>(jsonString) ?? new List<CCTVInfo>();
CctvInfoList = new ObservableCollection<CCTVInfo>(cctvInfoList);
}
public void CreateAndSetMockUpCCTVInfoList(string filePath)
{
CctvInfoList.Clear();
CctvInfoList = new ObservableCollection<CCTVInfo>()
{
new()
{
DeviceId = "000001",
DeviceName = "CCTV 1",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-07",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000002",
DeviceName = "CCTV 2",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-08",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000003",
DeviceName = "CCTV 3",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-01",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000004",
DeviceName = "CCTV 4",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-02",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000005",
DeviceName = "CCTV 5",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-03",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000006",
DeviceName = "CCTV 6",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-04",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000007",
DeviceName = "CCTV 7",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-05",
Status = CCTVStatus.Disconnected
},
new()
{
DeviceId = "000008",
DeviceName = "CCTV 8",
RtspUrl = "rtsp://210.217.121.58:8554/CAM-06",
Status = CCTVStatus.Disconnected
}
};
string updatedJson = JsonConvert.SerializeObject(CctvInfoList, Formatting.Indented);
File.WriteAllText(filePath, updatedJson);
}
public ObservableCollection<CCTVInfo> GetCCTVInfoList()
{
return CctvInfoList;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged(string name) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public enum MonitorTab
{
Tank,
Filter,
Sterilizer
}
public enum GraphType
{
LINE,
BOX,
SCATTER,
STEP,
STACKAREA,
PIE
}
public enum StepFieldKind
{
Time,
Status, // 전원/상태
Sensor, // 센서 값
Energy, // 에너지 소비량
GHG // 온실가스 배출량
}
public enum PowerStatus
{
Off,
On
}
public enum DataType
{
Energy,
GreenhouseGas
}
public enum CCTVStatus
{
Disconnected,
Connected
}
}

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SmartAquaViewer.Model
{
public class FileModel
{
public string? Name { get; set; }
public DateTime RecordedDate { get; set; }
public string? Content { get; set; }
}
}

@ -0,0 +1,852 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:da="clr-namespace:SmartAquaViewer.DataAnalysis"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:shell="clr-namespace:System.Windows.Shell;assembly=PresentationFramework">
<classes:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<classes:InverseBoolConverter x:Key="InverseBoolConverter"/>
<classes:EnumEqualsConverter x:Key="EnumEqualsConverter"/>
<classes:BoolToPowerConverter x:Key="BoolToPowerConverter"/>
<classes:OneBasedConverter x:Key="OneBasedConverter"/>
<classes:PageIndexToDisplayConverter x:Key="PageIndexToDisplayConverter"/>
<classes:CurrentPageEqualsConverter x:Key="CurrentPageEqualsConverter"/>
<FontFamily x:Key="Pretendard_Thin">pack://application:,,,/Fonts/#Pretendard Thin</FontFamily>
<FontFamily x:Key="Pretendard_ExtraLight">pack://application:,,,/Fonts/#Pretendard ExtraLight</FontFamily>
<FontFamily x:Key="Pretendard_Light">pack://application:,,,/Fonts/#Pretendard Light</FontFamily>
<FontFamily x:Key="Pretendard_Regular">pack://application:,,,/Fonts/#Pretendard Regular</FontFamily>
<FontFamily x:Key="Pretendard_Medium">pack://application:,,,/Fonts/#Pretendard Medium</FontFamily>
<FontFamily x:Key="Pretendard_SemiBold">pack://application:,,,/Fonts/#Pretendard SemiBold</FontFamily>
<FontFamily x:Key="Pretendard_Bold">pack://application:,,,/Fonts/#Pretendard Bold</FontFamily>
<FontFamily x:Key="Pretendard_ExtraBold">pack://application:,,,/Fonts/#Pretendard ExtraBold</FontFamily>
<FontFamily x:Key="Pretendard_Black">pack://application:,,,/Fonts/#Pretendard Black</FontFamily>
<Style TargetType="{x:Type TextBlock}" x:Key="TitleTextBlockStyle">
<Setter Property="FontFamily" Value="{StaticResource Pretendard_SemiBold}"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style x:Key="WindowChromeStyle" TargetType="{x:Type Window}">
<Setter Property="shell:WindowChrome.WindowChrome">
<Setter.Value>
<shell:WindowChrome CaptionHeight="35"
CornerRadius="0"
GlassFrameThickness="0"
NonClientFrameEdges="None"
ResizeBorderThickness="0"
UseAeroCaptionButtons="True" />
</Setter.Value>
</Setter>
</Style>
<Style x:Key="WindowChromeButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Background" Value="#00000000"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="BorderBrush" Value="#00000000"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Width" Value="40"/>
<Setter Property="Height" Value="35"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="1">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#E8582B"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter Property="Background" Value="#D58C3A"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="RadioBtnContentTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
<Setter Property="Foreground" Value="#999"/>
<Style.Triggers>
<!-- 🔹 라디오버튼이 체크되었을 때 텍스트 색 변경 -->
<DataTrigger
Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=RadioButton}}"
Value="True">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="CheckBoxContentTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
<Setter Property="Foreground" Value="#999"/>
<Style.Triggers>
<!-- 🔹 라디오버튼이 체크되었을 때 텍스트 색 변경 -->
<DataTrigger
Binding="{Binding IsChecked, RelativeSource={RelativeSource AncestorType=CheckBox}}"
Value="True">
<Setter Property="Foreground" Value="White" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="FieldNameRunStyle" TargetType="{x:Type Run}">
<Setter Property="FontSize" Value="18"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
<Setter Property="Foreground" Value="#4178BE"/>
</Style>
<Style x:Key="FieldValueRunStyle" TargetType="{x:Type Run}">
<Setter Property="FontSize" Value="24"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_SemiBold}"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
</Style>
<Style x:Key="ImageRadioButtonStyle" TargetType="RadioButton">
<Setter Property="Foreground" Value="#999"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid>
<Image x:Name="ButtonImage" Stretch="Fill"
Source="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helper:RadioButtonHelper.UnPressedImage)}"/>
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.FontFamily="{StaticResource Pretendard_SemiBold}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="ButtonImage" Property="Source"
Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=(helper:RadioButtonHelper.PressedImage)}"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RadioButtonSegmentedStyle" TargetType="RadioButton">
<Setter Property="Foreground" Value="#999"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Width" Value="249"/>
<Setter Property="Height" Value="56"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Grid>
<Border Background="{TemplateBinding Background}" CornerRadius="30"
Width="{TemplateBinding Width}" Height="{TemplateBinding Height}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.FontFamily="{StaticResource Pretendard_SemiBold}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Background" Value="#4178BE"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="RadioButtonTabStyle" TargetType="RadioButton">
<Setter Property="Foreground" Value="#999"/>
<Setter Property="FontSize" Value="26"/>
<Setter Property="Background" Value="#325C80"/>
<Setter Property="Opacity" Value="0.4"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="RadioButton">
<Border Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
TextElement.FontFamily="{StaticResource Pretendard_SemiBold}"
TextElement.FontSize="{TemplateBinding FontSize}"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Setter Property="Opacity" Value="0.8"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="PageCommandButtonStyle"
TargetType="Button"
BasedOn="{StaticResource MaterialDesignFlatLightBgButton}">
<!-- 기본 속성 재정의 -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Width" Value="20"/>
<Setter Property="Height" Value="20"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Margin" Value="4 0"/>
<Setter Property="Opacity" Value="1"/>
</Style>
<Style x:Key="PageNumButtonStyle" TargetType="{x:Type Button}">
<Setter Property="Width" Value="24"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="#264A60" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="#264A60"/>
<Setter Property="Padding" Value="6 0"/>
<Setter Property="Margin" Value="6"/>
<Style.Triggers>
<DataTrigger Value="True">
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource CurrentPageEqualsConverter}">
<!-- 현재 버튼의 인덱스 (각 아이템, int) -->
<Binding />
<!-- TanksPager.PageIndex -->
<Binding Path="DataContext.PageIndex"
RelativeSource="{RelativeSource AncestorType=ItemsControl}" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="#325C80" />
<Setter Property="Foreground" Value="White" />
<Setter Property="BorderThickness" Value="0" />
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="ImageButtonStyle" TargetType="Button">
<Setter Property="Width" Value="58"/>
<Setter Property="Height" Value="42"/>
<Setter Property="Margin" Value="2.5 0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Image x:Name="PART_Image"
Source="{TemplateBinding helper:ImageButtonHelper.ImageSource}"
Stretch="Uniform"/>
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.8"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.6"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="PART_Image" Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TrapezoidButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Width" Value="92"/>
<Setter Property="Height" Value="30"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Polygon x:Name="shape"
Fill="#1D3649"
Points="85.6111,0 92,30 0,30 6.38889,0" />
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="shape" Property="Fill" Value="#264A63"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="shape" Property="Fill" Value="#162938"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="shape" Property="Fill" Value="#555"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="VerticalTrapezoidButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="92"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<Polygon x:Name="shape"
Fill="#1D3649"
Points="0,85.6111 30,92 30,0 0,6.38889" />
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="0"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="shape" Property="Fill" Value="#264A63"/>
</Trigger>
<Trigger Property="IsPressed" Value="True">
<Setter TargetName="shape" Property="Fill" Value="#162938"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter TargetName="shape" Property="Fill" Value="#555"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="LineComboBg" Color="#1D3649" />
<!-- 바 전체 배경 -->
<SolidColorBrush x:Key="LineComboText" Color="#999999" />
<!-- LINE 텍스트 색 -->
<SolidColorBrush x:Key="LineComboArrow" Color="#C0C3C6" />
<!-- 화살표 색 -->
<SolidColorBrush x:Key="LineComboHover" Color="#1D3E55" />
<SolidColorBrush x:Key="LineComboItemHover" Color="#325C80" />
<SolidColorBrush x:Key="LineComboItemSelected" Color="#325C80" />
<Style x:Key="ComboDropToggleStyle" TargetType="ToggleButton">
<Setter Property="Focusable" Value="False" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderThickness="0">
<ContentPresenter HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<ControlTemplate.Triggers>
<!-- 마우스 올렸을 때 살짝만 진하게, 싫으면 이 트리거 삭제 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bd" Property="Background"
Value="{StaticResource LineComboHover}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 아이템 스타일 (드롭다운 내부 항목) -->
<Style x:Key="LineComboBoxItemStyle" TargetType="ComboBoxItem">
<Setter Property="Foreground" Value="{StaticResource LineComboText}" />
<Setter Property="Background" Value="{StaticResource LineComboBg}" />
<Setter Property="Padding" Value="10,4" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBoxItem">
<Border x:Name="Bd"
Background="{TemplateBinding Background}"
BorderThickness="0 1 0 0"
BorderBrush="#264A60">
<ContentPresenter
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsHighlighted" Value="True">
<Setter TargetName="Bd" Property="Background"
Value="{StaticResource LineComboItemHover}" />
</Trigger>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="Bd" Property="Background"
Value="{StaticResource LineComboItemSelected}" />
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- 콤보박스 본체 스타일 -->
<Style x:Key="ComboBoxStyle" TargetType="ComboBox">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Background" Value="{StaticResource LineComboBg}"/>
<Setter Property="Foreground" Value="{StaticResource LineComboText}"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_SemiBold}"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Padding" Value="12 0" />
<Setter Property="ItemContainerStyle" Value="{StaticResource LineComboBoxItemStyle}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="32" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="ContentSite"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Content="{TemplateBinding SelectionBoxItem}"
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
RecognizesAccessKey="True" />
<ToggleButton x:Name="DropDownToggle"
Grid.Column="1"
Style="{StaticResource ComboDropToggleStyle}"
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay,
RelativeSource={RelativeSource TemplatedParent}}">
<Path Stroke="{StaticResource LineComboArrow}"
StrokeThickness="1.6"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M 3 4 L 8 9 L 13 4" />
</ToggleButton>
<Popup x:Name="PART_Popup"
Placement="Bottom"
AllowsTransparency="True"
Focusable="False"
IsOpen="{TemplateBinding IsDropDownOpen}"
PopupAnimation="Fade"
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<Border Background="{StaticResource LineComboBg}"
BorderThickness="0"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource TemplatedParent}}">
<!-- 🔴 여기: 흰 배경 안 나오게 -->
<ScrollViewer Background="{StaticResource LineComboBg}">
<StackPanel IsItemsHost="True" />
</ScrollViewer>
</Border>
</Popup>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.4" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- DataGrid -->
<Style TargetType="{x:Type DataGridCell}">
<Setter Property="Padding" Value="0" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type DataGridCell}">
<Grid>
<ContentPresenter Margin="0" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="DataGridStyle" TargetType="DataGrid">
<Setter Property="EnableRowVirtualization" Value="True"/>
<Setter Property="EnableColumnVirtualization" Value="True"/>
<Setter Property="VirtualizingStackPanel.IsVirtualizing" Value="True"/>
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling"/>
<Setter Property="ScrollViewer.CanContentScroll" Value="True"/>
<Setter Property="ScrollViewer.IsDeferredScrollingEnabled" Value="True"/>
<Setter Property="HeadersVisibility" Value="Column"/>
<Setter Property="AutoGenerateColumns" Value="False"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="CanUserAddRows" Value="False"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="HorizontalGridLinesBrush" Value="#767676"/>
<Setter Property="VerticalGridLinesBrush" Value="#767676"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="#999"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="TextElement.FontSize" Value="14"/>
</Style>
<Style x:Key="DataGridRowStyle" TargetType="{x:Type DataGridRow}">
<Setter Property="Background" Value="#292929"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
<Setter Property="Foreground" Value="#BDBDBD"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style x:Key="DataGridColumnHeaderStyle" TargetType="{x:Type DataGridColumnHeader}">
<Setter Property="Background" Value="#264A60"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_SemiBold}"/>
<Setter Property="Height" Value="44"/>
</Style>
<Style x:Key="HighlightLeftBorderHeaderStyle"
TargetType="{x:Type DataGridColumnHeader}"
BasedOn="{StaticResource DataGridColumnHeaderStyle}">
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="BorderThickness" Value="1 0 0 0"/>
</Style>
<Style x:Key="HighlightLeftBorderCellStyle"
TargetType="DataGridCell"
BasedOn="{StaticResource MaterialDesignDataGridCell}">
<Setter Property="BorderBrush" Value="White"/>
<Setter Property="BorderThickness" Value="1 0 0 0"/>
</Style>
<Style x:Key="DataGridElmenetStyle" TargetType="{x:Type TextBlock}">
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
</Style>
<Style x:Key="GridViewColumnHeaderStyle" TargetType="{x:Type GridViewColumnHeader}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
<Border BorderThickness="0,0,0,1" BorderBrush="#FFF"
Background="#1D3649"
Width="{TemplateBinding Width}" >
<TextBlock Text="{TemplateBinding Content}"
FontSize="14"
FontFamily="{StaticResource Pretendard_SemiBold}"
Foreground="White"
Padding="0"
TextAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="14" />
<Setter Property="Height" Value="40"/>
</Style>
<Style x:Key="GridViewTextBlockStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
</Style>
<Style TargetType="ListBoxItem" x:Key="TransparentListBoxItemStyle" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border Background="Transparent">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<!-- Hover 배경 제거 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Transparent"/>
</Trigger>
<!-- 선택 배경 제거 -->
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="Transparent"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBoxItem" x:Key="ListBoxItemStyle" BasedOn="{StaticResource MaterialDesignListBoxItem}">
<Setter Property="Background" Value="#1D3649"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Border x:Name="RootBorder"
Background="{TemplateBinding Background}"
CornerRadius="15"
Margin="0 0 4 8">
<ContentPresenter/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Opacity" Value="0.6"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SquareCheckToggleButtonStyle" TargetType="ToggleButton">
<!-- 기본 값들 -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Foreground" Value="#808890"/>
<!-- 기본 숫자색: 회색 -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<!-- 네모 체크박스 부분 -->
<Border x:Name="CheckBoxBorder"
Width="18"
Height="18"
Margin="0,0,4,0"
Background="Transparent"
BorderBrush="#294A63"
BorderThickness="1.5">
<!-- 체크 표시 -->
<Path x:Name="CheckMark"
Stroke="White"
StrokeThickness="2"
Data="M3,9 L7,13 L14,4"
Stretch="Uniform"
SnapsToDevicePixels="True"
Visibility="Collapsed"/>
</Border>
<!-- 숫자(컨텐츠) -->
<ContentPresenter x:Name="contentPresenter"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"/>
</StackPanel>
<ControlTemplate.Triggers>
<!-- 체크 되었을 때 -->
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBoxBorder"
Property="Background"
Value="#28567A"/>
<!-- 파란 배경 -->
<Setter TargetName="CheckBoxBorder"
Property="BorderBrush"
Value="#28567A"/>
<Setter TargetName="CheckMark"
Property="Visibility"
Value="Visible"/>
<Setter Property="Foreground" Value="White"/>
<!-- 숫자 흰색 -->
</Trigger>
<!-- 마우스 오버 효과 약간 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CheckBoxBorder"
Property="BorderBrush"
Value="#3A6C90"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CircleCheckToggleButtonStyle" TargetType="ToggleButton">
<!-- 기본 값들 -->
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="Foreground" Value="#999999"/>
<!-- 기본 숫자색: 회색 -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToggleButton">
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Left" VerticalAlignment="Center">
<!-- 네모 체크박스 부분 -->
<Border x:Name="CheckBoxBorder"
Width="18"
Height="18"
Background="Transparent"
BorderBrush="#264A60"
BorderThickness="1.5"
CornerRadius="9"
Margin="0 6 8 6">
<!-- 체크 표시 -->
<Path x:Name="CheckMark"
Stroke="White"
StrokeThickness="2"
Data="M3,9 L7,13 L14,4"
Stretch="Uniform"
SnapsToDevicePixels="True"
Visibility="Collapsed"/>
</Border>
<!-- 숫자(컨텐츠) -->
<ContentPresenter x:Name="contentPresenter"
VerticalAlignment="Center"
TextElement.Foreground="{TemplateBinding Foreground}"
TextElement.FontSize="12"
TextElement.FontFamily="{StaticResource Pretendard_Medium}"/>
</StackPanel>
<ControlTemplate.Triggers>
<!-- 체크 되었을 때 -->
<Trigger Property="IsChecked" Value="True">
<Setter TargetName="CheckBoxBorder"
Property="Background"
Value="#325C80"/>
<!-- 파란 배경 -->
<Setter TargetName="CheckBoxBorder"
Property="BorderBrush"
Value="#325C80"/>
<Setter TargetName="CheckMark"
Property="Visibility"
Value="Visible"/>
<Setter Property="Foreground" Value="White"/>
<!-- 숫자 흰색 -->
</Trigger>
<!-- 마우스 오버 효과 약간 -->
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="CheckBoxBorder"
Property="BorderBrush"
Value="#3A6C90"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<SolidColorBrush x:Key="CustomSliderBackgroundSolidColorBrush" Color="#1D3649" />
<SolidColorBrush x:Key="CustomSliderForegroundBrush" Color="#4178BE" />
<SolidColorBrush x:Key="CustomSliderThumBrush" Color="#E5E5E8" />
<Style x:Key="CustomSliderThumbStyle" TargetType="{x:Type Thumb}">
<Setter Property="Focusable" Value="false"/>
<Setter Property="SnapsToDevicePixels" Value="true"/>
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="Height" Value="16"/>
<Setter Property="Width" Value="16"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Thumb}">
<Canvas SnapsToDevicePixels="true">
<Border x:Name="Background"
Background="{StaticResource CustomSliderThumBrush}"
BorderBrush="#4dffffff"
BorderThickness="2"
Height="16" Width="16"
CornerRadius="25"/>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<ControlTemplate x:Key="CustomSliderControlTemplate" TargetType="{x:Type Slider}">
<Border Background="Transparent" SnapsToDevicePixels="True">
<Grid>
<DockPanel LastChildFill="True">
<Border x:Name="PART_SelectionRange" Height="8" ClipToBounds="True" Visibility="Visible">
<Rectangle Margin="0 0 -10 0" RadiusX="5" RadiusY="5" Fill="{StaticResource CustomSliderForegroundBrush}"/>
</Border>
<Border ClipToBounds="True" Height="8" Visibility="Visible">
<Rectangle Margin="0 0 0 0" RadiusX="5" RadiusY="5" Fill="{StaticResource CustomSliderBackgroundSolidColorBrush}" />
</Border>
</DockPanel>
<Track x:Name="PART_Track">
<Track.Thumb>
<Thumb Style="{StaticResource CustomSliderThumbStyle}" VerticalAlignment="Center"
OverridesDefaultStyle="True" Focusable="False" />
</Track.Thumb>
</Track>
</Grid>
</Border>
</ControlTemplate>
<Style x:Key="CustomSliderStyle" TargetType="{x:Type Slider}">
<Setter Property="Template" Value="{StaticResource CustomSliderControlTemplate}" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Height" Value="30" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="AutoToolTipPlacement" Value="None" />
<Setter Property="IsMoveToPointEnabled" Value="True" />
<Setter Property="IsSelectionRangeEnabled" Value="True" />
<Setter Property="SelectionStart" Value="0" />
<Setter Property="SelectionEnd" Value="{Binding Path=Value, RelativeSource={RelativeSource Self}}" />
<Setter Property="Stylus.IsPressAndHoldEnabled" Value="false" />
</Style>
<Style x:Key="GeneralButtonStyle" TargetType="Button" BasedOn="{StaticResource MaterialDesignFlatLightBgButton}">
<Setter Property="FontFamily" Value="{StaticResource Pretendard_SemiBold}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="16" />
<Setter Property="Background" Value="#325C80" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="Width" Value="120" />
<Setter Property="Height" Value="38" />
</Style>
<Style x:Key="CCTVTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource Pretendard_Medium}" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontSize" Value="16" />
</Style>
</ResourceDictionary>

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 986 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 958 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -6,6 +6,128 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<Platforms>AnyCPU;x64</Platforms>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<None Remove="Fonts\Pretendard-Black.otf" />
<None Remove="Fonts\Pretendard-Bold.otf" />
<None Remove="Fonts\Pretendard-ExtraBold.otf" />
<None Remove="Fonts\Pretendard-ExtraLight.otf" />
<None Remove="Fonts\Pretendard-Light.otf" />
<None Remove="Fonts\Pretendard-Medium.otf" />
<None Remove="Fonts\Pretendard-Regular.otf" />
<None Remove="Fonts\Pretendard-SemiBold.otf" />
<None Remove="Fonts\Pretendard-Thin.otf" />
<None Remove="Fonts\SCDream1.otf" />
<None Remove="Fonts\SCDream2.otf" />
<None Remove="Fonts\SCDream3.otf" />
<None Remove="Fonts\SCDream4.otf" />
<None Remove="Fonts\SCDream5.otf" />
<None Remove="Fonts\SCDream6.otf" />
<None Remove="Fonts\SCDream7.otf" />
<None Remove="Fonts\SCDream8.otf" />
<None Remove="Fonts\SCDream9.otf" />
<None Remove="Resources\Images\add_folder.png" />
<None Remove="Resources\Images\add_folder_gray.png" />
<None Remove="Resources\Images\arrow_down.png" />
<None Remove="Resources\Images\arrow_left.png" />
<None Remove="Resources\Images\arrow_right.png" />
<None Remove="Resources\Images\arrow_up.png" />
<None Remove="Resources\Images\background.png" />
<None Remove="Resources\Images\cctv.png" />
<None Remove="Resources\Images\file_logo.png" />
<None Remove="Resources\Images\ListImage.png" />
<None Remove="Resources\Images\max.png" />
<None Remove="Resources\Images\NIFS_logo_w.png" />
<None Remove="Resources\Images\normal.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_1.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_1_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_2.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_2_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_3.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_3_press.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_4.png" />
<None Remove="Resources\Images\SegmentedControl\select_btn_4_press.png" />
<None Remove="Resources\Images\tab_bg.png" />
<None Remove="Resources\Images\tab_bg_off.png" />
<None Remove="Resources\Images\top_bg.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FFmpeg.AutoGen" Version="4.4.0" />
<PackageReference Include="MaterialDesignColors" Version="5.2.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.2.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-rc.1.efcore.9.0.0" />
<PackageReference Include="System.Drawing.Common" Version="9.0.8" />
</ItemGroup>
<ItemGroup>
<Resource Include="Fonts\Pretendard-Black.otf" />
<Resource Include="Fonts\Pretendard-Bold.otf" />
<Resource Include="Fonts\Pretendard-ExtraBold.otf" />
<Resource Include="Fonts\Pretendard-ExtraLight.otf" />
<Resource Include="Fonts\Pretendard-Light.otf" />
<Resource Include="Fonts\Pretendard-Medium.otf" />
<Resource Include="Fonts\Pretendard-Regular.otf" />
<Resource Include="Fonts\Pretendard-SemiBold.otf" />
<Resource Include="Fonts\Pretendard-Thin.otf" />
<Resource Include="Fonts\SCDream1.otf" />
<Resource Include="Fonts\SCDream2.otf" />
<Resource Include="Fonts\SCDream3.otf" />
<Resource Include="Fonts\SCDream4.otf" />
<Resource Include="Fonts\SCDream5.otf" />
<Resource Include="Fonts\SCDream6.otf" />
<Resource Include="Fonts\SCDream7.otf" />
<Resource Include="Fonts\SCDream8.otf" />
<Resource Include="Fonts\SCDream9.otf" />
<Resource Include="Resources\Images\add_folder.png" />
<Resource Include="Resources\Images\add_folder_gray.png" />
<Resource Include="Resources\Images\arrow_down.png" />
<Resource Include="Resources\Images\arrow_left.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\arrow_right.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\arrow_up.png" />
<Resource Include="Resources\Images\background.png" />
<Resource Include="Resources\Images\cctv.png" />
<Resource Include="Resources\Images\file_logo.png" />
<Resource Include="Resources\Images\ListImage.png" />
<Resource Include="Resources\Images\max.png" />
<Resource Include="Resources\Images\NIFS_logo_w.png" />
<Resource Include="Resources\Images\normal.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_1.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_1_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_2.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_2_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_3.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_3_press.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_4.png" />
<Resource Include="Resources\Images\SegmentedControl\select_btn_4_press.png" />
<Resource Include="Resources\Images\tab_bg.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\tab_bg_off.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\top_bg.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Resource>
</ItemGroup>
</Project>

@ -0,0 +1,193 @@
<UserControl x:Class="SmartAquaViewer.View.CCTVView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:controls="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Border BorderBrush="#1D3649" BorderThickness="0 0 2 0">
<md:DrawerHost RightDrawerBackground="Transparent" IsRightDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Border>
<Grid Background="Transparent">
<ItemsControl ItemsSource="{Binding PlayerVMs}" Margin="0 0 46 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid x:Name="ugrdFFPlayer" Margin="-4"
Rows="{Binding RowCount}" Columns="{Binding ColumnCount}" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Margin" Value="4"/>
<!-- 원하는 간격값 -->
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:FFPlayerControl/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Grid HorizontalAlignment="Right">
<Button Name="btnVisibilityRight" Tag="right"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityRight}">
<Path Data="M13.0607 12.1111L8.11067 17.0611L9.52467 18.4751L15.8887 12.1111L9.52467 5.7471L8.11067 7.1611L13.0607 12.1111Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
<Button Name="btnVisibilityLeft" Tag="left"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityLeft}">
<Path Data="M11.1615 12.1111 L16.11153 17.0611 L14.69753 18.4751 L8.3335 12.1111 L14.69753 5.7471 L16.11153 7.1611 Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
</Grid>
</Border>
<md:DrawerHost.RightDrawerContent>
<Border Width="348" Background="#1D3649" CornerRadius="0 4 4 0">
<Border Margin="16" CornerRadius="4" Background="#152935"
BorderBrush="#325C80" BorderThickness="1">
<Grid Margin="24">
<Grid.RowDefinitions>
<RowDefinition Height="41"/>
<RowDefinition/>
<RowDefinition Height="81"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0 0 0 16" Width="250">
<Image Source="/Resources/Images/cctv.png"
VerticalAlignment="Center"
Width="20" Height="20"/>
<TextBlock Text="CCTV 목록" FontFamily="{StaticResource Pretendard_SemiBold}"
FontSize="18" FontWeight="Bold" Foreground="White"
VerticalAlignment="Center" Margin="8 0"/>
</StackPanel>
<ListView Grid.Row="1" Width="250"
ItemsSource="{Binding CCTVInfoList}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
BorderThickness="0" Background="Transparent">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListViewItem">
<Border x:Name="RowBorder"
BorderBrush="#767676"
BorderThickness="0 0 0 1"
Background="Transparent">
<GridViewRowPresenter
Content="{TemplateBinding Content}"
Columns="{Binding RelativeSource={RelativeSource AncestorType=ListView},
Path=View.Columns}"
VerticalAlignment="Center"
Height="40"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="RowBorder" Property="Background" Value="#2D4B63"/>
</Trigger>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="RowBorder" Property="Background" Value="#223E52"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ListView.ItemContainerStyle>
<ListView.View>
<GridView ColumnHeaderContainerStyle="{StaticResource GridViewColumnHeaderStyle}">
<GridViewColumn Header="상태" Width="60">
<GridViewColumn.CellTemplate>
<DataTemplate>
<Ellipse Width="14" Height="14">
<Ellipse.Style>
<Style TargetType="Ellipse">
<Setter Property="Fill" Value="Red"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Status}" Value="{x:Static model:CCTVStatus.Connected}">
<Setter Property="Fill" Value="LimeGreen"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
</Ellipse>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="ID" Width="95">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DeviceId}"
Style="{StaticResource GridViewTextBlockStyle}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="CCTV" Width="95">
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock
Text="{Binding DeviceName}"
Style="{StaticResource GridViewTextBlockStyle}"/>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
</GridView>
</ListView.View>
</ListView>
<!--하단 버튼-->
<Grid Grid.Row="2">
<Button Padding="0" Content="재생"
Style="{StaticResource GeneralButtonStyle}"
Margin="0 16 0 0"
Width="250" Height="41"
Visibility="{Binding BtnVisibilityPlay}"
HorizontalAlignment="Center" VerticalAlignment="Bottom"
Command="{Binding PlayAllCCTVCommand}">
<Button.ToolTip>
<ToolTip Content="CCTV 재생"/>
</Button.ToolTip>
</Button>
<Button Padding="0" Content="중지"
Style="{StaticResource GeneralButtonStyle}"
Margin="0 16 0 0"
Width="250" Height="41"
Visibility="{Binding BtnVisibilityStop}"
HorizontalAlignment="Stretch" VerticalAlignment="Bottom"
Command="{Binding StopAllCCTVCommand}">
<Button.ToolTip>
<ToolTip Content="CCTV 재생"/>
</Button.ToolTip>
</Button>
</Grid>
</Grid>
</Border>
</Border>
</md:DrawerHost.RightDrawerContent>
</md:DrawerHost>
</Border>
</UserControl>

@ -0,0 +1,87 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// CCTVView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class CCTVView : UserControl
{
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCancellationTokenSource;
private CancellationTokenSource _renderingCancellationTokenSource;
private bool _stopThread = false;
private ConcurrentQueue<Bitmap> _frameQueue = new ConcurrentQueue<Bitmap>();
private readonly object _lockObject = new object();
public CCTVView()
{
InitializeComponent();
}
public void StartMedia(string rtspURL)
{
ClosePlayer();
_stopThread = false;
_videoThread = new Thread(new ThreadStart(OpenMedia));
_renderingThread = new Thread(new ThreadStart(RenderImage));
_videoThread.Priority = ThreadPriority.Highest; // 우선순위 설정
_videoThread.Start();
_renderingThread.Start();
}
private void OpenMedia()
{
throw new NotImplementedException();
}
private void RenderImage()
{
throw new NotImplementedException();
}
public void ClosePlayer()
{
_stopThread = true;
lock (_lockObject)
{
if (_videoCancellationTokenSource != null)
{
_videoCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): videoThread 종료함");
}
if (_renderingCancellationTokenSource != null)
{
_renderingCancellationTokenSource.Cancel();
Debug.WriteLine("ClosePlayer(): RenderingThread 종료함");
}
}
}
}
}

@ -0,0 +1,507 @@
<UserControl x:Class="SmartAquaViewer.View.EnegyView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="128"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Margin="0 0 0 16" BorderBrush="#3E4C60" BorderThickness="1" CornerRadius="4" Background="#264A60">
<UniformGrid Columns="9">
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="총 소비 전력" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="모래여과기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalCirculationPumpEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="순환펌프" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalCirculationPumpEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="히트펌프" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalHeatPumpEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="에어브로와" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalAirBlowerEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="오존발생기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalOzoneGeneratorEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="자외선 살균기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalUVSterilizerEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="오존용해장치" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalOzoneDissolverEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border>
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="배오존장치" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalExcessOzoneDestroyerEnergy, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
</UniformGrid>
</Border>
<md:DrawerHost Grid.Row="1" RightDrawerBackground="Transparent" IsRightDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Border BorderBrush="#1D3649" BorderThickness="0 0 2 0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<DataGrid ItemsSource="{Binding PagedItems}"
Style="{StaticResource DataGridStyle}" Background="Transparent"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
helper:DataGridAutoPageSizeBehavior.Enable="True"
HorizontalAlignment="Stretch" Margin="0 0 16 0"
ColumnWidth="*" RowHeight="30">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간" Width="90"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.SandFilterEnergy, StringFormat=\{0:F2\}}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="모래" HorizontalAlignment="Center"/>
<TextBlock Text="여과기" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="순환펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.CirculationPumpEnergy, StringFormat=\{0:F2\}}"/>
<DataGridTextColumn Header="히트펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.HeatPumpEnergy, StringFormat=\{0:F2\}}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.AirBlowerEnergy, StringFormat=\{0:F2\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="에어" HorizontalAlignment="Center"/>
<TextBlock Text="브로와" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneGeneratorEnergy, StringFormat=\{0:F2\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="오존" HorizontalAlignment="Center"/>
<TextBlock Text="발생기" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.UVSterilizerEnergy, StringFormat=\{0:F2\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="자외선" HorizontalAlignment="Center"/>
<TextBlock Text="살균기" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneDissolverEnergy, StringFormat=\{0:F2\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="오존용해" HorizontalAlignment="Center"/>
<TextBlock Text="장치" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.ExcessOzoneDestroyerEnergy, StringFormat=\{0:F2\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="배오존" HorizontalAlignment="Center"/>
<TextBlock Text="장치" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="총 전력" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding TotalEnergy, StringFormat=\{0:F2\}}"/>
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="1" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Command="{Binding FirstPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageFirst"/>
</Button>
<Button Command="{Binding PrevPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronLeft"/>
</Button>
<ItemsControl ItemsSource="{Binding PageNumbers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Margin="32 0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.GoToPageCommand,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
Style="{StaticResource PageNumButtonStyle}">
<Button.Content>
<Binding Converter="{StaticResource PageIndexToDisplayConverter}" />
</Button.Content>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Command="{Binding NextPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronRight"/>
</Button>
<Button Command="{Binding LastPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageLast"/>
</Button>
</StackPanel>
</Grid>
<Grid Grid.Column="1">
<Button Name="btnVisibilityDown" Tag="right"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityRight}">
<Path Data="M13.0607 12.1111L8.11067 17.0611L9.52467 18.4751L15.8887 12.1111L9.52467 5.7471L8.11067 7.1611L13.0607 12.1111Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
<Button Name="btnVisibilityUp" Tag="left"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityLeft}">
<Path Data="M11.1615 12.1111 L16.11153 17.0611 L14.69753 18.4751 L8.3335 12.1111 L14.69753 5.7471 L16.11153 7.1611 Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
</Grid>
</Border>
<md:DrawerHost.RightDrawerContent>
<Border x:Name="RightDrawerRoot" Width="626" CornerRadius="0 4 4 0" Background="#1D3649">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="358"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border Margin="16 16 16 12" CornerRadius="4"
Background="#152935" BorderBrush="#325C80" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="62"/>
</Grid.RowDefinitions>
<StackPanel Margin="24 24 24 0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox Margin="0" Height="35" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
FontFamily="{StaticResource Pretendard_Medium}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="0 10 0 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLineNStackArea" 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="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenPie" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel>
<StackPanel Style="{StaticResource VisibleWhenLineNStackArea}">
<Grid Margin="0 0 0 10" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<TextBlock Text="{Binding SelectedXField.Display}" VerticalAlignment="Center"
Grid.Column="1"
FontSize="18" FontFamily="{StaticResource Pretendard_Medium}" Foreground="White"/>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Top" Foreground="White">
<Run Text="Y축" FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}"/>
<LineBreak/>
<Run Text="(복수 선택 가능)" FontSize="14"
Foreground="#767676" FontFamily="{StaticResource Pretendard_Medium}"/>
</TextBlock>
<ListBox ItemsSource="{Binding YFieldCandidates}"
Grid.Column="1"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignFilterChipListBox}"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Display}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource CircleCheckToggleButtonStyle}"
Margin="12 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource VisibleWhenPie}">
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드" VerticalAlignment="Top"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ListBox ItemsSource="{Binding YFieldCandidates}"
Grid.Column="1"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignFilterChipListBox}"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Display}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource CircleCheckToggleButtonStyle}"
Margin="12 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="집계 방식" VerticalAlignment="Top"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<RadioButton x:Name="rbStatus"
GroupName="pie" Margin="0 0 12 0"
Foreground="#325C80"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Converter={StaticResource InverseBoolConverter}, Mode=TwoWay}">
<TextBlock Text="합계" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
<RadioButton x:Name="pie"
GroupName="pie"
Foreground="#325C80"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Mode=TwoWay}">
<TextBlock Text="평균" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
</StackPanel>
</Grid>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="24 0 0 0" Grid.Row="1">
<CheckBox IsChecked="{Binding ShowMarkers}" Margin="0 0 12 0"
Foreground="#264A60">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="마커" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding ShowLegends}" Margin="0 0 12 0"
Foreground="#264A60">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="범례" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding IsDonut}" Margin="0 0 12 0"
Foreground="#264A60"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="도넛모드" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1"
HorizontalAlignment="Right" Margin="0 0 24 24">
<Button Content="그래프 생성"
Style="{StaticResource GeneralButtonStyle}"
Command="{Binding DrawGraphCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="2" Grid.Column="1" Grid.RowSpan="2" Margin="16 0 16 16" CornerRadius="4"
Background="#152935" BorderBrush="#325C80" BorderThickness="1">
<control:GraphControl
x:Name="graphControl"
Margin="16" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</Border>
</md:DrawerHost.RightDrawerContent>
</md:DrawerHost>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// EnegyView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class EnegyView : UserControl
{
public EnegyView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,112 @@
<UserControl x:Class="SmartAquaViewer.View.FileListView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:vm="clr-namespace:SmartAquaViewer.ViewModel"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="1040" d:DesignWidth="260">
<UserControl.DataContext>
<vm:FileListViewModel/>
</UserControl.DataContext>
<Border Background="#264A60">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<!-- Header Row -->
<RowDefinition Height="*"/>
<!-- File List Row -->
</Grid.RowDefinitions>
<Grid>
<StackPanel Orientation="Horizontal" Margin="24 0 0 0">
<Border Width="30" Height="30"
HorizontalAlignment="Left"
Margin="0 0 12 0"
Background="#4178BE"
CornerRadius="2">
<Image Source="/Resources/Images/ListImage.png"
VerticalAlignment="Center" HorizontalAlignment="Center"
Width="24" Height="24"/>
</Border>
<TextBlock Text="파일 목록" FontFamily="{StaticResource Pretendard_SemiBold}"
FontSize="24" FontWeight="Bold" Foreground="White"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</StackPanel>
<Button Width="36" Height="36"
Margin="24 0" Padding="0"
HorizontalAlignment="Right"
Background="Transparent"
BorderThickness="0"
Command="{Binding OpenFileDialogCommand}">
<Border Background="#325C80" CornerRadius="25"
Width="36" Height="36">
<Image Source="/Resources/Images/add_folder.png"
Width="24" Height="24"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
</Border>
</Button>
</Grid>
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
<StackPanel.Style>
<Style TargetType="StackPanel">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding FileList.Count}" Value="0">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</StackPanel.Style>
<Image Source="/Resources/Images/add_folder_gray.png" VerticalAlignment="Top"
Width="24" Height="24" Margin="0 0 8 0" Stretch="Fill"/>
<TextBlock Text="파일을 업로드하세요."
FontSize="20" Foreground="#999999"
FontFamily="{StaticResource Pretendard_Medium}"/>
</StackPanel>
<ListView
Grid.Row="1" Padding="0"
ItemsSource="{Binding FileList}"
SelectedItem="{Binding SelectedFile}"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
BorderThickness="0">
<ListView.ItemContainerStyle>
<Style TargetType="ListViewItem">
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
</ListView.ItemContainerStyle>
<ListView.ItemTemplate>
<DataTemplate>
<Border x:Name="bdRoot" Height="56" Margin="24 0 0 0">
<StackPanel Orientation="Horizontal">
<Image Source="/Resources/Images/file_logo.png"
Width="24" Height="24" Margin="0 0 8 0"/>
<TextBlock Text="{Binding Name}"
FontSize="20" Foreground="White"
FontFamily="{StaticResource Pretendard_Medium}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<DataTemplate.Triggers>
<DataTrigger
Value="True"
Binding="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListViewItem}}">
<Setter TargetName="bdRoot" Property="Background" Value="#325C80"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// FileListView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class FileListView : UserControl
{
public FileListView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,513 @@
<UserControl x:Class="SmartAquaViewer.View.GreenHouseView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<UserControl.Resources>
<classes:InverseBoolConverter x:Key="InverseBoolConverter"/>
<classes:OneBasedConverter x:Key="OneBasedConverter"/>
<classes:PageIndexToDisplayConverter x:Key="PageIndexToDisplayConverter"/>
<classes:CurrentPageEqualsConverter x:Key="CurrentPageEqualsConverter"/>
</UserControl.Resources>
<Border>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="128"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Border Margin="0 0 0 16" BorderBrush="#3E4C60" BorderThickness="1" CornerRadius="4" Background="#264A60">
<UniformGrid Columns="9">
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="총 배출량" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="모래여과기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalSandFilterGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="순환펌프" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalCirculationPumpGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="히트펌프" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalHeatPumpGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="에어브로와" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalAirBlowerGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="오존발생기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalOzoneGeneratorGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="자외선 살균기" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalUVSterilizerGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border BorderThickness="0 0 1 0" BorderBrush="#99325C80">
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="오존용해장치" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalOzoneDissolverGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
<Border>
<TextBlock VerticalAlignment="Center" TextAlignment="Center">
<Run Text="배오존장치" Style="{StaticResource FieldNameRunStyle}"/>
<LineBreak/>
<Run Text="{Binding TotalExcessOzoneDestroyerGreenhouseGas, StringFormat=\{0:F2\}}"
Style="{StaticResource FieldValueRunStyle}"/>
</TextBlock>
</Border>
</UniformGrid>
</Border>
<md:DrawerHost Grid.Row="1" RightDrawerBackground="Transparent" IsRightDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Border BorderBrush="#1D3649" BorderThickness="0 0 2 0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="40"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="30"/>
</Grid.ColumnDefinitions>
<DataGrid Style="{StaticResource DataGridStyle}" Background="Transparent"
ItemsSource="{Binding PagedItems}"
RowStyle="{StaticResource DataGridRowStyle}" ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
helper:DataGridAutoPageSizeBehavior.Enable="True"
Margin="0 0 16 0"
ColumnWidth="*" RowHeight="30"
HorizontalAlignment="Stretch">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.SandFilterGreenhouseGas, StringFormat=\{0:F3\}}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="모래" HorizontalAlignment="Center"/>
<TextBlock Text="여과기" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="순환펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.CirculationPumpGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn Header="히트펌프" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.HeatPumpGreenhouseGas, StringFormat=\{0:F3\}}"/>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Filtering.AirBlowerGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="에어" HorizontalAlignment="Center"/>
<TextBlock Text="브로와" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneGeneratorGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="오존" HorizontalAlignment="Center"/>
<TextBlock Text="발생기" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.UVSterilizerGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="자외선"/>
<TextBlock Text="살균기"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.OzoneDissolverGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="오존용해" HorizontalAlignment="Center"/>
<TextBlock Text="장치" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding Sterilizing.ExcessOzoneDestroyerGreenhouseGas, StringFormat=\{0:F3\}}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="배오존" HorizontalAlignment="Center"/>
<TextBlock Text="장치" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="총 배출량" ElementStyle="{StaticResource DataGridElmenetStyle}"
Binding="{Binding TotalGreenhouseGas, StringFormat=\{0:F3\}}"/>
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="1" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Command="{Binding FirstPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageFirst"/>
</Button>
<Button Command="{Binding PrevPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronLeft"/>
</Button>
<ItemsControl ItemsSource="{Binding PageNumbers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Margin="32 0"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.GoToPageCommand,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}"
Style="{StaticResource PageNumButtonStyle}">
<Button.Content>
<Binding Converter="{StaticResource PageIndexToDisplayConverter}" />
</Button.Content>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Command="{Binding NextPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronRight"/>
</Button>
<Button Command="{Binding LastPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageLast"/>
</Button>
</StackPanel>
</Grid>
<Grid Grid.Column="1">
<Button Name="btnVisibilityDown" Tag="right"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityRight}">
<Path Data="M13.0607 12.1111L8.11067 17.0611L9.52467 18.4751L15.8887 12.1111L9.52467 5.7471L8.11067 7.1611L13.0607 12.1111Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
<Button Name="btnVisibilityUp" Tag="left"
Style="{StaticResource VerticalTrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Top" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityLeft}">
<Path Data="M11.1615 12.1111 L16.11153 17.0611 L14.69753 18.4751 L8.3335 12.1111 L14.69753 5.7471 L16.11153 7.1611 Z"
Fill="White" Stretch="Uniform"
Width="7.78" Height="12.728"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
</Grid>
</Border>
<md:DrawerHost.RightDrawerContent>
<Border x:Name="RightDrawerRoot" Width="626" CornerRadius="0 4 4 0" Background="#1D3649">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="358"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Border Margin="16 16 16 12" CornerRadius="4"
Background="#152935" BorderBrush="#325C80" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="62"/>
</Grid.RowDefinitions>
<StackPanel Margin="24 24 24 0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox Height="35" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
FontFamily="{StaticResource Pretendard_Medium}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="0 10 0 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStackArea" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenLineNStackArea" 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="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenPie" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel>
<StackPanel Style="{StaticResource VisibleWhenLineNStackArea}">
<Grid Margin="0 0 0 10" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<TextBlock Text="{Binding SelectedXField.Display}" VerticalAlignment="Center"
Grid.Column="1"
FontSize="18" FontFamily="{StaticResource Pretendard_Medium}" Foreground="White"/>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Top" Foreground="White">
<Run Text="Y축" FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}"/>
<LineBreak/>
<Run Text="(복수 선택 가능)" FontSize="14"
Foreground="#767676" FontFamily="{StaticResource Pretendard_Medium}"/>
</TextBlock>
<ListBox ItemsSource="{Binding YFieldCandidates}"
Grid.Column="1"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignFilterChipListBox}"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Display}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource CircleCheckToggleButtonStyle}"
Margin="12 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</StackPanel>
<StackPanel Style="{StaticResource VisibleWhenPie}">
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드" VerticalAlignment="Top"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ListBox ItemsSource="{Binding YFieldCandidates}"
Grid.Column="1"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignFilterChipListBox}"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Display}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource CircleCheckToggleButtonStyle}"
Margin="12 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="146"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="집계 방식" VerticalAlignment="Top"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<StackPanel Orientation="Horizontal" Grid.Column="1">
<RadioButton x:Name="rbStatus"
GroupName="pie" Margin="0 0 12 0"
Foreground="#325C80"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Converter={StaticResource InverseBoolConverter}, Mode=TwoWay}">
<TextBlock Text="합계" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
<RadioButton x:Name="pie"
GroupName="pie"
Foreground="#325C80"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding UseAverage, Mode=TwoWay}">
<TextBlock Text="평균" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
</StackPanel>
</Grid>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="24 0 0 0" Grid.Row="1">
<CheckBox IsChecked="{Binding ShowMarkers}" Margin="0 0 12 0" Foreground="#264A60">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="마커" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding ShowLegends}" Margin="0 0 12 0" Foreground="#264A60">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="STACKAREA">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="범례" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding IsDonut}" Margin="0 0 12 0"
Foreground="#264A60"
VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="PIE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="도넛모드" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0 0 24 24"
Grid.Row="1" HorizontalAlignment="Right">
<Button Content="그래프 생성"
Style="{StaticResource GeneralButtonStyle}"
FontWeight="Bold" Command="{Binding DrawGraphCommand}"/>
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="2" Grid.Column="1" Grid.RowSpan="2" Margin="16 0 16 16" CornerRadius="4"
Background="#152935" BorderBrush="#325C80" BorderThickness="1">
<control:GraphControl
x:Name="graphControl"
Margin="16" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</Border>
</md:DrawerHost.RightDrawerContent>
</md:DrawerHost>
</Grid>
</Border>
</UserControl>

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace SmartAquaViewer.View
{
/// <summary>
/// GreenHouseView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class GreenHouseView : UserControl
{
public GreenHouseView()
{
InitializeComponent();
}
}
}

@ -0,0 +1,878 @@
<UserControl x:Class="SmartAquaViewer.View.MonitoringView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:SmartAquaViewer.View"
xmlns:control="clr-namespace:SmartAquaViewer.Controls"
xmlns:helper="clr-namespace:SmartAquaViewer.Helper"
xmlns:model="clr-namespace:SmartAquaViewer.Model"
xmlns:classes="clr-namespace:SmartAquaViewer.Classes"
xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="940" d:DesignWidth="1650">
<Border>
<md:DrawerHost BottomDrawerBackground="Transparent" IsBottomDrawerOpen="{Binding IsOpenMode}" OpenMode="Standard">
<Border BorderBrush="#1D3649" BorderThickness="0 0 0 2">
<Grid Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="*"/>
<RowDefinition Height="60"/>
</Grid.RowDefinitions>
<Grid Margin="0 0 0 12">
<!--<Grid.Background>
<ImageBrush ImageSource="/Resources/Images/top_bg.png" Stretch="Fill"/>
</Grid.Background>-->
<control:SegmentedControl x:Name="segmentedControl" HorizontalAlignment="Left"
SelectedTab="{Binding SelectedTab, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Grid>
<DataGrid x:Name="dgTanks" ItemsSource="{Binding TanksPager.PagedItems}"
Style="{StaticResource DataGridStyle}"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}"
helper:DataGridAutoPageSizeBehavior.Enable="True"
helper:DataGridAutoPageSizeBehavior.Pager="{Binding TanksPager}"
helper:DataGridAutoPageSizeBehavior.Suspend="{Binding IsOpenMode}"
helper:DataGridAutoPageSizeBehavior.ThrottleMs="120"
Grid.Row="1" Margin="0"
ColumnWidth="*" RowHeight="30"
HorizontalAlignment="Stretch">
<DataGrid.Columns>
<!-- 측정 시각 -->
<DataGridTextColumn
Header="시간" Width="84.8"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<!-- Tank 값들 -->
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[0].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[0].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="DO" HorizontalAlignment="Center"/>
<TextBlock Text="(mg/L)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[0].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[0].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="ORP" HorizontalAlignment="Center"/>
<TextBlock Text="(mV)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[0].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="온도" HorizontalAlignment="Center"/>
<TextBlock Text="(℃)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[0].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="유량" HorizontalAlignment="Center"/>
<TextBlock Text="(m³/s)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[1].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[1].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="DO" HorizontalAlignment="Center"/>
<TextBlock Text="(mg/L)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[1].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[1].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="ORP" HorizontalAlignment="Center"/>
<TextBlock Text="(mV)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[1].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="온도" HorizontalAlignment="Center"/>
<TextBlock Text="(℃)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[1].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="유량" HorizontalAlignment="Center"/>
<TextBlock Text="(m³/s)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="번호" Binding="{Binding Tanks[2].Number}"
ElementStyle="{StaticResource DataGridElmenetStyle}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[2].DOValue, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="DO" HorizontalAlignment="Center"/>
<TextBlock Text="(mg/L)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Header="pH" Binding="{Binding Tanks[2].PH, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Binding="{Binding Tanks[2].ORP, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="ORP" HorizontalAlignment="Center"/>
<TextBlock Text="(mV)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[2].Temperature, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="온도" HorizontalAlignment="Center"/>
<TextBlock Text="(℃)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Tanks[2].FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="유량" HorizontalAlignment="Center"/>
<TextBlock Text="(m³/s)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource="{Binding PagedItems}" x:Name="dgFilter"
Style="{StaticResource DataGridStyle}"
ColumnWidth="*" RowHeight="30"
helper:DataGridAutoPageSizeBehavior.Enable="True"
helper:DataGridAutoPageSizeBehavior.Suspend="{Binding IsOpenMode}"
helper:DataGridAutoPageSizeBehavior.ThrottleMs="120"
Grid.Row="1" Margin="0"
Background="Transparent"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Binding="{Binding Filtering.SandFilterPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="모래여과기" HorizontalAlignment="Center"/>
<TextBlock Text="전원" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.SumpPH}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="섬프탱크" HorizontalAlignment="Center"/>
<TextBlock Text="pH" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.SumpORP, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="섬프탱크" HorizontalAlignment="Center"/>
<TextBlock Text="ORP(mV)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.SumpWaterLevel, StringFormat=\{0:F0\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="섬프탱크" HorizontalAlignment="Center"/>
<TextBlock Text="수위(m)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.SumpFlowRate, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="섬프탱크" HorizontalAlignment="Center"/>
<TextBlock Text="유량(m³/s)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.SumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="섬프탱크" HorizontalAlignment="Center"/>
<TextBlock Text="수온(°C)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.CirculationPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="순환펌프" HorizontalAlignment="Center"/>
<TextBlock Text="전원" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.InverterControllerStatus}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="인버터 제어기" HorizontalAlignment="Center"/>
<TextBlock Text="상태" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.FlowRate, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="순환펌프" HorizontalAlignment="Center"/>
<TextBlock Text="유량(m³/s)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.HeatPumpPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="히트펌프" HorizontalAlignment="Center"/>
<TextBlock Text="전원" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.HeatPumpTemperature, StringFormat=\{0:F2\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="히트펌프" HorizontalAlignment="Center"/>
<TextBlock Text="온도(°C)" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding Filtering.AirBlowerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}">
<DataGridTextColumn.Header>
<StackPanel>
<TextBlock Text="에어브로와" HorizontalAlignment="Center"/>
<TextBlock Text="전원" HorizontalAlignment="Center"/>
</StackPanel>
</DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<DataGrid ItemsSource="{Binding PagedItems}" x:Name="dgSterilizer"
Style="{StaticResource DataGridStyle}"
ColumnWidth="*" RowHeight="30"
helper:DataGridAutoPageSizeBehavior.Enable="True"
helper:DataGridAutoPageSizeBehavior.Pager="{Binding TanksPager}"
helper:DataGridAutoPageSizeBehavior.Suspend="{Binding IsOpenMode}"
Grid.Row="1" Margin="0"
VerticalScrollBarVisibility="Auto"
RowStyle="{StaticResource DataGridRowStyle}"
ColumnHeaderStyle="{StaticResource DataGridColumnHeaderStyle}">
<DataGrid.Columns>
<DataGridTextColumn
Header="시간"
Binding="{Binding RecordedTime, StringFormat=\{0:HH:mm:ss\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존 발생기 전원" Binding="{Binding Sterilizing.OzoneGeneratorPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"
CellStyle="{StaticResource HighlightLeftBorderCellStyle}"
HeaderStyle="{StaticResource HighlightLeftBorderHeaderStyle}"/>
<DataGridTextColumn Header="자외선 살균기 ID" Binding="{Binding Sterilizing.UVSterilizerId}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="자외선 살균기 전원" Binding="{Binding Sterilizing.UVSterilizerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 전원" Binding="{Binding Sterilizing.OzoneDissolverPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="오존용해장치 압력(kPa)" Binding="{Binding Sterilizing.OzoneDissolverPressure, StringFormat=\{0:F1\}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
<DataGridTextColumn Header="배오존장치 전원" Binding="{Binding Sterilizing.ExcessOzoneDestroyerPower, Converter={StaticResource BoolToPowerConverter}}"
ElementStyle="{StaticResource DataGridElmenetStyle}"/>
</DataGrid.Columns>
</DataGrid>
<Grid Grid.Row="2">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="{x:Static model:MonitorTab.Tank}">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Command="{Binding TanksPager.FirstPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageFirst"/>
</Button>
<Button Command="{Binding TanksPager.PrevPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronLeft"/>
</Button>
<ItemsControl DataContext="{Binding TanksPager}"
ItemsSource="{Binding PageNumbers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Margin="32 0"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource PageNumButtonStyle}"
Command="{Binding DataContext.GoToPageCommand,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}">
<Button.Content>
<!-- 각 아이템은 int(0,1,2,...) 이므로 그대로 컨버터 -->
<Binding Converter="{StaticResource PageIndexToDisplayConverter}" />
</Button.Content>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Command="{Binding TanksPager.NextPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronRight"/>
</Button>
<Button Command="{Binding TanksPager.LastPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageLast"/>
</Button>
</StackPanel>
</Grid>
<!-- (2) 필터/살균기 공용 페이지바 -->
<Grid Grid.Row="2">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="{x:Static model:MonitorTab.Tank}">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Command="{Binding FirstPageCommand}"
Style="{StaticResource PageCommandButtonStyle}"
Margin="4,0">
<md:PackIcon Kind="PageFirst"/>
</Button>
<Button Command="{Binding PrevPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronLeft"/>
</Button>
<ItemsControl ItemsSource="{Binding PageNumbers}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Margin="32 0"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Style="{StaticResource PageNumButtonStyle}"
Command="{Binding DataContext.GoToPageCommand,
RelativeSource={RelativeSource AncestorType=ItemsControl}}"
CommandParameter="{Binding}">
<Button.Content>
<Binding Converter="{StaticResource PageIndexToDisplayConverter}" />
</Button.Content>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Command="{Binding NextPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="ChevronRight"/>
</Button>
<Button Command="{Binding LastPageCommand}"
Style="{StaticResource PageCommandButtonStyle}">
<md:PackIcon Kind="PageLast"/>
</Button>
</StackPanel>
</Grid>
<Grid Grid.Row="3" VerticalAlignment="Bottom">
<Button Name="btnVisibilityDown" Tag="down"
Style="{StaticResource TrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityDown}">
<Path Data="M6.364 4.94983L1.414 -0.000167768L3.54069e-07 1.41383L6.364 7.77783L12.728 1.41383L11.314 -0.000167217L6.364 4.94983Z"
Fill="White" Stretch="Uniform"
Width="12.728" Height="7.78"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
<Button Name="btnVisibilityUp" Tag="up"
Style="{StaticResource TrapezoidButtonStyle}" Command="{Binding ChangeDrawerStatusCommand}"
VerticalAlignment="Bottom" HorizontalAlignment="Left" Visibility="{Binding BtnVisibilityUp}">
<Path Data="M6.364 2.828L1.414 7.778L0 6.364L6.364 0L12.728 6.364L11.314 7.778Z"
Fill="White" Stretch="Uniform"
Width="12.728" Height="7.78"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Button>
</Grid>
</Grid>
</Border>
<md:DrawerHost.BottomDrawerContent>
<Border x:Name="BottomDrawerRoot" Height="400" CornerRadius="0 0 4 4" Background="#1D3649">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="520"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Margin="16 16 12 16" CornerRadius="5"
Background="#152935" BorderBrush="#325C80" BorderThickness="1" >
<Grid>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="62"/>
</Grid.RowDefinitions>
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<Style x:Key="VisibleWhenTank" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="Tank">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<StackPanel>
<Grid Margin="24 24 24 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="그래프" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="35" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
FontFamily="{StaticResource Pretendard_Medium}"
ItemsSource="{Binding GraphTypes}"
SelectedIndex="{Binding SelectedGraphIndex, Mode=TwoWay}"
helper:ComboBoxHelper.SelectFirstOnItemsChange="True"
IsEditable="False" IsTextSearchEnabled="False"/>
</Grid>
<Grid Margin="24 0 24 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="X축" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox Margin="15 0 0 0" Height="35" Grid.Column="1"
Style="{StaticResource ComboBoxStyle}"
FontFamily="{StaticResource Pretendard_Medium}"
ItemsSource="{Binding XFieldCandidates}"
SelectedItem="{Binding SelectedXField}"
DisplayMemberPath="Display"/>
</Grid>
<Grid Margin="24 0 24 10" Style="{StaticResource VisibleWhenTank}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Center" Padding="0"
Foreground="White">
<Run Text="수조" FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}"/>
<LineBreak/>
<Run Text="(복수 선택 가능)" FontFamily="{StaticResource Pretendard_Medium}" FontSize="14" Foreground="#767676"/>
</TextBlock>
<ListBox ItemsSource="{Binding TankGroups}"
Grid.Column="1" Margin="15 0 0 0"
VerticalAlignment="Center"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedDictionary="{Binding SelectedWaterTanks}"
helper:MultiSelectBehavior.KeyPath="Key"
helper:MultiSelectBehavior.ValuePath="Value"
FontSize="18" FontWeight="Bold"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource MaterialDesignChoiceChipListBox}"
ItemContainerStyle="{StaticResource TransparentListBoxItemStyle}"
Background="Transparent">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Key}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource SquareCheckToggleButtonStyle}"
Margin="0 0 20 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
<Grid Margin="24 0">
<Grid.Resources>
<Style TargetType="FrameworkElement">
<Setter Property="Visibility" Value="Collapsed"/>
</Style>
<!-- 보이기 토글용 스타일 -->
<Style x:Key="VisibleWhenLine" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="LINE">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="VisibleWhenStep" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<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>
<Style x:Key="VisibleWhenTime" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedXField.Name}" Value="RecordedTime">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
<Style x:Key="CollpaseWhenLineAndNotTank" TargetType="FrameworkElement" BasedOn="{StaticResource {x:Type FrameworkElement}}">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="Filter">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedTab}" Value="Sterilizer">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Resources>
<!-- LINE -->
<StackPanel Style="{StaticResource VisibleWhenLine}">
<Grid Style="{StaticResource VisibleWhenLine}" Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center" Style="{StaticResource VisibleWhenLine}"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField, Mode=TwoWay}"
Grid.Column="1"
FontFamily="{StaticResource Pretendard_Medium}"
DisplayMemberPath="Display" Margin="15 0 0 0"
Height="35" Style="{StaticResource ComboBoxStyle}"/>
</Grid>
</StackPanel>
<!--STEP-->
<StackPanel Style="{StaticResource VisibleWhenStep}">
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="필드 종류" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<StackPanel Orientation="Horizontal" Grid.Column="1" HorizontalAlignment="Left" Margin="15 0">
<RadioButton x:Name="rbStatus"
GroupName="strpPlot" Margin="0 0 16 0"
Foreground="#325C80"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Status}}">
<TextBlock Text="전원/상태" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
<RadioButton x:Name="rbValue"
GroupName="strpPlot"
Foreground="#325C80"
Style="{StaticResource MaterialDesignUserForegroundRadioButton}"
IsChecked="{Binding SelectedKind, Mode=TwoWay,
Converter={StaticResource EnumEqualsConverter},
ConverterParameter={x:Static model:StepFieldKind.Sensor}}">
<TextBlock Text="센서 값" Style="{StaticResource RadioBtnContentTextBlockStyle}"/>
</RadioButton>
</StackPanel>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock VerticalAlignment="Top" Foreground="White">
<Run Text="Y축" FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}"/>
<LineBreak/>
<Run Text="(복수 선택 가능)" FontSize="14" Foreground="#767676"/>
</TextBlock>
<!-- SelectedItems 바인딩을 위한 간단 Behavior는 아래 3) 참고 -->
<Border CornerRadius="10"
Margin="15 0 0 0" Grid.Column="1">
<ListBox ItemsSource="{Binding YFieldCandidates}"
SelectionMode="Extended"
helper:MultiSelectBehavior.SelectedItems="{Binding SelectedYFields, Mode=OneWay}"
Height="Auto" Background="White"
Style="{StaticResource MaterialDesignFilterChipListBox}"
ItemContainerStyle="{StaticResource ListBoxItemStyle}">
<ListBox.ItemTemplate>
<DataTemplate>
<ToggleButton Content="{Binding Display}"
Focusable="False"
IsChecked="{Binding IsSelected,
RelativeSource={RelativeSource AncestorType=ListBoxItem}}"
Style="{StaticResource CircleCheckToggleButtonStyle}"
Margin="12 0"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Grid>
</StackPanel>
<!-- SCATTER: 단일 Y + 옵션 -->
<StackPanel Style="{StaticResource VisibleWhenScatter}">
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="Y축" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="35"
Grid.Column="1" Grid.ColumnSpan="3" Margin="15 0 0 0"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="마커 크기" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<Slider Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Left" Margin="15 0 0 0"
Minimum="1" Maximum="15" Value="{Binding ScatterMarkerSize}" Width="280" IsSnapToTickEnabled="True" TickFrequency="1"
Style="{StaticResource CustomSliderStyle}"/>
<TextBlock Text="{Binding ScatterMarkerSize}" Margin="15 0"
Grid.Column="2" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_Medium}" Foreground="White"/>
</Grid>
</StackPanel>
<!-- BOX: 값 필드 + 그룹 필드 + 옵션 -->
<StackPanel Style="{StaticResource VisibleWhenBox}">
<Grid Style="{StaticResource VisibleWhenTime}" Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="시간 범위" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<Slider Margin="15 0 0 0" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Left"
Minimum="1" Maximum="24" TickFrequency="1" IsSnapToTickEnabled="True"
Value="{Binding BoxTimeSpan}" Width="280"
Style="{StaticResource CustomSliderStyle}"/>
<TextBlock Text="{Binding BoxTimeSpan}"
Margin="15 0" Grid.Column="2"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="18" FontFamily="{StaticResource Pretendard_Medium}" Foreground="White"/>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<TextBlock Text="값 필드" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<ComboBox ItemsSource="{Binding YFieldCandidates}"
SelectedItem="{Binding SelectedYField}"
DisplayMemberPath="Display" Height="35"
Grid.Column="1" Margin="15 0 0 0"
FontFamily="{StaticResource Pretendard_Medium}"
Style="{StaticResource ComboBoxStyle}"/>
</Grid>
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="110"/>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="박스 너비" VerticalAlignment="Center"
FontSize="18" FontFamily="{StaticResource Pretendard_SemiBold}" Foreground="White"/>
<Slider Margin="15 0 0 0" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Left"
Minimum="0.1" Maximum="1.0" TickFrequency="0.05" IsSnapToTickEnabled="True"
Value="{Binding BoxWidth}" Width="280"
Style="{StaticResource CustomSliderStyle}"/>
<TextBlock Text="{Binding BoxWidth, StringFormat=F2}"
Margin="15 0" Grid.Column="2"
VerticalAlignment="Center" HorizontalAlignment="Right"
FontSize="18" FontFamily="{StaticResource Pretendard_Medium}" Foreground="White"/>
</Grid>
</StackPanel>
</Grid>
</StackPanel>
<StackPanel Orientation="Horizontal" Grid.Row="1" Margin="24 0 0 0">
<CheckBox IsChecked="{Binding ShowLegends}"
Foreground="#264A60"
Margin="0 0 4 0" VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Visible"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedTab}" Value="Filter">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
<DataTrigger Binding="{Binding SelectedTab}" Value="Sterilizer">
<Setter Property="Visibility" Value="Collapsed"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="범례" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding ShowMarkers}"
Foreground="#264A60"
Margin="0 0 4 0" VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<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>
</CheckBox.Style>
<TextBlock Text="마커" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
<CheckBox IsChecked="{Binding ShowRegression}"
Foreground="#264A60"
Margin="0 0 4 0" VerticalContentAlignment="Center">
<CheckBox.Style>
<Style TargetType="CheckBox" BasedOn="{StaticResource SquareCheckToggleButtonStyle}">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedGraphType}" Value="SCATTER">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
</Style.Triggers>
</Style>
</CheckBox.Style>
<TextBlock Text="회귀선" Style="{StaticResource CheckBoxContentTextBlockStyle}"/>
</CheckBox>
</StackPanel>
<Button Content="그래프 생성" Grid.Row="1" HorizontalAlignment="Right"
Style="{StaticResource GeneralButtonStyle}"
Margin="0 0 24 24" Padding="20 0"
Command="{Binding DrawGraphCommand}"/>
</Grid>
</Border>
<Border Grid.Column="1" Margin="0 16 12 16" CornerRadius="5"
Background="#152935" BorderBrush="#325C80" BorderThickness="1">
<control:GraphControl x:Name="graphControl"
Margin="16 12" DataContext="{Binding GraphControlVM}"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
</Border>
</Grid>
</Border>
</md:DrawerHost.BottomDrawerContent>
</md:DrawerHost>
</Border>
</UserControl>

@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using Newtonsoft.Json.Linq;
using SmartAquaViewer.Classes;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
using SmartAquaViewer.ViewModel;
namespace SmartAquaViewer.View
{
/// <summary>
/// MonitoringView.xaml에 대한 상호 작용 논리
/// </summary>
public partial class MonitoringView : UserControl
{
private MonitoringViewModel? monitoringViewModel;
private readonly Dictionary<MonitorTab, UIElement> _tabMap;
public MonitoringView()
{
InitializeComponent();
_tabMap = new Dictionary<MonitorTab, UIElement>
{
{ MonitorTab.Tank, dgTanks },
{ MonitorTab.Filter, dgFilter },
{ MonitorTab.Sterilizer, dgSterilizer }
};
Loaded += MonitoringView_Loaded;
}
private void MonitoringView_Loaded(object sender, RoutedEventArgs e)
{
monitoringViewModel = this.DataContext as MonitoringViewModel;
monitoringViewModel.PropertyChanged += VmOnPropertyChanged;
SetActiveTab(monitoringViewModel.SelectedTab); // 초기 반영
}
private void VmOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(MonitoringViewModel.SelectedTab) && sender is MonitoringViewModel vm)
SetActiveTab(vm.SelectedTab);
}
private void SetActiveTab(MonitorTab tab)
{
// 전부 Collapsed
dgTanks.Visibility = Visibility.Collapsed;
dgFilter.Visibility = Visibility.Collapsed;
dgSterilizer.Visibility = Visibility.Collapsed;
// 대상만 Visible
if (_tabMap.TryGetValue(tab, out var target))
target.Visibility = Visibility.Visible;
}
}
}

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class CCTVViewModel : INotifyPropertyChanged
{
public ObservableCollection<CCTVInfo> CCTVInfoList { get; } = new ObservableCollection<CCTVInfo>();
public ObservableCollection<FFPlayerViewModel> PlayerVMs { get; } = new();
private int _columnCount;
public int ColumnCount
{
get => _columnCount;
set
{
if (_columnCount != value)
{
_columnCount = value;
OnPropertyChanged();
}
}
}
private int _rowCount;
public int RowCount
{
get => _rowCount;
set
{
if (_rowCount != value)
{
_rowCount = value;
OnPropertyChanged();
}
}
}
private bool _isOpenMode;
public bool IsOpenMode
{
get => _isOpenMode;
set
{
if (_isOpenMode != value)
{
_isOpenMode = value;
OnPropertyChanged();
BtnVisibilityLeft = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
BtnVisibilityRight = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
}
}
}
private Visibility _btnVisibilityLeft;
public Visibility BtnVisibilityLeft
{
get => _btnVisibilityLeft;
set
{
if (_btnVisibilityLeft != value)
{
_btnVisibilityLeft = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityRight;
public Visibility BtnVisibilityRight
{
get => _btnVisibilityRight;
set
{
if (_btnVisibilityRight != value)
{
_btnVisibilityRight = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityPlay;
public Visibility BtnVisibilityPlay
{
get => _btnVisibilityPlay;
set
{
if (_btnVisibilityPlay != value)
{
_btnVisibilityPlay = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityStop;
public Visibility BtnVisibilityStop
{
get => _btnVisibilityStop;
set
{
if (_btnVisibilityStop != value)
{
_btnVisibilityStop = value;
OnPropertyChanged();
}
}
}
public ICommand ChangeDrawerStatusCommand { get; }
public ICommand PlayAllCCTVCommand { get; }
public ICommand StopAllCCTVCommand { get; }
public CCTVViewModel()
{
ColumnCount = 4; // Default value
RowCount = 2; // Default value
IsOpenMode = false;
BtnVisibilityRight = Visibility.Collapsed;
BtnVisibilityStop = Visibility.Collapsed;
CCTVInfoList = Datas.Instance.GetCCTVInfoList();
BuildPlayers(CCTVInfoList);
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
PlayAllCCTVCommand = new RelayCommand(PlayAllCCTV);
StopAllCCTVCommand = new RelayCommand(StopAllCCTV);
}
private void PlayAllCCTV(object obj)
{
foreach (var ff in PlayerVMs)
ff.StartMedia(ff.CCTVInfo.RtspUrl!);
BtnVisibilityPlay = Visibility.Collapsed;
BtnVisibilityStop = Visibility.Visible;
}
private void StopAllCCTV(object obj)
{
foreach (var ff in PlayerVMs)
ff.ClosePlayer();
BtnVisibilityPlay = Visibility.Visible;
BtnVisibilityStop = Visibility.Collapsed;
}
public void BuildPlayers(IEnumerable<CCTVInfo> infos)
{
PlayerVMs.Clear();
foreach (var info in infos)
PlayerVMs.Add(new FFPlayerViewModel(info));
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,435 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class EnergyViewModel : PagingViewModelBase<WaterQualityVO>, INotifyPropertyChanged
{
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; set; }
private double _totalEnergy;
public double TotalEnergy
{
get => _totalEnergy;
set
{
if (_totalEnergy != value)
{
_totalEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalSandFilterEnergy;
public double TotalSandFilterEnergy
{
get => _totalSandFilterEnergy;
set
{
if (_totalSandFilterEnergy != value)
{
_totalSandFilterEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalCirculationPumpEnergy;
public double TotalCirculationPumpEnergy
{
get => _totalCirculationPumpEnergy;
set
{
if (_totalCirculationPumpEnergy != value)
{
_totalCirculationPumpEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalHeatPumpEnergy;
public double TotalHeatPumpEnergy
{
get => _totalHeatPumpEnergy;
set
{
if (_totalHeatPumpEnergy != value)
{
_totalHeatPumpEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalAirBlowerEnergy;
public double TotalAirBlowerEnergy
{
get => _totalAirBlowerEnergy;
set
{
if (_totalAirBlowerEnergy != value)
{
_totalAirBlowerEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneGeneratorEnergy;
public double TotalOzoneGeneratorEnergy
{
get => _totalOzoneGeneratorEnergy;
set
{
if (_totalOzoneGeneratorEnergy != value)
{
_totalOzoneGeneratorEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalUVSterilizerEnergy;
public double TotalUVSterilizerEnergy
{
get => _totalUVSterilizerEnergy;
set
{
if (_totalUVSterilizerEnergy != value)
{
_totalUVSterilizerEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneDissolverEnergy;
public double TotalOzoneDissolverEnergy
{
get => _totalOzoneDissolverEnergy;
set
{
if (_totalOzoneDissolverEnergy != value)
{
_totalOzoneDissolverEnergy = value;
OnPropertyChanged();
}
}
}
private double _totalExcessOzoneDestroyerEnergy;
public double TotalExcessOzoneDestroyerEnergy
{
get => _totalExcessOzoneDestroyerEnergy;
set
{
if (_totalExcessOzoneDestroyerEnergy != value)
{
_totalExcessOzoneDestroyerEnergy = value;
OnPropertyChanged();
}
}
}
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
{
get => _selectedGraphType;
set
{
if (_selectedGraphType != value)
{
_selectedGraphType = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowXSelector));
RebuildFieldCandidates();
var idx = GraphTypes.IndexOf(value);
if (idx != -1 && idx != _selectedGraphIndex)
{
_selectedGraphIndex = idx;
OnPropertyChanged(nameof(SelectedGraphIndex));
}
}
}
}
private StepFieldKind _selectedKind = StepFieldKind.Energy; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = value;
OnPropertyChanged();
}
}
}
private bool _isOpenMode;
public bool IsOpenMode
{
get => _isOpenMode;
set
{
if (_isOpenMode != value)
{
_isOpenMode = value;
OnPropertyChanged();
_btnVisibilityRight = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
BtnVisibilityLeft = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
}
}
}
private Visibility _btnVisibilityRight;
public Visibility BtnVisibilityRight
{
get => _btnVisibilityRight;
set
{
if (_btnVisibilityRight != value)
{
_btnVisibilityRight = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityLeft;
public Visibility BtnVisibilityLeft
{
get => _btnVisibilityLeft;
set
{
if (_btnVisibilityLeft != value)
{
_btnVisibilityLeft = value;
OnPropertyChanged();
}
}
}
public bool ShowXSelector => SelectedGraphType == GraphType.SCATTER;
// [필드 후보 목록] 탭/시스템에 따라 달라짐
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(); } }
}
private bool _useAverage;
public bool UseAverage { get => _useAverage; set { _useAverage = value; OnPropertyChanged(); } }
private bool _showMarkers;
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _isDonut;
public bool IsDonut { get => _isDonut; set { _isDonut = value; OnPropertyChanged(); } }
public ICommand ChangeDrawerStatusCommand { get; }
public ICommand DrawGraphCommand { get; }
public EnergyViewModel()
{
IsOpenMode = true;
BtnVisibilityLeft = Visibility.Collapsed;
GraphTypes = new ObservableCollection<GraphType>
{
GraphType.LINE,
GraphType.STACKAREA,
GraphType.PIE
};
WaterQualityList = Datas.Instance.WaterQualityView;
SyncItemsWithSource();
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnSourceChanged;
RecalcTotals();
SelectedKind = StepFieldKind.Energy; // 기본적으로 에너지 관련 필드만 표시
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
SelectedGraphIndex = 0;
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
GraphControlVM.SetMultiLineGraph(WaterQualityList.ToList(), SelectedYFields, DataType.Energy, ShowMarkers, ShowLegends);
break;
case GraphType.STACKAREA:
GraphControlVM.SetStackAreaPlot(WaterQualityList.ToList(), SelectedYFields, DataType.Energy, ShowMarkers, ShowLegends);
break;
case GraphType.PIE:
GraphControlVM.SetPieChart(WaterQualityList.ToList(), SelectedYFields, DataType.Energy, UseAverage, IsDonut);
break;
default:
break;
}
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) });
AvailableFields.Add(new FieldItem { Name = "TotalEnergy", Display = "총 전력", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterEnergy", Display = "모래여과기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpEnergy", Display = "순환펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpEnergy", Display = "히트펌프", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerEnergy", Display = "에어브로와", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorEnergy", Display = "오존발생기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.UVSterilizerEnergy", Display = "자외선 살균기", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverEnergy", Display = "오존용해장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerEnergy", Display = "배오존장치", DataType = typeof(double), Kind = StepFieldKind.Energy });
}
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STACKAREA) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.STACKAREA or GraphType.PIE)
{
// 수치형만 (LINE/STACKAREA/PIE 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
// Y축 후보: 수치형
foreach (var f in src)
{
if (SelectedGraphType is GraphType.STACKAREA or GraphType.PIE && f.Name.Equals("TotalEnergy")) continue;
YFieldCandidates.Add(f);
}
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
//var def = YFieldCandidates.FirstOrDefault();
//if (def != null) SelectedYFields.Add(def);
break;
case GraphType.STACKAREA:
SelectedYField = YFieldCandidates.FirstOrDefault();
break;
case GraphType.PIE:
//SelectedYField = YFieldCandidates.FirstOrDefault();
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
private void OnSourceChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SyncItemsWithSource();
RecalcTotals();
}
private void SyncItemsWithSource()
{
Items.Clear();
foreach (var w in WaterQualityList)
Items.Add(w);
}
private void RecalcTotals()
{
TotalSandFilterEnergy = WaterQualityList.Sum(vo => vo.Filtering.SandFilterEnergy);
TotalCirculationPumpEnergy = WaterQualityList.Sum(vo => vo.Filtering.CirculationPumpEnergy);
TotalHeatPumpEnergy = WaterQualityList.Sum(vo => vo.Filtering.HeatPumpEnergy);
TotalAirBlowerEnergy = WaterQualityList.Sum(vo => vo.Filtering.AirBlowerEnergy);
TotalOzoneGeneratorEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneGeneratorEnergy);
TotalUVSterilizerEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.UVSterilizerEnergy);
TotalOzoneDissolverEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneDissolverEnergy);
TotalExcessOzoneDestroyerEnergy = WaterQualityList.Sum(vo => vo.Sterilizing.ExcessOzoneDestroyerEnergy);
TotalEnergy = WaterQualityList.Sum(vo => vo.TotalEnergy);
}
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> base.OnPropertyChanged(name);
}
}

@ -0,0 +1,195 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Drawing;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using FFmpeg.AutoGen;
using SmartAquaViewer.Helper.FFHelper;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class FFPlayerViewModel : INotifyPropertyChanged
{
public CCTVInfo CCTVInfo { get; set; }
private ImageSource? _currentFrame;
public ImageSource? CurrentFrame
{
get => _currentFrame;
set { _currentFrame = value; OnPropertyChanged(); }
}
private Visibility _isPlayingVIsibility;
public Visibility IsPlayingVIsibility
{
get => _isPlayingVIsibility;
set { _isPlayingVIsibility = value; OnPropertyChanged(); }
}
//private readonly System.Windows.Controls.Image _img;
private Thread _videoThread;
private Thread _renderingThread;
private CancellationTokenSource _videoCts;
private CancellationTokenSource _renderCts;
private readonly object _lockObject = new object();
private readonly Queue<AVFrame> _frameQueue = new(); // lock 불필요
private volatile bool _disposed;
private readonly Dispatcher _ui;
public FFPlayerViewModel(CCTVInfo cctvInfo)
{
CCTVInfo = cctvInfo;
_ui = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; // UI 디스패처 보관
IsPlayingVIsibility = Visibility.Visible;
}
public void StartMedia(string rtspURL)
{
IsPlayingVIsibility = Visibility.Collapsed;
CCTVInfo.Status = CCTVStatus.Connected;
_videoCts = new CancellationTokenSource();
_renderCts = new CancellationTokenSource();
_videoThread = new Thread(OpenMedia) { IsBackground = true, Name = "RTSP-Decode" };
_renderingThread = new Thread(RenderImage) { IsBackground = true, Name = "RTSP-Render" };
_videoThread.Start();
_renderingThread.Start();
}
private unsafe void OpenMedia()
{
try
{
using (var sd = new StreamDecoder(CCTVInfo.RtspUrl!))
using (var vfc = new VideoFrameConverter(sd.FrameSize, sd.PixelFormat, sd.FrameSize, AVPixelFormat.AV_PIX_FMT_BGR24))
{
while (!_videoCts!.IsCancellationRequested)
{
if (!sd.TryDecodeNextFrame(out var frame))
{
// 디코드 실패: 너무 바쁘지 않게 살짝 쉼
Thread.Sleep(2);
continue;
}
try
{
AVFrame convertedFrame = vfc.Convert(frame);
_frameQueue.Enqueue(convertedFrame); // 큐 삽입 (길이 제한 적용)
}
catch (Exception ex)
{
Debug.WriteLine("Decode/Enqueue error: " + ex.Message);
}
}
}
}
catch (Exception ex)
{
Debug.WriteLine("OpenMedia() : " + ex);
}
}
private unsafe void RenderImage()
{
try
{
while (!_renderCts!.IsCancellationRequested)
{
if (_frameQueue.Count > 0)
{
AVFrame convertedFrame;
lock (_lockObject)
{
if (_frameQueue.Count > 0)
{
convertedFrame = _frameQueue.Dequeue();
}
else
{
continue;
}
}
try
{
Bitmap bitmap = new Bitmap(convertedFrame.width, convertedFrame.height, convertedFrame.linesize[0], System.Drawing.Imaging.PixelFormat.Format24bppRgb, (IntPtr)convertedFrame.data[0]);
var src = CreateBitmapSource(bitmap);
src.Freeze();
_ui.BeginInvoke(new Action(() =>
{
if (_disposed) return;
CurrentFrame = src;
}), DispatcherPriority.Render);
}
catch (Exception ex)
{
Debug.WriteLine("RenderLoop() : " + ex.Message);
}
}
}
}
catch (OperationCanceledException) { /* 정상 종료 */ }
finally
{
//ClearQueue();
}
}
private static BitmapSource CreateBitmapSource(Bitmap bitmap)
{
// GDI 핸들 사용 X. LockBits → BitmapSource.Create 경로만 사용.
var rect = new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height);
var data = bitmap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bitmap.PixelFormat);
try
{
// Format24bppRgb ↔ PixelFormats.Bgr24 매칭
return BitmapSource.Create(
data.Width, data.Height, 96, 96,
PixelFormats.Bgr24, null,
data.Scan0, data.Stride * data.Height, data.Stride);
}
finally
{
bitmap.UnlockBits(data);
}
}
public void ClosePlayer()
{
if (_disposed) return;
_disposed = true;
try { _videoCts?.Cancel(); } catch { }
try { _renderCts?.Cancel(); } catch { }
try { if (_videoThread?.IsAlive == true) _videoThread.Join(300); } catch { }
try { if (_renderingThread?.IsAlive == true) _renderingThread.Join(300); } catch { }
_frameQueue.Clear();
_videoCts?.Dispose();
_renderCts?.Dispose();
CurrentFrame = null;
IsPlayingVIsibility = Visibility.Visible;
CCTVInfo.Status = CCTVStatus.Disconnected;
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,111 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.Win32;
using Newtonsoft.Json;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class FileListViewModel : INotifyPropertyChanged
{
public ObservableCollection<FileModel> FileList { get; set; }
private FileModel? _selectedFile;
public FileModel SelectedFile
{
get => _selectedFile!;
set
{
if (_selectedFile != value)
{
_selectedFile = value;
OnPropertyChanged();
DeserializeContentToJson();
}
}
}
public ICommand OpenFileDialogCommand { get; }
public FileListViewModel()
{
FileList = new ObservableCollection<FileModel>();
OpenFileDialogCommand = new RelayCommand(OpenFileList);
}
private void OpenFileList(object obj)
{
OpenFileDialog openFileDialog = new OpenFileDialog
{
Multiselect = true,
Filter = "Json Files (*.json)|*.json|All Files (*.*)|*.*"
};
if (openFileDialog.ShowDialog() == true)
{
foreach (var filePath in openFileDialog.FileNames)
{
var fileName = Path.GetFileName(filePath).Replace(".json", "");
var year = int.Parse(fileName.Split("-")[0]);
var month = int.Parse(fileName.Split("-")[1]);
var day = int.Parse(fileName.Split("-")[2]);
var date = new DateTime(year, month, day);
var fileModel = new FileModel { Name = fileName, RecordedDate = date };
var fileContent = File.ReadAllText(filePath);
fileModel.Content = fileContent;
if (FileList.Any(f => f.Name == fileModel.Name)) continue;
FileList.Add(fileModel);
OrderFileListByDate();
}
}
}
private void DeserializeContentToJson()
{
if (SelectedFile == null || string.IsNullOrWhiteSpace(SelectedFile.Content))
return;
try
{
var list = JsonConvert.DeserializeObject<List<WaterQualityVO>>(SelectedFile.Content)
?? new List<WaterQualityVO>();
// 여기서 SSOT에 "내용만" 갱신
Datas.Instance.SetWaterQualityVO(list);
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
}
}
private void OrderFileListByDate()
{
var orderedList = FileList.OrderBy(f => f.RecordedDate).ToList();
FileList.Clear();
foreach (var file in orderedList)
{
FileList.Add(file);
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,441 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
using static MaterialDesignThemes.Wpf.Theme.ToolBar;
namespace SmartAquaViewer.ViewModel
{
public class GreenHouseGasViewModel : PagingViewModelBase<WaterQualityVO>
{
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; set; }
private double _totalGreenhouseGas;
public double TotalGreenhouseGas
{
get => _totalGreenhouseGas;
set
{
if (_totalGreenhouseGas != value)
{
_totalGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalSandFilterGreenhouseGas;
public double TotalSandFilterGreenhouseGas
{
get => _totalSandFilterGreenhouseGas;
set
{
if (_totalSandFilterGreenhouseGas != value)
{
_totalSandFilterGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalCirculationPumpGreenhouseGas;
public double TotalCirculationPumpGreenhouseGas
{
get => _totalCirculationPumpGreenhouseGas;
set
{
if (_totalCirculationPumpGreenhouseGas != value)
{
_totalCirculationPumpGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalHeatPumpGreenhouseGas;
public double TotalHeatPumpGreenhouseGas
{
get => _totalHeatPumpGreenhouseGas;
set
{
if (_totalHeatPumpGreenhouseGas != value)
{
_totalHeatPumpGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalAirBlowerGreenhouseGas;
public double TotalAirBlowerGreenhouseGas
{
get => _totalAirBlowerGreenhouseGas;
set
{
if (_totalAirBlowerGreenhouseGas != value)
{
_totalAirBlowerGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneGeneratorGreenhouseGas;
public double TotalOzoneGeneratorGreenhouseGas
{
get => _totalOzoneGeneratorGreenhouseGas;
set
{
if (_totalOzoneGeneratorGreenhouseGas != value)
{
_totalOzoneGeneratorGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalUVSterilizerGreenhouseGas;
public double TotalUVSterilizerGreenhouseGas
{
get => _totalUVSterilizerGreenhouseGas;
set
{
if (_totalUVSterilizerGreenhouseGas != value)
{
_totalUVSterilizerGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalOzoneDissolverGreenhouseGas;
public double TotalOzoneDissolverGreenhouseGas
{
get => _totalOzoneDissolverGreenhouseGas;
set
{
if (_totalOzoneDissolverGreenhouseGas != value)
{
_totalOzoneDissolverGreenhouseGas = value;
OnPropertyChanged();
}
}
}
private double _totalExcessOzoneDestroyerGreenhouseGas;
public double TotalExcessOzoneDestroyerGreenhouseGas
{
get => _totalExcessOzoneDestroyerGreenhouseGas;
set
{
if (_totalExcessOzoneDestroyerGreenhouseGas != value)
{
_totalExcessOzoneDestroyerGreenhouseGas = value;
OnPropertyChanged();
}
}
}
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
{
get => _selectedGraphType;
set
{
if (_selectedGraphType != value)
{
_selectedGraphType = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowXSelector));
RebuildFieldCandidates();
var idx = GraphTypes.IndexOf(value);
if (idx != -1 && idx != _selectedGraphIndex)
{
_selectedGraphIndex = idx;
OnPropertyChanged(nameof(SelectedGraphIndex));
}
}
}
}
private StepFieldKind _selectedKind = StepFieldKind.GHG; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = value;
OnPropertyChanged();
}
}
}
private bool _isOpenMode;
public bool IsOpenMode
{
get => _isOpenMode;
set
{
if (_isOpenMode != value)
{
_isOpenMode = value;
OnPropertyChanged();
_btnVisibilityRight = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
BtnVisibilityLeft = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
}
}
}
private Visibility _btnVisibilityRight;
public Visibility BtnVisibilityRight
{
get => _btnVisibilityRight;
set
{
if (_btnVisibilityRight != value)
{
_btnVisibilityRight = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityLeft;
public Visibility BtnVisibilityLeft
{
get => _btnVisibilityLeft;
set
{
if (_btnVisibilityLeft != value)
{
_btnVisibilityLeft = value;
OnPropertyChanged();
}
}
}
public bool ShowXSelector => SelectedGraphType == GraphType.SCATTER;
// [필드 후보 목록] 탭/시스템에 따라 달라짐
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(); } }
}
private bool _showMarkers; // Line
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = value; OnPropertyChanged(); } }
private bool _useAverage;
public bool UseAverage { get => _useAverage; set { _useAverage = value; OnPropertyChanged(); } }
private bool _isDonut;
public bool IsDonut { get => _isDonut; set { _isDonut = value; OnPropertyChanged(); } }
public ICommand ChangeDrawerStatusCommand { get; }
public ICommand DrawGraphCommand { get; }
public GreenHouseGasViewModel()
{
IsOpenMode = true;
BtnVisibilityLeft = Visibility.Collapsed;
GraphTypes = new ObservableCollection<GraphType>
{
GraphType.LINE,
GraphType.STACKAREA,
GraphType.PIE
};
WaterQualityList = Datas.Instance.WaterQualityView;
SyncItemsWithSource();
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnSourceChanged;
RecalcTotals();
SelectedKind = StepFieldKind.GHG;
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
SelectedGraphIndex = 0;
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
GraphControlVM.SetMultiLineGraph(WaterQualityList.ToList(), SelectedYFields, DataType.GreenhouseGas, ShowMarkers, ShowLegends);
break;
case GraphType.STACKAREA:
GraphControlVM.SetStackAreaPlot(WaterQualityList.ToList(), SelectedYFields, DataType.GreenhouseGas, ShowMarkers, ShowLegends);
break;
case GraphType.PIE:
GraphControlVM.SetPieChart(WaterQualityList.ToList(), SelectedYFields, DataType.GreenhouseGas, UseAverage, IsDonut);
break;
default:
break;
}
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime) });
AvailableFields.Add(new FieldItem { Name = "TotalGreenhouseGas", Display = "총 배출량", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterGreenhouseGas", Display = "모래여과기", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpGreenhouseGas", Display = "순환펌프", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpGreenhouseGas", Display = "히트펌프", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerGreenhouseGas", Display = "에어브로와", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorGreenhouseGas", Display = "오존발생기", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.UVSterilizerGreenhouseGas", Display = "자외선 살균기", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverGreenhouseGas", Display = "오존용해장치", DataType = typeof(double), Kind = StepFieldKind.GHG });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerGreenhouseGas", Display = "배오존장치", DataType = typeof(double), Kind = StepFieldKind.GHG });
}
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.LINE || SelectedGraphType == GraphType.STACKAREA) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
IEnumerable<FieldItem> src = AvailableFields;
if (SelectedGraphType is GraphType.LINE or GraphType.STACKAREA or GraphType.PIE)
{
// 수치형만 (LINE/STACKAREA/PIE 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
// Y축 후보: 수치형
foreach (var f in src)
{
if (SelectedGraphType is GraphType.STACKAREA or GraphType.PIE && f.Name!.Equals("TotalGreenhouseGas")) continue;
YFieldCandidates.Add(f);
}
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
//var def = YFieldCandidates.FirstOrDefault();
//if (def != null) SelectedYFields.Add(def);
break;
case GraphType.STACKAREA:
SelectedYField = YFieldCandidates.FirstOrDefault();
break;
case GraphType.PIE:
//SelectedYField = YFieldCandidates.FirstOrDefault();
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
private void OnSourceChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
SyncItemsWithSource();
RecalcTotals();
}
private void SyncItemsWithSource()
{
Items.Clear();
foreach (var w in WaterQualityList)
Items.Add(w);
}
private void RecalcTotals()
{
TotalSandFilterGreenhouseGas = WaterQualityList.Sum(vo => vo.Filtering.SandFilterGreenhouseGas);
TotalCirculationPumpGreenhouseGas = WaterQualityList.Sum(vo => vo.Filtering.CirculationPumpGreenhouseGas);
TotalHeatPumpGreenhouseGas = WaterQualityList.Sum(vo => vo.Filtering.HeatPumpGreenhouseGas);
TotalAirBlowerGreenhouseGas = WaterQualityList.Sum(vo => vo.Filtering.AirBlowerGreenhouseGas);
TotalOzoneGeneratorGreenhouseGas = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneGeneratorGreenhouseGas);
TotalUVSterilizerGreenhouseGas = WaterQualityList.Sum(vo => vo.Sterilizing.UVSterilizerGreenhouseGas);
TotalOzoneDissolverGreenhouseGas = WaterQualityList.Sum(vo => vo.Sterilizing.OzoneDissolverGreenhouseGas);
TotalExcessOzoneDestroyerGreenhouseGas = WaterQualityList.Sum(vo => vo.Sterilizing.ExcessOzoneDestroyerGreenhouseGas);
TotalGreenhouseGas = WaterQualityList.Sum(vo => vo.TotalGreenhouseGas);
}
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> base.OnPropertyChanged(name);
}
}

@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using FFmpeg.AutoGen;
using Newtonsoft.Json;
using SmartAquaViewer.Classes;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
public class MainViewModel : INotifyPropertyChanged
{
public string AppTitle { get; set; } = string.Empty;
private object? _selectedViewModel;
public object? SelectedViewModel
{
get => _selectedViewModel;
set
{
if (_selectedViewModel != value)
{
_selectedViewModel = value;
OnPropertyChanged(nameof(SelectedViewModel));
}
}
}
public ICommand SwapViewCommand { get; }
public MainViewModel()
{
ConfigData.Instance.LoadConfig();
AppTitle = ConfigData.Instance.Title;
SwapViewCommand = new RelayCommand(SwapView);
SelectedViewModel = new MonitoringViewModel(); // Default view
ffmpeg.RootPath = $"{Environment.CurrentDirectory}\\ffmpeg";
//더미데이터 생성 및 파일로 저장
//for (int i = 1; i <= 10; i++)
//{
// DateTime date = new(2025, 8, i);
// var dataList = WaterQualityVO.GetSampleData(date, date, 24);
// var jsonStr = JsonConvert.SerializeObject(dataList, Formatting.Indented);
// string fileName = date.ToString("yyyy-MM-dd") + ".json";
// File.WriteAllText(fileName, jsonStr);
//}
}
private void SwapView(object obj)
{
var tag = (string)obj;
switch (tag)
{
case "monitoring":
SelectedViewModel = new MonitoringViewModel();
break;
case "energy":
SelectedViewModel = new EnergyViewModel();
break;
case "greenHouseGas":
SelectedViewModel = new GreenHouseGasViewModel();
break;
case "cctv":
SelectedViewModel = new CCTVViewModel();
break;
default:
throw new ArgumentException("Unknown view tag", nameof(obj));
}
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}

@ -0,0 +1,546 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using SmartAquaViewer.Controls;
using SmartAquaViewer.DataAnalysis;
using SmartAquaViewer.Model;
namespace SmartAquaViewer.ViewModel
{
/// <summary>
/// 데이터에서 추출한 필드 정보
/// </summary>
public sealed class FieldItem
{
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 StepFieldKind Kind { get; init; }
public override string? ToString() => Display;
}
/// <summary>
/// 특정 시점에 기록된 수조 데이터 묶음
/// </summary>
public class TanksByTime
{
public DateTime RecordedTime { get; }
public List<WaterTank> Tanks { get; } = new();
public TanksByTime(DateTime time, List<WaterTank> tanks)
{
RecordedTime = time;
Tanks = tanks;
}
}
public class MonitoringViewModel : PagingViewModelBase<WaterQualityVO>
{
#region Properties
public GraphControlViewModel GraphControlVM { get; } = new GraphControlViewModel();
public ObservableCollection<GraphType> GraphTypes { get; }
public ReadOnlyObservableCollection<WaterQualityVO> WaterQualityList { get; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> TankGroups { get; set; }
public Dictionary<int, ObservableCollection<WaterQualityVO>> SelectedWaterTanks { get; } = new();
public ObservableCollection<TanksByTime> TanksByTimes { get; } = new();
public PagingViewModelBase<TanksByTime> TanksPager { get; } = new();
private MonitorTab _selectedTab;
public MonitorTab SelectedTab
{
get => _selectedTab;
set
{
if (_selectedTab != value)
{
_selectedTab = value;
OnPropertyChanged();
RebuildAvailableFields(); // 탭에 맞춰 필드 목록 재구성
SetGraphType();
OnPropertyChanged(nameof(IsTankAndLine));
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
{
get => _selectedGraphType;
set
{
if (_selectedGraphType != value)
{
_selectedGraphType = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowXSelector));
RebuildFieldCandidates();
var idx = GraphTypes.IndexOf(value);
if (idx != -1 && idx != _selectedGraphIndex)
{
_selectedGraphIndex = idx;
OnPropertyChanged(nameof(SelectedGraphIndex));
OnPropertyChanged(nameof(IsTankAndLine));
}
}
}
}
private StepFieldKind _selectedKind = StepFieldKind.Sensor; // 기본값은 센서
public StepFieldKind SelectedKind
{
get => _selectedKind;
set
{
if (_selectedKind != value)
{
_selectedKind = value;
OnPropertyChanged();
// 라디오 변경 시 Y 후보 재구성
RebuildFieldCandidates();
}
}
}
public bool IsTankAndLine
{
get => SelectedTab.Equals(MonitorTab.Tank) && SelectedGraphType.Equals(GraphType.LINE);
}
private bool _isOpenMode;
public bool IsOpenMode
{
get => _isOpenMode;
set
{
if (_isOpenMode != value)
{
_isOpenMode = value;
OnPropertyChanged();
BtnVisibilityDown = _isOpenMode ? Visibility.Visible : Visibility.Collapsed;
BtnVisibilityUp = _isOpenMode ? Visibility.Collapsed : Visibility.Visible;
}
}
}
private Visibility _btnVisibilityDown;
public Visibility BtnVisibilityDown
{
get => _btnVisibilityDown;
set
{
if (_btnVisibilityDown != value)
{
_btnVisibilityDown = value;
OnPropertyChanged();
}
}
}
private Visibility _btnVisibilityUp;
public Visibility BtnVisibilityUp
{
get => _btnVisibilityUp;
set
{
if (_btnVisibilityUp != value)
{
_btnVisibilityUp = value;
OnPropertyChanged();
}
}
}
public bool ShowXSelector => SelectedGraphType == GraphType.SCATTER;
// [필드 후보 목록] 탭/시스템에 따라 달라짐
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(); } }
}
// [옵션] 예시 — 필요하면 추가
private bool _showMarkers; // Line
public bool ShowMarkers { get => _showMarkers; set { _showMarkers = value; OnPropertyChanged(); } }
private bool _showLegends;
public bool ShowLegends { get => _showLegends; set { _showLegends = 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(); } }
private int _boxTimeSpan = 6; // Box
public int BoxTimeSpan
{
get => _boxTimeSpan;
set
{
if (_boxTimeSpan != value)
{
_boxTimeSpan = value;
OnPropertyChanged();
}
}
}
#endregion
public ICommand ChangeDrawerStatusCommand { get; }
public ICommand DrawGraphCommand { get; }
public delegate void SystemChangedEventHandler(MonitorTab selectedTab);
public event SystemChangedEventHandler OnSystemChanged;
public MonitoringViewModel()
{
IsOpenMode = true;
BtnVisibilityUp = Visibility.Collapsed;
WaterQualityList = Datas.Instance.WaterQualityView;
((INotifyCollectionChanged)WaterQualityList).CollectionChanged += OnWaterQualityChanged;
RebuildAllGroups();
GraphTypes = [];
SelectedTab = MonitorTab.Tank; // Default system
SetGraphType();
ChangeDrawerStatusCommand = new RelayCommand(_ => IsOpenMode = !IsOpenMode);
DrawGraphCommand = new RelayCommand(DrawGraph);
RebuildAvailableFields();
RebuildFieldCandidates();
}
private void OnWaterQualityChanged(object? sender, NotifyCollectionChangedEventArgs e) => RebuildAllGroups();
private void RebuildAllGroups()
{
var grouped = WaterQualityList
.GroupBy(x => x.Tank.Number) // 또는 x.Tank.Num
.OrderBy(g => g.Key)
.ToDictionary(
g => g.Key,
g => new ObservableCollection<WaterQualityVO>(
g.OrderBy(r => r.RecordedTime))
);
TankGroups = grouped;
OnPropertyChanged(nameof(TankGroups));
TanksPager.Items.Clear();
foreach (var w in WaterQualityList)
TanksPager.Items.Add(new TanksByTime(w.RecordedTime, w.Tanks));
var ordered = TanksPager.Items.OrderBy(t => t.RecordedTime).ToList();
TanksPager.Items.Clear();
foreach (var t in ordered) TanksPager.Items.Add(t);
// TankGroups를 깔끔하게 다시 구성
this.Items.Clear();
foreach (var w in WaterQualityList)
this.Items.Add(w);
}
private void DrawGraph(object obj)
{
switch (SelectedGraphType)
{
case GraphType.LINE:
if (SelectedTab.Equals(MonitorTab.Tank))
SetGraphData_Line_Tank();
else
GraphControlVM.SetDefaultLineGraph(
WaterQualityList.ToList(), SelectedTab, SelectedXField, SelectedYField, ShowMarkers);
break;
case GraphType.BOX:
var xFieldBox = SelectedXField;
SetGraphData_Box_Tank();
break;
case GraphType.SCATTER:
SetGraphData_Scatter_Tank();
break;
case GraphType.STEP:
var xFieldStep = SelectedXField?.Name == "RecordedTime" ? SelectedXField : null;
var tFieldsStep = SelectedYFields;
var yFiledStep = SelectedYField;
var showMarkerStep = ShowMarkers;
if (SelectedKind.Equals(StepFieldKind.Status))
GraphControlVM.SetStatusSeriesStopPlot(WaterQualityList.ToList(), SelectedYField, SelectedYFields, ShowMarkers, ShowLegends);
else
GraphControlVM.SetStepPlot(WaterQualityList.ToList(), SelectedTab, xFieldStep, SelectedYFields, ShowMarkers, ShowLegends);
break;
default:
break;
}
}
private void SetGraphData_Line_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetTankLineGraph(WaterQualityList, keys, SelectedXField, SelectedYField, ShowMarkers, ShowLegends);
}
private void SetGraphData_Box_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var boxTimeSpan = TimeSpan.FromHours(BoxTimeSpan);
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetBoxPlot(WaterQualityList, keys, SelectedXField!, SelectedYField, BoxWidth, boxTimeSpan);
}
private void SetGraphData_Scatter_Tank()
{
if (SelectedTab != MonitorTab.Tank) return;
if (SelectedYField == null) return;
var keys = SelectedWaterTanks.Keys.ToList();
GraphControlVM.SetScatterPlot(WaterQualityList, SelectedXField!, SelectedYField, keys, ScatterMarkerSize, ShowRegression, ShowLegends);
}
private void SetGraphType()
{
GraphTypes.Clear();
switch (SelectedTab)
{
case MonitorTab.Tank:
GraphTypes.Add(GraphType.LINE);
GraphTypes.Add(GraphType.BOX);
GraphTypes.Add(GraphType.SCATTER);
break;
case MonitorTab.Filter:
case MonitorTab.Sterilizer:
GraphTypes.Add(GraphType.LINE);
GraphTypes.Add(GraphType.STEP);
break;
default:
break;
}
Application.Current.Dispatcher.BeginInvoke(new Action(() =>
{
SelectedGraphIndex = -1;
SelectedGraphIndex = GraphTypes.Count > 0 ? 0 : -1;
}), DispatcherPriority.Background);
}
private void RebuildAvailableFields()
{
AvailableFields.Clear();
// 공통 시간
AvailableFields.Add(new FieldItem { Name = "RecordedTime", Display = "시간", DataType = typeof(DateTime), Kind = StepFieldKind.Time });
if (SelectedTab == MonitorTab.Tank)
{
AvailableFields.Add(new FieldItem { Name = "Number", Display = "수조", DataType = typeof(int), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "DOValue", Display = "DO (mg/L)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "PH", Display = "pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "ORP", Display = "ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Temperature", Display = "온도 (℃)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "FlowRate", Display = "유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
}
else if (SelectedTab == MonitorTab.Filter)
{
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpPH", Display = "섬프 pH", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpORP", Display = "섬프 ORP (mV)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpWaterLevel", Display = "섬프 수위 (m)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpFlowRate", Display = "섬프 유량 (m³/s)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SumpTemperature", Display = "섬프 수온 (°C)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.FlowRate", Display = "순환펌프 유량", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpTemperature", Display = "히트펌프 온도", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Filtering.SandFilterPower", Display = "모래여과기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.CirculationPumpPower", Display = "순환펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.InverterControllerStatus", Display = "인버터 제어기 상태", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.HeatPumpPower", Display = "히트펌프 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Filtering.AirBlowerPower", Display = "에어브로와 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
}
else // Sterilizer
{
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPressure", Display = "용해장치 압력 (kPa)", DataType = typeof(double), Kind = StepFieldKind.Sensor });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneGeneratorPower", Display = "오존 발생기 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.OzoneDissolverPower", Display = "오존용해장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AvailableFields.Add(new FieldItem { Name = "Sterilizing.ExcessOzoneDestroyerPower", Display = "배오존장치 전원", DataType = typeof(int), Kind = StepFieldKind.Status });
AddUvPowerFieldsPerId(WaterQualityList);
}
}
// rows: 현재 그리려는 데이터(필터링/정렬 반영된)
private void AddUvPowerFieldsPerId(IEnumerable<WaterQualityVO> rows)
{
// 케이스 A: 한 행이 UV 한 대의 상태를 담는 스키마
var idsA = rows
.Select(r => r?.Sterilizing?.UVSterilizerId)
.Where(id => !string.IsNullOrWhiteSpace(id))
.Distinct()
.ToList();
foreach (var id in idsA)
{
// Name 규칙: "Sterilizing.UVSterilizerPower[id=XXX]"
AvailableFields.Add(new FieldItem
{
Name = $"Sterilizing.UVSterilizerPower[id={id}]",
Display = $"자외선 살균기 {id} 전원",
DataType = typeof(int),
Kind = StepFieldKind.Status
});
}
}
// 그래프 타입이 바뀔 때 후보/기본 선택 재구성
private void RebuildFieldCandidates()
{
// 후보 초기화
XFieldCandidates.Clear();
YFieldCandidates.Clear();
// X축: 시간 우선
foreach (var f in AvailableFields)
{
if ((SelectedGraphType == GraphType.LINE
|| SelectedGraphType == GraphType.STEP
|| SelectedGraphType == GraphType.SCATTER)
&& f.Name!.Equals("Number")) continue;
if (SelectedGraphType == GraphType.LINE
&& (f.Kind.Equals(StepFieldKind.Status)
|| f.Name!.Equals("Filtering.InverterControllerStatus"))) continue;
XFieldCandidates.Add(f);
if (SelectedGraphType == GraphType.STEP || SelectedGraphType == GraphType.BOX) break;
}
SelectedXField = AvailableFields.FirstOrDefault(f => f.DataType == typeof(DateTime))
?? AvailableFields.FirstOrDefault();
if (SelectedGraphType != GraphType.STEP) SelectedKind = StepFieldKind.Sensor;
IEnumerable<FieldItem> src = AvailableFields.Where(f => f.Kind == SelectedKind);
if (SelectedGraphType is GraphType.LINE or GraphType.SCATTER or GraphType.BOX)
{
// 수치형만 (LINE/SCATTER/BOX는 연속값 위주)
src = src.Where(f => f.DataType == typeof(double));
}
else if (SelectedGraphType == GraphType.STEP)
{
// STEP은 상태 전환에 잘 맞음: int/bool 위주
src = src.Where(f => f.DataType == typeof(int) || f.DataType == typeof(bool) || f.DataType == typeof(double));
// (상태가 double로 들어오는 경우도 있을 수 있어 double 허용)
}
// Y축 후보: 수치형
foreach (var f in src) YFieldCandidates.Add(f);
// 기본 선택 세팅 (타입별)
SelectedYFields.Clear();
SelectedYField = null;
switch (SelectedGraphType)
{
case GraphType.LINE:
SelectedYField = YFieldCandidates.FirstOrDefault();
ShowMarkers = false;
UseSmoothing = false;
break;
case GraphType.STEP:
ShowMarkers = false;
UseSmoothing = false;
break;
case GraphType.SCATTER:
SelectedYField = YFieldCandidates.FirstOrDefault();
ScatterMarkerSize = 3;
ShowRegression = false;
break;
case GraphType.BOX:
SelectedYField = YFieldCandidates.FirstOrDefault();
BoxWidth = 0.3;
break;
}
OnPropertyChanged(nameof(SelectedYFields));
}
protected new void OnPropertyChanged([CallerMemberName] string? name = null)
=> base.OnPropertyChanged(name);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save