using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; using AxCopilot.Themes; namespace AxCopilot.Handlers; /// /// 파일 일괄 이름변경 핸들러. "rename" 프리픽스로 사용합니다. /// 지정된 폴더 내 파일을 패턴으로 일괄 이름변경합니다. /// 예: rename → 사용법 안내 /// rename C:\work\*.xlsx → 해당 폴더의 xlsx 파일 목록 /// rename C:\work\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ... /// {n}=순번, {date}=오늘 날짜, {orig}=원본명 /// Enter → 실행 전 미리보기, Shift+Enter → 실행. /// public class RenameHandler : IActionHandler { public string? Prefix => "rename"; public PluginMetadata Metadata => new( "Rename", "파일 일괄 이름변경 — rename", "1.0", "AX"); public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); if (string.IsNullOrWhiteSpace(q)) { return Task.FromResult>( [ new LauncherItem( "파일 일괄 이름변경", "rename [폴더\\패턴] [새이름 템플릿]", null, null, Symbol: Symbols.Rename), new LauncherItem( "사용 예시", "rename C:\\work\\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ...", null, null, Symbol: Symbols.Info), new LauncherItem( "변수: {n} 순번, {date} 날짜, {orig} 원본명", "rename D:\\photos\\*.jpg {date}_{n} → 2026-03-27_1.jpg ...", null, null, Symbol: Symbols.Info), ]); } // 파싱: [경로\패턴] [템플릿] var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries); var pattern = parts[0]; var template = parts.Length > 1 ? parts[1] : null; // 경로 분리 var dir = Path.GetDirectoryName(pattern); var glob = Path.GetFileName(pattern); if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir)) { return Task.FromResult>( [ new LauncherItem( "폴더를 찾을 수 없습니다", $"경로: {dir ?? "(비어 있음)"}", null, null, Symbol: Symbols.Warning) ]); } string[] files; try { files = Directory.GetFiles(dir, glob); } catch { files = Array.Empty(); } if (files.Length == 0) { return Task.FromResult>( [ new LauncherItem( "일치하는 파일이 없습니다", $"패턴: {glob} · 폴더: {dir}", null, null, Symbol: Symbols.Warning) ]); } Array.Sort(files); // 템플릿이 없으면 파일 목록만 표시 if (string.IsNullOrWhiteSpace(template)) { var items = files.Take(10).Select((f, i) => new LauncherItem( Path.GetFileName(f), dir, null, null, Symbol: Symbols.File)).ToList(); items.Insert(0, new LauncherItem( $"총 {files.Length}개 파일 발견", "뒤에 새 이름 템플릿을 추가하세요 (예: 보고서_{n})", null, null, Symbol: Symbols.Info)); return Task.FromResult>(items); } // 미리보기 생성 var today = DateTime.Today.ToString("yyyy-MM-dd"); var previews = new List<(string From, string To)>(); for (int i = 0; i < files.Length; i++) { var origName = Path.GetFileNameWithoutExtension(files[i]); var ext = Path.GetExtension(files[i]); var newName = template .Replace("{n}", (i + 1).ToString()) .Replace("{date}", today) .Replace("{orig}", origName); // 확장자 자동 유지 (템플릿에 확장자가 없으면) if (!Path.HasExtension(newName)) newName += ext; previews.Add((Path.GetFileName(files[i]), newName)); } var result = new List(); // 실행 항목 result.Add(new LauncherItem( $"총 {files.Length}개 파일 이름변경 실행", $"Enter로 실행 · {previews[0].From} → {previews[0].To} ...", null, ValueTuple.Create(dir, files, previews.Select(p => p.To).ToArray()), Symbol: Symbols.Rename)); // 미리보기 (최대 8개) foreach (var (from, to) in previews.Take(8)) { result.Add(new LauncherItem( $"{from} → {to}", "미리보기", null, null, Symbol: Symbols.File)); } if (files.Length > 8) { result.Add(new LauncherItem( $"... 외 {files.Length - 8}개", "", null, null, Symbol: Symbols.Info)); } return Task.FromResult>(result); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is not ValueTuple data) return Task.CompletedTask; var (dir, files, newNames) = data; int renamed = 0; int failed = 0; for (int i = 0; i < files.Length && i < newNames.Length; i++) { try { var dest = Path.Combine(dir, newNames[i]); if (File.Exists(dest)) { failed++; continue; } File.Move(files[i], dest); renamed++; } catch { failed++; } } var msg = failed > 0 ? $"{renamed}개 이름변경 완료, {failed}개 실패 (이미 존재하거나 접근 불가)" : $"{renamed}개 파일 이름변경 완료"; NotificationService.Notify("AX Copilot", msg); return Task.CompletedTask; } }