using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using System.Windows; using System.Windows.Input; using AxCopilot.Services; namespace AxCopilot.Views; /// /// L5-5: 배치 파일 이름변경 창. /// 여러 파일을 변수 패턴 또는 정규식으로 미리보기 후 일괄 적용합니다. /// public partial class BatchRenameWindow : Window { private readonly ObservableCollection _entries = new(); private bool _keepExt = true; private bool _regexMode = false; private int _startNumber = 1; // ────────────────────────────────────────────────────────────────────── public BatchRenameWindow() { InitializeComponent(); PreviewGrid.ItemsSource = _entries; // 항목 변경 → 카운트 배지 + 빈 상태 갱신 _entries.CollectionChanged += (_, _) => RefreshUI(); // 드래그 앤 드롭 Drop += Window_Drop; DragOver += Window_DragOver; DragLeave += (_, _) => DropHintOverlay.Visibility = Visibility.Collapsed; } // ─── 외부에서 파일 목록 주입 ───────────────────────────────────────── public void AddFiles(IEnumerable paths) { foreach (var p in paths) { if (!File.Exists(p)) continue; if (_entries.Any(e => string.Equals(e.OriginalPath, p, StringComparison.OrdinalIgnoreCase))) continue; _entries.Add(new RenameEntry { OriginalPath = p, OriginalName = Path.GetFileName(p) }); } UpdatePreviews(); } // ─── 패턴 엔진 ─────────────────────────────────────────────────────── private static string ApplyPattern( string pattern, string originalName, string ext, int index, int startNum, bool keepExt, bool regexMode) { if (regexMode) { // /regex/replacement/ 형식 var m = Regex.Match(pattern, @"^/(.+)/([^/]*)/?$"); if (m.Success) { var rxPat = m.Groups[1].Value; var repl = m.Groups[2].Value; try { var result = Regex.Replace(originalName, rxPat, repl); if (keepExt && !string.IsNullOrEmpty(ext) && !result.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) result += ext; return result; } catch { return "⚠ 정규식 오류"; } } // 패턴 불완전 → 원본 그대로 return originalName + ext; } // 변수 모드 var n = index + startNum; var newName = pattern; // {n:자릿수} — 자릿수 패딩 newName = Regex.Replace(newName, @"\{n:(\d+)\}", rm => { if (int.TryParse(rm.Groups[1].Value, out var digits)) return n.ToString($"D{digits}"); return n.ToString(); }); // {date:format} newName = Regex.Replace(newName, @"\{date:([^}]+)\}", rm => { try { return DateTime.Today.ToString(rm.Groups[1].Value); } catch { return DateTime.Today.ToString("yyyy-MM-dd"); } }); newName = newName .Replace("{n}", n.ToString()) .Replace("{name}", originalName) .Replace("{orig}", originalName) .Replace("{date}", DateTime.Today.ToString("yyyy-MM-dd")) .Replace("{ext}", ext.TrimStart('.')); // 확장자 유지 if (keepExt && !string.IsNullOrEmpty(ext)) { if (!Path.HasExtension(newName)) newName += ext; } return newName; } private void UpdatePreviews() { var pattern = PatternBox?.Text ?? "{n}_{name}"; var startNum = _startNumber; var keepExt = _keepExt; var regexMode = _regexMode; // 새 이름 계산 for (int i = 0; i < _entries.Count; i++) { var entry = _entries[i]; var ext = Path.GetExtension(entry.OriginalPath); var nameNoExt = Path.GetFileNameWithoutExtension(entry.OriginalPath); entry.NewName = ApplyPattern(pattern, nameNoExt, ext, i, startNum, keepExt, regexMode); } // 충돌 감지 (같은 폴더 내 같은 새 이름 OR 기존 파일) var grouped = _entries .GroupBy(e => Path.Combine( Path.GetDirectoryName(e.OriginalPath) ?? "", e.NewName), StringComparer.OrdinalIgnoreCase); var conflictPaths = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var g in grouped) { if (g.Count() > 1) foreach (var e in g) conflictPaths.Add(e.OriginalPath); } foreach (var entry in _entries) { var destPath = Path.Combine( Path.GetDirectoryName(entry.OriginalPath) ?? "", entry.NewName); // 충돌: 동명 중복 OR 목적지 파일이 이미 존재(원본 제외) var nameConflict = conflictPaths.Contains(entry.OriginalPath); var destExists = File.Exists(destPath) && !string.Equals(destPath, entry.OriginalPath, StringComparison.OrdinalIgnoreCase); entry.HasConflict = nameConflict || destExists; } RefreshUI(); } private void RefreshUI() { var count = _entries.Count; var conflicts = _entries.Count(e => e.HasConflict); FileCountBadge.Text = count > 0 ? $"— {count}개 파일" : ""; EmptyState.Visibility = count == 0 ? Visibility.Visible : Visibility.Collapsed; PreviewGrid.Visibility = count == 0 ? Visibility.Collapsed : Visibility.Visible; ConflictLabel.Text = conflicts > 0 ? $"⚠ 충돌 {conflicts}개" : ""; } // ─── 타이틀바 ──────────────────────────────────────────────────────── private void TitleBar_MouseDown(object sender, MouseButtonEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) DragMove(); } private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close(); // ─── 패턴 입력 ─────────────────────────────────────────────────────── private void PatternBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) => UpdatePreviews(); private void StartNumberBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { if (int.TryParse(StartNumberBox.Text, out var n) && n >= 0) _startNumber = n; UpdatePreviews(); } // ─── 모드 선택 ─────────────────────────────────────────────────────── private void BtnModeVar_Click(object sender, MouseButtonEventArgs e) { _regexMode = false; SetModeUI(); UpdatePreviews(); } private void BtnModeRegex_Click(object sender, MouseButtonEventArgs e) { _regexMode = true; SetModeUI(); if (string.IsNullOrWhiteSpace(PatternBox.Text) || !PatternBox.Text.StartsWith('/')) PatternBox.Text = "/(old)/(new)/"; UpdatePreviews(); } private void SetModeUI() { var accent = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.DodgerBlue; var sec = TryFindResource("SecondaryText") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.Gray; var dimBg = new System.Windows.Media.SolidColorBrush( System.Windows.Media.Color.FromArgb(0x18, 0xFF, 0xFF, 0xFF)); if (_regexMode) { BtnModeVar.Background = dimBg; BtnModeVarText.Foreground = sec; BtnModeRegex.Background = accent; BtnModeRegexText.Foreground = System.Windows.Media.Brushes.White; } else { BtnModeVar.Background = accent; BtnModeVarText.Foreground = System.Windows.Media.Brushes.White; BtnModeRegex.Background = dimBg; BtnModeRegexText.Foreground = sec; } } // ─── 확장자 유지 토글 ──────────────────────────────────────────────── private void ExtToggle_Click(object sender, MouseButtonEventArgs e) { _keepExt = !_keepExt; var accent = TryFindResource("AccentColor") as System.Windows.Media.Brush ?? System.Windows.Media.Brushes.DodgerBlue; var gray = new System.Windows.Media.SolidColorBrush( System.Windows.Media.Color.FromArgb(0x40, 0xFF, 0xFF, 0xFF)); ExtToggle.Background = _keepExt ? accent : gray; ExtThumb.HorizontalAlignment = _keepExt ? HorizontalAlignment.Right : HorizontalAlignment.Left; ExtThumb.Margin = _keepExt ? new Thickness(0, 0, 1, 0) : new Thickness(1, 0, 0, 0); UpdatePreviews(); } // ─── 힌트 팝업 ─────────────────────────────────────────────────────── private void BtnHint_Click(object sender, MouseButtonEventArgs e) => HintPopup.IsOpen = !HintPopup.IsOpen; // ─── 파일/폴더 추가 ────────────────────────────────────────────────── private void BtnAddFolder_Click(object sender, MouseButtonEventArgs e) { using var dlg = new System.Windows.Forms.FolderBrowserDialog { Description = "파일이 있는 폴더를 선택하세요", ShowNewFolderButton = false }; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var files = Directory.GetFiles(dlg.SelectedPath); Array.Sort(files); AddFiles(files); } private void BtnAddFiles_Click(object sender, MouseButtonEventArgs e) { using var dlg = new System.Windows.Forms.OpenFileDialog { Title = "이름변경할 파일 선택", Multiselect = true, Filter = "모든 파일 (*.*)|*.*" }; if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return; var files = dlg.FileNames.OrderBy(f => f).ToArray(); AddFiles(files); } private void BtnRemoveSelected_Click(object sender, MouseButtonEventArgs e) { var selected = PreviewGrid.SelectedItems.OfType().ToList(); foreach (var item in selected) _entries.Remove(item); UpdatePreviews(); } private void BtnClearAll_Click(object sender, MouseButtonEventArgs e) { _entries.Clear(); RefreshUI(); } // ─── 적용 ──────────────────────────────────────────────────────────── private void BtnApply_Click(object sender, MouseButtonEventArgs e) { var toRename = _entries .Where(en => en.NewName != en.OriginalName && !en.HasConflict) .ToList(); if (toRename.Count == 0) { NotificationService.Notify("배치 이름변경", "변경할 파일이 없습니다. 패턴을 확인하거나 충돌을 해소하세요."); return; } int ok = 0, fail = 0; foreach (var entry in toRename) { try { var destPath = Path.Combine( Path.GetDirectoryName(entry.OriginalPath) ?? "", entry.NewName); File.Move(entry.OriginalPath, destPath); // 성공 후 엔트리 갱신 (새 경로로 업데이트) entry.OriginalPath = destPath; entry.OriginalName = entry.NewName; ok++; } catch (Exception ex) { LogService.Warn($"배치 이름변경 실패: {entry.OriginalPath} → {entry.NewName} — {ex.Message}"); fail++; } } // 미리보기 갱신 (적용 후 남은 항목) UpdatePreviews(); var msg = fail > 0 ? $"{ok}개 이름변경 완료, {fail}개 실패" : $"{ok}개 파일 이름변경 완료"; NotificationService.Notify("AX Copilot", msg); LogService.Info($"배치 이름변경: {msg}"); } // ─── 드래그 앤 드롭 ────────────────────────────────────────────────── private void Window_DragOver(object sender, DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.FileDrop)) { e.Effects = DragDropEffects.Copy; DropHintOverlay.Visibility = Visibility.Visible; } else { e.Effects = DragDropEffects.None; } e.Handled = true; } private void Window_Drop(object sender, DragEventArgs e) { DropHintOverlay.Visibility = Visibility.Collapsed; if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return; var dropped = (string[])e.Data.GetData(DataFormats.FileDrop); var files = new List(); foreach (var p in dropped) { if (File.Exists(p)) { files.Add(p); } else if (Directory.Exists(p)) { files.AddRange(Directory.GetFiles(p).OrderBy(f => f)); } } AddFiles(files); } } // ─── RenameEntry 모델 ──────────────────────────────────────────────────────── public class RenameEntry : INotifyPropertyChanged { public string OriginalPath { get; set; } = ""; private string _originalName = ""; public string OriginalName { get => _originalName; set { _originalName = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); } } private string _newName = ""; public string NewName { get => _newName; set { _newName = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); } } private bool _hasConflict; public bool HasConflict { get => _hasConflict; set { _hasConflict = value; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); } } public string StatusText => HasConflict ? "⚠ 충돌" : OriginalName == NewName ? "─" : "✓"; public event PropertyChangedEventHandler? PropertyChanged; protected void OnPropertyChanged([CallerMemberName] string? name = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }