diff --git a/README.md b/README.md
index 36cd371..8be7161 100644
--- a/README.md
+++ b/README.md
@@ -1318,3 +1318,7 @@ MIT License
- 업데이트: 2026-04-06 18:09 (KST)
- 채팅 메시지의 좋아요/싫어요 토글을 다시 정리했다. [ChatWindow.MessageInteractions.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Views/ChatWindow.MessageInteractions.cs) 에서 두 버튼이 각자 상태를 따로 들고 있던 구조를 없애고, 하나의 shared feedback 상태(`like/dislike/null`)를 기준으로 상호배타 토글되도록 재구성했다.
- 이제 `좋아요`도 즉시 색상/배경 상태가 바뀌고, `싫어요`를 다시 누르면 원래 상태(null)로 정상 해제된다. 버튼 시각 표현도 같은 glyph를 유지하되 active 색상과 라운드 chip 배경/테두리로 구분해, 특정 filled glyph가 보이지 않던 문제를 함께 줄였다.
+- 업데이트: 2026-04-06 18:24 (KST)
+ - 런처 색인 구조를 임시 지연 실행에서 `영속 캐시 + watcher 증분 반영` 방식으로 바꿨다. [IndexService.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/Services/IndexService.cs)는 이제 `%APPDATA%\\AxCopilot\\index\\launcher-index.json`에 파일 시스템 인덱스를 저장하고, 앱 시작 시 캐시를 즉시 로드해 첫 검색부터 이전 색인을 재사용한다.
+ - `FileSystemWatcher`도 더 이상 파일 하나 바뀔 때마다 3초 뒤 전체 재빌드를 때리지 않고, 생성/삭제/파일 이름 변경은 가능한 범위에서 해당 항목만 증분 반영한다. 디렉터리 이름 변경처럼 하위 경로 전체 영향이 큰 경우에만 전체 재색인으로 폴백한다.
+ - [App.xaml.cs](/E:/AX%20Copilot%20-%20Codex/src/AxCopilot/App.xaml.cs) 는 앱 시작 시 캐시 로드와 watcher 시작을 바로 수행하고, 실제 무거운 전체 재색인은 첫 검색 시 `EnsureIndexWarmupStarted()`로 한 번만 보강 실행하도록 정리했다. 이 변경으로 런처는 즉시 검색 가능 상태를 유지하면서도, 평소엔 전체 재색인 비용을 반복해서 치르지 않게 됐다.
diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md
index 91e58fc..e59c397 100644
--- a/docs/DEVELOPMENT.md
+++ b/docs/DEVELOPMENT.md
@@ -4999,3 +4999,6 @@ ow + toggle ?쒓컖 ?몄뼱濡??ㅼ떆 ?뺣젹?덈떎.
- Document update: 2026-04-06 18:02 (KST) - Hardened vLLM response handling for IBM deployment endpoints by accepting `results[].generated_text`, `output_text`, and `choices[].message.content`, and by short-circuiting tool-use requests in `LlmService.ToolUse.cs` with a `ToolCallNotSupportedException` so IBM deployment chat connections do not receive an incompatible OpenAI function-calling payload.
- Document update: 2026-04-06 18:09 (KST) - Reworked message feedback toggles in `ChatWindow.MessageInteractions.cs`. `좋아요/싫어요` no longer keep separate local state with sibling reset callbacks; both buttons now derive from one shared `like/dislike/null` state and persist that single value back to the conversation/session.
- Document update: 2026-04-06 18:09 (KST) - The feedback buttons now use the same glyph in both idle/active states and express activation through color plus rounded chip background/border, which avoids cases where the like filled-glyph was visually missing and ensures pressing `싫어요` again properly returns the message to an unselected state.
+- Document update: 2026-04-06 18:24 (KST) - Reworked launcher indexing in `IndexService.cs` from a “full rebuild on every change” structure to a persisted cache plus incremental watcher model. Launcher file-system entries are now saved to `%APPDATA%\AxCopilot\index\launcher-index.json`, loaded immediately at startup, and merged with aliases/built-in app entries before the first search.
+- Document update: 2026-04-06 18:24 (KST) - `FileSystemWatcher` changes are now handled incrementally where possible. File create/delete/rename events update only the affected launcher index entries and debounce cache persistence, while high-impact directory rename/delete cases still fall back to a full rebuild for correctness.
+- Document update: 2026-04-06 18:24 (KST) - `App.xaml.cs` now restores eager cache availability by calling `LoadCachedIndex()` and `StartWatchers()` at startup, while keeping the heavier full scan on the existing `EnsureIndexWarmupStarted()` path. This keeps the launcher responsive from the first open without reintroducing continuous full rescans as idle background work.
diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs
index 97a658a..1027d0d 100644
--- a/src/AxCopilot/App.xaml.cs
+++ b/src/AxCopilot/App.xaml.cs
@@ -115,6 +115,8 @@ public partial class App : System.Windows.Application
_indexService = new IndexService(settings);
var indexService = _indexService;
+ indexService.LoadCachedIndex();
+ indexService.StartWatchers();
var fuzzyEngine = new FuzzyEngine(indexService);
var commandResolver = new CommandResolver(fuzzyEngine, settings);
var contextManager = new ContextManager(settings);
@@ -288,8 +290,9 @@ public partial class App : System.Windows.Application
() => _clipboardHistory?.Initialize(),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
- // ─── 인덱스 빌드/감시는 실제 런처를 사용할 때 시작 ───────────────────
- // 앱 시작 직후 전체 스캔과 감시 훅을 붙이지 않아 유휴 체감 부하를 줄임
+ // ─── 런처 인덱스 캐시/감시 초기화 ─────────────────────────────────────
+ // 앱 시작 시엔 저장된 캐시를 즉시 로드하고, 파일 감시는 증분 반영 위주로 유지합니다.
+ // 무거운 전체 재색인은 실제 검색이 시작될 때 한 번만 보강 실행합니다.
// ─── 글로벌 훅 + 스니펫 확장기 ───────────────────────────────────────
_inputListener = new InputListener();
diff --git a/src/AxCopilot/Services/IndexService.cs b/src/AxCopilot/Services/IndexService.cs
index ac44b04..eb5ec97 100644
--- a/src/AxCopilot/Services/IndexService.cs
+++ b/src/AxCopilot/Services/IndexService.cs
@@ -1,36 +1,92 @@
-using System.Diagnostics;
+using System.Diagnostics;
using System.IO;
+using System.Text;
+using System.Text.Json;
using AxCopilot.Models;
namespace AxCopilot.Services;
///
/// 파일/앱 인덱싱 서비스. Fuzzy 검색의 데이터 소스를 관리합니다.
-/// FileSystemWatcher로 변경 시 자동 재빌드(3초 디바운스)합니다.
+/// 파일 시스템 결과는 디스크 캐시에 영속 저장하고, FileSystemWatcher 이벤트는
+/// 가능한 범위에서 증분 반영하여 전체 재색인 비용을 줄입니다.
///
public class IndexService : IDisposable
{
private readonly SettingsService _settings;
- private List _index = new();
private readonly List _watchers = new();
- private System.Threading.Timer? _debounceTimer;
private readonly object _timerLock = new();
- private const int DebounceMs = 3000;
+ private readonly object _indexLock = new();
private readonly SemaphoreSlim _rebuildLock = new(1, 1);
+ private System.Threading.Timer? _rebuildTimer;
+ private System.Threading.Timer? _persistTimer;
+ private List _fileSystemEntries = new();
+ private List _index = new();
+ private const int RebuildDebounceMs = 3000;
+ private const int PersistDebounceMs = 800;
+ private static readonly JsonSerializerOptions CacheJsonOptions = new()
+ {
+ WriteIndented = false,
+ PropertyNameCaseInsensitive = true
+ };
public IReadOnlyList Entries => _index;
public event EventHandler? IndexRebuilt;
public TimeSpan LastIndexDuration { get; private set; }
public int LastIndexCount { get; private set; }
+ public bool HasCachedIndexLoaded { get; private set; }
public IndexService(SettingsService settings)
{
_settings = settings;
}
+ public void LoadCachedIndex()
+ {
+ try
+ {
+ var cachePath = GetCachePath();
+ if (!File.Exists(cachePath))
+ return;
+
+ var cache = JsonSerializer.Deserialize(File.ReadAllText(cachePath), CacheJsonOptions);
+ if (cache == null)
+ return;
+
+ var currentSignature = BuildCacheSignature();
+ if (!string.Equals(cache.Signature, currentSignature, StringComparison.Ordinal))
+ {
+ LogService.Info("런처 인덱스 캐시 무효화: 설정 서명이 변경되었습니다.");
+ return;
+ }
+
+ var entries = cache.Entries
+ .Select(FromCacheEntry)
+ .Where(static e => !string.IsNullOrWhiteSpace(e.Name) && !string.IsNullOrWhiteSpace(e.Path))
+ .ToList();
+
+ ComputeAllSearchCaches(entries);
+
+ lock (_indexLock)
+ {
+ _fileSystemEntries = entries;
+ PublishIndexSnapshot_NoLock();
+ }
+
+ HasCachedIndexLoaded = entries.Count > 0;
+ LastIndexDuration = TimeSpan.Zero;
+ LastIndexCount = _index.Count;
+ LogService.Info($"런처 인덱스 캐시 로드: {entries.Count}개 파일 시스템 항목");
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"런처 인덱스 캐시 로드 실패: {ex.Message}");
+ }
+ }
+
///
- /// 앱 시작 시 전체 인덱스 빌드 후 FileSystemWatcher 시작.
+ /// 전체 파일 시스템 항목을 다시 스캔한 뒤 디스크 캐시를 갱신합니다.
///
public async Task BuildAsync(CancellationToken ct = default)
{
@@ -38,52 +94,33 @@ public class IndexService : IDisposable
var sw = Stopwatch.StartNew();
try
{
- var entries = new List();
- var paths = _settings.Settings.IndexPaths
- .Select(p => Environment.ExpandEnvironmentVariables(p));
- var allowedExts = new HashSet(
- _settings.Settings.IndexExtensions
- .Select(e => e.ToLowerInvariant().StartsWith(".") ? e.ToLowerInvariant() : "." + e.ToLowerInvariant()),
- StringComparer.OrdinalIgnoreCase);
-
+ var fileSystemEntries = new List();
+ var paths = GetExpandedIndexPaths();
+ var allowedExts = GetAllowedExtensions();
var indexSpeed = _settings.Settings.IndexSpeed ?? "normal";
foreach (var dir in paths)
{
- if (!Directory.Exists(dir)) continue;
- await ScanDirectoryAsync(dir, entries, allowedExts, indexSpeed, ct);
+ if (!Directory.Exists(dir))
+ continue;
+
+ await ScanDirectoryAsync(dir, fileSystemEntries, allowedExts, indexSpeed, ct);
}
- // Alias도 인덱스에 포함 (type에 따라 IndexEntryType 구분)
- foreach (var alias in _settings.Settings.Aliases)
+ ComputeAllSearchCaches(fileSystemEntries);
+
+ lock (_indexLock)
{
- entries.Add(new IndexEntry
- {
- Name = alias.Key,
- DisplayName = alias.Description ?? alias.Key,
- Path = alias.Target,
- AliasType = alias.Type,
- Type = alias.Type switch
- {
- "app" => IndexEntryType.App,
- "folder" => IndexEntryType.Folder,
- _ => IndexEntryType.Alias // url, batch, api, clipboard
- },
- Score = 100 // Alias는 최우선
- });
+ _fileSystemEntries = fileSystemEntries;
+ PublishIndexSnapshot_NoLock();
}
- // Built-in 앱 별칭 (한글+영문 이름으로 즉시 실행)
- RegisterBuiltInApps(entries);
-
- // 검색 가속 캐시 일괄 계산 (ToLower·자모·초성을 빌드 시 1회만 수행)
- ComputeAllSearchCaches(entries);
-
- _index = entries;
sw.Stop();
LastIndexDuration = sw.Elapsed;
- LastIndexCount = entries.Count;
- LogService.Info($"인덱싱 완료: {entries.Count}개 항목 ({sw.Elapsed.TotalSeconds:F1}초)");
+ LastIndexCount = _index.Count;
+ HasCachedIndexLoaded = fileSystemEntries.Count > 0;
+ PersistCache();
+ LogService.Info($"런처 인덱싱 완료: {fileSystemEntries.Count}개 파일 시스템 항목 ({sw.Elapsed.TotalSeconds:F1}초)");
IndexRebuilt?.Invoke(this, EventArgs.Empty);
}
finally
@@ -94,30 +131,31 @@ public class IndexService : IDisposable
///
/// 인덱스 경로에 대한 FileSystemWatcher를 시작합니다.
- /// BuildAsync() 완료 후 호출하세요.
+ /// 캐시 로드 후에도 바로 호출해 증분 반영을 유지합니다.
///
public void StartWatchers()
{
StopWatchers();
- foreach (var rawPath in _settings.Settings.IndexPaths)
+ foreach (var dir in GetExpandedIndexPaths())
{
- var dir = Environment.ExpandEnvironmentVariables(rawPath);
- if (!Directory.Exists(dir)) continue;
+ if (!Directory.Exists(dir))
+ continue;
try
{
- var w = new FileSystemWatcher(dir)
+ var watcher = new FileSystemWatcher(dir)
{
- Filter = "*.*",
+ Filter = "*",
IncludeSubdirectories = true,
- NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName,
- EnableRaisingEvents = true
+ NotifyFilter = NotifyFilters.FileName | NotifyFilters.DirectoryName,
+ EnableRaisingEvents = true
};
- w.Created += OnWatcherEvent;
- w.Deleted += OnWatcherEvent;
- w.Renamed += OnWatcherRenamed;
- _watchers.Add(w);
+
+ watcher.Created += OnWatcherEvent;
+ watcher.Deleted += OnWatcherEvent;
+ watcher.Renamed += OnWatcherRenamed;
+ _watchers.Add(watcher);
}
catch (Exception ex)
{
@@ -125,45 +163,293 @@ public class IndexService : IDisposable
}
}
- LogService.Info($"파일 감시 시작: {_watchers.Count}개 경로");
+ LogService.Info($"런처 파일 감시 시작: {_watchers.Count}개 경로");
}
+ private IEnumerable GetExpandedIndexPaths() =>
+ _settings.Settings.IndexPaths
+ .Select(path => Environment.ExpandEnvironmentVariables(path))
+ .Distinct(StringComparer.OrdinalIgnoreCase);
+
+ private HashSet GetAllowedExtensions() =>
+ new(
+ _settings.Settings.IndexExtensions
+ .Select(ext => ext.ToLowerInvariant().StartsWith(".") ? ext.ToLowerInvariant() : "." + ext.ToLowerInvariant()),
+ StringComparer.OrdinalIgnoreCase);
+
private void StopWatchers()
{
- foreach (var w in _watchers)
+ foreach (var watcher in _watchers)
{
- w.EnableRaisingEvents = false;
- w.Dispose();
+ watcher.EnableRaisingEvents = false;
+ watcher.Dispose();
}
+
_watchers.Clear();
}
private void OnWatcherEvent(object sender, FileSystemEventArgs e)
{
+ var rootPath = (sender as FileSystemWatcher)?.Path;
+ if (TryApplyIncrementalChange(e.ChangeType, e.FullPath, rootPath))
+ return;
+
ScheduleRebuild(e.FullPath);
}
private void OnWatcherRenamed(object sender, RenamedEventArgs e)
{
+ var rootPath = (sender as FileSystemWatcher)?.Path;
+ if (TryApplyIncrementalRename(e.OldFullPath, e.FullPath, rootPath))
+ return;
+
ScheduleRebuild(e.FullPath);
}
+ private bool TryApplyIncrementalChange(WatcherChangeTypes changeType, string fullPath, string? rootPath)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(rootPath))
+ return false;
+
+ // 디렉터리 삭제/이름변경은 하위 파일 경로까지 바뀔 수 있어 전체 재색인으로 폴백합니다.
+ if (changeType == WatcherChangeTypes.Deleted && Path.GetExtension(fullPath).Length == 0)
+ return false;
+
+ var changed = changeType switch
+ {
+ WatcherChangeTypes.Created => AddPathEntryIncrementally(fullPath, rootPath),
+ WatcherChangeTypes.Deleted => RemovePathEntriesIncrementally(fullPath),
+ _ => false
+ };
+
+ if (!changed)
+ return false;
+
+ AfterIncrementalUpdate(fullPath);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"런처 인덱스 증분 반영 실패: {fullPath} - {ex.Message}");
+ return false;
+ }
+ }
+
+ private bool TryApplyIncrementalRename(string oldFullPath, string newFullPath, string? rootPath)
+ {
+ try
+ {
+ if (string.IsNullOrWhiteSpace(rootPath))
+ return false;
+
+ if (Directory.Exists(newFullPath))
+ return false;
+
+ var removed = RemovePathEntriesIncrementally(oldFullPath);
+ var added = AddPathEntryIncrementally(newFullPath, rootPath);
+
+ if (!removed && !added)
+ return false;
+
+ AfterIncrementalUpdate(newFullPath);
+ return true;
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"런처 인덱스 이름변경 증분 반영 실패: {newFullPath} - {ex.Message}");
+ return false;
+ }
+ }
+
+ private void AfterIncrementalUpdate(string triggerPath)
+ {
+ lock (_indexLock)
+ PublishIndexSnapshot_NoLock();
+
+ PersistCacheDeferred();
+ LastIndexCount = _index.Count;
+ IndexRebuilt?.Invoke(this, EventArgs.Empty);
+ LogService.Info($"런처 인덱스 증분 갱신: {triggerPath}");
+ }
+
+ private bool AddPathEntryIncrementally(string fullPath, string rootPath)
+ {
+ var updated = false;
+
+ lock (_indexLock)
+ {
+ var snapshot = CloneEntries(_fileSystemEntries);
+
+ if (Directory.Exists(fullPath))
+ {
+ if (IsTopLevelDirectory(rootPath, fullPath))
+ updated = UpsertEntry(snapshot, CreateFolderEntry(fullPath));
+ }
+ else if (File.Exists(fullPath))
+ {
+ var ext = Path.GetExtension(fullPath).ToLowerInvariant();
+ var allowedExts = GetAllowedExtensions();
+ if (allowedExts.Count == 0 || allowedExts.Contains(ext))
+ updated = UpsertEntry(snapshot, CreateFileEntry(fullPath));
+ }
+
+ if (!updated)
+ return false;
+
+ ComputeAllSearchCaches(snapshot);
+ _fileSystemEntries = snapshot;
+ }
+
+ return true;
+ }
+
+ private bool RemovePathEntriesIncrementally(string fullPath)
+ {
+ var normalizedPath = NormalizePath(fullPath);
+ lock (_indexLock)
+ {
+ var snapshot = CloneEntries(_fileSystemEntries);
+ var removed = snapshot.RemoveAll(entry =>
+ string.Equals(NormalizePath(entry.Path), normalizedPath, StringComparison.OrdinalIgnoreCase)) > 0;
+
+ if (!removed)
+ return false;
+
+ _fileSystemEntries = snapshot;
+ return true;
+ }
+ }
+
+ private static bool UpsertEntry(List entries, IndexEntry newEntry)
+ {
+ var normalizedPath = NormalizePath(newEntry.Path);
+ var index = entries.FindIndex(entry =>
+ string.Equals(NormalizePath(entry.Path), normalizedPath, StringComparison.OrdinalIgnoreCase));
+
+ if (index >= 0)
+ {
+ entries[index] = newEntry;
+ return true;
+ }
+
+ entries.Add(newEntry);
+ return true;
+ }
+
private void ScheduleRebuild(string triggerPath)
{
- LogService.Info($"파일 변경 감지: {triggerPath} — {DebounceMs}ms 후 재빌드 예약");
+ LogService.Info($"파일 변경 감지: {triggerPath} — {RebuildDebounceMs}ms 후 전체 재빌드 예약");
lock (_timerLock)
{
- _debounceTimer?.Dispose();
- _debounceTimer = new System.Threading.Timer(__ =>
- {
- _ = BuildAsync();
- }, null, DebounceMs, System.Threading.Timeout.Infinite);
+ _rebuildTimer?.Dispose();
+ _rebuildTimer = new System.Threading.Timer(_ => _ = BuildAsync(), null, RebuildDebounceMs, System.Threading.Timeout.Infinite);
}
}
+ private void PersistCacheDeferred()
+ {
+ lock (_timerLock)
+ {
+ _persistTimer?.Dispose();
+ _persistTimer = new System.Threading.Timer(_ => PersistCache(), null, PersistDebounceMs, System.Threading.Timeout.Infinite);
+ }
+ }
+
+ private void PersistCache()
+ {
+ try
+ {
+ List cacheEntries;
+ lock (_indexLock)
+ {
+ cacheEntries = _fileSystemEntries
+ .Select(ToCacheEntry)
+ .ToList();
+ }
+
+ var cache = new LauncherIndexCache
+ {
+ Signature = BuildCacheSignature(),
+ SavedAtUtc = DateTime.UtcNow,
+ Entries = cacheEntries
+ };
+
+ var cachePath = GetCachePath();
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ File.WriteAllText(cachePath, JsonSerializer.Serialize(cache, CacheJsonOptions));
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"런처 인덱스 캐시 저장 실패: {ex.Message}");
+ }
+ }
+
+ private string GetCachePath() =>
+ Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
+ "AxCopilot",
+ "index",
+ "launcher-index.json");
+
+ private string BuildCacheSignature()
+ {
+ var pathPart = string.Join("|", GetExpandedIndexPaths()
+ .Select(path => NormalizePath(path))
+ .OrderBy(path => path, StringComparer.OrdinalIgnoreCase));
+
+ var extPart = string.Join("|", GetAllowedExtensions()
+ .OrderBy(ext => ext, StringComparer.OrdinalIgnoreCase));
+
+ return $"{pathPart}::{extPart}";
+ }
+
+ private void PublishIndexSnapshot_NoLock()
+ {
+ var entries = CloneEntries(_fileSystemEntries);
+
+ foreach (var alias in _settings.Settings.Aliases)
+ {
+ entries.Add(new IndexEntry
+ {
+ Name = alias.Key,
+ DisplayName = alias.Description ?? alias.Key,
+ Path = alias.Target,
+ AliasType = alias.Type,
+ Type = alias.Type switch
+ {
+ "app" => IndexEntryType.App,
+ "folder" => IndexEntryType.Folder,
+ _ => IndexEntryType.Alias
+ },
+ Score = 100
+ });
+ }
+
+ RegisterBuiltInApps(entries);
+ ComputeAllSearchCaches(entries);
+ _index = entries;
+ }
+
+ private static List CloneEntries(IEnumerable entries) =>
+ entries.Select(entry => new IndexEntry
+ {
+ Name = entry.Name,
+ DisplayName = entry.DisplayName,
+ Path = entry.Path,
+ Type = entry.Type,
+ Score = entry.Score,
+ AliasType = entry.AliasType,
+ NameLower = entry.NameLower,
+ NameJamo = entry.NameJamo,
+ NameChosung = entry.NameChosung
+ }).ToList();
+
public void Dispose()
{
- _debounceTimer?.Dispose();
+ _rebuildTimer?.Dispose();
+ _persistTimer?.Dispose();
StopWatchers();
}
@@ -176,153 +462,57 @@ public class IndexService : IDisposable
// (displayName, keywords[], exePath)
var builtIns = new (string Display, string[] Names, string? Exe)[]
{
- // 메모장
- ("메모장 (Notepad)", new[] { "메모장", "notepad", "note", "txt" },
- @"C:\Windows\notepad.exe"),
- // 계산기
- ("계산기 (Calculator)", new[] { "계산기", "calc", "calculator" },
- "calculator:"), // UWP protocol
- // 캡처 도구 (Snipping Tool)
- ("캡처 도구 (Snipping Tool)", new[] { "캡처", "캡처도구", "snippingtool", "snip", "스크린샷" },
- @"C:\Windows\System32\SnippingTool.exe"),
- // 그림판
- ("그림판 (Paint)", new[] { "그림판", "mspaint", "paint" },
- @"C:\Windows\System32\mspaint.exe"),
- // 탐색기
- ("파일 탐색기 (Explorer)", new[] { "탐색기", "explorer", "파일탐색기" },
- @"C:\Windows\explorer.exe"),
- // 명령 프롬프트
- ("명령 프롬프트 (CMD)", new[] { "cmd", "명령프롬프트", "커맨드", "터미널" },
- @"C:\Windows\System32\cmd.exe"),
- // PowerShell
- ("PowerShell", new[] { "powershell", "파워쉘" },
- @"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"),
- // 제어판
- ("제어판 (Control Panel)", new[] { "제어판", "control", "controlpanel" },
- @"C:\Windows\System32\control.exe"),
- // 작업 관리자
- ("작업 관리자 (Task Manager)", new[] { "작업관리자", "taskmgr", "taskmanager" },
- @"C:\Windows\System32\Taskmgr.exe"),
- // 원격 데스크톱
- ("원격 데스크톱 (Remote Desktop)", new[] { "원격", "mstsc", "rdp", "원격데스크톱" },
- @"C:\Windows\System32\mstsc.exe"),
- // 레지스트리 편집기
- ("레지스트리 편집기", new[] { "regedit", "레지스트리" },
- @"C:\Windows\regedit.exe"),
- // 장치 관리자
- ("장치 관리자", new[] { "장치관리자", "devmgmt", "devicemanager" },
- @"C:\Windows\System32\devmgmt.msc"),
- // Microsoft Edge
- ("Microsoft Edge", new[] { "edge", "엣지", "브라우저" },
- @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"),
- // Excel — 확장자 + MS prefix + 한글
- ("Microsoft Excel", new[] { "엑셀", "excel", "msexcel", "xlsx", "xls", "csv" },
- FindOfficeApp("EXCEL.EXE")),
- // PowerPoint
- ("Microsoft PowerPoint", new[] { "파워포인트", "powerpoint", "mspowerpoint", "pptx", "ppt" },
- FindOfficeApp("POWERPNT.EXE")),
- // Word
- ("Microsoft Word", new[] { "워드", "word", "msword", "docx", "doc" },
- FindOfficeApp("WINWORD.EXE")),
- // Outlook
- ("Microsoft Outlook", new[] { "아웃룩", "outlook", "메일" },
- FindOfficeApp("OUTLOOK.EXE")),
- // Teams
- ("Microsoft Teams", new[] { "팀즈", "teams" },
- FindTeams()),
- // OneNote
- ("Microsoft OneNote", new[] { "원노트", "onenote" },
- FindOfficeApp("ONENOTE.EXE")),
- // Access
- ("Microsoft Access", new[] { "액세스", "access", "msaccess" },
- FindOfficeApp("MSACCESS.EXE")),
- // VS Code
- ("Visual Studio Code", new[] { "vscode", "비주얼스튜디오코드", "코드에디터", "code" },
- FindInPath("code.cmd") ?? FindInLocalAppData(@"Programs\Microsoft VS Code\Code.exe")),
- // Visual Studio
+ ("메모장 (Notepad)", new[] { "메모장", "notepad", "note", "txt" }, @"C:\Windows\notepad.exe"),
+ ("계산기 (Calculator)", new[] { "계산기", "calc", "calculator" }, "calculator:"),
+ ("캡처 도구 (Snipping Tool)", new[] { "캡처", "캡처도구", "snippingtool", "snip", "스크린샷" }, @"C:\Windows\System32\SnippingTool.exe"),
+ ("그림판 (Paint)", new[] { "그림판", "mspaint", "paint" }, @"C:\Windows\System32\mspaint.exe"),
+ ("파일 탐색기 (Explorer)", new[] { "탐색기", "explorer", "파일탐색기" }, @"C:\Windows\explorer.exe"),
+ ("명령 프롬프트 (CMD)", new[] { "cmd", "명령프롬프트", "커맨드", "터미널" }, @"C:\Windows\System32\cmd.exe"),
+ ("PowerShell", new[] { "powershell", "파워쉘" }, @"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"),
+ ("제어판 (Control Panel)", new[] { "제어판", "control", "controlpanel" }, @"C:\Windows\System32\control.exe"),
+ ("작업 관리자 (Task Manager)", new[] { "작업관리자", "taskmgr", "taskmanager" }, @"C:\Windows\System32\Taskmgr.exe"),
+ ("원격 데스크톱 (Remote Desktop)", new[] { "원격", "mstsc", "rdp", "원격데스크톱" }, @"C:\Windows\System32\mstsc.exe"),
+ ("레지스트리 편집기", new[] { "regedit", "레지스트리" }, @"C:\Windows\regedit.exe"),
+ ("장치 관리자", new[] { "장치관리자", "devmgmt", "devicemanager" }, @"C:\Windows\System32\devmgmt.msc"),
+ ("Microsoft Edge", new[] { "edge", "엣지", "브라우저" }, @"C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe"),
+ ("Microsoft Excel", new[] { "엑셀", "excel", "msexcel", "xlsx", "xls", "csv" }, FindOfficeApp("EXCEL.EXE")),
+ ("Microsoft PowerPoint", new[] { "파워포인트", "powerpoint", "mspowerpoint", "pptx", "ppt" }, FindOfficeApp("POWERPNT.EXE")),
+ ("Microsoft Word", new[] { "워드", "word", "msword", "docx", "doc" }, FindOfficeApp("WINWORD.EXE")),
+ ("Microsoft Outlook", new[] { "아웃룩", "outlook", "메일" }, FindOfficeApp("OUTLOOK.EXE")),
+ ("Microsoft Teams", new[] { "팀즈", "teams" }, FindTeams()),
+ ("Microsoft OneNote", new[] { "원노트", "onenote" }, FindOfficeApp("ONENOTE.EXE")),
+ ("Microsoft Access", new[] { "액세스", "access", "msaccess" }, FindOfficeApp("MSACCESS.EXE")),
+ ("Visual Studio Code", new[] { "vscode", "비주얼스튜디오코드", "코드에디터", "code" }, FindInPath("code.cmd") ?? FindInLocalAppData(@"Programs\Microsoft VS Code\Code.exe")),
("Visual Studio", new[] { "비주얼스튜디오", "devenv", "vs2022", "vs2019", "visualstudio" },
FindInProgramFiles(@"Microsoft Visual Studio\2022\Community\Common7\IDE\devenv.exe")
?? FindInProgramFiles(@"Microsoft Visual Studio\2022\Professional\Common7\IDE\devenv.exe")
?? FindInProgramFiles(@"Microsoft Visual Studio\2022\Enterprise\Common7\IDE\devenv.exe")),
- // Windows Terminal
- ("Windows Terminal", new[] { "윈도우터미널", "wt", "windowsterminal", "터미널" },
- FindInLocalAppData(@"Microsoft\WindowsApps\wt.exe")),
- // OneDrive
- ("OneDrive", new[] { "원드라이브", "onedrive" },
- FindInLocalAppData(@"Microsoft\OneDrive\OneDrive.exe")),
- // Google Chrome
- ("Google Chrome", new[] { "크롬", "구글크롬", "chrome", "google" },
- FindInProgramFiles(@"Google\Chrome\Application\chrome.exe")
- ?? FindInLocalAppData(@"Google\Chrome\Application\chrome.exe")),
- // Firefox
- ("Mozilla Firefox", new[] { "파이어폭스", "불여우", "firefox" },
- FindInProgramFiles(@"Mozilla Firefox\firefox.exe")),
- // Naver Whale
- ("Naver Whale", new[] { "웨일", "네이버웨일", "whale" },
- FindInLocalAppData(@"Naver\Naver Whale\Application\whale.exe")),
- // KakaoTalk
- ("KakaoTalk", new[] { "카카오톡", "카톡", "kakaotalk", "kakao" },
- FindInLocalAppData(@"Kakao\KakaoTalk\KakaoTalk.exe")),
- // KakaoWork
- ("KakaoWork", new[] { "카카오워크", "카워크", "kakaowork" },
- FindInLocalAppData(@"Kakao\KakaoWork\KakaoWork.exe")),
- // Zoom
- ("Zoom", new[] { "줌", "zoom" },
- FindInRoaming(@"Zoom\bin\Zoom.exe")
- ?? FindInLocalAppData(@"Zoom\bin\Zoom.exe")),
- // Slack
- ("Slack", new[] { "슬랙", "slack" },
- FindInLocalAppData(@"slack\slack.exe")),
- // Figma
- ("Figma", new[] { "피그마", "figma" },
- FindInLocalAppData(@"Figma\Figma.exe")),
- // Notepad++
- ("Notepad++", new[] { "노트패드++", "npp", "notepad++" },
- FindInProgramFiles(@"Notepad++\notepad++.exe")),
- // 7-Zip
- ("7-Zip", new[] { "7zip", "7집", "세븐집", "7z" },
- FindInProgramFiles(@"7-Zip\7zFM.exe")),
- // Bandizip
- ("Bandizip", new[] { "반디집", "bandizip" },
- FindInProgramFiles(@"Bandizip\Bandizip.exe")
- ?? FindInLocalAppData(@"Bandizip\Bandizip.exe")),
- // ALZip
- ("ALZip", new[] { "알집", "alzip", "이스트소프트" },
- FindInProgramFiles(@"ESTsoft\ALZip\ALZip.exe")),
- // PotPlayer
- ("PotPlayer", new[] { "팟플레이어", "팟플", "potplayer" },
- FindInProgramFiles(@"DAUM\PotPlayer\PotPlayerMini64.exe")
- ?? FindInProgramFiles(@"DAUM\PotPlayer\PotPlayerMini.exe")),
- // GOM Player
- ("GOM Player", new[] { "곰플레이어", "곰플", "gomplayer", "gom" },
- FindInProgramFiles(@"GRETECH\GomPlayer\GOM.EXE")),
- // Adobe Photoshop
- ("Adobe Photoshop", new[] { "포토샵", "포샵", "photoshop", "ps" },
- FindInProgramFiles(@"Adobe\Adobe Photoshop 2025\Photoshop.exe")
- ?? FindInProgramFiles(@"Adobe\Adobe Photoshop 2024\Photoshop.exe")
- ?? FindInProgramFiles(@"Adobe\Adobe Photoshop 2023\Photoshop.exe")),
- // Adobe Acrobat
- ("Adobe Acrobat", new[] { "아크로뱃", "acrobat", "아도비pdf" },
- FindInProgramFiles(@"Adobe\Acrobat DC\Acrobat\Acrobat.exe")),
- // 한컴 한글
- ("한컴 한글", new[] { "한글", "한컴한글", "hwp", "hangul", "아래아한글" },
- FindInProgramFiles(@"HNC\Hwp\Hwp.exe")
- ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Hwp.exe")
- ?? FindInProgramFiles(@"Hnc\Hwp80\Hwp.exe")),
- // 한컴 한셀
- ("한컴 한셀", new[] { "한셀", "hcell", "한컴스프레드" },
- FindInProgramFiles(@"HNC\Hwp\Hcell.exe")
- ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Hcell.exe")),
- // 한컴 한쇼
- ("한컴 한쇼", new[] { "한쇼", "hshow", "한컴프레젠테이션" },
- FindInProgramFiles(@"HNC\Hwp\Show.exe")
- ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Show.exe")),
+ ("Windows Terminal", new[] { "윈도우터미널", "wt", "windowsterminal", "터미널" }, FindInLocalAppData(@"Microsoft\WindowsApps\wt.exe")),
+ ("OneDrive", new[] { "원드라이브", "onedrive" }, FindInLocalAppData(@"Microsoft\OneDrive\OneDrive.exe")),
+ ("Google Chrome", new[] { "크롬", "구글크롬", "chrome", "google" }, FindInProgramFiles(@"Google\Chrome\Application\chrome.exe") ?? FindInLocalAppData(@"Google\Chrome\Application\chrome.exe")),
+ ("Mozilla Firefox", new[] { "파이어폭스", "불여우", "firefox" }, FindInProgramFiles(@"Mozilla Firefox\firefox.exe")),
+ ("Naver Whale", new[] { "웨일", "네이버웨일", "whale" }, FindInLocalAppData(@"Naver\Naver Whale\Application\whale.exe")),
+ ("KakaoTalk", new[] { "카카오톡", "카톡", "kakaotalk", "kakao" }, FindInLocalAppData(@"Kakao\KakaoTalk\KakaoTalk.exe")),
+ ("KakaoWork", new[] { "카카오워크", "카워크", "kakaowork" }, FindInLocalAppData(@"Kakao\KakaoWork\KakaoWork.exe")),
+ ("Zoom", new[] { "줌", "zoom" }, FindInRoaming(@"Zoom\bin\Zoom.exe") ?? FindInLocalAppData(@"Zoom\bin\Zoom.exe")),
+ ("Slack", new[] { "슬랙", "slack" }, FindInLocalAppData(@"slack\slack.exe")),
+ ("Figma", new[] { "피그마", "figma" }, FindInLocalAppData(@"Figma\Figma.exe")),
+ ("Notepad++", new[] { "노트패드++", "npp", "notepad++" }, FindInProgramFiles(@"Notepad++\notepad++.exe")),
+ ("7-Zip", new[] { "7zip", "7집", "세븐집", "7z" }, FindInProgramFiles(@"7-Zip\7zFM.exe")),
+ ("Bandizip", new[] { "반디집", "bandizip" }, FindInProgramFiles(@"Bandizip\Bandizip.exe") ?? FindInLocalAppData(@"Bandizip\Bandizip.exe")),
+ ("ALZip", new[] { "알집", "alzip", "이스트소프트" }, FindInProgramFiles(@"ESTsoft\ALZip\ALZip.exe")),
+ ("PotPlayer", new[] { "팟플레이어", "팟플", "potplayer" }, FindInProgramFiles(@"DAUM\PotPlayer\PotPlayerMini64.exe") ?? FindInProgramFiles(@"DAUM\PotPlayer\PotPlayerMini.exe")),
+ ("GOM Player", new[] { "곰플레이어", "곰플", "gomplayer", "gom" }, FindInProgramFiles(@"GRETECH\GomPlayer\GOM.EXE")),
+ ("Adobe Photoshop", new[] { "포토샵", "포샵", "photoshop", "ps" }, FindInProgramFiles(@"Adobe\Adobe Photoshop 2025\Photoshop.exe") ?? FindInProgramFiles(@"Adobe\Adobe Photoshop 2024\Photoshop.exe") ?? FindInProgramFiles(@"Adobe\Adobe Photoshop 2023\Photoshop.exe")),
+ ("Adobe Acrobat", new[] { "아크로뱃", "acrobat", "아도비pdf" }, FindInProgramFiles(@"Adobe\Acrobat DC\Acrobat\Acrobat.exe")),
+ ("한컴 한글", new[] { "한글", "한컴한글", "hwp", "hangul", "아래아한글" }, FindInProgramFiles(@"HNC\Hwp\Hwp.exe") ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Hwp.exe") ?? FindInProgramFiles(@"Hnc\Hwp80\Hwp.exe")),
+ ("한컴 한셀", new[] { "한셀", "hcell", "한컴스프레드" }, FindInProgramFiles(@"HNC\Hwp\Hcell.exe") ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Hcell.exe")),
+ ("한컴 한쇼", new[] { "한쇼", "hshow", "한컴프레젠테이션" }, FindInProgramFiles(@"HNC\Hwp\Show.exe") ?? FindInProgramFiles(@"HNC\Office NEO\HOffice NEO\Bin\Show.exe")),
};
- // 기존 항목의 이름 → 인덱스 매핑 + Path 기반 중복 방지
var nameToIdx = new Dictionary(StringComparer.OrdinalIgnoreCase);
var pathToIdx = new Dictionary(StringComparer.OrdinalIgnoreCase);
- for (int i = 0; i < entries.Count; i++)
+ for (var i = 0; i < entries.Count; i++)
{
nameToIdx.TryAdd(entries[i].Name, i);
pathToIdx.TryAdd(entries[i].Path, i);
@@ -330,45 +520,39 @@ public class IndexService : IDisposable
foreach (var (display, names, exe) in builtIns)
{
- if (string.IsNullOrEmpty(exe)) continue;
- // UWP protocol은 파일 존재 체크 불필요
- if (!exe.Contains(":") && !File.Exists(exe) && !exe.EndsWith(".msc")) continue;
+ if (string.IsNullOrEmpty(exe))
+ continue;
+
+ if (!exe.Contains(":") && !File.Exists(exe) && !exe.EndsWith(".msc", StringComparison.OrdinalIgnoreCase))
+ continue;
- // 같은 경로가 이미 인덱스에 있으면 DisplayName과 Score만 업데이트
if (pathToIdx.TryGetValue(exe, out var existingIdx))
{
entries[existingIdx].DisplayName = display;
- if (entries[existingIdx].Score < 95) entries[existingIdx].Score = 95;
+ if (entries[existingIdx].Score < 95)
+ entries[existingIdx].Score = 95;
}
- // ── 키워드별 IndexEntry 등록 ─────────────────────────────────────────
- // - 한글 이름: 항상 IndexEntry로 추가 (앱이 이미 스캔됐어도)
- // → "엑" 입력 시 "엑셀" IndexEntry의 StartsWith("엑")으로 매칭
- // - 영문/약어: 앱이 미스캔 상태일 때 첫 키워드만 대표 항목으로 등록
- // - 경로 중복은 CommandResolver 레이어에서 seenPaths로 최고점만 표시
- bool mainAdded = pathToIdx.ContainsKey(exe);
+ var mainAdded = pathToIdx.ContainsKey(exe);
foreach (var name in names)
{
if (nameToIdx.ContainsKey(name))
{
- // 이미 같은 이름 존재 → Score만 부스트
if (nameToIdx.TryGetValue(name, out var idx) && entries[idx].Score < 95)
entries[idx].Score = 95;
continue;
}
- // 한글 음절이 포함된 이름은 항상 IndexEntry로 추가
- bool isKoreanName = name.Any(c => c >= '\uAC00' && c <= '\uD7A3');
-
+ var isKoreanName = name.Any(static c => c is >= '\uAC00' and <= '\uD7A3');
if (!mainAdded || isKoreanName)
{
entries.Add(new IndexEntry
{
- Name = name,
+ Name = name,
DisplayName = display,
- Path = exe,
- Type = IndexEntryType.App,
- Score = 95
+ Path = exe,
+ Type = IndexEntryType.App,
+ Score = 95
});
var newIdx = entries.Count - 1;
nameToIdx[name] = newIdx;
@@ -380,7 +564,6 @@ public class IndexService : IDisposable
}
else
{
- // 영문/약어는 pathToIdx 참조만 (FuzzyEngine은 영문 앱명 직접 매칭)
nameToIdx[name] = pathToIdx[exe];
}
}
@@ -394,13 +577,16 @@ public class IndexService : IDisposable
@"C:\Program Files\Microsoft Office\root\Office16",
@"C:\Program Files (x86)\Microsoft Office\root\Office16",
@"C:\Program Files\Microsoft Office\Office16",
- @"C:\Program Files (x86)\Microsoft Office\Office16",
+ @"C:\Program Files (x86)\Microsoft Office\Office16"
};
+
foreach (var root in roots)
{
var path = Path.Combine(root, exeName);
- if (File.Exists(path)) return path;
+ if (File.Exists(path))
+ return path;
}
+
return null;
}
@@ -408,8 +594,9 @@ public class IndexService : IDisposable
{
var localApp = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var path = Path.Combine(localApp, @"Microsoft\Teams\current\Teams.exe");
- if (File.Exists(path)) return path;
- // New Teams (MSIX)
+ if (File.Exists(path))
+ return path;
+
var msTeams = Path.Combine(localApp, @"Microsoft\WindowsApps\ms-teams.exe");
return File.Exists(msTeams) ? msTeams : null;
}
@@ -419,10 +606,14 @@ public class IndexService : IDisposable
var pathEnv = Environment.GetEnvironmentVariable("PATH") ?? "";
foreach (var dir in pathEnv.Split(';'))
{
- if (string.IsNullOrWhiteSpace(dir)) continue;
+ if (string.IsNullOrWhiteSpace(dir))
+ continue;
+
var full = Path.Combine(dir.Trim(), fileName);
- if (File.Exists(full)) return full;
+ if (File.Exists(full))
+ return full;
}
+
return null;
}
@@ -433,18 +624,18 @@ public class IndexService : IDisposable
return File.Exists(path) ? path : null;
}
- /// ProgramFiles / ProgramFiles(x86) 두 곳을 탐색합니다.
private static string? FindInProgramFiles(string relativePath)
{
- var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
+ var pf = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
var pf86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
- var p1 = Path.Combine(pf, relativePath);
- if (File.Exists(p1)) return p1;
- var p2 = Path.Combine(pf86, relativePath);
- return File.Exists(p2) ? p2 : null;
+ var path1 = Path.Combine(pf, relativePath);
+ if (File.Exists(path1))
+ return path1;
+
+ var path2 = Path.Combine(pf86, relativePath);
+ return File.Exists(path2) ? path2 : null;
}
- /// AppData\Roaming 경로를 탐색합니다.
private static string? FindInRoaming(string relativePath)
{
var roaming = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
@@ -452,41 +643,41 @@ public class IndexService : IDisposable
return File.Exists(path) ? path : null;
}
- // ─── 검색 가속 캐시 계산 ──────────────────────────────────────────────────
-
- /// 빌드 완료 후 전체 항목의 검색 캐시를 한 번에 계산합니다.
private static void ComputeAllSearchCaches(List entries)
{
- foreach (var e in entries)
- ComputeSearchCache(e);
+ foreach (var entry in entries)
+ ComputeSearchCache(entry);
}
- /// 항목 1개의 NameLower / NameJamo / NameChosung 캐시를 계산합니다.
private static void ComputeSearchCache(IndexEntry entry)
{
entry.NameLower = entry.Name.ToLowerInvariant();
- // 자모 분리 (FuzzyEngine static 메서드 — 동일 어셈블리 internal 접근)
entry.NameJamo = AxCopilot.Core.FuzzyEngine.DecomposeToJamo(entry.NameLower);
- // 초성 문자열 (예: "엑셀" → "ㅇㅅ", "메모장" → "ㅁㅁㅈ")
- var sb = new System.Text.StringBuilder(entry.NameLower.Length);
+
+ var sb = new StringBuilder(entry.NameLower.Length);
foreach (var c in entry.NameLower)
{
var cho = AxCopilot.Core.FuzzyEngine.GetChosung(c);
- if (cho != '\0') sb.Append(cho);
+ if (cho != '\0')
+ sb.Append(cho);
}
+
entry.NameChosung = sb.ToString();
}
- /// 인덱싱 속도에 따른 throttle 간격(ms). N개 파일마다 yield.
- private static (int batchSize, int delayMs) GetThrottle(string speed) => speed switch
+ private static (int BatchSize, int DelayMs) GetThrottle(string speed) => speed switch
{
- "fast" => (500, 0), // 최대 속도, CPU 양보 없음
- "slow" => (50, 15), // 50개마다 15ms 양보 → PC 부하 최소
- _ => (150, 5), // normal: 150개마다 5ms → 적정 균형
+ "fast" => (500, 0),
+ "slow" => (50, 15),
+ _ => (150, 5)
};
- private static async Task ScanDirectoryAsync(string dir, List entries,
- HashSet allowedExts, string indexSpeed, CancellationToken ct)
+ private static async Task ScanDirectoryAsync(
+ string dir,
+ List entries,
+ HashSet allowedExts,
+ string indexSpeed,
+ CancellationToken ct)
{
var (batchSize, delayMs) = GetThrottle(indexSpeed);
@@ -494,48 +685,29 @@ public class IndexService : IDisposable
{
try
{
- int count = 0;
-
- // 파일 인덱싱 (확장자 필터 적용)
+ var count = 0;
foreach (var file in Directory.EnumerateFiles(dir, "*.*", SearchOption.AllDirectories))
{
ct.ThrowIfCancellationRequested();
- var ext = Path.GetExtension(file).ToLowerInvariant();
- if (allowedExts.Count > 0 && !allowedExts.Contains(ext)) continue;
- var name = Path.GetFileNameWithoutExtension(file);
- if (string.IsNullOrEmpty(name)) continue;
- var type = ext switch
- {
- ".exe" => IndexEntryType.App,
- ".lnk" or ".url" => IndexEntryType.File,
- _ => IndexEntryType.File
- };
- entries.Add(new IndexEntry
- {
- Name = name,
- DisplayName = ext is ".exe" or ".lnk" or ".url" ? name : name + ext,
- Path = file,
- Type = type
- });
- // 속도 조절: batchSize개마다 CPU 양보
+ var ext = Path.GetExtension(file).ToLowerInvariant();
+ if (allowedExts.Count > 0 && !allowedExts.Contains(ext))
+ continue;
+
+ entries.Add(CreateFileEntry(file));
+
if (delayMs > 0 && ++count % batchSize == 0)
await Task.Delay(delayMs, ct);
}
- // 폴더 인덱싱 (1단계 하위 폴더)
foreach (var subDir in Directory.EnumerateDirectories(dir, "*", SearchOption.TopDirectoryOnly))
{
ct.ThrowIfCancellationRequested();
var name = Path.GetFileName(subDir);
- if (name.StartsWith(".")) continue;
- entries.Add(new IndexEntry
- {
- Name = name,
- DisplayName = name,
- Path = subDir,
- Type = IndexEntryType.Folder
- });
+ if (name.StartsWith(".", StringComparison.Ordinal))
+ continue;
+
+ entries.Add(CreateFolderEntry(subDir));
}
}
catch (UnauthorizedAccessException ex)
@@ -544,25 +716,111 @@ public class IndexService : IDisposable
}
}, ct);
}
+
+ private static IndexEntry CreateFileEntry(string filePath)
+ {
+ var ext = Path.GetExtension(filePath).ToLowerInvariant();
+ var name = Path.GetFileNameWithoutExtension(filePath);
+ var type = ext switch
+ {
+ ".exe" => IndexEntryType.App,
+ ".lnk" or ".url" => IndexEntryType.File,
+ _ => IndexEntryType.File
+ };
+
+ return new IndexEntry
+ {
+ Name = name,
+ DisplayName = ext is ".exe" or ".lnk" or ".url" ? name : name + ext,
+ Path = filePath,
+ Type = type
+ };
+ }
+
+ private static IndexEntry CreateFolderEntry(string folderPath)
+ {
+ var name = Path.GetFileName(folderPath);
+ return new IndexEntry
+ {
+ Name = name,
+ DisplayName = name,
+ Path = folderPath,
+ Type = IndexEntryType.Folder
+ };
+ }
+
+ private static bool IsTopLevelDirectory(string rootPath, string directoryPath)
+ {
+ var normalizedRoot = NormalizePath(rootPath).TrimEnd(Path.DirectorySeparatorChar);
+ var parent = Directory.GetParent(directoryPath)?.FullName;
+ if (parent == null)
+ return false;
+
+ return string.Equals(
+ NormalizePath(parent).TrimEnd(Path.DirectorySeparatorChar),
+ normalizedRoot,
+ StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static string NormalizePath(string path) =>
+ Path.GetFullPath(path)
+ .TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+
+ private static LauncherIndexCacheEntry ToCacheEntry(IndexEntry entry) => new()
+ {
+ Name = entry.Name,
+ DisplayName = entry.DisplayName,
+ Path = entry.Path,
+ Type = entry.Type,
+ Score = entry.Score,
+ AliasType = entry.AliasType
+ };
+
+ private static IndexEntry FromCacheEntry(LauncherIndexCacheEntry entry) => new()
+ {
+ Name = entry.Name,
+ DisplayName = entry.DisplayName,
+ Path = entry.Path,
+ Type = entry.Type,
+ Score = entry.Score,
+ AliasType = entry.AliasType
+ };
+
+ private sealed class LauncherIndexCache
+ {
+ public string Signature { get; set; } = "";
+ public DateTime SavedAtUtc { get; set; }
+ public List Entries { get; set; } = new();
+ }
+
+ private sealed class LauncherIndexCacheEntry
+ {
+ public string Name { get; set; } = "";
+ public string DisplayName { get; set; } = "";
+ public string Path { get; set; } = "";
+ public IndexEntryType Type { get; set; }
+ public int Score { get; set; }
+ public string? AliasType { get; set; }
+ }
}
public class IndexEntry
{
- public string Name { get; set; } = "";
+ public string Name { get; set; } = "";
public string DisplayName { get; set; } = "";
- public string Path { get; set; } = "";
+ public string Path { get; set; } = "";
public IndexEntryType Type { get; set; }
- public int Score { get; set; } = 0;
- /// Alias 항목의 원본 type 문자열 (url | folder | app | batch | api | clipboard)
- public string? AliasType { get; set; }
-
- // ─ 검색 가속 캐시 (BuildAsync 시 1회 계산, 검색마다 재계산 방지) ──────
- /// ToLowerInvariant() 결과 캐시
- public string NameLower { get; set; } = "";
- /// 자모 분리 문자열 캐시 (DecomposeToJamo 결과)
- public string NameJamo { get; set; } = "";
- /// 초성만 연결한 문자열 캐시 (예: "엑셀" → "ㅇㅅ")
+ public int Score { get; set; }
+ public string? AliasType { get; set; }
+ public string NameLower { get; set; } = "";
+ public string NameJamo { get; set; } = "";
public string NameChosung { get; set; } = "";
}
-public enum IndexEntryType { App, File, Alias, Folder }
+public enum IndexEntryType
+{
+ App,
+ File,
+ Alias,
+ Folder
+}