using System.IO; using System.Text.Json; using AxCopilot.Models; namespace AxCopilot.Services; /// /// 일별 사용 통계를 기록하고 조회합니다. /// 저장 위치: %APPDATA%\AxCopilot\stats\YYYY-MM-DD.json /// 30일 초과 파일은 자동 삭제(롤링). /// internal static class UsageStatisticsService { private static readonly string _statsDir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "stats"); private static DailyUsageStats _today = new(); private static bool _initialized; private static readonly object _lock = new(); private const int RetentionDays = 30; // ─── 공개 API ──────────────────────────────────────────────────────────── public static void RecordLauncherOpen() { EnsureInitialized(); lock (_lock) { _today.LauncherOpens++; } _ = SaveTodayAsync(); } public static void RecordCommandUsage(string commandKey) { if (string.IsNullOrWhiteSpace(commandKey)) return; EnsureInitialized(); lock (_lock) { _today.CommandUsage.TryGetValue(commandKey, out var cur); _today.CommandUsage[commandKey] = cur + 1; } _ = SaveTodayAsync(); } public static void AddActiveSeconds(int seconds) { if (seconds <= 0) return; EnsureInitialized(); lock (_lock) { _today.ActiveSeconds += seconds; } _ = SaveTodayAsync(); } /// 당일 기록된 PC 활성 시간(초)을 반환합니다. 재시작 전 누적분 포함. public static int GetTodayActiveSeconds() { EnsureInitialized(); lock (_lock) { return _today.ActiveSeconds; } } /// AX Agent 대화를 탭별로 기록합니다. public static void RecordChat(string tab) { if (string.IsNullOrWhiteSpace(tab)) tab = "Chat"; EnsureInitialized(); lock (_lock) { _today.ChatCounts.TryGetValue(tab, out var cur); _today.ChatCounts[tab] = cur + 1; } _ = SaveTodayAsync(); } /// 토큰 사용량을 기록합니다. public static void RecordTokens(int promptTokens, int completionTokens) { if (promptTokens <= 0 && completionTokens <= 0) return; EnsureInitialized(); lock (_lock) { _today.PromptTokens += promptTokens; _today.CompletionTokens += completionTokens; _today.TotalTokens += promptTokens + completionTokens; } _ = SaveTodayAsync(); } /// 최근 일치 통계를 날짜 오름차순으로 반환합니다. public static List GetStats(int days = 30) { EnsureInitialized(); var result = new List(); var today = DateTime.Today; for (int i = days - 1; i >= 0; i--) { var date = today.AddDays(-i); var dateStr = date.ToString("yyyy-MM-dd"); if (dateStr == _today.Date) { DailyUsageStats snapshot; lock (_lock) { snapshot = CloneToday(); } result.Add(snapshot); continue; } var path = GetFilePath(dateStr); if (File.Exists(path)) { try { var json = File.ReadAllText(path); var stat = JsonSerializer.Deserialize(json); if (stat != null) result.Add(stat); else result.Add(new DailyUsageStats { Date = dateStr }); } catch { result.Add(new DailyUsageStats { Date = dateStr }); } } else { result.Add(new DailyUsageStats { Date = dateStr }); } } return result; } // ─── 내부 ──────────────────────────────────────────────────────────────── private static void EnsureInitialized() { if (_initialized) return; lock (_lock) { if (_initialized) return; try { Directory.CreateDirectory(_statsDir); var todayStr = DateTime.Today.ToString("yyyy-MM-dd"); var path = GetFilePath(todayStr); if (File.Exists(path)) { var json = File.ReadAllText(path); var stat = JsonSerializer.Deserialize(json); _today = stat ?? new DailyUsageStats { Date = todayStr }; } else { _today = new DailyUsageStats { Date = todayStr }; } PurgeOldFiles(); } catch (Exception ex) { LogService.Warn($"통계 서비스 초기화 실패: {ex.Message}"); _today = new DailyUsageStats { Date = DateTime.Today.ToString("yyyy-MM-dd") }; } _initialized = true; } } private static void PurgeOldFiles() { try { var cutoff = DateTime.Today.AddDays(-RetentionDays); foreach (var file in Directory.GetFiles(_statsDir, "????-??-??.json")) { var name = Path.GetFileNameWithoutExtension(file); if (DateTime.TryParse(name, out var fileDate) && fileDate < cutoff) File.Delete(file); } } catch (Exception ex) { LogService.Warn($"통계 파일 정리 실패: {ex.Message}"); } } private static async Task SaveTodayAsync() { try { DailyUsageStats snapshot; lock (_lock) { snapshot = CloneToday(); } var path = GetFilePath(snapshot.Date); var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = false }); await File.WriteAllTextAsync(path, json); } catch (Exception ex) { LogService.Warn($"통계 저장 실패: {ex.Message}"); } } private static DailyUsageStats CloneToday() => new() { Date = _today.Date, LauncherOpens = _today.LauncherOpens, ActiveSeconds = _today.ActiveSeconds, CommandUsage = new Dictionary(_today.CommandUsage), ChatCounts = new Dictionary(_today.ChatCounts), TotalTokens = _today.TotalTokens, PromptTokens = _today.PromptTokens, CompletionTokens = _today.CompletionTokens, }; private static string GetFilePath(string dateStr) => Path.Combine(_statsDir, $"{dateStr}.json"); }