chore: HERV 통합 저장소 재초기화 커밋

손상된 .git 히스토리(missing tree)로 재초기화 후 작업트리 전체 커밋.
.claude/ 만 제외(로컬 에이전트 설정). 구 저장소 백업(.git_corrupt_backup/) 포함.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jeon
2026-06-16 09:29:03 +09:00
commit a502322188
630 changed files with 65126 additions and 0 deletions
+218
View File
@@ -0,0 +1,218 @@
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<LogSample, double> Value = _ => 0;
public bool Default;
}
SeriesDef[] _defs = Array.Empty<SeriesDef>();
LineSeries<DateTimePoint>[] _series = Array.Empty<LineSeries<DateTimePoint>>();
int _builtCount;
bool[] _shown = Array.Empty<bool>(); // 시리즈별 표시(체크) 상태 — 체크된 것만 차트/범례에 노출
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<SeriesDef>
{
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<DateTimePoint>
{
Name = d.Label,
Values = new ObservableCollection<DateTimePoint>(),
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<DateTimePoint>)_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<DateTimePoint>)_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<LogSample> samples)
{
string[] modeName = { "환기", "자동", "바이패스", "공청" };
string[] autoName = { "", "집중", "분산" };
var head = new List<string> { "날짜", "시간", "전원", "운전모드", "자동상태", "풍량", "스마트수면", "쾌적조리", "안심회복" };
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<string>
{
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();
}
}