Initial commit to new repository

This commit is contained in:
2026-04-03 18:23:52 +09:00
commit deffb33cf9
5248 changed files with 267762 additions and 0 deletions

View File

@@ -0,0 +1,492 @@
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;
/// <summary>하루 사용 통계를 막대 차트 아이템으로 표현</summary>
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;
}
/// <summary>인기 명령어 순위 아이템</summary>
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 (픽셀)
}
/// <summary>즐겨찾기 항목 (통계 화면에 표시)</summary>
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; }
}
/// <summary>탭별 대화 비율 아이템</summary>
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<DayBarItem> LauncherOpenBars { get; } = new();
public ObservableCollection<DayBarItem> ActiveTimeBars { get; } = new();
public ObservableCollection<CommandStatItem> TopCommands { get; } = new();
public ObservableCollection<FavoriteStatItem> TopFavorites { get; } = new();
// AX Agent 통계
public ObservableCollection<DayBarItem> ChatCountBars { get; } = new(); // 대화 빈도
public ObservableCollection<DayBarItem> TokenUsageBars { get; } = new(); // 토큰 사용량
// 추가 차트
public ObservableCollection<DayBarItem> WeekdayAvgBars { get; } = new(); // 요일별 평균 호출
public ObservableCollection<TabRatioItem> 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<DailyUsageStats> 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<DailyUsageStats> 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<DailyUsageStats> 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<DailyUsageStats> 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<DailyUsageStats> stats)
{
// 요일별 평균 호출 (30일 데이터 → 요일 그룹)
var groups = new Dictionary<DayOfWeek, List<int>>();
foreach (DayOfWeek dow in Enum.GetValues<DayOfWeek>())
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<DailyUsageStats> 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<DailyUsageStats> 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<DailyUsageStats> stats)
{
// 30일 집계
var aggregate = new Dictionary<string, int>(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<DailyUsageStats> 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<List<FavFileEntry>>(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));
}