using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Windows; using System.Windows.Controls; using LiveChartsCore.Defaults; using LiveChartsCore.SkiaSharpView; using ErvDashboard.Model; using ErvDashboard.Storage; namespace ErvDashboard { // 로그 그래프 창 : 가로=시간, 세로=각실 댐퍼on/off·센서값·운전/시나리오모드 (LiveCharts2). // 좌측 체크박스로 표시 시리즈 선택. 데이터는 SQLite(LogDb)에서 최근 구간 로드 + 실시간 append. public partial class GraphWindow : Window { readonly LogDb _db; readonly string[] _roomNames; const int MaxSamples = 17280; // 차트 표시 상한(최근 24h @ 5초). DB에는 전체 보관. sealed class SeriesDef { public string Group = ""; public string Label = ""; public Func Value = _ => 0; public bool Default; } SeriesDef[] _defs = Array.Empty(); LineSeries[] _series = Array.Empty>(); int _builtCount; bool[] _shown = Array.Empty(); // 시리즈별 표시(체크) 상태 — 체크된 것만 차트/범례에 노출 DateTime _selectedDate = DateTime.Today; // 현재 로드된 날짜 (기본 오늘) public GraphWindow(string[] roomNames, LogDb db) { InitializeComponent(); _db = db; _roomNames = roomNames; BuildDefs(); BuildChart(); BuildCheckboxes(); DatePick.SelectedDate = DateTime.Today; _selectedDate = DateTime.Today; BuildForDate(); // 초기: 오늘 로드(실시간). 이후 날짜는 '불러오기' 버튼으로 명시적 로드. } void BuildDefs() { var list = new List { new() { Group = "운전", Label = "운전모드", Value = s => s.RunMode }, new() { Group = "운전", Label = "자동-집중", Value = s => s.AutoMode == 1 ? 1 : 0 }, new() { Group = "운전", Label = "자동-분산", Value = s => s.AutoMode == 2 ? 1 : 0 }, new() { Group = "운전", Label = "프리셋-ECO", Value = s => s.HystPreset == 0 ? 1 : 0 }, new() { Group = "운전", Label = "프리셋-NORMAL", Value = s => s.HystPreset == 1 ? 1 : 0 }, new() { Group = "운전", Label = "프리셋-TURBO", Value = s => s.HystPreset == 2 ? 1 : 0 }, new() { Group = "운전", Label = "풍량", Value = s => s.FanMode }, new() { Group = "운전", Label = "전원", Value = s => s.Power ? 1 : 0 }, new() { Group = "시나리오", Label = "스마트수면", Value = s => s.SmartSleep ? 1 : 0 }, new() { Group = "시나리오", Label = "쾌적조리", Value = s => s.ComfortCook ? 1 : 0 }, new() { Group = "시나리오", Label = "안심회복", Value = s => s.ReliefRecover ? 1 : 0 }, }; for (int r = 0; r < _roomNames.Length; r++) { int ri = r; string nm = _roomNames[r]; list.Add(new() { Group = nm, Label = $"{nm} CO2", Value = s => s.Rooms[ri].Co2, Default = true }); list.Add(new() { Group = nm, Label = $"{nm} PM2.5", Value = s => s.Rooms[ri].Pm25 }); list.Add(new() { Group = nm, Label = $"{nm} PM10", Value = s => s.Rooms[ri].Pm10 }); list.Add(new() { Group = nm, Label = $"{nm} VOC", Value = s => s.Rooms[ri].Voc }); list.Add(new() { Group = nm, Label = $"{nm} 온도", Value = s => s.Rooms[ri].Temp }); list.Add(new() { Group = nm, Label = $"{nm} 습도", Value = s => s.Rooms[ri].Humi }); list.Add(new() { Group = nm, Label = $"{nm} LED", Value = s => s.Rooms[ri].Led }); list.Add(new() { Group = nm, Label = $"{nm} 부하점수", Value = s => s.Rooms[ri].Level }); list.Add(new() { Group = nm, Label = $"{nm} 급기댐퍼", Value = s => s.Rooms[ri].DamperSa ? 1 : 0 }); list.Add(new() { Group = nm, Label = $"{nm} 배기댐퍼", Value = s => s.Rooms[ri].DamperRa ? 1 : 0 }); } _defs = list.ToArray(); } void BuildChart() { _series = _defs.Select(d => new LineSeries { Name = d.Label, Values = new ObservableCollection(), GeometrySize = 0, LineSmoothness = 0, }).ToArray(); _shown = new bool[_defs.Length]; Array.Fill(_shown, true); // 그래프 열면 전체 선택(좌측 체크박스도 전부 체크) // 가로축: 날짜 제거, 시간(HH:mm:ss)만 표시 (날짜는 상단 DatePicker) Chart.XAxes = new[] { new DateTimeAxis(TimeSpan.FromSeconds(5), dt => dt.ToString("HH:mm:ss")) }; Chart.ZoomMode = LiveChartsCore.Measure.ZoomAndPanMode.X; Chart.LegendTextSize = 10; // 범례 글씨 작게 Chart.LegendPosition = LiveChartsCore.Measure.LegendPosition.Bottom; // 범례 하단 ApplyVisible(); } // 체크된 시리즈만 차트(+범례)에 노출. 체크 해제하면 범례에서도 사라짐. void ApplyVisible() { Chart.Series = _series.Where((s, i) => _shown[i]).ToArray(); } void BuildCheckboxes() { string? group = null; for (int i = 0; i < _defs.Length; i++) { if (_defs[i].Group != group) { group = _defs[i].Group; CheckPanel.Children.Add(new TextBlock { Text = group, FontWeight = FontWeights.Bold, Margin = new Thickness(0, 8, 0, 2) }); } int idx = i; var label = _defs[i].Label.StartsWith(group + " ") ? _defs[i].Label.Substring(group!.Length + 1) : _defs[i].Label; var cb = new CheckBox { Content = label, IsChecked = true, Margin = new Thickness(4, 1, 0, 1) }; cb.Checked += (_, _) => { _shown[idx] = true; ApplyVisible(); }; cb.Unchecked += (_, _) => { _shown[idx] = false; ApplyVisible(); }; CheckPanel.Children.Add(cb); } } // 선택 날짜(_selectedDate)의 하루치 데이터를 DB에서 로드해 차트 구성. void BuildForDate() { var samples = _db.LoadByDate(_selectedDate); for (int k = 0; k < _series.Length; k++) { var col = (ObservableCollection)_series[k].Values!; col.Clear(); foreach (var s in samples) col.Add(new DateTimePoint(s.Time, _defs[k].Value(s))); } _builtCount = samples.Count; bool today = _selectedDate == DateTime.Today; CountText.Text = $"{_selectedDate:yyyy-MM-dd} · {samples.Count}개 (5초){(today ? " · 실시간" : "")}"; } // "불러오기" : 선택한 날짜를 확정 로드. 오늘이면 실시간 갱신, 과거면 정적 유지. void Load_Click(object sender, RoutedEventArgs e) { _selectedDate = DatePick.SelectedDate?.Date ?? DateTime.Today; BuildForDate(); } // 메인이 새 샘플을 저장할 때 호출 — 오늘 보기일 때만 차트에 증분 1건 추가(상한 초과분 트림). public void OnSampleAdded(LogSample s) { if (_selectedDate != DateTime.Today) return; // 과거 날짜 보기 중엔 실시간 추가 안 함 for (int k = 0; k < _series.Length; k++) { var col = (ObservableCollection)_series[k].Values!; col.Add(new DateTimePoint(s.Time, _defs[k].Value(s))); if (col.Count > MaxSamples) col.RemoveAt(0); } _builtCount++; CountText.Text = $"{_selectedDate:yyyy-MM-dd} · {Math.Min(_builtCount, MaxSamples)}개 (5초) · 실시간"; } void Refresh_Click(object sender, RoutedEventArgs e) => BuildForDate(); // 선택 날짜의 데이터를 CSV(Excel 호환, UTF-8 BOM)로 저장. 날짜·시간 컬럼 분리. void Excel_Click(object sender, RoutedEventArgs e) { var samples = _db.LoadByDate(_selectedDate); if (samples.Count == 0) { MessageBox.Show("저장할 데이터가 없습니다."); return; } var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "CSV (Excel) (*.csv)|*.csv", FileName = $"HERV_Log_{_selectedDate:yyyyMMdd}.csv" }; if (dlg.ShowDialog() != true) return; try { ExportCsv(dlg.FileName, samples); MessageBox.Show($"저장 완료:\n{dlg.FileName}"); } catch (Exception ex) { MessageBox.Show($"저장 실패: {ex.Message}"); } } void ExportCsv(string path, List samples) { string[] modeName = { "환기", "자동", "바이패스", "공청" }; string[] autoName = { "", "집중", "분산" }; var head = new List { "날짜", "시간", "전원", "운전모드", "자동상태", "풍량", "스마트수면", "쾌적조리", "안심회복" }; foreach (var nm in _roomNames) head.AddRange(new[] { $"{nm}_급기댐퍼", $"{nm}_배기댐퍼", $"{nm}_CO2", $"{nm}_PM2.5", $"{nm}_PM10", $"{nm}_VOC", $"{nm}_온도", $"{nm}_습도", $"{nm}_LED", $"{nm}_부하점수" }); var sb = new System.Text.StringBuilder(); sb.AppendLine(string.Join(",", head)); foreach (var s in samples) { var row = new List { s.Time.ToString("yyyy-MM-dd"), s.Time.ToString("HH:mm:ss"), s.Power ? "1" : "0", s.RunMode < modeName.Length ? modeName[s.RunMode] : s.RunMode.ToString(), s.AutoMode < autoName.Length ? autoName[s.AutoMode] : "", s.FanMode.ToString(), s.SmartSleep ? "1" : "0", s.ComfortCook ? "1" : "0", s.ReliefRecover ? "1" : "0", }; foreach (var rm in s.Rooms) { row.Add(rm.DamperSa ? "1" : "0"); row.Add(rm.DamperRa ? "1" : "0"); row.Add(rm.Co2.ToString()); row.Add(rm.Pm25.ToString()); row.Add(rm.Pm10.ToString()); row.Add(rm.Voc.ToString()); row.Add(rm.Temp.ToString()); row.Add(rm.Humi.ToString()); row.Add(rm.Led.ToString()); row.Add(rm.Level.ToString()); } sb.AppendLine(string.Join(",", row)); } System.IO.File.WriteAllText(path, sb.ToString(), new System.Text.UTF8Encoding(true)); } void Close_Click(object sender, RoutedEventArgs e) => Close(); } }