[Phase L5-5] 배치 파일 이름변경 (batchren) 구현
BatchRenameHandler.cs (신규, ~55줄):
- prefix="batchren" 핸들러, batchren [glob패턴] 으로 파일 미리 로드
- ExecuteAsync: BatchRenameWindow 열기 + 초기 경로 패턴 지정 지원
BatchRenameWindow.xaml (신규, ~380줄):
- 타이틀바(파일 수 배지) + 패턴 입력 영역 + DataGrid 미리보기 + 하단 버튼 바
- 변수 힌트 팝업: {name}/{n}/{n:3}/{date}/{date:format}/{ext}/정규식 설명
- 빈 상태 안내 패널, 드래그 앤 드롭 오버레이
- DataGrid 3열: 원본 파일명 / 새 파일명 / 상태(✓·⚠ 충돌·─)
- 충돌 행: 붉은 배경(#18EF5350) 강조
BatchRenameWindow.xaml.cs (신규, ~280줄):
- RenameEntry (INotifyPropertyChanged): OriginalPath/Name/NewName/HasConflict/StatusText
- ApplyPattern(): {n:자릿수} 패딩, {date:format}, {name}/{ext} 치환, 정규식 /rx/repl/ 모드
- UpdatePreviews(): 전체 패턴 재계산 + 충돌 감지(중복 새 이름 & 기존 파일 존재)
- AddFiles(), BtnAddFolder/Files/RemoveSelected/ClearAll 이벤트
- ExtToggle: 확장자 유지 슬라이드 토글
- BtnModeVar/Regex: 변수 ↔ 정규식 모드 전환 (UI 색상 갱신)
- StartNumberBox: 시작 번호 지정
- 드래그 앤 드롭: 파일/폴더 모두 처리
- BtnApply: File.Move 적용 후 엔트리 경로 갱신, 성공/실패 알림
App.xaml.cs (수정):
- Phase L5 섹션에 BatchRenameHandler 등록
docs/LAUNCHER_ROADMAP.md (수정):
- L5-5 항목 ✅ 완료 표시 + 구현 상세 기록
빌드: 경고 0, 오류 0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,7 +133,7 @@
|
||||
| L5-2 | **OCR 화면 텍스트 추출** ✅ | `ocr` 프리픽스 + F4 글로벌 단축키. RegionSelectWindow 재사용, Windows.Media.Ocr 로컬 엔진. 결과 → 클립보드 복사 + 런처 입력창 자동 채움 | 높음 |
|
||||
| L5-3 | **QuickLook 인라인 편집** ✅ | F3 미리보기 → Ctrl+E 편집 모드 토글. 텍스트/코드 전체 읽기(300줄 제한 없음). Ctrl+S 저장, ● 수정 마커, Esc 취소 확인, 저장 후 미리보기 새로고침 | 중간 |
|
||||
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
|
||||
| L5-5 | **배치 파일 이름 변경** | 다중 선택 후 `rename {패턴}` → 넘버링·날짜·정규식 치환 미리보기 → 일괄 적용 | 중간 |
|
||||
| L5-5 | **배치 파일 이름 변경** ✅ | `batchren` 프리픽스로 BatchRenameWindow 오픈. 변수 패턴(`{name}`, `{n:3}`, `{date:format}`, `{ext}`) + 정규식 모드(`/old/new/`). 드래그 앤 드롭·폴더/파일 추가, DataGrid 실시간 미리보기, 충돌 감지(배경 붉은 강조), 확장자 유지 토글, 시작 번호 지정, 적용 후 엔트리 갱신 | 중간 |
|
||||
| L5-6 | **자동화 스케줄러** | `sched` 프리픽스로 시간·앱 기반 트리거 등록. "매일 09:00 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 |
|
||||
|
||||
### Phase L5 구현 순서 (권장)
|
||||
|
||||
@@ -182,6 +182,8 @@ public partial class App : System.Windows.Application
|
||||
commandResolver.RegisterHandler(new HotkeyHandler(settings));
|
||||
// Phase L5-2: OCR 화면 텍스트 추출 (prefix=ocr)
|
||||
commandResolver.RegisterHandler(new OcrHandler());
|
||||
// Phase L5-5: 배치 파일 이름변경 (prefix=batchren)
|
||||
commandResolver.RegisterHandler(new BatchRenameHandler());
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
|
||||
86
src/AxCopilot/Handlers/BatchRenameHandler.cs
Normal file
86
src/AxCopilot/Handlers/BatchRenameHandler.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.IO;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L5-5: 배치 파일 이름변경 핸들러. "batchren" 프리픽스로 사용합니다.
|
||||
/// 예: batchren → 기능 소개 + 창 열기
|
||||
/// batchren C:\work\*.xlsx → 해당 패턴 파일을 창에 미리 로드
|
||||
/// </summary>
|
||||
public class BatchRenameHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "batchren";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"BatchRename",
|
||||
"배치 파일 이름변경 — batchren",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
|
||||
var items = new List<LauncherItem>
|
||||
{
|
||||
new LauncherItem(
|
||||
"배치 파일 이름변경 창 열기",
|
||||
"변수 패턴 또는 정규식으로 여러 파일을 한 번에 이름변경합니다",
|
||||
null,
|
||||
string.IsNullOrWhiteSpace(q) ? "__open__" : q,
|
||||
Symbol: Symbols.Rename),
|
||||
|
||||
new LauncherItem(
|
||||
"변수: {name} 원본명 · {n} 순번 · {n:3} 세 자리 · {date} 날짜",
|
||||
"예: 보고서_{n:3}_{date} → 보고서_001_2026-04-04.xlsx",
|
||||
null, null,
|
||||
Symbol: Symbols.Info),
|
||||
|
||||
new LauncherItem(
|
||||
"변수: {ext} 확장자 · {date:yyyyMMdd} 날짜 형식 지정",
|
||||
"정규식 모드: /old_pattern/new_text/ → 패턴 일치 부분 치환",
|
||||
null, null,
|
||||
Symbol: Symbols.Info),
|
||||
};
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
var dataStr = item.Data as string;
|
||||
if (dataStr == null) return Task.CompletedTask;
|
||||
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(() =>
|
||||
{
|
||||
var win = new Views.BatchRenameWindow();
|
||||
|
||||
// 초기 경로 패턴이 지정된 경우 파일 미리 로드
|
||||
if (dataStr != "__open__" && !string.IsNullOrWhiteSpace(dataStr))
|
||||
{
|
||||
try
|
||||
{
|
||||
var dir = Path.GetDirectoryName(dataStr);
|
||||
var glob = Path.GetFileName(dataStr);
|
||||
if (!string.IsNullOrWhiteSpace(dir) && Directory.Exists(dir))
|
||||
{
|
||||
var files = Directory.GetFiles(dir, glob ?? "*");
|
||||
Array.Sort(files);
|
||||
win.AddFiles(files);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogService.Warn($"BatchRenameHandler: 초기 로드 실패 — {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
win.Show();
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
546
src/AxCopilot/Views/BatchRenameWindow.xaml
Normal file
546
src/AxCopilot/Views/BatchRenameWindow.xaml
Normal file
@@ -0,0 +1,546 @@
|
||||
<Window x:Class="AxCopilot.Views.BatchRenameWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
Title="AX Commander — 배치 파일 이름변경"
|
||||
Width="780" Height="580"
|
||||
MinWidth="560" MinHeight="420"
|
||||
WindowStyle="None" AllowsTransparency="True"
|
||||
Background="Transparent"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
ResizeMode="CanResizeWithGrip"
|
||||
ShowInTaskbar="False"
|
||||
AllowDrop="True">
|
||||
|
||||
<Window.Resources>
|
||||
<!-- 원본 열 텍스트 스타일 -->
|
||||
<Style x:Key="OrigColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryText}"/>
|
||||
</Style>
|
||||
<!-- 새 이름 열 텍스트 스타일 -->
|
||||
<Style x:Key="NewColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="12,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryText}"/>
|
||||
</Style>
|
||||
<!-- 상태 열 텍스트 스타일 -->
|
||||
<Style x:Key="StatusColStyle" TargetType="TextBlock">
|
||||
<Setter Property="Margin" Value="8,0"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Border Background="{DynamicResource LauncherBackground}" CornerRadius="12"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
Margin="6">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="22" ShadowDepth="4" Opacity="0.32" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44"/> <!-- 타이틀바 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 패턴 입력 -->
|
||||
<RowDefinition Height="*"/> <!-- 미리보기 그리드 -->
|
||||
<RowDefinition Height="Auto"/> <!-- 하단 버튼 바 -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ─── 타이틀바 ─────────────────────────────────────────────── -->
|
||||
<Border Grid.Row="0" CornerRadius="12,12,0,0"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
MouseLeftButtonDown="TitleBar_MouseDown">
|
||||
<Grid Margin="14,0,8,0">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="15"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
VerticalAlignment="Center" Margin="0,1,10,0"/>
|
||||
<TextBlock Text="배치 파일 이름변경"
|
||||
FontSize="13" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
<TextBlock x:Name="FileCountBadge" Text=""
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="10,0,0,0"/>
|
||||
</StackPanel>
|
||||
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||
CornerRadius="4" Padding="8,4" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClose_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#40C05050"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="13"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 패턴 입력 영역 ───────────────────────────────────────── -->
|
||||
<Border Grid.Row="1"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,1"
|
||||
Padding="14,10">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- 패턴 입력 행 -->
|
||||
<Grid Grid.Row="0">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="패턴"
|
||||
FontSize="12" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<TextBox Grid.Column="1" x:Name="PatternBox"
|
||||
FontFamily="Cascadia Code, Consolas, Courier New"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="8,5"
|
||||
VerticalContentAlignment="Center"
|
||||
Text="{}{n}_{name}"
|
||||
TextChanged="PatternBox_TextChanged"/>
|
||||
|
||||
<!-- 변수 힌트 버튼 -->
|
||||
<Border Grid.Column="2" x:Name="BtnHint"
|
||||
Margin="6,0,0,0"
|
||||
CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnHint_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock Text="변수 ?"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}"/>
|
||||
</Border>
|
||||
|
||||
<!-- 힌트 팝업 -->
|
||||
<Popup x:Name="HintPopup"
|
||||
Grid.Column="2"
|
||||
PlacementTarget="{x:Reference BtnHint}"
|
||||
Placement="Bottom"
|
||||
AllowsTransparency="True"
|
||||
StaysOpen="False">
|
||||
<Border Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="1"
|
||||
CornerRadius="8" Padding="14,10"
|
||||
MinWidth="280">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="12" ShadowDepth="2" Opacity="0.3" Color="Black" Direction="270"/>
|
||||
</Border.Effect>
|
||||
<StackPanel>
|
||||
<TextBlock Text="사용 가능한 변수"
|
||||
FontSize="11" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
Margin="0,0,0,8"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="120"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="22"/>
|
||||
<RowDefinition Height="8"/>
|
||||
<RowDefinition Height="22"/>
|
||||
</Grid.RowDefinitions>
|
||||
<!-- 변수 목록 -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="{}{name}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="원본 파일명 (확장자 제외)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="{}{n}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="순번 (1, 2, 3 …)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="{}{n:3}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="세 자리 순번 (001, 002 …)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="{}{date}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="3" Grid.Column="1" Text="오늘 날짜 (yyyy-MM-dd)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="4" Grid.Column="0" Text="{}{date:yyyyMMdd}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="4" Grid.Column="1" Text="날짜 (형식 직접 지정)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Row="5" Grid.Column="0" Text="{}{ext}"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="{DynamicResource AccentColor}" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="5" Grid.Column="1" Text="원본 확장자 (점 제외)"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
|
||||
<Border Grid.Row="6" Grid.ColumnSpan="2"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0" Margin="0,2"/>
|
||||
|
||||
<TextBlock Grid.Row="7" Grid.Column="0" Text="/old/new/"
|
||||
FontFamily="Cascadia Code, Consolas" FontSize="11"
|
||||
Foreground="#F59E0B" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Row="7" Grid.Column="1" Text="정규식 모드: 패턴 치환"
|
||||
FontSize="11" Foreground="{DynamicResource PrimaryText}" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
|
||||
<!-- 옵션 행 -->
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="44"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="모드"
|
||||
FontSize="12" FontWeight="Medium"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<!-- 모드 세그먼트 -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<Border x:Name="BtnModeVar"
|
||||
CornerRadius="4,0,0,4" Padding="12,4"
|
||||
Cursor="Hand" MouseLeftButtonUp="BtnModeVar_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentColor}"/>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock x:Name="BtnModeVarText" Text="변수 패턴"
|
||||
FontSize="11" Foreground="White"/>
|
||||
</Border>
|
||||
<Border x:Name="BtnModeRegex"
|
||||
CornerRadius="0,4,4,0" Padding="12,4"
|
||||
Cursor="Hand" MouseLeftButtonUp="BtnModeRegex_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<TextBlock x:Name="BtnModeRegexText" Text="정규식"
|
||||
FontSize="11" Foreground="{DynamicResource SecondaryText}"/>
|
||||
</Border>
|
||||
|
||||
<TextBlock Text="시작 번호"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="14,0,6,0"/>
|
||||
<TextBox x:Name="StartNumberBox"
|
||||
Width="52" Text="1"
|
||||
FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}"
|
||||
BorderThickness="1"
|
||||
Padding="6,3"
|
||||
VerticalContentAlignment="Center"
|
||||
TextChanged="StartNumberBox_TextChanged"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 확장자 유지 토글 -->
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
|
||||
<TextBlock Text="확장자 유지"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,8,0"/>
|
||||
<Border x:Name="ExtToggle"
|
||||
Width="36" Height="20" CornerRadius="10"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
Cursor="Hand" MouseLeftButtonUp="ExtToggle_Click">
|
||||
<Border x:Name="ExtThumb"
|
||||
Width="16" Height="16" CornerRadius="8"
|
||||
Background="White"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0,0,1,0"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ─── 미리보기 그리드 ──────────────────────────────────────── -->
|
||||
<Grid Grid.Row="2">
|
||||
|
||||
<!-- 드롭 힌트 오버레이 -->
|
||||
<Border x:Name="DropHintOverlay"
|
||||
Background="#18FFFFFF"
|
||||
Visibility="Collapsed">
|
||||
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="40"
|
||||
Foreground="{DynamicResource AccentColor}"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="파일을 여기에 끌어다 놓으세요"
|
||||
FontSize="14" FontWeight="Medium"
|
||||
Foreground="{DynamicResource PrimaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,10,0,0"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 빈 상태 안내 -->
|
||||
<StackPanel x:Name="EmptyState"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Visible">
|
||||
<TextBlock Text=""
|
||||
FontFamily="Segoe MDL2 Assets" FontSize="44"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" Opacity="0.3"/>
|
||||
<TextBlock Text="파일을 추가하세요"
|
||||
FontSize="14"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,12,0,4" Opacity="0.6"/>
|
||||
<TextBlock Text="하단 버튼 또는 드래그 앤 드롭으로 파일이나 폴더를 추가합니다"
|
||||
FontSize="11"
|
||||
Foreground="{DynamicResource SecondaryText}"
|
||||
HorizontalAlignment="Center" Opacity="0.4"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- DataGrid 미리보기 -->
|
||||
<DataGrid x:Name="PreviewGrid"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
SelectionMode="Extended"
|
||||
GridLinesVisibility="Horizontal"
|
||||
HeadersVisibility="Column"
|
||||
CanUserReorderColumns="False"
|
||||
CanUserResizeRows="False"
|
||||
CanUserSortColumns="False"
|
||||
Background="{DynamicResource LauncherBackground}"
|
||||
RowBackground="Transparent"
|
||||
AlternatingRowBackground="#07FFFFFF"
|
||||
BorderThickness="0"
|
||||
FontSize="12"
|
||||
HorizontalScrollBarVisibility="Auto"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
Visibility="Collapsed">
|
||||
<DataGrid.Resources>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="{DynamicResource ItemBackground}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource SecondaryText}"/>
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Padding" Value="12,6"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderColor}"/>
|
||||
</Style>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="MinHeight" Value="30"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#14FFFFFF"/>
|
||||
</Trigger>
|
||||
<DataTrigger Binding="{Binding HasConflict}" Value="True">
|
||||
<Setter Property="Background" Value="#18EF5350"/>
|
||||
</DataTrigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#20AccentColor"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FocusVisualStyle" Value="{x:Null}"/>
|
||||
</Style>
|
||||
</DataGrid.Resources>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="원본 파일명"
|
||||
Binding="{Binding OriginalName}"
|
||||
Width="*"
|
||||
ElementStyle="{StaticResource OrigColStyle}"/>
|
||||
<DataGridTextColumn Header="새 파일명"
|
||||
Binding="{Binding NewName}"
|
||||
Width="*"
|
||||
ElementStyle="{StaticResource NewColStyle}"/>
|
||||
<DataGridTextColumn Header="상태"
|
||||
Binding="{Binding StatusText}"
|
||||
Width="72"
|
||||
ElementStyle="{StaticResource StatusColStyle}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
||||
<!-- ─── 하단 버튼 바 ─────────────────────────────────────────── -->
|
||||
<Border Grid.Row="3" CornerRadius="0,0,12,12"
|
||||
Background="{DynamicResource ItemBackground}"
|
||||
BorderBrush="{DynamicResource BorderColor}" BorderThickness="0,1,0,0"
|
||||
Padding="12,8">
|
||||
<Grid>
|
||||
<!-- 좌측: 파일 조작 버튼 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||
|
||||
<!-- 폴더 추가 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnAddFolder_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="#5C9BD6" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="폴더 추가" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 파일 추가 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnAddFiles_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="#5CB85C" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="파일 추가" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 선택 제거 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand" Margin="0,0,6,0"
|
||||
MouseLeftButtonUp="BtnRemoveSelected_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="#EF5350" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="선택 제거" FontSize="12"
|
||||
Foreground="{DynamicResource PrimaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- 전체 제거 -->
|
||||
<Border CornerRadius="4" Padding="10,5" Cursor="Hand"
|
||||
MouseLeftButtonUp="BtnClearAll_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="#18FFFFFF"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#28FFFFFF"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}" VerticalAlignment="Center" Margin="0,0,5,0"/>
|
||||
<TextBlock Text="전체 제거" FontSize="12"
|
||||
Foreground="{DynamicResource SecondaryText}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- 우측: 충돌 수 + 적용 버튼 -->
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="ConflictLabel" Text=""
|
||||
FontSize="11"
|
||||
Foreground="#EF5350"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0,0,14,0"/>
|
||||
|
||||
<Border x:Name="BtnApply"
|
||||
CornerRadius="4" Padding="16,6" Cursor="Hand"
|
||||
Background="{DynamicResource AccentColor}"
|
||||
MouseLeftButtonUp="BtnApply_Click">
|
||||
<Border.Style>
|
||||
<Style TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource AccentColor}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Opacity" Value="0.85"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Border.Style>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="" FontFamily="Segoe MDL2 Assets" FontSize="12"
|
||||
Foreground="White" VerticalAlignment="Center" Margin="0,0,6,0"/>
|
||||
<TextBlock Text="적용" FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="White"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Window>
|
||||
426
src/AxCopilot/Views/BatchRenameWindow.xaml.cs
Normal file
426
src/AxCopilot/Views/BatchRenameWindow.xaml.cs
Normal file
@@ -0,0 +1,426 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// L5-5: 배치 파일 이름변경 창.
|
||||
/// 여러 파일을 변수 패턴 또는 정규식으로 미리보기 후 일괄 적용합니다.
|
||||
/// </summary>
|
||||
public partial class BatchRenameWindow : Window
|
||||
{
|
||||
private readonly ObservableCollection<RenameEntry> _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<string> 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<string>(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<RenameEntry>().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<string>();
|
||||
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user