[Phase L5-3] QuickLook 인라인 편집 기능 구현

QuickLookWindow.xaml:
- 타이틀바 우측에 ✏ 편집 버튼(BtnEdit) 추가 (텍스트 파일일 때만 Visible)
  - 닫기 버튼과 StackPanel으로 묶음, 호버 시 #28FFFFFF 배경
- TextEditBox (TextBox) 추가 — TextScrollViewer와 동일 위치(Grid.Row=1)
  - Cascadia Code 12px, Border 없음, AcceptsReturn/Tab, 기본 Collapsed
  - TextChanged → TextEditBox_TextChanged 이벤트 연결

QuickLookWindow.xaml.cs:
- 편집 상태 필드: _currentFilePath, _currentFileExt, _isEditMode, _isModified, _suppressTextChanged
- LoadPreview() 수정:
  - _currentFilePath/_currentFileExt 저장
  - TextExts 파일일 때만 BtnEdit.Visibility=Visible
- BtnEdit_Click / BtnEdit_Click → ToggleEditMode()
- TextEditBox_TextChanged → _isModified=true, 파일명에 "● " 마커 추가
- OnKeyDown 확장:
  - 편집 모드: Ctrl+S(저장), Ctrl+E(토글), Esc(미저장 확인)
  - 미리보기 모드: Ctrl+E(편집 모드 진입)
  - BtnClose_Click: 미저장 확인 후 닫기
- EnterEditMode(): 파일 전체 읽기(300줄 제한 없음), TextEditBox 표시, 아이콘→👁, 상태바 힌트
- ExitEditMode(): TextScrollViewer 복원, 아이콘→✏, ReloadPreview() 호출
- SaveEditedFile(): File.WriteAllText(UTF8), ● 마커 제거, 상태바 갱신, 완료 알림
- ReloadPreview(): 저장 후 PreviewText 새로고침 (편집 내용 반영)

HelpDetailWindow.Shortcuts.cs:
- "기타 창" 카테고리에 "Ctrl+E (QuickLook)" 및 "Ctrl+S (QuickLook 편집 중)" 단축키 추가

docs/LAUNCHER_ROADMAP.md:
- L5-3  완료 표시

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 12:25:25 +09:00
parent ce02343624
commit 3bfa82c06d
4 changed files with 301 additions and 19 deletions

View File

@@ -131,7 +131,7 @@
|---|------|------|----------|
| L5-1 | **항목별 전용 핫키** ✅ | 앱·URL·폴더에 `Ctrl+Alt+숫자` 등 글로벌 단축키 직접 할당. `hotkey` 프리픽스로 관리. `HotkeyAssignment` 모델 + `InputListener` 확장 + 설정창 "전용 핫키" 탭 | 높음 |
| L5-2 | **OCR 화면 텍스트 추출** ✅ | `ocr` 프리픽스 + F4 글로벌 단축키. RegionSelectWindow 재사용, Windows.Media.Ocr 로컬 엔진. 결과 → 클립보드 복사 + 런처 입력창 자동 채움 | 높음 |
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기에서 텍스트·마크다운 파일 직접 편집 + Ctrl+S 저장. 변경 감지(수정 표시 `●`), Esc 취소 | 중간 |
| L5-3 | **QuickLook 인라인 편집** | F3 미리보기 → Ctrl+E 편집 모드 토글. 텍스트/코드 전체 읽기(300줄 제한 없음). Ctrl+S 저장, ● 수정 마커, Esc 취소 확인, 저장 후 미리보기 새로고침 | 중간 |
| L5-4 | **앱 세션 스냅** | 여러 앱을 지정 레이아웃으로 한번에 열기. `snap 세션이름` → 등록된 앱 목록을 각 레이아웃에 배치 | 중간 |
| L5-5 | **배치 파일 이름 변경** | 다중 선택 후 `rename {패턴}` → 넘버링·날짜·정규식 치환 미리보기 → 일괄 적용 | 중간 |
| L5-6 | **자동화 스케줄러** | `sched` 프리픽스로 시간·앱 기반 트리거 등록. "매일 09:00 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 |

View File

@@ -72,6 +72,16 @@ public partial class HelpDetailWindow
"화면 영역 텍스트 추출 (OCR)",
"런처를 닫고 화면 드래그 영역 선택 모드를 즉시 실행합니다. 선택한 영역의 텍스트를 자동으로 인식해 클립보드에 복사하고 런처 입력창에 채웁니다.",
"\uE8D2", "#0F766E"));
// ── QuickLook 편집 ─────────────────────────────────────────────────
items.Add(MakeShortcut("기타 창", "Ctrl + E (QuickLook)",
"QuickLook 편집 모드 전환",
"F3 미리보기 창에서 텍스트/코드 파일을 직접 편집할 수 있습니다. 편집 모드에서 Ctrl+S로 저장, Esc로 취소합니다. 이미지·PDF·Office 파일은 편집 불가입니다.",
"\uE70F", "#6B2C91"));
items.Add(MakeShortcut("기타 창", "Ctrl + S (QuickLook 편집 중)",
"편집 내용 즉시 저장",
"QuickLook 편집 모드에서 현재 편집 내용을 파일에 저장합니다. 저장 완료 시 알림이 표시됩니다.",
"\uE74E", "#059669"));
items.Add(MakeShortcut("런처 기능", "F1",
"도움말 창 열기",
"이 화면을 직접 엽니다. 'help' 를 입력하는 것과 동일합니다.",

View File

@@ -44,9 +44,34 @@
MaxWidth="270"/>
</StackPanel>
<!-- 닫기 버튼 -->
<Border HorizontalAlignment="Right" VerticalAlignment="Center"
<!-- 우측 버튼 그룹 -->
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Center"
Orientation="Horizontal">
<!-- 편집 버튼 (텍스트 파일일 때만 표시) -->
<Border x:Name="BtnEdit"
CornerRadius="4" Padding="8,4" Cursor="Hand"
Visibility="Collapsed" Margin="0,0,2,0"
ToolTip="편집 모드 전환 (Ctrl+E)"
MouseLeftButtonUp="BtnEdit_Click">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="#28FFFFFF"/>
</Trigger>
</Style.Triggers>
</Style>
</Border.Style>
<TextBlock x:Name="BtnEditIcon"
Text="&#xE70F;"
FontFamily="Segoe MDL2 Assets" FontSize="13"
Foreground="{DynamicResource SecondaryText}"/>
</Border>
<!-- 닫기 버튼 -->
<Border CornerRadius="4" Padding="8,4" Cursor="Hand"
MouseLeftButtonUp="BtnClose_Click">
<Border.Style>
<Style TargetType="Border">
@@ -61,6 +86,7 @@
<TextBlock Text="&#xE711;" FontFamily="Segoe MDL2 Assets" FontSize="13"
Foreground="{DynamicResource SecondaryText}"/>
</Border>
</StackPanel>
</Grid>
</Border>
@@ -113,6 +139,23 @@
</Grid>
</ScrollViewer>
<!-- 인라인 편집 TextBox (편집 모드에서만 표시) -->
<TextBox x:Name="TextEditBox"
FontFamily="Cascadia Code, Consolas, Courier New"
FontSize="12"
Foreground="{DynamicResource PrimaryText}"
Background="{DynamicResource LauncherBackground}"
BorderThickness="0"
Padding="14,12"
AcceptsReturn="True"
AcceptsTab="True"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
TextWrapping="NoWrap"
SpellCheck.IsEnabled="False"
Visibility="Collapsed"
TextChanged="TextEditBox_TextChanged"/>
<!-- PDF 미리보기 (텍스트 추출) -->
<ScrollViewer x:Name="PdfScrollViewer"
HorizontalScrollBarVisibility="Disabled"

View File

@@ -54,6 +54,14 @@ public partial class QuickLookWindow : Window
private static readonly HashSet<string> ExcelExts = new(StringComparer.OrdinalIgnoreCase)
{ ".xlsx", ".xls" };
// ─── 편집 상태 ────────────────────────────────────────────────────────────
private string _currentFilePath = ""; // 현재 표시 중인 파일 경로
private string _currentFileExt = ""; // 현재 파일 확장자
private bool _isEditMode; // 편집 모드 활성 여부
private bool _isModified; // 미저장 변경 있음
private bool _suppressTextChanged; // TextChanged 이벤트 억제 플래그
// ─── 생성 ─────────────────────────────────────────────────────────────────
public QuickLookWindow(string path, Window owner)
@@ -68,6 +76,50 @@ public partial class QuickLookWindow : Window
private void OnKeyDown(object sender, KeyEventArgs e)
{
// ─ 편집 모드 단축키 ─────────────────────────────────────────
if (_isEditMode)
{
// Ctrl+S → 저장
if (e.Key == Key.S && Keyboard.Modifiers == ModifierKeys.Control)
{
SaveEditedFile();
e.Handled = true;
return;
}
// Ctrl+E → 편집 모드 토글
if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
{
ToggleEditMode();
e.Handled = true;
return;
}
// Esc → 편집 모드 종료 (미저장 확인)
if (e.Key == Key.Escape)
{
if (_isModified)
{
var result = CustomMessageBox.Show(
"저장하지 않은 변경 내용이 있습니다.\n편집을 취소하고 미리보기로 돌아갈까요?",
"AX Copilot — 편집 취소",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) { e.Handled = true; return; }
}
ExitEditMode();
e.Handled = true;
return;
}
return; // 편집 모드에서 F3 등은 무시 (TextBox가 처리)
}
// ─ 미리보기 모드 단축키 ─────────────────────────────────────
if (e.Key == Key.E && Keyboard.Modifiers == ModifierKeys.Control)
{
ToggleEditMode();
e.Handled = true;
return;
}
if (e.Key is Key.Escape or Key.F3)
{
Close();
@@ -80,7 +132,162 @@ public partial class QuickLookWindow : Window
if (e.LeftButton == MouseButtonState.Pressed) DragMove();
}
private void BtnClose_Click(object sender, MouseButtonEventArgs e) => Close();
private void BtnClose_Click(object sender, MouseButtonEventArgs e)
{
if (_isEditMode && _isModified)
{
var result = CustomMessageBox.Show(
"저장하지 않은 변경 내용이 있습니다.\n저장하지 않고 닫을까요?",
"AX Copilot — 저장 확인",
MessageBoxButton.YesNo,
MessageBoxImage.Question);
if (result != MessageBoxResult.Yes) return;
}
Close();
}
// ─── 편집 버튼 클릭 ─────────────────────────────────────────────────────
private void BtnEdit_Click(object sender, MouseButtonEventArgs e) => ToggleEditMode();
// ─── 편집 텍스트 변경 감지 ───────────────────────────────────────────────
private void TextEditBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
if (_suppressTextChanged) return;
if (!_isModified)
{
_isModified = true;
// 타이틀에 ● 수정 마커 추가
if (!FileNameText.Text.StartsWith("● "))
FileNameText.Text = "● " + FileNameText.Text;
}
}
// ─── 편집 모드 진입/종료 ─────────────────────────────────────────────────
private void ToggleEditMode()
{
if (!_isEditMode)
EnterEditMode();
else
{
if (_isModified)
{
var result = CustomMessageBox.Show(
"저장하지 않은 변경 내용이 있습니다.\n저장하고 미리보기로 돌아갈까요?",
"AX Copilot — 편집 모드 종료",
MessageBoxButton.YesNoCancel,
MessageBoxImage.Question);
if (result == MessageBoxResult.Cancel) return;
if (result == MessageBoxResult.Yes) SaveEditedFile();
}
ExitEditMode();
}
}
private void EnterEditMode()
{
if (string.IsNullOrEmpty(_currentFilePath) || !TextExts.Contains(_currentFileExt)) return;
try
{
// 파일 전체 내용 읽기 (미리보기의 300줄 제한 없이)
string fullContent;
try { fullContent = File.ReadAllText(_currentFilePath, Encoding.UTF8); }
catch { fullContent = File.ReadAllText(_currentFilePath); }
_suppressTextChanged = true;
TextEditBox.Text = fullContent;
_suppressTextChanged = false;
// UI 전환: 미리보기 → 편집
TextScrollViewer.Visibility = Visibility.Collapsed;
TextEditBox.Visibility = Visibility.Visible;
TextEditBox.Focus();
// 편집 버튼 아이콘 → 👁 (보기 모드로 전환 의미)
BtnEditIcon.Text = "\uE890"; // Eye / View icon
BtnEdit.ToolTip = "미리보기 모드로 전환 (Ctrl+E)";
BtnEditIcon.Foreground = TryFindResource("AccentColor") as Brush
?? new SolidColorBrush(System.Windows.Media.Color.FromRgb(75, 94, 252));
_isEditMode = true;
_isModified = false;
// 상태 힌트를 하단 FooterMeta에 추가
FooterMeta.Text = FooterMeta.Text + " · ✏ 편집 모드 (Ctrl+S 저장 · Esc 취소)";
}
catch (Exception ex)
{
Services.LogService.Warn($"편집 모드 진입 실패: {ex.Message}");
Services.NotificationService.Notify("AX Copilot", $"파일을 열 수 없습니다: {ex.Message}");
}
}
private void ExitEditMode()
{
TextEditBox.Visibility = Visibility.Collapsed;
TextScrollViewer.Visibility = Visibility.Visible;
// 편집 버튼 아이콘 복원 → ✏
BtnEditIcon.Text = "\uE70F";
BtnEdit.ToolTip = "편집 모드 전환 (Ctrl+E)";
BtnEditIcon.Foreground = TryFindResource("SecondaryText") as Brush
?? Brushes.Gray;
_isEditMode = false;
_isModified = false;
// 미리보기 다시 로드 (편집된 내용 반영)
ReloadPreview();
}
private void SaveEditedFile()
{
if (string.IsNullOrEmpty(_currentFilePath)) return;
try
{
File.WriteAllText(_currentFilePath, TextEditBox.Text, Encoding.UTF8);
_isModified = false;
// ● 마커 제거
if (FileNameText.Text.StartsWith("● "))
FileNameText.Text = FileNameText.Text[2..];
// 하단 상태 갱신
var info = new FileInfo(_currentFilePath);
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm} · ✏ 편집 모드 (Ctrl+S 저장 · Esc 취소)";
Services.NotificationService.Notify("저장 완료", $"{Path.GetFileName(_currentFilePath)} 저장됨");
Services.LogService.Info($"인라인 편집 저장: {_currentFilePath}");
}
catch (Exception ex)
{
Services.LogService.Error($"파일 저장 실패: {ex.Message}");
CustomMessageBox.Show(
$"파일을 저장하지 못했습니다.\n{ex.Message}",
"저장 오류",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
}
private void ReloadPreview()
{
// 편집 후 미리보기 새로고침 (변경된 내용 반영)
if (string.IsNullOrEmpty(_currentFilePath) || !File.Exists(_currentFilePath)) return;
// 파일명 ● 마커 제거
if (FileNameText.Text.StartsWith("● "))
FileNameText.Text = FileNameText.Text[2..];
// 상태 바 복원
var info = new FileInfo(_currentFilePath);
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm}";
// 텍스트 뷰 갱신
LoadTextPreview(_currentFilePath, _currentFileExt);
}
// ─── 미리보기 로드 ───────────────────────────────────────────────────────
@@ -105,22 +312,44 @@ public partial class QuickLookWindow : Window
var ext = Path.GetExtension(path);
var info = new FileInfo(path);
// 편집 상태 저장
_currentFilePath = path;
_currentFileExt = ext;
FooterPath.Text = path;
FooterMeta.Text = $"{FormatSize(info.Length)} · {info.LastWriteTime:yyyy-MM-dd HH:mm}";
if (ImageExts.Contains(ext))
{
BtnEdit.Visibility = Visibility.Collapsed;
LoadImagePreview(path, info);
}
else if (PdfExts.Contains(ext))
{
BtnEdit.Visibility = Visibility.Collapsed;
LoadPdfPreview(path, info);
}
else if (WordExts.Contains(ext))
{
BtnEdit.Visibility = Visibility.Collapsed;
LoadWordPreview(path, info);
}
else if (ExcelExts.Contains(ext))
{
BtnEdit.Visibility = Visibility.Collapsed;
LoadExcelPreview(path, info);
}
else if (TextExts.Contains(ext))
{
BtnEdit.Visibility = Visibility.Visible; // ✏ 편집 버튼 표시
LoadTextPreview(path, ext);
}
else
{
BtnEdit.Visibility = Visibility.Collapsed;
LoadFileInfo(path, ext, info);
}
}
catch (Exception ex)
{
ShowInfo("\uE783", $"미리보기 오류", ex.Message);