using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows.Media; using AxCopilot.Models; using AxCopilot.Views; using AxCopilot.Services; namespace AxCopilot.ViewModels; /// 하루 사용 통계를 막대 차트 아이템으로 표현 public class DayBarItem { public string DateLabel { get; init; } = ""; // "3/26" 형태 public string DayLabel { get; init; } = ""; // "월" 형태 public int Value { get; init; } public double BarHeight { get; init; } // 0~66 (픽셀) public string ValueLabel { get; init; } = ""; public string ToolTipText { get; init; } = ""; // 마우스 호버 시 표시 public bool IsToday { get; init; } public bool HasData => Value > 0; } /// 인기 명령어 순위 아이템 public class CommandStatItem { public int Rank { get; init; } public string Command { get; init; } = ""; public int Count { get; init; } public double BarWidth { get; init; } // 0~200 (픽셀) } /// 즐겨찾기 항목 (통계 화면에 표시) public class FavoriteStatItem { public int Rank { get; init; } public string Name { get; init; } = ""; public string Path { get; init; } = ""; public string Icon { get; init; } = "\uE8B7"; // 기본: Folder public bool Exists { get; init; } } /// 탭별 대화 비율 아이템 public class TabRatioItem { public string TabName { get; init; } = ""; public int Count { get; init; } public double Percentage { get; init; } // 0~100 public double BarWidth { get; init; } // 0~300 (픽셀) public string PercentLabel { get; init; } = ""; public Brush Color { get; init; } = Brushes.MediumSlateBlue; } public class StatisticsViewModel : INotifyPropertyChanged { private const double MaxBarHeight = 66.0; private const double MaxBarWidth = 200.0; // ─── 차트 데이터 ───────────────────────────────────────────────────────── public ObservableCollection LauncherOpenBars { get; } = new(); public ObservableCollection ActiveTimeBars { get; } = new(); public ObservableCollection TopCommands { get; } = new(); public ObservableCollection TopFavorites { get; } = new(); // AX Agent 통계 public ObservableCollection ChatCountBars { get; } = new(); // 대화 빈도 public ObservableCollection TokenUsageBars { get; } = new(); // 토큰 사용량 // 추가 차트 public ObservableCollection WeekdayAvgBars { get; } = new(); // 요일별 평균 호출 public ObservableCollection TabChatRatios { get; } = new(); // 탭별 대화 비율 private bool _hasFavorites; public bool HasFavorites { get => _hasFavorites; private set { _hasFavorites = value; OnPropertyChanged(); } } // ─── 요약 카드 ─────────────────────────────────────────────────────────── private string _todayDate = ""; private string _todaySummary = ""; private string _peakDate = ""; private string _peakDaySummary = ""; private string _weekSummary = ""; private string _totalOpensSummary = ""; public string TodayDate { get => _todayDate; set { _todayDate = value; OnPropertyChanged(); } } public string TodaySummary { get => _todaySummary; set { _todaySummary = value; OnPropertyChanged(); } } public string PeakDate { get => _peakDate; set { _peakDate = value; OnPropertyChanged(); } } public string PeakDaySummary { get => _peakDaySummary; set { _peakDaySummary = value; OnPropertyChanged(); } } public string WeekSummary { get => _weekSummary; set { _weekSummary = value; OnPropertyChanged(); } } public string TotalOpensSummary { get => _totalOpensSummary; set { _totalOpensSummary = value; OnPropertyChanged(); } } private string _agentSummary = ""; public string AgentSummary { get => _agentSummary; set { _agentSummary = value; OnPropertyChanged(); } } // 토큰 비율 private double _promptBarWidth; public double PromptBarWidth { get => _promptBarWidth; set { _promptBarWidth = value; OnPropertyChanged(); } } private double _completionBarWidth; public double CompletionBarWidth { get => _completionBarWidth; set { _completionBarWidth = value; OnPropertyChanged(); } } private string _promptTokenLabel = ""; public string PromptTokenLabel { get => _promptTokenLabel; set { _promptTokenLabel = value; OnPropertyChanged(); } } private string _completionTokenLabel = ""; public string CompletionTokenLabel { get => _completionTokenLabel; set { _completionTokenLabel = value; OnPropertyChanged(); } } private string _tokenRatioSummary = ""; public string TokenRatioSummary { get => _tokenRatioSummary; set { _tokenRatioSummary = value; OnPropertyChanged(); } } public StatisticsViewModel() { Refresh(); } public void Refresh() { var stats = UsageStatisticsService.GetStats(30); var today = DateTime.Today.ToString("yyyy-MM-dd"); BuildLauncherOpenChart(stats, today); BuildActiveTimeChart(stats, today); BuildChatCountChart(stats, today); BuildTokenUsageChart(stats, today); BuildTopCommands(stats); BuildWeekdayAvgChart(stats); BuildTabChatRatios(stats); BuildTokenRatio(stats); BuildSummaryCards(stats, today); BuildTopFavorites(); } // ─── 차트 빌더 ─────────────────────────────────────────────────────────── private void BuildLauncherOpenChart(List stats, string today) { // 최근 14일만 표시 var recent = stats.TakeLast(14).ToList(); int maxVal = recent.Max(s => s.LauncherOpens); if (maxVal == 0) maxVal = 1; LauncherOpenBars.Clear(); foreach (var s in recent) { var date = DateTime.TryParse(s.Date, out var d) ? d : DateTime.MinValue; LauncherOpenBars.Add(new DayBarItem { DateLabel = date != DateTime.MinValue ? $"{date.Month}/{date.Day}" : "", DayLabel = date != DateTime.MinValue ? GetKoreanDayOfWeek(date.DayOfWeek) : "", Value = s.LauncherOpens, BarHeight = MaxBarHeight * s.LauncherOpens / maxVal, ValueLabel = s.LauncherOpens > 0 ? s.LauncherOpens.ToString() : "", ToolTipText = date != DateTime.MinValue ? $"{date:yyyy-MM-dd} ({GetKoreanDayOfWeek(date.DayOfWeek)})\n호출 {s.LauncherOpens}회" : "", IsToday = s.Date == today }); } } private void BuildActiveTimeChart(List stats, string today) { var recent = stats.TakeLast(14).ToList(); int maxVal = recent.Max(s => s.ActiveSeconds); if (maxVal == 0) maxVal = 1; ActiveTimeBars.Clear(); foreach (var s in recent) { var date = DateTime.TryParse(s.Date, out var d) ? d : DateTime.MinValue; ActiveTimeBars.Add(new DayBarItem { DateLabel = date != DateTime.MinValue ? $"{date.Month}/{date.Day}" : "", DayLabel = date != DateTime.MinValue ? GetKoreanDayOfWeek(date.DayOfWeek) : "", Value = s.ActiveSeconds, BarHeight = MaxBarHeight * s.ActiveSeconds / maxVal, ValueLabel = FormatActiveTime(s.ActiveSeconds), ToolTipText = date != DateTime.MinValue ? $"{date:yyyy-MM-dd} ({GetKoreanDayOfWeek(date.DayOfWeek)})\n활성 {FormatActiveTime(s.ActiveSeconds)}" : "", IsToday = s.Date == today }); } } private void BuildChatCountChart(List stats, string today) { var recent = stats.TakeLast(14).ToList(); // 탭별 합산 int maxVal = recent.Max(s => { int sum = 0; foreach (var kv in s.ChatCounts) sum += kv.Value; return sum; }); if (maxVal == 0) maxVal = 1; ChatCountBars.Clear(); foreach (var s in recent) { var dt = DateTime.TryParse(s.Date, out var d) ? d : DateTime.MinValue; int chat = s.ChatCounts.GetValueOrDefault("Chat"); int cowork = s.ChatCounts.GetValueOrDefault("Cowork"); int code = s.ChatCounts.GetValueOrDefault("Code"); int total = chat + cowork + code; var isToday = s.Date == today; ChatCountBars.Add(new DayBarItem { DateLabel = dt.ToString("M/d"), DayLabel = dt.ToString("ddd")[..1], Value = total, BarHeight = total > 0 ? total * MaxBarHeight / maxVal : 2, ValueLabel = total > 0 ? total.ToString() : "", ToolTipText = $"{s.Date} ({dt:ddd})\nChat: {chat}회 · Cowork: {cowork}회 · Code: {code}회", IsToday = isToday, }); } } private void BuildTokenUsageChart(List stats, string today) { var recent = stats.TakeLast(14).ToList(); long maxVal = recent.Max(s => s.TotalTokens); if (maxVal == 0) maxVal = 1; TokenUsageBars.Clear(); foreach (var s in recent) { var dt = DateTime.TryParse(s.Date, out var d) ? d : DateTime.MinValue; var isToday = s.Date == today; var tokens = s.TotalTokens; TokenUsageBars.Add(new DayBarItem { DateLabel = dt.ToString("M/d"), DayLabel = dt.ToString("ddd")[..1], Value = (int)Math.Min(tokens, int.MaxValue), BarHeight = tokens > 0 ? (double)tokens * MaxBarHeight / maxVal : 2, ValueLabel = tokens > 0 ? FormatTokens(tokens) : "", ToolTipText = $"{s.Date} ({dt:ddd})\n프롬프트: {s.PromptTokens:N0} · 완료: {s.CompletionTokens:N0} · 합계: {s.TotalTokens:N0}", IsToday = isToday, }); } } private static string FormatTokens(long tokens) => tokens switch { >= 1_000_000 => $"{tokens / 1_000_000.0:F1}M", >= 1_000 => $"{tokens / 1_000.0:F1}K", _ => tokens.ToString(), }; private void BuildWeekdayAvgChart(List stats) { // 요일별 평균 호출 (30일 데이터 → 요일 그룹) var groups = new Dictionary>(); foreach (DayOfWeek dow in Enum.GetValues()) groups[dow] = new(); foreach (var s in stats) { if (DateTime.TryParse(s.Date, out var d)) groups[d.DayOfWeek].Add(s.LauncherOpens); } // 월~일 순서 var ordered = new[] { DayOfWeek.Monday, DayOfWeek.Tuesday, DayOfWeek.Wednesday, DayOfWeek.Thursday, DayOfWeek.Friday, DayOfWeek.Saturday, DayOfWeek.Sunday }; double maxAvg = ordered.Max(dow => groups[dow].Count > 0 ? groups[dow].Average() : 0); if (maxAvg < 1) maxAvg = 1; WeekdayAvgBars.Clear(); foreach (var dow in ordered) { var list = groups[dow]; var avg = list.Count > 0 ? list.Average() : 0; WeekdayAvgBars.Add(new DayBarItem { DateLabel = "", DayLabel = GetKoreanDayOfWeek(dow), Value = (int)Math.Round(avg), BarHeight = avg > 0 ? avg * MaxBarHeight / maxAvg : 2, ValueLabel = avg > 0 ? avg.ToString("F1") : "", ToolTipText = $"{GetKoreanDayOfWeek(dow)}요일 평균 {avg:F1}회 ({list.Count}일 데이터)", IsToday = DateTime.Today.DayOfWeek == dow, }); } } private void BuildTabChatRatios(List stats) { // 탭별 대화 비율 (30일 합계) int chat = 0, cowork = 0, code = 0; foreach (var s in stats) { chat += s.ChatCounts.GetValueOrDefault("Chat"); cowork += s.ChatCounts.GetValueOrDefault("Cowork"); code += s.ChatCounts.GetValueOrDefault("Code"); } int total = chat + cowork + code; if (total == 0) total = 1; const double MaxWidth = 300.0; TabChatRatios.Clear(); TabChatRatios.Add(new TabRatioItem { TabName = "Chat", Count = chat, Percentage = 100.0 * chat / total, BarWidth = MaxWidth * chat / total, PercentLabel = $"{100.0 * chat / total:F0}% ({chat}회)", Color = ThemeResourceHelper.HexBrush("#818CF8") }); TabChatRatios.Add(new TabRatioItem { TabName = "Cowork", Count = cowork, Percentage = 100.0 * cowork / total, BarWidth = MaxWidth * cowork / total, PercentLabel = $"{100.0 * cowork / total:F0}% ({cowork}회)", Color = ThemeResourceHelper.HexBrush("#10B981") }); TabChatRatios.Add(new TabRatioItem { TabName = "Code", Count = code, Percentage = 100.0 * code / total, BarWidth = MaxWidth * code / total, PercentLabel = $"{100.0 * code / total:F0}% ({code}회)", Color = ThemeResourceHelper.HexBrush("#F59E0B") }); } private void BuildTokenRatio(List stats) { long prompt = stats.Sum(s => s.PromptTokens); long completion = stats.Sum(s => s.CompletionTokens); long total = prompt + completion; if (total == 0) total = 1; const double TotalWidth = 400.0; PromptBarWidth = TotalWidth * prompt / total; CompletionBarWidth = TotalWidth * completion / total; PromptTokenLabel = $"입력 {FormatTokens(prompt)} ({100.0 * prompt / total:F0}%)"; CompletionTokenLabel = $"출력 {FormatTokens(completion)} ({100.0 * completion / total:F0}%)"; TokenRatioSummary = $"30일 합계 입력 {FormatTokens(prompt)} + 출력 {FormatTokens(completion)} = {FormatTokens(prompt + completion)}"; } private void BuildTopCommands(List stats) { // 30일 집계 var aggregate = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var s in stats) foreach (var (key, count) in s.CommandUsage) { aggregate.TryGetValue(key, out var cur); aggregate[key] = cur + count; } var top = aggregate .OrderByDescending(kv => kv.Value) .Take(10) .ToList(); int maxCount = top.Count > 0 ? top[0].Value : 1; TopCommands.Clear(); for (int i = 0; i < top.Count; i++) { TopCommands.Add(new CommandStatItem { Rank = i + 1, Command = top[i].Key, Count = top[i].Value, BarWidth = MaxBarWidth * top[i].Value / maxCount }); } } private void BuildSummaryCards(List stats, string today) { // 오늘 날짜 표시 TodayDate = DateTime.Today.ToString("yyyy년 M월 d일"); // 오늘 통계 var todayStat = stats.FirstOrDefault(s => s.Date == today); TodaySummary = todayStat != null ? $"{todayStat.LauncherOpens}회 호출 · {FormatActiveTime(todayStat.ActiveSeconds)}" : "데이터 없음"; // 역대 최고 호출일 var peakDay = stats.OrderByDescending(s => s.LauncherOpens).FirstOrDefault(); if (peakDay != null && peakDay.LauncherOpens > 0) { PeakDate = DateTime.TryParse(peakDay.Date, out var pd) ? pd.ToString("yyyy년 M월 d일") : peakDay.Date; PeakDaySummary = $"{peakDay.LauncherOpens}회 호출"; } else { PeakDate = ""; PeakDaySummary = "기록 없음"; } // 이번 주 합계 var weekStart = DateTime.Today.AddDays(-(int)DateTime.Today.DayOfWeek); var weekTotal = stats .Where(s => DateTime.TryParse(s.Date, out var d) && d >= weekStart) .Sum(s => s.LauncherOpens); WeekSummary = $"이번 주 {weekTotal}회"; // 30일 총 호출 var totalOpens = stats.Sum(s => s.LauncherOpens); TotalOpensSummary = $"30일 합계 {totalOpens}회"; // AX Agent 요약 var totalChats = stats.Sum(s => s.ChatCounts.Values.Sum()); var totalTokens = stats.Sum(s => s.TotalTokens); var todayChats = todayStat?.ChatCounts.Values.Sum() ?? 0; var todayTokens = todayStat?.TotalTokens ?? 0; AgentSummary = $"오늘 {todayChats}회 대화 · {FormatTokens(todayTokens)} 토큰 | 30일 합계 {totalChats}회 · {FormatTokens(totalTokens)} 토큰"; } private void BuildTopFavorites() { TopFavorites.Clear(); var favFile = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "favorites.json"); if (!File.Exists(favFile)) { HasFavorites = false; return; } try { var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var list = JsonSerializer.Deserialize>(File.ReadAllText(favFile), opts) ?? new(); var top = list.Take(6).ToList(); if (top.Count == 0) { HasFavorites = false; return; } for (int i = 0; i < top.Count; i++) { var entry = top[i]; var expanded = Environment.ExpandEnvironmentVariables(entry.Path); var isDir = Directory.Exists(expanded); var exists = isDir || File.Exists(expanded); TopFavorites.Add(new FavoriteStatItem { Rank = i + 1, Name = entry.Name, Path = entry.Path, Icon = isDir ? "\uE8B7" : "\uE8A5", // Folder / Page Exists = exists }); } HasFavorites = TopFavorites.Count > 0; } catch (Exception) { HasFavorites = false; } } // favorites.json 역직렬화용 내부 레코드 private class FavFileEntry { [JsonPropertyName("name")] public string Name { get; set; } = ""; [JsonPropertyName("path")] public string Path { get; set; } = ""; } // ─── 헬퍼 ──────────────────────────────────────────────────────────────── private static string GetKoreanDayOfWeek(DayOfWeek dow) => dow switch { DayOfWeek.Monday => "월", DayOfWeek.Tuesday => "화", DayOfWeek.Wednesday => "수", DayOfWeek.Thursday => "목", DayOfWeek.Friday => "금", DayOfWeek.Saturday => "토", _ => "일" }; private static string FormatActiveTime(int totalSeconds) { if (totalSeconds <= 0) return ""; int h = totalSeconds / 3600; int m = (totalSeconds % 3600) / 60; if (h > 0 && m > 0) return $"{h}시간 {m}분"; if (h > 0) return $"{h}시간"; if (m > 0) return $"{m}분"; return $"{totalSeconds}초"; } public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? n = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n)); }