변경 목적: 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개를 확인했습니다.
203 lines
8.3 KiB
C#
203 lines
8.3 KiB
C#
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",
|
|
};
|
|
}
|