- Agent Compare 기준으로 런처 빠른 실행 칩, 검색 히스토리 탐색, 선택 항목 미리보기 패널을 현재 런처에 이식 - 하단 위젯 바, QuickLook(F3), 화면 OCR(F4), 관련 서비스/partial 파일을 현재 LauncherWindow/LauncherViewModel 구조에 연결 - UsageRankingService 상위 항목 조회와 SearchHistoryService를 추가해 실행 상위 경로/검색 기록이 실제 런처 동작에 반영되도록 정리 - README.md, docs/DEVELOPMENT.md에 이식 범위와 검증 결과를 2026-04-05 11:58 (KST) 기준으로 기록 검증 결과 - dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 경고 0 / 오류 0
123 lines
4.1 KiB
C#
123 lines
4.1 KiB
C#
using System.IO;
|
|
using System.Text.Json;
|
|
|
|
namespace AxCopilot.Services;
|
|
|
|
/// <summary>
|
|
/// 런처 항목 실행 횟수를 추적하여 퍼지 검색 결과 정렬에 활용합니다.
|
|
/// 저장 위치: %APPDATA%\AxCopilot\usage.json
|
|
/// 형식: { "키": 횟수 } — 키는 IndexEntry.Path (파일/폴더/앱 경로)
|
|
/// </summary>
|
|
internal static class UsageRankingService
|
|
{
|
|
private static readonly string _dataFile = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"AxCopilot", "usage.json");
|
|
|
|
private static Dictionary<string, int> _counts = new(StringComparer.OrdinalIgnoreCase);
|
|
private static bool _loaded = false;
|
|
private static readonly object _lock = new();
|
|
|
|
/// <summary>
|
|
/// 항목 실행 시 호출하여 카운트를 증가시킵니다.
|
|
/// </summary>
|
|
public static void RecordExecution(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key)) return;
|
|
EnsureLoaded();
|
|
lock (_lock)
|
|
{
|
|
_counts.TryGetValue(key, out var current);
|
|
_counts[key] = current + 1;
|
|
}
|
|
_ = SaveAsync();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 주어진 키의 실행 횟수를 반환합니다. 없으면 0.
|
|
/// </summary>
|
|
public static int GetScore(string key)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(key)) return 0;
|
|
EnsureLoaded();
|
|
lock (_lock)
|
|
{
|
|
return _counts.TryGetValue(key, out var count) ? count : 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 실행 횟수 기준으로 내림차순 정렬하는 컴파러를 반환합니다.
|
|
/// 동점이면 원래 순서 유지 (stable sort).
|
|
/// </summary>
|
|
public static IEnumerable<T> SortByUsage<T>(IEnumerable<T> items, Func<T, string?> keySelector)
|
|
{
|
|
EnsureLoaded();
|
|
return items
|
|
.Select((item, idx) => (item, idx, score: GetScore(keySelector(item) ?? "")))
|
|
.OrderByDescending(x => x.score)
|
|
.ThenBy(x => x.idx)
|
|
.Select(x => x.item);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 실행 횟수 상위 항목을 반환합니다.
|
|
/// 빠른 실행 칩처럼 경로 자체 목록이 필요한 화면에서 사용합니다.
|
|
/// </summary>
|
|
public static IReadOnlyList<KeyValuePair<string, int>> GetTopItems(int maxCount)
|
|
{
|
|
EnsureLoaded();
|
|
lock (_lock)
|
|
{
|
|
return _counts
|
|
.OrderByDescending(x => x.Value)
|
|
.ThenBy(x => x.Key, StringComparer.OrdinalIgnoreCase)
|
|
.Take(Math.Max(0, maxCount))
|
|
.ToList();
|
|
}
|
|
}
|
|
|
|
// ─── 내부 ──────────────────────────────────────────────────────────────────
|
|
|
|
private static void EnsureLoaded()
|
|
{
|
|
if (_loaded) return;
|
|
lock (_lock)
|
|
{
|
|
if (_loaded) return;
|
|
try
|
|
{
|
|
if (File.Exists(_dataFile))
|
|
{
|
|
var json = File.ReadAllText(_dataFile);
|
|
var data = JsonSerializer.Deserialize<Dictionary<string, int>>(json);
|
|
if (data != null)
|
|
_counts = new Dictionary<string, int>(data, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn($"usage.json 로드 실패: {ex.Message}");
|
|
}
|
|
_loaded = true;
|
|
}
|
|
}
|
|
|
|
private static async Task SaveAsync()
|
|
{
|
|
try
|
|
{
|
|
Dictionary<string, int> snapshot;
|
|
lock (_lock) { snapshot = new Dictionary<string, int>(_counts); }
|
|
|
|
Directory.CreateDirectory(Path.GetDirectoryName(_dataFile)!);
|
|
var json = JsonSerializer.Serialize(snapshot, new JsonSerializerOptions { WriteIndented = false });
|
|
await File.WriteAllTextAsync(_dataFile, json);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Warn($"usage.json 저장 실패: {ex.Message}");
|
|
}
|
|
}
|
|
}
|