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)
{