AX Commander 비교본 런처 기능 대량 이식

변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다.

핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다.

핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다.

문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다.

검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
This commit is contained in:
2026-04-05 00:59:45 +09:00
parent 0929778ca7
commit 0336904258
115 changed files with 30749 additions and 1 deletions

View File

@@ -0,0 +1,202 @@
using System.IO;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L13-3: 드라이브 정보 핸들러. "drive" 프리픽스로 사용합니다.
///
/// 예: drive → 전체 드라이브 목록 + 용량 요약
/// drive C → C 드라이브 상세 정보
/// drive C:\ → 경로 형식도 지원
/// drive large → 사용량 많은 순서로 정렬
/// Enter → 드라이브 정보를 클립보드에 복사.
/// </summary>
public class DriveHandler : IActionHandler
{
public string? Prefix => "drive";
public PluginMetadata Metadata => new(
"Drive",
"드라이브 정보 — 용량 · 파일시스템 · 여유공간",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
var drives = GetDrives();
if (string.IsNullOrWhiteSpace(q))
{
var totalSize = drives.Sum(d => d.TotalSize);
var totalFree = drives.Sum(d => d.AvailableFree);
var totalUsed = totalSize - totalFree;
items.Add(new LauncherItem(
$"드라이브 {drives.Count}개",
$"전체 {FormatBytes(totalSize)} · 사용 {FormatBytes(totalUsed)} · 여유 {FormatBytes(totalFree)}",
null, null, Symbol: "\uEDA2"));
foreach (var d in drives.OrderBy(d => d.Name))
items.Add(MakeDriveSummaryItem(d));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var sub = q.ToUpperInvariant().TrimEnd(':', '\\', '/');
if (sub == "LARGE")
{
// 사용량 많은 순
foreach (var d in drives.OrderByDescending(d => d.UsedSpace))
items.Add(MakeDriveSummaryItem(d));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 특정 드라이브 상세
var target = drives.FirstOrDefault(d =>
d.Name.StartsWith(sub, StringComparison.OrdinalIgnoreCase));
if (target == null)
{
items.Add(new LauncherItem("드라이브 없음", $"'{q}' 드라이브를 찾을 수 없습니다", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.AddRange(BuildDetailItems(target));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("Drive", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 드라이브 정보 수집 ────────────────────────────────────────────────────
private record DriveInfo2(
string Name,
string VolumeLabel,
string DriveFormat,
DriveType DriveType,
long TotalSize,
long AvailableFree,
long UsedSpace,
bool IsReady);
private static List<DriveInfo2> GetDrives()
{
return DriveInfo.GetDrives()
.Select(d =>
{
if (!d.IsReady)
return new DriveInfo2(d.Name, "", "", d.DriveType, 0, 0, 0, false);
try
{
return new DriveInfo2(
d.Name,
d.VolumeLabel,
d.DriveFormat,
d.DriveType,
d.TotalSize,
d.AvailableFreeSpace,
d.TotalSize - d.AvailableFreeSpace,
true);
}
catch
{
return new DriveInfo2(d.Name, "", d.DriveFormat, d.DriveType, 0, 0, 0, false);
}
})
.ToList();
}
private static IEnumerable<LauncherItem> BuildDetailItems(DriveInfo2 d)
{
var usagePercent = d.TotalSize > 0 ? (double)d.UsedSpace / d.TotalSize * 100 : 0;
var bar = MakeBar(usagePercent, 20);
var summary = $"""
드라이브: {d.Name}
볼륨 레이블: {(string.IsNullOrEmpty(d.VolumeLabel) ? "()" : d.VolumeLabel)}
: {d.DriveFormat}
: {DriveTypeName(d.DriveType)}
: {FormatBytes(d.TotalSize)}
: {FormatBytes(d.UsedSpace)} ({usagePercent:F1}%)
: {FormatBytes(d.AvailableFree)}
""";
yield return new LauncherItem(
$"{d.Name} {FormatBytes(d.TotalSize)}",
$"사용 {usagePercent:F0}% {bar} 여유 {FormatBytes(d.AvailableFree)}",
null, ("copy", summary), Symbol: "\uEDA2");
yield return new LauncherItem("볼륨 레이블", string.IsNullOrEmpty(d.VolumeLabel) ? "(없음)" : d.VolumeLabel, null, null, Symbol: "\uEDA2");
yield return new LauncherItem("파일 시스템", d.DriveFormat, null, null, Symbol: "\uEDA2");
yield return new LauncherItem("드라이브 종류", DriveTypeName(d.DriveType), null, null, Symbol: "\uEDA2");
yield return new LauncherItem("전체 용량", FormatBytes(d.TotalSize), null, ("copy", FormatBytes(d.TotalSize)), Symbol: "\uEDA2");
yield return new LauncherItem("사용 중", $"{FormatBytes(d.UsedSpace)} ({usagePercent:F1}%)", null, ("copy", FormatBytes(d.UsedSpace)), Symbol: "\uEDA2");
yield return new LauncherItem("여유 공간", FormatBytes(d.AvailableFree), null, ("copy", FormatBytes(d.AvailableFree)), Symbol: "\uEDA2");
}
private static LauncherItem MakeDriveSummaryItem(DriveInfo2 d)
{
if (!d.IsReady)
return new LauncherItem(d.Name, $"준비 안됨 ({DriveTypeName(d.DriveType)})", null, null, Symbol: "\uEDA2");
var usagePercent = d.TotalSize > 0 ? (double)d.UsedSpace / d.TotalSize * 100 : 0;
var bar = MakeBar(usagePercent, 12);
var label = string.IsNullOrEmpty(d.VolumeLabel) ? d.Name : $"{d.Name} ({d.VolumeLabel})";
return new LauncherItem(
label,
$"{bar} {usagePercent:F0}% · 여유 {FormatBytes(d.AvailableFree)} / {FormatBytes(d.TotalSize)}",
null,
("copy", $"{d.Name} {FormatBytes(d.TotalSize)} 사용{usagePercent:F0}% 여유{FormatBytes(d.AvailableFree)}"),
Symbol: "\uEDA2");
}
// ── 유틸 ─────────────────────────────────────────────────────────────────
private static string MakeBar(double percent, int width)
{
var filled = (int)(percent / 100.0 * width);
filled = Math.Clamp(filled, 0, width);
return "[" + new string('█', filled) + new string('░', width - filled) + "]";
}
private static string DriveTypeName(DriveType dt) => dt switch
{
DriveType.Fixed => "고정 디스크",
DriveType.Removable => "이동식 디스크",
DriveType.Network => "네트워크 드라이브",
DriveType.CDRom => "CD/DVD",
DriveType.Ram => "RAM 디스크",
DriveType.NoRootDirectory => "루트 없음",
_ => "알 수 없음",
};
private static string FormatBytes(long bytes) => bytes switch
{
>= 1024L * 1024 * 1024 * 1024 => $"{bytes / 1024.0 / 1024 / 1024 / 1024:F2} TB",
>= 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024 / 1024:F1} GB",
>= 1024L * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
>= 1024L => $"{bytes / 1024.0:F0} KB",
_ => $"{bytes} B",
};
}