From 3bfa82c06d9c4fb601f26214a139724bb4fed3f3 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 12:25:25 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=20L5-3]=20QuickLook=20=EC=9D=B8=EB=9D=BC?= =?UTF-8?q?=EC=9D=B8=20=ED=8E=B8=EC=A7=91=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/LAUNCHER_ROADMAP.md | 2 +- .../Views/HelpDetailWindow.Shortcuts.cs | 10 + src/AxCopilot/Views/QuickLookWindow.xaml | 77 ++++-- src/AxCopilot/Views/QuickLookWindow.xaml.cs | 231 +++++++++++++++++- 4 files changed, 301 insertions(+), 19 deletions(-) diff --git a/docs/LAUNCHER_ROADMAP.md b/docs/LAUNCHER_ROADMAP.md index bda5c62..0a2f767 100644 --- a/docs/LAUNCHER_ROADMAP.md +++ b/docs/LAUNCHER_ROADMAP.md @@ -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 = 크롬 열기", "캐치 앱 실행 시 = 알림" | 낮음 | diff --git a/src/AxCopilot/Views/HelpDetailWindow.Shortcuts.cs b/src/AxCopilot/Views/HelpDetailWindow.Shortcuts.cs index c36f4ef..b6a1e9d 100644 --- a/src/AxCopilot/Views/HelpDetailWindow.Shortcuts.cs +++ b/src/AxCopilot/Views/HelpDetailWindow.Shortcuts.cs @@ -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' 를 입력하는 것과 동일합니다.", diff --git a/src/AxCopilot/Views/QuickLookWindow.xaml b/src/AxCopilot/Views/QuickLookWindow.xaml index f10bbf7..d4eb4af 100644 --- a/src/AxCopilot/Views/QuickLookWindow.xaml +++ b/src/AxCopilot/Views/QuickLookWindow.xaml @@ -44,23 +44,49 @@ MaxWidth="270"/> - - - - - - - + + + + + + + + + + + + + + + + + + + @@ -113,6 +139,23 @@ + + + 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,21 +312,43 @@ 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) {