[Phase L2-7] 퀵 액션 바 — 최근 실행 항목 칩 표시

Models/QuickActionChip.cs (신규 12줄):
- Title, Symbol, Path, Background 레코드

Services/UsageRankingService.cs:
- GetTopItems(int n) 메서드 추가: 실행 횟수 상위 N개 (경로, 횟수) 반환

ViewModels/LauncherViewModel.cs:
- QuickActionItems (ObservableCollection<QuickActionChip>) 프로퍼티 추가
- ShowQuickActions: 입력 비었을 때 칩 표시 조건
- LoadQuickActions(): 상위 8개 경로 → 파일 존재 확인 → 타입별 아이콘/색상 칩 생성
- OnShown()에서 LoadQuickActions() 호출
- InputText 변경 시 ShowQuickActions 알림

Views/LauncherWindow.xaml:
- 입력 Grid를 2행 구조로 변환 (RowDefinitions 추가)
- Row 1: ItemsControl + WrapPanel + DataTemplate 칩 UI
  - CornerRadius=10 Border, 아이콘+제목 StackPanel
  - 호버 시 AccentColor 테두리, 최대 너비 100px 말줄임

Views/LauncherWindow.Shell.cs:
- QuickActionChip_Click 핸들러: 창 숨김 → 경로 실행 → 사용 통계 기록

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 10:44:02 +09:00
parent 679de30f68
commit f557f53449
5 changed files with 220 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
using System.Windows.Media;
namespace AxCopilot.Models;
/// <summary>
/// 퀵 액션 바에 표시되는 최근 실행 항목 칩 모델.
/// 입력창 아래에 가로 칩 형태로 표시되며 클릭 시 즉시 실행됩니다.
/// </summary>
public record QuickActionChip(
string Title,
string Symbol,
string Path,
Brush Background
);

View File

@@ -46,6 +46,21 @@ internal static class UsageRankingService
}
}
/// <summary>
/// 실행 횟수 기준 상위 N개의 (경로, 횟수) 목록을 반환합니다.
/// 퀵 액션 바 표시에 활용됩니다.
/// </summary>
public static IReadOnlyList<(string Path, int Count)> GetTopItems(int n)
{
EnsureLoaded();
lock (_lock)
return _counts
.OrderByDescending(kv => kv.Value)
.Take(n)
.Select(kv => (kv.Key, kv.Value))
.ToList();
}
/// <summary>
/// 실행 횟수 기준으로 내림차순 정렬하는 컴파러를 반환합니다.
/// 동점이면 원래 순서 유지 (stable sort).

View File

@@ -48,6 +48,15 @@ public partial class LauncherViewModel : INotifyPropertyChanged
/// </summary>
public ICollectionView GroupedResults { get; }
/// <summary>
/// 퀵 액션 바 — 가장 많이 실행한 항목을 입력창 아래 칩으로 표시합니다.
/// 입력창이 비어 있을 때만 표시됩니다.
/// </summary>
public ObservableCollection<QuickActionChip> QuickActionItems { get; } = new();
/// <summary>퀵 액션 칩 표시 조건: 입력이 없고 칩이 하나 이상 있을 때</summary>
public bool ShowQuickActions => string.IsNullOrEmpty(_inputText) && QuickActionItems.Count > 0;
// ─── 기본 프로퍼티 ────────────────────────────────────────────────────────
public string InputText
@@ -66,6 +75,8 @@ public partial class LauncherViewModel : INotifyPropertyChanged
OnPropertyChanged(nameof(ShowMergeHint));
OnPropertyChanged(nameof(MergeHintText));
OnPropertyChanged(nameof(ShowQuickActions));
// 연속 입력 시 이전 검색 즉시 취소 + 50ms 디바운스 후 실제 검색 시작
_searchCts?.Cancel();
_debounceTimer?.Dispose();
@@ -266,6 +277,47 @@ public partial class LauncherViewModel : INotifyPropertyChanged
Results.Clear();
_lastSearchQuery = "";
ClearMerge();
LoadQuickActions();
}
/// <summary>
/// UsageRankingService 상위 항목에서 퀵 액션 칩을 생성합니다.
/// 실제로 존재하는 파일/폴더만 표시하며 최대 8개로 제한합니다.
/// </summary>
public void LoadQuickActions()
{
QuickActionItems.Clear();
var topItems = UsageRankingService.GetTopItems(16); // 여유분 확보 (일부 경로가 없을 수 있음)
var added = 0;
foreach (var (path, _) in topItems)
{
if (added >= 8) break;
var expanded = Environment.ExpandEnvironmentVariables(path);
var isFolder = Directory.Exists(expanded);
var isFile = !isFolder && File.Exists(expanded);
if (!isFolder && !isFile) continue; // 삭제된 항목 건너뜀
var ext = Path.GetExtension(expanded).ToLowerInvariant();
var title = Path.GetFileNameWithoutExtension(expanded);
if (string.IsNullOrEmpty(title)) title = Path.GetFileName(expanded);
var symbol = isFolder ? Symbols.Folder
: ext == ".exe" ? Symbols.App
: ext is ".lnk"
or ".url" ? Symbols.App
: Symbols.File;
var color = isFolder ? Color.FromRgb(0x10, 0x7C, 0x10)
: ext == ".exe" ? Color.FromRgb(0x4B, 0x5E, 0xFC)
: ext is ".lnk"
or ".url" ? Color.FromRgb(0x4B, 0x5E, 0xFC)
: Color.FromRgb(0x5B, 0x4E, 0x7E);
var bg = new SolidColorBrush(Color.FromArgb(0x26, color.R, color.G, color.B));
QuickActionItems.Add(new QuickActionChip(title, symbol, path, bg));
added++;
}
OnPropertyChanged(nameof(ShowQuickActions));
}
// ─── 검색 ────────────────────────────────────────────────────────────────

View File

@@ -163,6 +163,53 @@ public partial class LauncherWindow
_ = _vm.ExecuteSelectedAsync();
}
// ─── F3 QuickLook 토글 ────────────────────────────────────────────────
/// <summary>현재 열린 QuickLook 창 참조 (토글/닫기 추적용)</summary>
private QuickLookWindow? _quickLookWindow;
/// <summary>
/// F3 빠른 미리보기 토글.
/// 이미 열려 있으면 닫고, 없으면 선택된 파일/폴더로 미리보기 창을 엽니다.
/// </summary>
internal void ToggleQuickLook()
{
// 이미 열린 창이 있으면 닫기 (토글)
if (_quickLookWindow != null)
{
_quickLookWindow.Close();
_quickLookWindow = null;
return;
}
var selected = _vm.SelectedItem;
string? path = null;
if (selected?.Data is Services.IndexEntry indexEntry)
path = Environment.ExpandEnvironmentVariables(indexEntry.Path);
if (string.IsNullOrEmpty(path)) return;
if (!System.IO.File.Exists(path) && !System.IO.Directory.Exists(path)) return;
// 런처 창 오른쪽에 배치 (화면 경계 고려)
var qlLeft = Left + ActualWidth + 8;
var qlTop = Top;
// 화면 오른쪽 여백 확인 (화면 밖으로 나가면 왼쪽에 배치)
var screen = System.Windows.Forms.Screen.FromHandle(
new System.Windows.Interop.WindowInteropHelper(this).Handle);
if (qlLeft + 400 > screen.WorkingArea.Right)
qlLeft = Left - 408;
_quickLookWindow = new QuickLookWindow(path, this)
{
Left = qlLeft,
Top = qlTop
};
_quickLookWindow.Closed += (_, _) => _quickLookWindow = null;
_quickLookWindow.Show();
}
// ─── 창 이벤트 / 스크롤 / 알림 ─────────────────────────────────────────
private void Window_Deactivated(object sender, EventArgs e)
@@ -171,6 +218,16 @@ public partial class LauncherWindow
if (_vm.CloseOnFocusLost) Hide();
}
private void Window_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// 런처 숨김 시 QuickLook도 함께 닫기
if (!(bool)e.NewValue)
{
_quickLookWindow?.Close();
_quickLookWindow = null;
}
}
private void ScrollToSelected()
{
if (_vm.SelectedItem != null)
@@ -182,4 +239,33 @@ public partial class LauncherWindow
// 시스템 트레이 토스트 알림 표시
// App.xaml.cs의 TrayIcon을 통해 처리
}
// ─── 퀵 액션 칩 클릭 ──────────────────────────────────────────────────────
/// <summary>
/// 입력창 아래 최근 실행 칩 클릭 시 — 경로를 즉시 실행하고 런처를 닫습니다.
/// </summary>
private async void QuickActionChip_Click(object sender, MouseButtonEventArgs e)
{
if (((System.Windows.FrameworkElement)sender).DataContext
is not Models.QuickActionChip chip) return;
var expanded = Environment.ExpandEnvironmentVariables(chip.Path);
Hide(); // 창 먼저 숨겨 체감 속도 확보
try
{
await Task.Run(() =>
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(expanded)
{ UseShellExecute = true }));
}
catch (Exception ex)
{
Services.LogService.Error($"퀵 액션 실행 실패: {expanded} - {ex.Message}");
}
// 사용 통계 기록 (비동기)
_ = Task.Run(() => Services.UsageRankingService.RecordExecution(chip.Path));
}
}

View File

@@ -207,6 +207,10 @@
<!-- ─── 입력 영역 ─── -->
<Grid Grid.Row="0" Margin="20,16,20,16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
@@ -417,6 +421,55 @@
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<!-- ─── 퀵 액션 칩 바 (Row 1) — 입력이 없을 때 최근 실행 항목 표시 ─── -->
<ItemsControl Grid.Row="1" Grid.ColumnSpan="3"
ItemsSource="{Binding QuickActionItems}"
Margin="0,10,0,0"
Visibility="{Binding ShowQuickActions, Converter={StaticResource BoolToVisibilityConverter}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="0,0,6,6"
Padding="9,5"
CornerRadius="10"
Cursor="Hand"
Background="{Binding Background}"
BorderThickness="1"
BorderBrush="{DynamicResource BorderColor}"
MouseLeftButtonUp="QuickActionChip_Click">
<Border.Style>
<Style TargetType="Border">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="BorderBrush" Value="{DynamicResource AccentColor}"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Symbol}"
FontFamily="Segoe MDL2 Assets"
FontSize="11"
Foreground="{DynamicResource SecondaryText}"
VerticalAlignment="Center"
Margin="0,0,5,0"/>
<TextBlock Text="{Binding Title}"
FontFamily="Segoe UI, Malgun Gothic"
FontSize="11"
Foreground="{DynamicResource PrimaryText}"
VerticalAlignment="Center"
MaxWidth="100"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- ─── 파일 액션 모드 breadcrumb 바 ─── -->