Initial commit to new repository

This commit is contained in:
2026-04-03 18:23:52 +09:00
commit deffb33cf9
5248 changed files with 267762 additions and 0 deletions

BIN
src/AxCopilot/.DS_Store vendored Normal file

Binary file not shown.

24
src/AxCopilot/App.xaml Normal file
View File

@@ -0,0 +1,24 @@
<Application x:Class="AxCopilot.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:AxCopilot.Themes"
ShutdownMode="OnExplicitShutdown">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- 기본 테마 (Dark) - ApplyTheme()에서 교체됨 -->
<ResourceDictionary Source="Themes/Dark.xaml"/>
</ResourceDictionary.MergedDictionaries>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter"/>
<converters:InverseBoolToVisibilityConverter x:Key="InverseBoolToVisibilityConverter"/>
<converters:CountToVisibilityConverter x:Key="CountToVisibilityConverter"/>
<converters:SymbolToBackgroundConverter x:Key="SymbolToBackgroundConverter"/>
<converters:NullToCollapsedConverter x:Key="NullToCollapsedConverter"/>
<converters:IndexToNumberConverter x:Key="IndexToNumberConverter"/>
<converters:WarningSubtitleColorConverter x:Key="WarningSubtitleColorConverter"/>
<converters:ClipboardThumbnailConverter x:Key="ClipboardThumbnailConverter"/>
<converters:ClipboardHasImageConverter x:Key="ClipboardHasImageConverter"/>
</ResourceDictionary>
</Application.Resources>
</Application>

776
src/AxCopilot/App.xaml.cs Normal file
View File

@@ -0,0 +1,776 @@
using System.Windows;
using System.Windows.Forms;
using Microsoft.Win32;
using AxCopilot.Core;
using AxCopilot.Handlers;
using AxCopilot.Services;
using AxCopilot.Services.Agent;
using AxCopilot.ViewModels;
using AxCopilot.Views;
namespace AxCopilot;
public partial class App : System.Windows.Application
{
private System.Threading.Mutex? _singleInstanceMutex;
private InputListener? _inputListener;
private LauncherWindow? _launcher;
private NotifyIcon? _trayIcon;
private Views.TrayMenuWindow? _trayMenu;
private SettingsService? _settings;
private SettingsWindow? _settingsWindow;
private PluginHost? _pluginHost;
private ClipboardHistoryService? _clipboardHistory;
private DockBarWindow? _dockBar;
private FileDialogWatcher? _fileDialogWatcher;
private volatile IndexService? _indexService;
public IndexService? IndexService => _indexService;
public SettingsService? SettingsService => _settings;
public ClipboardHistoryService? ClipboardHistoryService => _clipboardHistory;
private SessionTrackingService? _sessionTracking;
private WorktimeReminderService? _worktimeReminder;
private ScreenCaptureHandler? _captureHandler;
private AgentMemoryService? _memoryService;
public AgentMemoryService? MemoryService => _memoryService;
private LlmService? _sharedLlm;
private PluginInstallService? _pluginInstallService;
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// ─── 보안: 디버거/디컴파일러 감지 (Release 빌드만 활성) ───
Security.AntiTamper.Check();
// 전역 예외 핸들러 — 미처리 예외 시 로그 + 메시지 (크래시 방지)
DispatcherUnhandledException += (_, ex) =>
{
LogService.Error($"미처리 예외: {ex.Exception}");
CustomMessageBox.Show(
$"오류가 발생했습니다:\n{ex.Exception.Message}\n\n로그 폴더를 확인하세요.",
"AX Copilot 오류", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Error);
ex.Handled = true;
};
// ─── 중복 실행 방지 ──────────────────────────────────────────────────
_singleInstanceMutex = new System.Threading.Mutex(
initiallyOwned: true, "Global\\AXCopilot_SingleInstance", out bool createdNew);
if (!createdNew)
{
_singleInstanceMutex.Dispose();
_singleInstanceMutex = null;
CustomMessageBox.Show("AX Copilot가 이미 실행 중입니다.\n트레이 아이콘을 확인하세요.",
"AX Copilot", System.Windows.MessageBoxButton.OK, System.Windows.MessageBoxImage.Information);
Shutdown();
return;
}
LogService.Info("=== AX Copilot 시작 ===");
// ─── 서비스 초기화 ────────────────────────────────────────────────────
_settings = new SettingsService();
var settings = _settings;
settings.Load();
// Phase L3 공유 서비스
_sharedLlm = new LlmService(settings);
_pluginInstallService = new PluginInstallService();
// 마이그레이션 알림 (UI가 준비된 후 표시)
if (settings.MigrationSummary != null)
{
Dispatcher.BeginInvoke(() =>
{
CustomMessageBox.Show(
settings.MigrationSummary,
"AX Copilot — 설정 업데이트",
System.Windows.MessageBoxButton.OK,
System.Windows.MessageBoxImage.Information);
}, System.Windows.Threading.DispatcherPriority.Loaded);
}
// 언어 초기화
L10n.SetLanguage(settings.Settings.Launcher.Language);
// 첫 실행 시 자동 시작 등록 (레지스트리에 아직 없으면)
if (!IsAutoStartEnabled())
SetAutoStart(true);
_memoryService = new AgentMemoryService();
_memoryService.Load(settings.Settings.Llm.WorkFolder);
_indexService = new IndexService(settings);
var indexService = _indexService;
var fuzzyEngine = new FuzzyEngine(indexService);
var commandResolver = new CommandResolver(fuzzyEngine, settings);
var contextManager = new ContextManager(settings);
// ─── 세션 추적 / 사용 통계 ────────────────────────────────────────────
_sessionTracking = new SessionTrackingService();
// ─── 잠금 해제 알림 서비스 ────────────────────────────────────────────
_worktimeReminder = new WorktimeReminderService(settings, Dispatcher);
// ─── 클립보드 히스토리 서비스 ──────────────────────────────────────────
_clipboardHistory = new ClipboardHistoryService(settings);
// ─── 빌트인 핸들러 등록 ──────────────────────────────────────────────
commandResolver.RegisterHandler(new CalculatorHandler());
commandResolver.RegisterHandler(new SystemCommandHandler(settings));
commandResolver.RegisterHandler(new SnippetHandler(settings));
commandResolver.RegisterHandler(new ClipboardHistoryHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WorkspaceHandler(contextManager, settings));
commandResolver.RegisterHandler(new UrlAliasHandler(settings));
commandResolver.RegisterHandler(new FolderAliasHandler(settings));
commandResolver.RegisterHandler(new BatchHandler(settings));
commandResolver.RegisterHandler(new ClipboardHandler(settings));
commandResolver.RegisterHandler(new BookmarkHandler());
commandResolver.RegisterHandler(new WebSearchHandler(settings));
commandResolver.RegisterHandler(new ProcessHandler());
commandResolver.RegisterHandler(new MediaHandler());
commandResolver.RegisterHandler(new SystemInfoHandler());
commandResolver.RegisterHandler(new StarInfoHandler()); // * 단축키 (info와 동일)
commandResolver.RegisterHandler(new EmojiHandler());
commandResolver.RegisterHandler(new ColorHandler());
commandResolver.RegisterHandler(new RecentFilesHandler());
commandResolver.RegisterHandler(new NoteHandler());
commandResolver.RegisterHandler(new UninstallHandler());
commandResolver.RegisterHandler(new PortHandler());
commandResolver.RegisterHandler(new EnvHandler());
commandResolver.RegisterHandler(new JsonHandler());
commandResolver.RegisterHandler(new EncodeHandler());
commandResolver.RegisterHandler(new SnapHandler());
_captureHandler = new ScreenCaptureHandler(settings);
commandResolver.RegisterHandler(_captureHandler);
commandResolver.RegisterHandler(new ColorPickHandler());
commandResolver.RegisterHandler(new DateCalcHandler());
commandResolver.RegisterHandler(new ServiceHandler(_clipboardHistory));
commandResolver.RegisterHandler(new ClipboardPipeHandler());
commandResolver.RegisterHandler(new JournalHandler());
commandResolver.RegisterHandler(new RoutineHandler());
commandResolver.RegisterHandler(new BatchTextHandler());
commandResolver.RegisterHandler(new DiffHandler(_clipboardHistory));
commandResolver.RegisterHandler(new WindowSwitchHandler());
commandResolver.RegisterHandler(new RunHandler());
commandResolver.RegisterHandler(new TextStatsHandler());
commandResolver.RegisterHandler(new FavoriteHandler());
commandResolver.RegisterHandler(new RenameHandler());
commandResolver.RegisterHandler(new MonitorHandler());
commandResolver.RegisterHandler(new ScaffoldHandler());
commandResolver.RegisterHandler(new EverythingHandler());
commandResolver.RegisterHandler(new HelpHandler(settings));
commandResolver.RegisterHandler(new ChatHandler(settings));
// ─── Phase L3 핸들러 ──────────────────────────────────────────────────
commandResolver.RegisterHandler(new QuickLinkHandler(settings));
var snippetTemplateSvc = new SnippetTemplateService(settings, _sharedLlm);
commandResolver.RegisterHandler(new AiSnippetHandler(settings, snippetTemplateSvc));
commandResolver.RegisterHandler(new WebSearchSummaryHandler(settings, _sharedLlm));
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();
_pluginHost = pluginHost;
// ─── 런처 윈도우 ──────────────────────────────────────────────────────
var vm = new LauncherViewModel(commandResolver, settings);
_launcher = new LauncherWindow(vm)
{
OpenSettingsAction = OpenSettings
};
// ─── 클립보드 히스토리 초기화 (메시지 펌프 시작 직후 — 런처 표시 불필요) ──
Dispatcher.BeginInvoke(
() => _clipboardHistory?.Initialize(),
System.Windows.Threading.DispatcherPriority.ApplicationIdle);
// ─── ChatWindow 미리 생성 (앱 유휴 시점에 숨겨진 채로 초기화) ──────────
// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 열림
Dispatcher.BeginInvoke(
() => PrewarmChatWindow(),
System.Windows.Threading.DispatcherPriority.SystemIdle);
// ─── 인덱스 빌드 (백그라운드) + 완료 후 FileSystemWatcher 시작 ────────
_ = indexService.BuildAsync().ContinueWith(_ => indexService.StartWatchers());
// ─── 글로벌 훅 + 스니펫 확장기 ───────────────────────────────────────
_inputListener = new InputListener();
_inputListener.HotkeyTriggered += OnHotkeyTriggered;
_inputListener.CaptureHotkeyTriggered += OnCaptureHotkeyTriggered;
_inputListener.HookFailed += OnHookFailed;
// 설정에 저장된 핫키로 초기화 (기본: Alt+Space)
_inputListener.UpdateHotkey(settings.Settings.Hotkey);
// 글로벌 캡처 단축키 초기화
_inputListener.UpdateCaptureHotkey(
settings.Settings.ScreenCapture.GlobalHotkey,
settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
var snippetExpander = new SnippetExpander(settings);
_inputListener.KeyFilter = snippetExpander.HandleKey;
_inputListener.Start();
// ─── 시스템 트레이 ─────────────────────────────────────────────────────
SetupTrayIcon(pluginHost, settings);
// ─── 파일 대화상자 감지 ──────────────────────────────────────────────
_fileDialogWatcher = new FileDialogWatcher();
_fileDialogWatcher.FileDialogOpened += (_, hwnd) =>
{
Dispatcher.Invoke(() =>
{
if (_launcher == null || _launcher.IsVisible) return;
if (_settings?.Settings.Launcher.EnableFileDialogIntegration != true) return;
WindowTracker.Capture();
_launcher.Show();
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
() => _launcher.SetInputText("cd "));
});
};
_fileDialogWatcher.Start();
// 독 바 자동 표시
if (settings.Settings.Launcher.DockBarAutoShow)
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Loaded, () => ToggleDockBar());
LogService.Info($"초기화 완료. {settings.Settings.Hotkey}로 실행하세요.");
}
private void OnHotkeyTriggered(object? sender, EventArgs e)
{
// 런처가 열리기 전 현재 활성 창을 저장 (SnapHandler, ScreenCaptureHandler용)
WindowTracker.Capture();
// 선택 텍스트 감지는 UI 스레드 밖에서 수행해야 SendInput이 정상 처리됨
var launcherSettings = _settings?.Settings.Launcher;
var enableTextAction = launcherSettings?.EnableTextAction == true;
// ── 런처 토글(닫기)은 텍스트 감지 없이 즉시 처리 ──
bool isVisible = false;
Dispatcher.Invoke(() => { isVisible = _launcher?.IsVisible == true; });
if (isVisible)
{
Dispatcher.Invoke(() => _launcher?.Hide());
return;
}
// ── 텍스트 감지가 비활성이면 즉시 런처 표시 ──
if (!enableTextAction)
{
Dispatcher.Invoke(() =>
{
if (_launcher == null) return;
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
});
return;
}
// ── 텍스트 감지 활성: 런처를 먼저 열고, 텍스트 감지를 비동기로 처리 ──
string? selectedText = TryGetSelectedText();
Dispatcher.Invoke(() =>
{
if (_launcher == null) return;
if (!string.IsNullOrWhiteSpace(selectedText))
{
var enabledCmds = launcherSettings?.TextActionCommands ?? new();
// 활성 명령이 1개뿐이면 팝업 없이 바로 실행
if (enabledCmds.Count == 1)
{
var directAction = TextActionPopup.AvailableCommands
.FirstOrDefault(c => c.Key == enabledCmds[0]);
if (!string.IsNullOrEmpty(directAction.Key))
{
var actionResult = enabledCmds[0] switch
{
"translate" => TextActionPopup.ActionResult.Translate,
"summarize" => TextActionPopup.ActionResult.Summarize,
"grammar" => TextActionPopup.ActionResult.GrammarFix,
"explain" => TextActionPopup.ActionResult.Explain,
"rewrite" => TextActionPopup.ActionResult.Rewrite,
_ => TextActionPopup.ActionResult.None,
};
if (actionResult != TextActionPopup.ActionResult.None)
{
ExecuteTextAction(actionResult, selectedText);
return;
}
}
}
// 여러 개 → 팝업 표시
var popup = new TextActionPopup(selectedText, enabledCmds);
popup.Closed += (_, _) =>
{
switch (popup.SelectedAction)
{
case TextActionPopup.ActionResult.OpenLauncher:
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
break;
case TextActionPopup.ActionResult.None:
break; // Esc 또는 포커스 잃음
default:
// AI 명령 실행 → AX Agent 대화로 전달
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
break;
}
};
popup.Show();
}
else
{
UsageStatisticsService.RecordLauncherOpen();
_launcher.Show();
}
});
}
/// <summary>현재 선택된 텍스트를 가져옵니다 (Ctrl+C 시뮬레이션).</summary>
/// <remarks>후크 스레드에서 호출됩니다 (UI 스레드 아님). 클립보드 접근은 STA 스레드가 필요합니다.</remarks>
private string? TryGetSelectedText()
{
try
{
// 클립보드는 STA 스레드에서만 접근 가능 → Dispatcher로 읽기
string? prevText = null;
bool hadText = false;
Dispatcher.Invoke(() =>
{
hadText = System.Windows.Clipboard.ContainsText();
prevText = hadText ? System.Windows.Clipboard.GetText() : null;
System.Windows.Clipboard.Clear();
});
System.Threading.Thread.Sleep(30);
// Ctrl+C 시뮬레이션 (후크 스레드에서 → 대상 앱이 처리)
var inputs = new[]
{
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x43, dwFlags = 0x0002 } } },
new NativeMethods.INPUT { type = 1, u = new NativeMethods.InputUnion { ki = new NativeMethods.KEYBDINPUT { wVk = 0x11, dwFlags = 0x0002 } } },
};
NativeMethods.SendInput((uint)inputs.Length, inputs, System.Runtime.InteropServices.Marshal.SizeOf<NativeMethods.INPUT>());
// 클립보드 업데이트 대기 (앱마다 다름)
System.Threading.Thread.Sleep(80);
// 클립보드에서 새 텍스트 확인 (STA 필요)
string? selected = null;
Dispatcher.Invoke(() =>
{
if (System.Windows.Clipboard.ContainsText())
{
var copied = System.Windows.Clipboard.GetText();
if (copied != prevText && !string.IsNullOrWhiteSpace(copied))
selected = copied;
}
// 클립보드 복원
if (selected != null && prevText != null)
System.Windows.Clipboard.SetText(prevText);
else if (selected != null && !hadText)
System.Windows.Clipboard.Clear();
});
return selected;
}
catch (Exception)
{
return null;
}
}
/// <summary>AI 텍스트 명령을 AX Agent 대화로 전달합니다.</summary>
private void ExecuteTextAction(TextActionPopup.ActionResult action, string text)
{
var prompt = action switch
{
TextActionPopup.ActionResult.Translate => $"다음 텍스트를 번역해줘:\n\n{text}",
TextActionPopup.ActionResult.Summarize => $"다음 텍스트를 요약해줘:\n\n{text}",
TextActionPopup.ActionResult.GrammarFix => $"다음 텍스트의 문법을 교정해줘:\n\n{text}",
TextActionPopup.ActionResult.Explain => $"다음 텍스트를 쉽게 설명해줘:\n\n{text}",
TextActionPopup.ActionResult.Rewrite => $"다음 텍스트를 다시 작성해줘:\n\n{text}",
_ => text
};
// ! 프리픽스로 AX Agent에 전달
_launcher?.Show();
_launcher?.SetInputText($"! {prompt}");
}
private static class NativeMethods
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct INPUT { public uint type; public InputUnion u; }
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Explicit)]
public struct InputUnion { [System.Runtime.InteropServices.FieldOffset(0)] public KEYBDINPUT ki; }
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct KEYBDINPUT { public ushort wVk; public ushort wScan; public uint dwFlags; public uint time; public IntPtr dwExtraInfo; }
}
private void OnCaptureHotkeyTriggered(object? sender, EventArgs e)
{
// 런처 없이 직접 캡처 실행
WindowTracker.Capture();
var mode = _settings?.Settings.ScreenCapture.GlobalHotkeyMode ?? "screen";
Dispatcher.Invoke(async () =>
{
if (_captureHandler == null) return;
try
{
await _captureHandler.CaptureDirectAsync(mode);
}
catch (Exception ex)
{
LogService.Error($"글로벌 캡처 실패: {ex.Message}");
}
});
}
private void OnHookFailed(object? sender, EventArgs e)
{
Dispatcher.Invoke(() =>
{
_trayIcon?.ShowBalloonTip(3000, "AX Copilot",
$"글로벌 단축키 등록에 실패했습니다.\n다른 앱과 {_settings?.Settings.Hotkey ?? ""}가 충돌하고 있을 수 있습니다.",
ToolTipIcon.Warning);
});
}
private void SetupTrayIcon(PluginHost pluginHost, SettingsService settings)
{
_trayIcon = new NotifyIcon
{
Text = "AX Copilot",
Visible = true,
Icon = LoadAppIcon()
};
// ─── WPF 커스텀 트레이 메뉴 구성 ──────────────────────────────────
_trayMenu = new Views.TrayMenuWindow();
_trayMenu
.AddItem("\uE7C5", "AX Commander 호출하기", () =>
Dispatcher.Invoke(() => _launcher?.Show()))
.AddItem("\uE8BD", "AX Agent 대화하기", () =>
Dispatcher.Invoke(() => OpenAiChat()), out var aiTrayItem)
.AddItem("\uE8A7", "독 바 표시", () =>
Dispatcher.Invoke(() => ToggleDockBar()))
.AddSeparator()
.AddItem("\uE72C", "플러그인 재로드", () =>
{
pluginHost.Reload();
_trayIcon!.ShowBalloonTip(2000, "AX Copilot", "플러그인이 재로드되었습니다.", ToolTipIcon.None);
})
.AddItem("\uE838", "로그 폴더 열기", () =>
{
var logDir = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "logs");
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo("explorer.exe", logDir)
{ UseShellExecute = true });
})
.AddItem("\uE9D9", "사용 통계", () =>
Dispatcher.Invoke(() => new StatisticsWindow().Show()))
.AddItem("\uE736", "사용 가이드 문서보기", () =>
{
try { Dispatcher.Invoke(() => new Views.GuideViewerWindow().Show()); }
catch (Exception ex) { LogService.Error($"사용 가이드 열기 실패: {ex.Message}"); }
})
.AddSeparator()
.AddToggleItem("\uE82F", "Windows 시작 시 자동 실행", IsAutoStartEnabled(),
isChecked => SetAutoStart(isChecked), out _, out _)
.AddSeparator()
.AddItem("\uE713", "설정", () =>
Dispatcher.Invoke(OpenSettings))
.AddItem("\uE946", "개발 정보", () =>
Dispatcher.Invoke(() => new AboutWindow().Show()))
.AddItem("\uE711", "종료", () =>
{
_inputListener?.Dispose();
_trayIcon?.Dispose();
Shutdown();
});
// AI 기능 활성화 여부에 따라 메뉴 항목 가시성 동적 업데이트
_trayMenu.Opening += () =>
{
aiTrayItem.Visibility = settings.Settings.AiEnabled
? System.Windows.Visibility.Visible
: System.Windows.Visibility.Collapsed;
};
// 우클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글
_trayIcon.MouseClick += (_, e) =>
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
Dispatcher.Invoke(() => _launcher?.Show());
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate());
};
// 타이머/알람 풍선 알림 서비스 연결
NotificationService.Initialize((title, msg) =>
{
Dispatcher.Invoke(() =>
_trayIcon?.ShowBalloonTip(4000, title, msg, ToolTipIcon.None));
});
}
/// <summary>ChatWindow 등 외부에서 설정 창을 여는 공개 메서드.</summary>
public void OpenSettingsFromChat() => Dispatcher.Invoke(OpenSettings);
/// <summary>AX Agent 창 열기 (트레이 메뉴 등에서 호출).</summary>
private Views.ChatWindow? _chatWindow;
/// <summary>
/// ChatWindow를 백그라운드에서 미리 생성합니다 (앱 시작 후 저우선순위로 호출).
/// 이후 OpenAiChat() 시 창 생성 비용 없이 즉시 Show/Activate만 수행합니다.
/// </summary>
internal void PrewarmChatWindow()
{
if (_chatWindow != null || _settings == null) return;
_chatWindow = new Views.ChatWindow(_settings);
}
private void OpenAiChat()
{
if (_settings == null) return;
if (_chatWindow == null)
{
_chatWindow = new Views.ChatWindow(_settings);
}
_chatWindow.Show();
_chatWindow.Activate();
}
public void ToggleDockBar()
{
if (_dockBar != null && _dockBar.IsVisible)
{
_dockBar.Hide();
return;
}
if (_dockBar == null)
{
_dockBar = new DockBarWindow();
_dockBar.OnQuickSearch = query =>
{
if (_launcher == null) return;
_launcher.Show();
_launcher.Activate(); // 독 바 뒤가 아닌 전면에 표시
if (!string.IsNullOrEmpty(query))
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
() => _launcher.SetInputText(query));
};
_dockBar.OnCapture = async () =>
{
WindowTracker.Capture();
if (_captureHandler != null)
await _captureHandler.CaptureDirectAsync("region");
};
_dockBar.OnOpenAgent = () =>
{
if (_launcher == null) return;
_launcher.Show();
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
() => _launcher.SetInputText("!"));
};
}
var launcher = _settings?.Settings.Launcher;
var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
_dockBar.BuildFromSettings(dockItems);
_dockBar.OnPositionChanged = (left, top) =>
{
if (_settings != null)
{
_settings.Settings.Launcher.DockBarLeft = left;
_settings.Settings.Launcher.DockBarTop = top;
_settings.Save();
}
};
_dockBar.Show();
_dockBar.ApplySettings(
launcher?.DockBarOpacity ?? 0.92,
launcher?.DockBarLeft ?? -1,
launcher?.DockBarTop ?? -1,
launcher?.DockBarRainbowGlow ?? false);
}
/// <summary>독 바를 현재 설정으로 즉시 새로고침합니다.</summary>
public void RefreshDockBar()
{
if (_dockBar == null || !_dockBar.IsVisible) return;
var launcher = _settings?.Settings.Launcher;
var dockItems = launcher?.DockBarItems ?? new() { "launcher", "clipboard", "capture", "agent", "clock", "cpu" };
_dockBar.BuildFromSettings(dockItems);
_dockBar.ApplySettings(
launcher?.DockBarOpacity ?? 0.92,
launcher?.DockBarLeft ?? -1,
launcher?.DockBarTop ?? -1,
launcher?.DockBarRainbowGlow ?? false);
}
private void OpenSettings()
{
if (_settingsWindow != null && _settingsWindow.IsVisible)
{
_settingsWindow.Activate();
return;
}
if (_settings == null || _launcher == null) return;
var vm = new ViewModels.SettingsViewModel(_settings);
// 미리보기 콜백: 현재 편집 중인 색상(vm.ColorRows)으로 런처에 즉시 반영
void PreviewCallback(string themeKey)
{
if (themeKey == "custom")
{
var tempColors = new AxCopilot.Models.CustomThemeColors();
foreach (var row in vm.ColorRows)
{
var prop = typeof(AxCopilot.Models.CustomThemeColors).GetProperty(row.Property);
prop?.SetValue(tempColors, row.Hex);
}
_launcher.ApplyTheme(themeKey, tempColors);
}
else
{
_launcher.ApplyTheme(themeKey, _settings.Settings.Launcher.CustomTheme);
}
}
// 취소/X 닫기 콜백: 파일에 저장된 원본 설정으로 복원
void RevertCallback()
{
_launcher.ApplyTheme(
_settings.Settings.Launcher.Theme ?? "system",
_settings.Settings.Launcher.CustomTheme);
}
_settingsWindow = new Views.SettingsWindow(vm, PreviewCallback, RevertCallback)
{
// 핫키 녹화 중 글로벌 핫키 일시 정지
SuspendHotkeyCallback = suspend =>
{
if (_inputListener != null)
_inputListener.SuspendHotkey = suspend;
}
};
// 저장 완료 시 InputListener 핫키 갱신 + 알림 타이머 재시작
vm.SaveCompleted += (_, _) =>
{
if (_inputListener != null && _settings != null)
{
_inputListener.UpdateHotkey(_settings.Settings.Hotkey);
_inputListener.UpdateCaptureHotkey(
_settings.Settings.ScreenCapture.GlobalHotkey,
_settings.Settings.ScreenCapture.GlobalHotkeyEnabled);
}
_worktimeReminder?.RestartTimer();
};
_settingsWindow.Show();
}
// ─── 자동 시작 레지스트리 헬퍼 ──────────────────────────────────────────
private static System.Drawing.Icon LoadAppIcon()
{
// DPI 인식 아이콘 크기 (기본 16 → 고DPI에서 20/24/32)
var iconSize = System.Windows.Forms.SystemInformation.SmallIconSize;
// 1) 파일 시스템에서 로드 (개발 환경)
try
{
var exeDir = System.IO.Path.GetDirectoryName(Environment.ProcessPath)
?? AppContext.BaseDirectory;
var path = System.IO.Path.Combine(exeDir, "Assets", "icon.ico");
if (System.IO.File.Exists(path))
return new System.Drawing.Icon(path, iconSize);
}
catch (Exception) { }
// 2) 내장 리소스에서 로드 (PublishSingleFile 배포)
try
{
var uri = new Uri("pack://application:,,,/Assets/icon.ico");
var stream = System.Windows.Application.GetResourceStream(uri)?.Stream;
if (stream != null)
return new System.Drawing.Icon(stream, iconSize);
}
catch (Exception) { }
return System.Drawing.SystemIcons.Application;
}
private const string AutoRunKey = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string AutoRunName = "AxCopilot";
private static bool IsAutoStartEnabled()
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: false);
return key?.GetValue(AutoRunName) != null;
}
catch (Exception) { return false; }
}
private static void SetAutoStart(bool enable)
{
try
{
using var key = Registry.CurrentUser.OpenSubKey(AutoRunKey, writable: true);
if (key == null) return;
if (enable)
{
var exePath = Environment.ProcessPath
?? System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
?? string.Empty;
if (!string.IsNullOrEmpty(exePath))
key.SetValue(AutoRunName, $"\"{exePath}\"");
}
else
{
key.DeleteValue(AutoRunName, throwOnMissingValue: false);
}
}
catch (Exception ex)
{
LogService.Warn($"자동 시작 레지스트리 설정 실패: {ex.Message}");
}
}
protected override void OnExit(ExitEventArgs e)
{
_chatWindow?.ForceClose(); // 미리 생성된 ChatWindow 진짜 닫기
_inputListener?.Dispose();
_clipboardHistory?.Dispose();
_indexService?.Dispose();
_sessionTracking?.Dispose();
_worktimeReminder?.Dispose();
_trayIcon?.Dispose();
try { _singleInstanceMutex?.ReleaseMutex(); } catch (Exception) { }
_singleInstanceMutex?.Dispose();
LogService.Info("=== AX Copilot 종료 ===");
base.OnExit(e);
}
}

BIN
src/AxCopilot/Assets/.DS_Store vendored Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,831 @@
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AX Copilot 사용 가이드 — 단축키 &amp; 예약어 완전 정복</title>
<style>
/* ─── 기본 레이아웃 ─────────────────────────────────────────── */
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Pretendard', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif;
font-size: 15px;
color: #1a1a2e;
background: #f8f9fe;
line-height: 1.7;
}
.wrap {
max-width: 860px;
margin: 0 auto;
padding: 40px 20px 80px;
}
/* ─── 헤더 ────────────────────────────────────────────────── */
.post-header {
background: linear-gradient(135deg, #1a1b2e 0%, #2d3a6b 60%, #4b5efc 100%);
border-radius: 20px;
padding: 48px 40px 40px;
margin-bottom: 40px;
color: white;
}
.post-header .badge {
display: inline-block;
background: rgba(255,255,255,0.15);
border: 1px solid rgba(255,255,255,0.25);
border-radius: 20px;
padding: 4px 14px;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.5px;
margin-bottom: 16px;
}
.post-header h1 {
font-size: 28px;
font-weight: 800;
line-height: 1.3;
margin-bottom: 12px;
}
.post-header .subtitle {
font-size: 14px;
color: rgba(255,255,255,0.65);
line-height: 1.6;
}
.post-header .version-tag {
display: inline-block;
background: #4b5efc;
border-radius: 6px;
padding: 2px 10px;
font-size: 12px;
font-weight: 700;
margin-top: 14px;
}
/* ─── 섹션 헤더 ────────────────────────────────────────────── */
.section-title {
font-size: 20px;
font-weight: 800;
color: #1a1b2e;
margin: 40px 0 16px;
padding-left: 14px;
border-left: 4px solid #4b5efc;
line-height: 1.3;
}
.section-desc {
font-size: 13.5px;
color: #6b6b8a;
margin-bottom: 14px;
padding: 10px 16px;
background: #eef1ff;
border-radius: 8px;
border-left: 3px solid #4b5efc;
}
.sub-title {
font-size: 15px;
font-weight: 700;
color: #2e3a6b;
margin: 24px 0 10px;
}
/* ─── 카드 테이블 ─────────────────────────────────────────── */
.table-wrap {
overflow-x: auto;
border-radius: 12px;
box-shadow: 0 2px 12px rgba(75,94,252,0.08);
margin-bottom: 20px;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
font-size: 13.5px;
}
thead tr th {
padding: 11px 14px;
text-align: left;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.3px;
color: white;
}
th.hd-blue { background: #2e5fb6; }
th.hd-dark { background: #1a1b2e; }
th.hd-purple { background: #6b2c91; }
th.hd-green { background: #107c10; }
th.hd-orange { background: #c55a11; }
tbody tr:nth-child(odd) td { background: #ffffff; }
tbody tr:nth-child(even) td { background: #f5f7ff; }
tbody tr:hover td { background: #eef1ff; transition: background 0.15s; }
td {
padding: 9px 14px;
border-bottom: 1px solid #eeeef8;
vertical-align: middle;
font-size: 13.5px;
color: #333355;
}
/* ─── 키 배지 ─────────────────────────────────────────────── */
.kbd {
display: inline-block;
font-family: 'Consolas', 'D2Coding', monospace;
background: #f0f0f8;
border: 1px solid #d0d0e8;
border-bottom: 2px solid #c0c0d8;
border-radius: 5px;
padding: 1px 8px;
font-size: 12.5px;
white-space: nowrap;
color: #2b3280;
letter-spacing: 0.2px;
}
.sym {
display: inline-block;
font-family: 'Consolas', monospace;
font-size: 16px;
font-weight: 700;
color: #c0392b;
background: #fff5f5;
border: 1px solid #fccaca;
border-radius: 4px;
padding: 0px 7px;
line-height: 1.6;
}
.kw {
display: inline-block;
font-family: 'Consolas', monospace;
font-weight: 700;
font-size: 13px;
color: #1f7a4a;
background: #f0fff8;
border: 1px solid #b2f0d4;
border-radius: 4px;
padding: 1px 8px;
}
.tip {
font-size: 12px;
color: #888;
font-style: italic;
}
/* ─── 하이라이트 박스 ─────────────────────────────────────── */
.tip-box {
background: #fffbe6;
border-left: 4px solid #ffc107;
border-radius: 0 8px 8px 0;
padding: 12px 16px;
font-size: 13px;
color: #7d5a00;
margin: 16px 0;
}
.info-box {
background: #e8f4fd;
border-left: 4px solid #2196f3;
border-radius: 0 8px 8px 0;
padding: 12px 16px;
font-size: 13px;
color: #0d3a58;
margin: 16px 0;
}
/* ─── 2단 그리드 (빠른 참조) ──────────────────────────────── */
.quick-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-bottom: 20px;
}
.quick-card {
background: white;
border-radius: 10px;
padding: 12px 14px;
box-shadow: 0 1px 6px rgba(0,0,0,0.07);
display: flex;
align-items: center;
gap: 10px;
}
.quick-icon {
width: 34px;
height: 34px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.quick-card .key { font-size: 11.5px; font-weight: 700; color: #2b3280; font-family: monospace; }
.quick-card .desc { font-size: 11.5px; color: #6b6b8a; }
/* ─── 버전 이력 ─────────────────────────────────────────── */
.version-section {
margin-top: 50px;
border-top: 2px solid #e8eaff;
padding-top: 30px;
}
.version-section h2 {
font-size: 20px;
font-weight: 800;
color: #1a1b2e;
margin-bottom: 20px;
padding-left: 14px;
border-left: 4px solid #4b5efc;
}
.version-entry {
display: flex;
gap: 16px;
align-items: flex-start;
padding: 14px 18px;
background: white;
border-radius: 10px;
margin-bottom: 10px;
box-shadow: 0 1px 6px rgba(0,0,0,0.06);
}
.version-meta {
flex-shrink: 0;
text-align: center;
min-width: 80px;
}
.version-badge {
display: inline-block;
background: #4b5efc;
color: white;
border-radius: 6px;
padding: 2px 10px;
font-size: 12px;
font-weight: 700;
font-family: monospace;
margin-bottom: 4px;
}
.version-date {
color: #9999bb;
font-size: 11px;
line-height: 1.4;
}
.version-changes {
flex: 1;
}
.version-changes ul {
margin: 0;
padding-left: 18px;
}
.version-changes ul li {
font-size: 13px;
color: #333355;
margin-bottom: 4px;
line-height: 1.6;
}
.version-changes ul li strong {
color: #2b3280;
}
/* ─── 이전 이력 접기 ──────────────────────────────────────── */
.version-fold {
margin-top: 12px;
}
.version-fold summary {
cursor: pointer;
font-size: 14px;
font-weight: 700;
color: #4b5efc;
padding: 10px 16px;
background: #eef1ff;
border-radius: 10px;
list-style: none;
user-select: none;
}
.version-fold summary::-webkit-details-marker { display: none; }
.version-fold summary::before {
content: '▶ ';
font-size: 11px;
transition: transform 0.2s;
display: inline-block;
}
.version-fold[open] summary::before {
content: '▼ ';
}
/* ─── 푸터 ─────────────────────────────────────────────────── */
.post-footer {
margin-top: 40px;
padding: 24px 28px;
background: #1a1b2e;
border-radius: 14px;
color: rgba(255,255,255,0.55);
font-size: 12.5px;
text-align: center;
line-height: 1.8;
}
.post-footer strong { color: rgba(255,255,255,0.85); }
</style>
</head>
<body>
<div class="wrap">
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 헤더 -->
<!-- ══════════════════════════════════════════════════════════ -->
<div class="post-header">
<div class="badge">🚀 AX Copilot 사용 가이드</div>
<h1>단축키 &amp; 예약어 완전 정복<br>— 키보드 하나로 모든 업무를</h1>
<p class="subtitle">
Windows 전용 시맨틱 런처 <strong style="color:#fff">AX Copilot</strong>의 모든 단축키와 명령 예약어를 한 페이지에 정리했습니다.<br>
Alt+Space 한 번으로 앱 실행·파일 검색·계산·클립보드·화면 캡처·창 전환·시스템 제어 등 40개 이상의 기능을 즉시 호출합니다.
</p>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 목차 -->
<!-- ══════════════════════════════════════════════════════════ -->
<div style="background:rgba(30,34,60,0.95);border:1px solid rgba(75,94,252,0.5);border-radius:12px;padding:18px 26px;margin:0 0 24px 0;">
<h3 style="margin:0 0 10px 0;font-size:15px;font-weight:800;color:#fff;letter-spacing:0.3px;">📑 목차</h3>
<ol style="margin:0;padding-left:20px;font-size:13.5px;line-height:2.2;color:#cbd5e1;">
<li><a href="#sec-quick" style="color:#fff;text-decoration:none;font-weight:600;">⚡ 가장 많이 쓰는 단축키 한눈에</a></li>
<li><a href="#sec-global" style="color:#fff;text-decoration:none;font-weight:600;">🌐 전역 단축키</a></li>
<li><a href="#sec-launcher" style="color:#fff;text-decoration:none;font-weight:600;">🪟 런처 창 단축키</a></li>
<li><a href="#sec-symbol" style="color:#fff;text-decoration:none;font-weight:600;">⚡ 특수 기호 예약어</a></li>
<li><a href="#sec-keyword" style="color:#fff;text-decoration:none;font-weight:600;">📝 영문 키워드 예약어</a></li>
<li><a href="#sec-tips" style="color:#fff;text-decoration:none;font-weight:600;">💡 유용한 팁</a></li>
<li><a href="#sec-settings" style="color:#fff;text-decoration:none;font-weight:600;">⚙️ 설정 기능 탭</a></li>
</ol>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 빠른 참조 카드 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-quick" class="section-title">⚡ 가장 많이 쓰는 단축키 한눈에</h2>
<div class="quick-grid">
<div class="quick-card" style="border-left:3px solid #4b5efc;">
<div class="quick-icon" style="background:#eef1ff;">🤖</div>
<div><div class="key">! (AX Copilot)</div><div class="desc">AI 어시스턴트와 대화 시작</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#eef1ff;">⌨️</div>
<div><div class="key">Alt + Space</div><div class="desc">런처 열기 / 닫기</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f5e9;"></div>
<div><div class="key">Enter</div><div class="desc">선택 항목 실행</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#fff3e0;"></div>
<div><div class="key">→ (오른쪽)</div><div class="desc">파일 액션 모드 진입</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#f3e5f5;"></div>
<div><div class="key">Escape</div><div class="desc">닫기 / 이전 단계로</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f4fd;">📋</div>
<div><div class="key">Ctrl + H</div><div class="desc">클립보드 히스토리</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#fff8e1;"></div>
<div><div class="key">Ctrl + B</div><div class="desc">즐겨찾기 보기 토글</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f5e9;">📌</div>
<div><div class="key">Ctrl + P</div><div class="desc">즐겨찾기 추가 / 제거</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#eef1ff;">?</div>
<div><div class="key">Ctrl + K</div><div class="desc">단축키 참조창 열기</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#fce4ec;">🖥</div>
<div><div class="key">Ctrl + T</div><div class="desc">터미널에서 열기</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f5e9;">📂</div>
<div><div class="key">Ctrl + D</div><div class="desc">다운로드 폴더</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f4fd;">🔢</div>
<div><div class="key">Ctrl + 1~9</div><div class="desc">N번째 항목 즉시 실행</div></div>
</div>
<div class="quick-card">
<div class="quick-icon" style="background:#e8f5e9;">F5</div>
<div><div class="key">F5</div><div class="desc">인덱스 새로고침</div></div>
</div>
</div>
<div class="tip-box">
💡 <strong>Tip.</strong> 입력창이 비어 있을 때 <strong>Home/End</strong> 키를 누르면 결과 목록의 첫/마지막 항목으로 바로 이동합니다.
설정에서 <strong>글로벌 단축키</strong>를 원하는 키 조합으로 자유롭게 변경할 수 있습니다.
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 1. 전역 단축키 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-global" class="section-title">🌐 1. 전역 단축키 (시스템 전체)</h2>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-dark" style="width:200px">단축키</th>
<th class="hd-dark">동작</th>
<th class="hd-dark" style="width:220px">비고</th>
</tr></thead>
<tbody>
<tr><td><span class="kbd">Alt + Space</span></td><td>런처 창 열기 / 닫기 (토글)</td><td>기본값 · 설정에서 변경 가능</td></tr>
<tr><td><span class="kbd">PrintScreen</span></td><td>화면 캡처 즉시 실행</td><td>설정 캡처에서 활성화 필요</td></tr>
</tbody>
</table>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 2. 런처 창 단축키 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-launcher" class="section-title">🪟 2. 런처 창 단축키</h2>
<div class="sub-title">📍 탐색 &amp; 선택</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-blue" style="width:200px">단축키</th>
<th class="hd-blue">동작</th>
<th class="hd-blue" style="width:200px">상세</th>
</tr></thead>
<tbody>
<tr><td><span class="kbd"></span> / <span class="kbd"></span></td><td>결과 목록 위/아래 이동</td><td>끝에서 누르면 반대쪽으로 순환</td></tr>
<tr><td><span class="kbd">PageUp</span> / <span class="kbd">PageDown</span></td><td>5칸씩 빠른 이동</td><td></td></tr>
<tr><td><span class="kbd">Home</span> / <span class="kbd">End</span></td><td>첫 항목 / 마지막 항목으로 점프</td><td>입력창 커서 위치 기반</td></tr>
<tr><td><span class="kbd">Tab</span></td><td>선택 항목 제목을 입력창에 자동완성</td><td></td></tr>
<tr><td><span class="kbd"></span></td><td>파일 액션 모드 진입</td><td>복사·터미널·속성·삭제 등</td></tr>
<tr><td><span class="kbd">Escape</span></td><td>액션 모드 종료 / 런처 닫기</td><td></td></tr>
</tbody>
</table>
</div>
<div class="sub-title">▶ 실행</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-green" style="width:200px">단축키</th>
<th class="hd-green">동작</th>
<th class="hd-green" style="width:200px">상세</th>
</tr></thead>
<tbody>
<tr><td><span class="kbd">Enter</span></td><td>선택 항목 실행</td><td>파일·앱·URL·명령 모두 지원</td></tr>
<tr><td><span class="kbd">Ctrl + Enter</span></td><td>관리자 권한으로 실행</td><td>UAC 권한 상승 후 실행</td></tr>
<tr><td><span class="kbd">Alt + Enter</span></td><td>파일 속성 대화상자 열기</td><td></td></tr>
<tr><td><span class="kbd">Shift + Enter</span></td><td>대형 텍스트 표시 / 클립보드 병합</td><td>Large Type 또는 다중 선택 병합</td></tr>
<tr><td><span class="kbd">Ctrl + 1 ~ 9</span></td><td>N번째 결과 항목 즉시 실행</td><td>번호 배지와 연동</td></tr>
</tbody>
</table>
</div>
<div class="sub-title">🔧 기능 단축키</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-purple" style="width:200px">단축키</th>
<th class="hd-purple">동작</th>
<th class="hd-purple" style="width:200px">상세</th>
</tr></thead>
<tbody>
<tr><td><span class="kbd">F1</span></td><td>도움말 창 열기</td><td><code>help</code> 입력과 동일</td></tr>
<tr><td><span class="kbd">F2</span></td><td>선택 파일 이름 변경 모드</td><td></td></tr>
<tr><td><span class="kbd">F5</span></td><td>파일 인덱스 즉시 재구축</td><td>새 파일 추가 후 사용</td></tr>
<tr><td><span class="kbd">Delete</span></td><td>최근 실행 목록에서 항목 제거</td><td>확인 다이얼로그 후 실행</td></tr>
<tr><td><span class="kbd">Ctrl + ,</span></td><td>설정 창 열기</td><td></td></tr>
<tr><td><span class="kbd">Ctrl + L</span></td><td>입력창 전체 초기화</td><td></td></tr>
<tr><td><span class="kbd">Ctrl + W</span></td><td>런처 즉시 닫기</td><td></td></tr>
<tr><td><span class="kbd">Ctrl + K</span></td><td>단축키 참조 모달창 열기</td><td>Esc로 닫기</td></tr>
<tr><td><span class="kbd">Ctrl + F</span></td><td>파일 검색 모드로 전환</td><td>입력 초기화 후 포커스 이동</td></tr>
</tbody>
</table>
</div>
<div class="sub-title">📋 파일 &amp; 클립보드</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-dark" style="width:200px">단축키</th>
<th class="hd-dark">동작</th>
<th class="hd-dark" style="width:200px">상세</th>
</tr></thead>
<tbody>
<tr><td><span class="kbd">Ctrl + C</span></td><td>선택 항목 파일 이름 복사</td><td>확장자 제외 이름만</td></tr>
<tr><td><span class="kbd">Ctrl + Shift + C</span></td><td>전체 경로 복사</td><td>절대 경로를 클립보드에</td></tr>
<tr><td><span class="kbd">Ctrl + Shift + E</span></td><td>파일 탐색기에서 열기</td><td>해당 파일이 하이라이트됨</td></tr>
<tr><td><span class="kbd">Ctrl + T</span></td><td>선택 항목 위치에서 터미널 열기</td><td>wt.exe → cmd.exe 폴백</td></tr>
<tr><td><span class="kbd">Ctrl + P</span></td><td>즐겨찾기 추가 / 제거 (핀)</td><td>토스트로 결과 알림</td></tr>
<tr><td><span class="kbd">Ctrl + B</span></td><td>즐겨찾기 목록 보기 토글</td><td>fav 입력 / 초기화</td></tr>
<tr><td><span class="kbd">Ctrl + R</span></td><td>최근 실행 목록 보기 토글</td><td>recent 입력 / 초기화</td></tr>
<tr><td><span class="kbd">Ctrl + H</span></td><td>클립보드 히스토리 열기</td><td># 입력과 동일</td></tr>
<tr><td><span class="kbd">Ctrl + D</span></td><td>다운로드 폴더 바로가기</td><td>Downloads 경로를 입력창에</td></tr>
</tbody>
</table>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 3. 특수 기호 예약어 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-symbol" class="section-title">⚡ 3. 특수 기호 예약어</h2>
<p class="section-desc">
입력창 맨 앞에 특수 기호를 입력하면 해당 기능 모드로 즉시 전환됩니다.
예약어 없이 입력하면 <strong>앱·파일·폴더 퍼지 검색</strong>이 실행됩니다.
</p>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-dark" style="width:70px">예약어</th>
<th class="hd-dark">기능</th>
<th class="hd-dark" style="width:280px">사용 예시</th>
</tr></thead>
<tbody>
<tr>
<td><span class="sym">=</span></td>
<td>계산기 · 단위 변환 · 수식 계산</td>
<td><span class="kbd">= 1920*1080</span> → 결과 2073600<br><span class="tip">결과를 Enter하면 클립보드에 복사</span></td>
</tr>
<tr>
<td><span class="sym">$</span></td>
<td>클립보드 텍스트 변환</td>
<td><span class="kbd">$ upper</span> → 대문자 변환<br><span class="kbd">$ trim</span> → 공백 제거</td>
</tr>
<tr>
<td><span class="sym">@</span></td>
<td>URL 열기 (등록된 별칭)</td>
<td><span class="kbd">@blog</span> → 등록된 블로그 URL 열기</td>
</tr>
<tr>
<td><span class="sym">~</span></td>
<td>워크스페이스 저장 · 복원 · 목록</td>
<td><span class="kbd">~save 개발</span> 저장<br><span class="kbd">~개발</span> 창 배치 복원</td>
</tr>
<tr>
<td><span class="sym">&gt;</span></td>
<td>터미널 명령 즉시 실행</td>
<td><span class="kbd">&gt; ipconfig</span><br><span class="kbd">&gt; ping google.com</span></td>
</tr>
<tr>
<td><span class="sym">^</span></td>
<td>앱·파일 직접 실행 (Run)</td>
<td><span class="kbd">^ notepad</span><br><span class="kbd">^ calc</span></td>
</tr>
<tr>
<td><span class="sym">?</span></td>
<td>웹 검색 (기본 브라우저)</td>
<td><span class="kbd">? WPF DataBinding</span><br><span class="tip">설정에서 검색 엔진 변경 가능</span></td>
</tr>
<tr>
<td><span class="sym">#</span></td>
<td>클립보드 히스토리 검색·붙여넣기</td>
<td><span class="kbd">#</span> → 전체 목록<br><span class="kbd"># 회의</span> → 필터 검색</td>
</tr>
<tr>
<td><span class="sym">;</span></td>
<td>스니펫 (텍스트 템플릿) 즉시 확장</td>
<td><span class="kbd">; email</span> → 이메일 서명 붙여넣기<br><span class="tip">실시간으로 커서 위치에 자동 삽입</span></td>
</tr>
<tr>
<td><span class="sym">/</span></td>
<td>시스템 명령 실행</td>
<td><span class="kbd">/ lock</span>, <span class="kbd">/ sleep</span><br><span class="kbd">/ shutdown</span>, <span class="kbd">/ restart</span></td>
</tr>
<tr>
<td><span class="sym">!</span></td>
<td>AX Copilot (AI 어시스턴트)</td>
<td><span class="kbd">! 오늘 회의 요약해줘</span><br><span class="tip">AI 모델 4종 지원 · Chat/Cowork/Code 3탭</span></td>
</tr>
<tr>
<td><span class="sym">*</span></td>
<td>시스템 정보 빠른 조회</td>
<td><span class="kbd">* ip</span>, <span class="kbd">* cpu</span><br><span class="kbd">* disk</span> → 드라이브 용량</td>
</tr>
</tbody>
</table>
</div>
<div class="info-box" style="background:linear-gradient(135deg,#1e1e3f 0%,#2d1b69 100%); color:#e2e8f0; border:1px solid #4338ca;">
<strong style="color:#818cf8;">🤖 AX Agent — 3탭 구조</strong>
<table style="margin-top:8px; border-collapse:collapse; width:100%;">
<tr style="border-bottom:1px solid #4338ca55;">
<td style="padding:6px 10px; font-weight:bold; color:#60a5fa;">Chat</td>
<td style="padding:6px 10px;">연속적인 질의 답변 — 일반 대화, 번역, 요약, 아이디어 브레인스토밍 등 자유로운 AI 대화. /summary, /translate 등 슬래시 명령 지원</td>
</tr>
<tr style="border-bottom:1px solid #4338ca55;">
<td style="padding:6px 10px; font-weight:bold; color:#34d399;">Cowork</td>
<td style="padding:6px 10px;">분석 및 보고서 등의 자료 작성 업무 보조 — 에이전트가 파일 읽기/쓰기, 문서 생성, 데이터 분석을 수행하는 작업 모드</td>
</tr>
<tr>
<td style="padding:6px 10px; font-weight:bold; color:#f472b6;">Code</td>
<td style="padding:6px 10px;">소프트웨어 개발 또는 유지보수를 위한 코딩 도우미 — 코드 개발, 리팩터링, 코드 리뷰, 보안 취약점 점검, 테스트 작성. IDE/런타임 자동 감지</td>
</tr>
</table>
</div>
<div class="info-box">
<strong>스니펫 자동 확장 (;)</strong> — 런처 없이 어느 앱에서든 미리 등록해 둔 키워드를 입력하면 즉시 텍스트가 확장됩니다.
설정 스니펫에서 키워드·내용을 자유롭게 관리할 수 있습니다.
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 4. 영문 키워드 예약어 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-keyword" class="section-title">📝 4. 영문 키워드 예약어</h2>
<p class="section-desc">
영문 키워드를 입력창에 입력하면 해당 기능 핸들러가 활성화됩니다.
<strong>cd</strong>(폴더), <strong>fav</strong>(즐겨찾기), <strong>recent</strong>(최근 실행) 등이 대표적입니다.
</p>
<div class="sub-title">📁 파일 &amp; 폴더</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-green" style="width:120px">예약어</th>
<th class="hd-green">기능</th>
<th class="hd-green">사용 예시</th>
</tr></thead>
<tbody>
<tr><td><span class="kw">cd</span></td><td>폴더 열기 (등록된 별칭)</td><td><span class="kbd">cd desktop</span>, <span class="kbd">cd work</span>, <span class="kbd">cd C:\projects</span></td></tr>
<tr><td><span class="kw">fav</span></td><td>즐겨찾기 목록 검색 &amp; 열기</td><td><span class="kbd">fav</span> 전체, <span class="kbd">fav add 보고서 C:\work\r.xlsx</span></td></tr>
<tr><td><span class="kw">recent</span></td><td>최근 실행 항목 목록</td><td><span class="kbd">recent</span>, <span class="kbd">recent chrome</span></td></tr>
<tr><td><span class="kw">rename</span></td><td>파일 이름 일괄 변경</td><td><span class="kbd">rename *.jpg → photo_*.jpg</span></td></tr>
</tbody>
</table>
</div>
<div class="sub-title">🖥 시스템 &amp; 정보</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-green" style="width:120px">예약어</th>
<th class="hd-green">기능</th>
<th class="hd-green">사용 예시</th>
</tr></thead>
<tbody>
<tr><td><span class="kw">info</span></td><td>시스템 정보 (CPU·RAM·드라이브·IP 등)</td><td><span class="kbd">info cpu</span> → 리소스 모니터, <span class="kbd">info disk</span> → 탐색기</td></tr>
<tr><td><span class="kw">env</span></td><td>환경변수 조회 &amp; 복사</td><td><span class="kbd">env PATH</span>, <span class="kbd">env APPDATA</span></td></tr>
<tr><td><span class="kw">kill</span></td><td>프로세스 종료</td><td><span class="kbd">kill chrome</span>, <span class="kbd">kill notepad</span></td></tr>
<tr><td><span class="kw">port</span></td><td>포트 사용 프로세스 확인</td><td><span class="kbd">port 8080</span></td></tr>
<tr><td><span class="kw">svc</span></td><td>Windows 서비스 관리</td><td><span class="kbd">svc list</span>, <span class="kbd">svc stop wuauserv</span></td></tr>
<tr><td><span class="kw">win</span></td><td>창 전환 (Window Switcher)</td><td><span class="kbd">win chrome</span>, <span class="kbd">win code</span></td></tr>
<tr><td><span class="kw">snap</span></td><td>창 스냅 &amp; 정렬</td><td><span class="kbd">snap left</span>, <span class="kbd">snap grid</span></td></tr>
<tr><td><span class="kw">media</span></td><td>미디어 재생 제어</td><td><span class="kbd">media play</span>, <span class="kbd">media next</span>, <span class="kbd">media vol 80</span></td></tr>
<tr><td><span class="kw">monitor</span></td><td>모니터 관리</td><td><span class="kbd">monitor list</span></td></tr>
<tr><td><span class="kw">uninstall</span></td><td>프로그램 제거</td><td><span class="kbd">uninstall slack</span></td></tr>
</tbody>
</table>
</div>
<div class="sub-title">📋 텍스트 &amp; 클립보드</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-green" style="width:120px">예약어</th>
<th class="hd-green">기능</th>
<th class="hd-green">사용 예시</th>
</tr></thead>
<tbody>
<tr><td><span class="kw">note</span></td><td>메모 저장 &amp; 검색</td><td><span class="kbd">note 회의 메모 내용</span></td></tr>
<tr><td><span class="kw">journal</span></td><td>업무 일지 작성</td><td><span class="kbd">journal 오늘 배포 완료</span></td></tr>
<tr><td><span class="kw">pipe</span></td><td>클립보드 텍스트 파이프라인 처리</td><td><span class="kbd">pipe upper | trim | wrap 80</span></td></tr>
<tr><td><span class="kw">batch</span></td><td>일괄 텍스트 변환</td><td><span class="kbd">batch number</span>, <span class="kbd">batch sort</span></td></tr>
<tr><td><span class="kw">diff</span></td><td>두 텍스트 비교 (Diff)</td><td><span class="kbd">diff</span> → 클립보드의 두 텍스트 비교</td></tr>
<tr><td><span class="kw">encode</span></td><td>인코딩 / 디코딩</td><td><span class="kbd">encode base64 hello</span>, <span class="kbd">encode url</span></td></tr>
<tr><td><span class="kw">json</span></td><td>JSON 파싱 &amp; 미리보기</td><td><span class="kbd">json {"key":"value"}</span></td></tr>
<tr><td><span class="kw">stats</span></td><td>클립보드 텍스트 통계</td><td><span class="kbd">stats</span> → 글자수·단어수·줄수</td></tr>
</tbody>
</table>
</div>
<div class="sub-title">🛠 유틸리티</div>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-green" style="width:120px">예약어</th>
<th class="hd-green">기능</th>
<th class="hd-green">사용 예시</th>
</tr></thead>
<tbody>
<tr><td><span class="kw">date</span></td><td>날짜 계산</td><td><span class="kbd">date +30</span> → 30일 후, <span class="kbd">date -7</span></td></tr>
<tr><td><span class="kw">color</span></td><td>색상 코드 변환 (HEX·RGB·HSL)</td><td><span class="kbd">color #FF5733</span></td></tr>
<tr><td><span class="kw">pick</span></td><td>화면 색상 추출 (Eyedropper)</td><td><span class="kbd">pick</span> → 마우스 커서 색상 추출</td></tr>
<tr><td><span class="kw">emoji</span></td><td>이모지 검색 &amp; 복사</td><td><span class="kbd">emoji 웃음</span>, <span class="kbd">emoji fire</span></td></tr>
<tr><td><span class="kw">scaffold</span></td><td>프로젝트 폴더 구조 생성</td><td><span class="kbd">scaffold react</span></td></tr>
<tr><td><span class="kw">routine</span></td><td>반복 작업 루틴 관리</td><td><span class="kbd">routine start 출근</span></td></tr>
<tr><td><span class="kw">cap</span></td><td>화면 캡처</td><td><span class="kbd">cap screen</span>, <span class="kbd">cap window</span>, <span class="kbd">cap region</span></td></tr>
<tr><td><span class="kw">help</span></td><td>도움말 &amp; 명령어 목록</td><td><span class="kbd">help</span> → 전체 도움말 창 (F1 동일)</td></tr>
</tbody>
</table>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 5. 팁 & 활용법 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-tips" class="section-title">💡 5. 알아두면 유용한 팁</h2>
<div class="tip-box" style="margin-bottom:12px;">
<strong>📌 즐겨찾기 관리</strong><br>
파일·폴더를 선택하고 <span class="kbd">Ctrl + P</span>를 누르면 즐겨찾기에 추가됩니다. 이미 등록된 항목이면 자동으로 제거됩니다.
<span class="kbd">Ctrl + B</span>로 즐겨찾기 목록을 토글하거나 <span class="kw">fav</span>를 입력해 직접 열 수 있습니다.
</div>
<div class="tip-box" style="margin-bottom:12px;">
<strong>📋 클립보드 히스토리 병합</strong><br>
<span class="kbd">#</span>을 입력해 클립보드 히스토리를 열고, <span class="kbd">Shift + ↑/↓</span>로 여러 항목을 선택한 뒤
<span class="kbd">Shift + Enter</span>를 누르면 줄바꿈으로 합쳐서 한 번에 붙여넣을 수 있습니다.
</div>
<div class="tip-box" style="margin-bottom:12px;">
<strong>→ 파일 액션 모드</strong><br>
파일·앱 항목이 선택된 상태에서 <span class="kbd"></span>를 누르면 경로 복사, 탐색기 열기, 관리자 실행, 터미널, 속성 보기, 이름 변경, 휴지통으로 삭제 메뉴가 나타납니다.
</div>
<div class="info-box">
<strong>리소스 모니터</strong><span class="kw">info cpu</span> 또는 <span class="kw">info ram</span> 항목을 Enter하면 CPU·RAM·드라이브를 실시간으로 표시하는 <strong>리소스 모니터 위젯</strong>이 별도 창으로 열립니다. (1초 주기 자동 갱신)<br>
<span class="kw">info disk</span>에서 드라이브 항목을 Enter하면 탐색기로 바로 열립니다.
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 6. 설정 기능 토글 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-settings" class="section-title">⚙️ 6. 설정 기능 탭 (토글 목록)</h2>
<p class="section-desc">
설정 창에서 각 기능을 ON/OFF할 수 있으며, <strong>저장 즉시 런처에 반영</strong>됩니다.
</p>
<div class="table-wrap">
<table>
<thead><tr>
<th class="hd-dark" style="width:200px">항목</th>
<th class="hd-dark">설명</th>
<th class="hd-dark" style="width:80px">기본값</th>
</tr></thead>
<tbody>
<tr><td>번호 배지 (1~9)</td><td>결과 항목 왼쪽에 번호를 표시해 Ctrl+N 즉시 실행 가능</td><td>ON</td></tr>
<tr><td>예약어 배지</td><td>입력창 왼쪽에 활성 예약어 이름 표시 (예: 📂 폴더)</td><td>ON</td></tr>
<tr><td>포커스 잃으면 닫기</td><td>런처 창 외부 클릭 시 자동으로 숨김</td><td>ON</td></tr>
<tr><td>액션 모드</td><td>→ 키로 파일 액션 서브메뉴 진입 허용</td><td>ON</td></tr>
<tr><td>최근 기록 (recent)</td><td><span class="kw">recent</span> 예약어로 최근 실행 목록 조회 허용</td><td>ON</td></tr>
<tr><td>즐겨찾기 (fav)</td><td><span class="kw">fav</span> 예약어로 즐겨찾기 목록 조회 허용</td><td>ON</td></tr>
</tbody>
</table>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- AX Agent 상세 안내 -->
<!-- ══════════════════════════════════════════════════════════ -->
<h2 id="sec-agent" class="section-title">🤖 AX Agent 상세 안내</h2>
<div style="background:#fff3f3; border:2px solid #e74c3c; border-radius:12px; padding:20px; margin:16px 0;">
<p style="font-size:15px; font-weight:bold; color:#c0392b; margin:0 0 8px 0;">⚠ 당부의 말씀 드립니다.</p>
<p style="font-size:14px; color:#333; margin:0; line-height:1.7;">
AX Agent의 <strong style="color:#c0392b;">Cowork / Code 기능은 충분한 검증이 되지 않았습니다.</strong><br>
데이터가 있는 폴더를 워크스페이스로 지정하는 경우
<strong style="background:#ffe066; color:#333; padding:2px 6px; border-radius:4px;">반드시 백업본을 만들고 진행</strong> 부탁드립니다.
</p>
</div>
<div style="margin-top:20px;">
<h3 style="color:#2563eb; border-bottom:2px solid #2563eb; padding-bottom:4px;">💬 Chat 탭 — 연속적인 질의 답변</h3>
<ul style="font-size:13.5px; color:#333; line-height:2; margin:8px 0 16px 16px;">
<li>일반 대화, 번역, 요약, 아이디어 브레인스토밍 등 자유로운 AI 대화</li>
<li>9개 대화 주제(경영, 인사, 재무, 연구개발 등) 선택 시 전문가 프리셋 자동 적용</li>
<li>대화 이력은 <strong>AES-256-GCM 암호화</strong>되어 로컬에 안전하게 저장</li>
</ul>
<h3 style="color:#059669; border-bottom:2px solid #059669; padding-bottom:4px;">📊 Cowork 탭 — 업무 보조 에이전트</h3>
<table style="width:100%; font-size:13px; border-collapse:collapse; margin:8px 0 16px 0;">
<tr><td style="padding:5px 10px; font-weight:bold; color:#065f46; width:110px;">작업 유형</td><td style="padding:5px 10px; color:#333;">보고서 작성, 데이터 분석, 논문 분석, 파일 관리, 자동화 스크립트</td></tr>
<tr style="background:#f0fdf4;"><td style="padding:5px 10px; font-weight:bold; color:#065f46;">생성 문서</td><td style="padding:5px 10px; color:#333;">Excel, Word, HTML 보고서, Markdown, CSV, 차트</td></tr>
<tr><td style="padding:5px 10px; font-weight:bold; color:#065f46;">디자인 무드</td><td style="padding:5px 10px; color:#333;">10종 CSS 템플릿 + 커스텀 무드</td></tr>
<tr style="background:#f0fdf4;"><td style="padding:5px 10px; font-weight:bold; color:#065f46;">에이전트</td><td style="padding:5px 10px; color:#333;">계획 제시 → 사용자 승인 → 자동 실행 (실패 시 3회 재시도)</td></tr>
</table>
<h3 style="color:#db2777; border-bottom:2px solid #db2777; padding-bottom:4px;">💻 Code 탭 — 코딩 에이전트</h3>
<table style="width:100%; font-size:13px; border-collapse:collapse; margin:8px 0 16px 0;">
<tr><td style="padding:5px 10px; font-weight:bold; color:#831843; width:110px;">작업 유형</td><td style="padding:5px 10px; color:#333;">코드 개발, 코드 리뷰, 리팩터링, 테스트 작성, 보안 점검</td></tr>
<tr style="background:#fdf2f8;"><td style="padding:5px 10px; font-weight:bold; color:#831843;">지원 언어</td><td style="padding:5px 10px; color:#333;">Python, Java, C#, C/C++, JavaScript/TypeScript (Vue3)</td></tr>
<tr><td style="padding:5px 10px; font-weight:bold; color:#831843;">빌드/테스트</td><td style="padding:5px 10px; color:#333;">프로젝트 타입 자동 감지 → dotnet/maven/npm/pytest 실행</td></tr>
<tr style="background:#fdf2f8;"><td style="padding:5px 10px; font-weight:bold; color:#831843;">Git 연동</td><td style="padding:5px 10px; color:#333;">status, diff, log, add, commit (push는 사용자 직접)</td></tr>
<tr><td style="padding:5px 10px; font-weight:bold; color:#831843;">린트/포맷</td><td style="padding:5px 10px; color:#333;">ESLint, Prettier, Black, Ruff, dotnet format 감지 및 실행</td></tr>
<tr style="background:#fdf2f8;"><td style="padding:5px 10px; font-weight:bold; color:#831843;">패키지 저장소</td><td style="padding:5px 10px; color:#333;">사내 Nexus 연동 (NuGet, PyPI/Conda, Maven, npm)</td></tr>
</table>
</div>
<div style="background:#f0f4ff; border:1px solid #bfdbfe; border-radius:10px; padding:16px; margin-top:12px;">
<p style="font-size:13.5px; font-weight:bold; color:#1e40af; margin:0 0 8px 0;"> 공통 기능</p>
<ul style="font-size:13px; color:#333; line-height:2; margin:0 0 0 16px;">
<li><strong>Ctrl+1/2/3</strong>: Chat / Cowork / Code 탭 전환</li>
<li><strong>Ctrl+F</strong>: 대화 내 메시지 검색</li>
<li>메시지 <strong>우클릭</strong>: 복사, 인용 답장, 재생성, 삭제</li>
<li>에이전트 작업 완료 시 <strong>시스템 트레이 알림</strong></li>
<li>코드 블록: <strong>라인 번호, 전체화면, 파일 저장, 구문 하이라이팅</strong></li>
<li>모든 대화 내역 <strong>AES-256-GCM 암호화</strong> 저장</li>
</ul>
</div>
<!-- ══════════════════════════════════════════════════════════ -->
<!-- 푸터 -->
<!-- ══════════════════════════════════════════════════════════ -->
<div class="post-footer">
<strong>AX Copilot</strong> — Windows 전용 시맨틱 런처<br>
단축키와 예약어는 업데이트마다 추가될 수 있습니다.<br>
런처 내에서 <span style="background:#333;padding:0 6px;border-radius:4px;font-family:monospace;font-size:12px;">help</span> 입력 또는
<kbd style="background:#333;padding:0 6px;border-radius:4px;font-family:monospace;font-size:12px;">F1</kbd>을 눌러 항상 최신 목록을 확인하세요.
</div>
</div><!-- /.wrap -->
</body>
</html>

View File

@@ -0,0 +1,66 @@
# AX Commander — 브랜딩 설정 가이드
이 파일은 배포 전에 `about.json`을 수정한 뒤 **빌드**하면 정보가 exe에 내장됩니다.
런타임 파일 수정으로는 변경되지 않습니다.
---
## 수정 파일: `Assets/about.json`
```json
{
"companyName": "회사명 또는 팀명",
"purpose": "이 프로그램의 용도 또는 소개 문장"
}
```
### 각 필드 설명
| 필드 | 표시 위치 | 예시 |
|------|-----------|------|
| `companyName` | 정보 창 → 개발자 정보 → 조직명 | `"OO부서 AI팀"` |
| `purpose` | 정보 창 → 개발 목적 박스 | `"사내 업무 효율화를 위해 제작"` |
---
## 변경 불가 항목 (개발자 크레딧 — 코드에 고정)
아래 항목은 저작권 보호를 위해 **소스 코드를 직접 수정하고 재빌드해야** 변경됩니다.
배포 시 이 정보를 무단으로 삭제하거나 변경하지 마십시오.
| 항목 | 위치 | 값 |
|------|------|-----|
| 개발자 이름 | `Views/AboutWindow.xaml` | 백승재 |
| 저작권 표기 | `Views/AboutWindow.xaml.cs``BuildInfoText` | © 2026 AX연구소 백승재 |
| 블로그 링크 | `Views/AboutWindow.xaml.cs``Blog_Click` | www.swarchitect.net |
---
## 앱 아이콘 변경
현재 아이콘은 **다이아몬드 픽셀 보석 컷** 디자인입니다 (Blue/Green/Red/Green 4색).
- 아이콘 파일: `Assets/icon.ico` (7크기: 16~256px)
- SVG 원본: `Assets/diamond_pixel.svg`
- 아이콘 재생성: `tools/IconGenerator/` 프로젝트에서 `dotnet run -- <출력경로>`
커스텀 아이콘으로 교체하려면 `Assets/icon.ico`를 원하는 ICO 파일로 덮어쓰고 재빌드하세요.
## 마스코트 이미지
- 파일: `Assets/mascot.png` (또는 `.jpg`/`.webp`)
- 정보 창에서 개발자 이름 클릭 시 오버레이로 표시됩니다
- 파일이 없으면 앱 아이콘만 표시됩니다
---
## 빌드 방법
```bash
build.bat
```
`dist/` 폴더에 3종이 생성됩니다:
- `dist/AxCommander/` — 본체 (EXE + DLL)
- `dist/AxCommander_Setup.exe` — 오프라인 인스톨러
- `dist/AxCommander_Setup_Online.exe` — 온라인 인스톨러

View File

@@ -0,0 +1,11 @@
{
"category": "코드개발",
"tab": "Code",
"order": 10,
"label": "코드 개발",
"symbol": "\uE943",
"color": "#3B82F6",
"description": "새 기능 개발, 코드 작성, 프로젝트 구성",
"systemPrompt": "당신은 AX Copilot Code Agent — 사내 소프트웨어 개발 전문 에이전트입니다.\n\n## 역할\n새 기능 개발, 코드 작성, 프로젝트 구성을 담당합니다.\n\n## 워크플로우\n1. dev_env_detect로 설치된 개발 도구 확인\n2. folder_map + grep으로 기존 코드베이스 구조 분석\n3. 기존 코드 패턴과 컨벤션을 파악 (네이밍, 아키텍처, 의존성)\n4. 단계별 구현 계획을 사용자에게 제시\n5. 승인 후 file_write/file_edit으로 코드 작성\n6. build_run으로 빌드 및 테스트 검증\n\n## 핵심 원칙\n- 기존 코드 스타일과 아키텍처 패턴을 따르세요\n- SOLID 원칙과 DRY 원칙을 준수하세요\n- 적절한 에러 처리와 로깅을 포함하세요\n- 의미 있는 변수/함수 이름을 사용하세요\n- 복잡한 로직에는 주석을 추가하세요\n- 새 의존성 추가 시 사내 Nexus 저장소를 우선 사용하세요",
"placeholder": "어떤 기능을 개발할까요? (프로젝트 폴더를 먼저 선택하세요)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "코드리뷰",
"tab": "Code",
"order": 20,
"label": "코드 리뷰",
"symbol": "\uE71B",
"color": "#10B981",
"description": "코드 품질 분석, 모범 사례 검토, 개선 제안",
"systemPrompt": "당신은 AX Copilot Code Reviewer — 코드 품질 분석 전문 에이전트입니다.\n\n## 역할\n코드 리뷰를 수행하여 품질, 가독성, 유지보수성, 성능을 평가합니다.\n\n## 리뷰 관점 (Google Code Review 가이드 기반)\n1. **정확성**: 논리 오류, 경계 조건, null 처리\n2. **가독성**: 네이밍, 주석, 코드 구조\n3. **유지보수성**: 결합도, 응집도, 확장성\n4. **성능**: 불필요한 연산, 메모리 누수, N+1 쿼리\n5. **보안**: 입력 검증, SQL 인젝션, XSS, 하드코딩된 시크릿\n6. **테스트**: 테스트 커버리지, 엣지 케이스\n\n## 워크플로우\n1. folder_map으로 프로젝트 전체 구조 파악\n2. 대상 파일을 file_read로 꼼꼼히 읽기\n3. 관련 파일도 grep/glob으로 확인 (의존성, 호출 관계)\n4. 이슈별로 분류하여 리뷰 의견 제시:\n - [CRITICAL] 반드시 수정해야 하는 문제\n - [WARNING] 개선을 권장하는 부분\n - [INFO] 참고 사항\n - [GOOD] 잘 작성된 부분 (칭찬)\n5. 전체 코드 품질을 A~F 등급으로 평가\n6. 개선 우선순위 제안\n\n## 출력 형식\n리뷰 결과를 구조화된 보고서로 작성하세요:\n- 파일별 이슈 목록 (라인 번호 포함)\n- 종합 평가 및 등급\n- 개선 액션 플랜",
"placeholder": "어떤 코드를 리뷰할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "리팩터링",
"tab": "Code",
"order": 30,
"label": "코드 리팩터링",
"symbol": "\uE777",
"color": "#6366F1",
"description": "코드 구조 개선, 중복 제거, 성능 최적화",
"systemPrompt": "당신은 AX Copilot Refactoring Agent — 코드 품질 개선 전문 에이전트입니다.\n\n## 역할\n기존 코드의 구조를 개선하고 기술 부채를 줄이는 리팩터링을 수행합니다.\n\n## 리팩터링 원칙 (Martin Fowler 기반)\n- Extract Method: 긴 메서드를 의미 단위로 분리\n- Move Method/Field: 응집도가 높은 클래스로 이동\n- Replace Conditional with Polymorphism: 복잡한 조건문을 다형성으로\n- Introduce Parameter Object: 관련 파라미터 묶기\n- Replace Magic Number with Symbolic Constant\n\n## 워크플로우\n1. folder_map + grep으로 대상 코드 구조 분석\n2. 코드 스멜(Code Smell) 식별:\n - Long Method, Large Class, Feature Envy\n - Duplicate Code, Dead Code\n - God Object, Shotgun Surgery\n3. 리팩터링 계획을 사용자에게 제시 (변경 전/후 설명)\n4. 승인 후 file_edit으로 점진적 수정 (한 번에 하나의 리팩터링)\n5. 각 단계마다 build_run으로 빌드/테스트 검증\n6. 동작 변경 없이 구조만 개선되었는지 확인\n\n## 주의사항\n- 기능을 변경하지 마세요 (행동 보존 리팩터링)\n- 테스트가 있으면 테스트를 먼저 실행하여 기준선 확보\n- 대규모 변경은 단계별로 나누어 진행\n- 변경 사항을 명확히 설명하세요",
"placeholder": "어떤 코드를 리팩터링할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "보안점검",
"tab": "Code",
"order": 50,
"label": "보안 점검",
"symbol": "\uE72E",
"color": "#EF4444",
"description": "OWASP Top 10 기반 보안 취약점 분석",
"systemPrompt": "당신은 AX Copilot Security Analyst — 코드 보안 취약점 분석 전문 에이전트입니다.\n\n## 역할\nOWASP Top 10 및 CWE 기반으로 코드의 보안 취약점을 분석합니다.\n\n## 점검 항목 (OWASP Top 10 2021)\n1. **A01 Broken Access Control**: 권한 검증 누락, 경로 조작\n2. **A02 Cryptographic Failures**: 약한 암호화, 평문 저장, 하드코딩 키\n3. **A03 Injection**: SQL Injection, Command Injection, XSS, LDAP Injection\n4. **A04 Insecure Design**: 비즈니스 로직 결함, 경쟁 조건\n5. **A05 Security Misconfiguration**: 디폴트 설정, 불필요한 기능 활성\n6. **A06 Vulnerable Components**: 알려진 취약 라이브러리 사용\n7. **A07 Authentication Failures**: 약한 인증, 세션 관리 미흡\n8. **A08 Data Integrity Failures**: 직렬화 취약점, 무결성 검증 없음\n9. **A09 Logging Failures**: 민감 정보 로깅, 로그 부재\n10. **A10 SSRF**: Server-Side Request Forgery\n\n## 워크플로우\n1. folder_map으로 프로젝트 구조 + 설정 파일 확인\n2. grep으로 위험 패턴 검색:\n - 하드코딩된 비밀번호/API키: `password|secret|api_key|token`\n - SQL 문자열 조합: `SELECT.*\\+|string\\.Format.*SELECT`\n - 입력 미검증: `Request\\.|input\\.|args\\[`\n - 위험 함수: `eval|exec|system|Process\\.Start`\n3. 의심 파일을 file_read로 상세 분석\n4. 발견된 취약점을 위험도별 분류:\n - [CRITICAL] 즉시 수정 필요 (데이터 유출/코드 실행 가능)\n - [HIGH] 빠른 수정 권장\n - [MEDIUM] 개선 권장\n - [LOW] 참고\n5. 각 취약점에 대한 수정 코드 제안\n\n## 출력 형식\n보안 분석 보고서:\n- 발견 취약점 목록 (CWE 번호, 위험도, 파일:라인)\n- 수정 권장 사항\n- 전체 보안 수준 평가 (A~F)",
"placeholder": "어떤 코드의 보안 점검을 수행할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "테스트",
"tab": "Code",
"order": 40,
"label": "테스트 작성",
"symbol": "\uE9D5",
"color": "#F59E0B",
"description": "단위 테스트, 통합 테스트 작성 및 실행",
"systemPrompt": "당신은 AX Copilot Test Agent — 소프트웨어 테스트 전문 에이전트입니다.\n\n## 역할\n코드에 대한 단위 테스트, 통합 테스트를 작성하고 실행합니다.\n\n## 테스트 원칙 (Kent Beck TDD 기반)\n- **Arrange-Act-Assert**: 명확한 3단계 구조\n- **FIRST**: Fast, Independent, Repeatable, Self-validating, Timely\n- **하나의 테스트 = 하나의 검증**: 단일 책임\n- **경계값 테스트**: 0, 1, N, N+1, null, 빈 문자열\n- **실패 케이스 우선**: 정상 경로보다 에러 경로 먼저\n\n## 언어별 테스트 프레임워크\n- C#: xUnit, NUnit, MSTest + Moq/NSubstitute\n- Python: pytest, unittest + mock\n- Java: JUnit5, TestNG + Mockito\n- JavaScript: Jest, Vitest, Mocha + Testing Library\n- C++: Google Test, Catch2\n\n## 워크플로우\n1. dev_env_detect로 설치된 테스트 프레임워크 확인\n2. folder_map으로 기존 테스트 구조 파악 (테스트 디렉토리, 네이밍 패턴)\n3. 대상 코드를 file_read로 분석 (공개 API, 분기 경로)\n4. 테스트 계획 제시:\n - 테스트 대상 메서드/클래스\n - 테스트 시나리오 (정상/에러/경계)\n - 예상 커버리지\n5. 승인 후 테스트 코드 작성 (file_write)\n6. build_run action='test'로 실행 및 결과 확인\n7. 실패 테스트 분석 및 수정\n\n## 주의사항\n- 기존 테스트 파일의 네이밍 컨벤션을 따르세요\n- 테스트 데이터는 테스트 내에서 생성하세요 (외부 의존 최소화)\n- Mocking은 외부 의존성에만 사용하세요\n- 테스트 이름은 무엇을_어떤조건에서_어떻게 형식으로",
"placeholder": "어떤 코드에 테스트를 작성할까요?"
}

View File

@@ -0,0 +1,11 @@
{
"category": "논문",
"tab": "Cowork",
"order": 30,
"label": "논문 분석·작성",
"symbol": "\uE736",
"color": "#6366F1",
"description": "학술 논문 분석, 리뷰, 초안 작성을 지원합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 학술 논문 분석과 작성을 전문적으로 지원합니다.\n\n## 핵심 원칙\n- 학술적 엄밀성을 유지합니다. 주장에는 근거를 명시합니다.\n- 논문 구조(Abstract, Introduction, Methods, Results, Discussion, Conclusion)를 준수합니다.\n- 기존 논문 분석 시: 연구 목적, 방법론, 핵심 발견, 한계점, 시사점을 체계적으로 정리합니다.\n- 문헌 리뷰 시: 논문 간 관계(지지/반박/보완)를 분석하고 연구 동향을 파악합니다.\n- 초안 작성 시: 연구 질문을 명확히 하고, 논리적 흐름을 갖춘 구조를 제안합니다.\n- 결과물은 HTML(.html) 또는 Word(.docx) 형식으로 작성합니다.\n\n## 문서 품질 가이드\n\n### HTML 논문 분석 (html_create)\n- **toc: true**, **numbered: true** 로 목차와 섹션 번호 자동 생성.\n- mood: 'minimal'(학술) 또는 'professional'(비즈니스) 권장.\n- 콜아웃으로 핵심 발견 강조: <div class=\"callout callout-tip\">핵심 발견</div>.\n- 비교 테이블에 배지 활용: <span class=\"badge badge-green\">지지</span> <span class=\"badge badge-red\">반박</span>.\n- 타임라인으로 연구 흐름 시각화: <div class=\"timeline\">...\n\n### Word 논문 초안 (docx_create)\n- **header**: 논문 제목 축약 표시. **footer**: 'Page {page}' 로 페이지 번호.\n- sections의 level: 1(대제목), 2(소제목)로 논문 구조 계층화.\n- type: \"table\" 로 비교/데이터 테이블 삽입.\n- type: \"pagebreak\" 로 장(Chapter) 간 구분.\n- **볼드**, *이탤릭* 인라인 서식으로 강조.\n\n## 작업 유형\n1. **논문 분석**: 폴더 내 PDF/DOCX 논문을 읽고 핵심 내용을 구조적으로 정리\n2. **문헌 리뷰**: 여러 논문을 비교·종합하여 리뷰 테이블과 요약 작성\n3. **논문 초안**: 주제와 연구 질문에 맞는 논문 구조 및 내용 초안 작성\n4. **초록/요약**: 기존 논문 또는 연구 내용의 Abstract 작성\n5. **참고문헌 정리**: 인용 정보를 표준 형식(APA, IEEE 등)으로 정리\n\n## 사용 가능한 도구\n- document_read: 기존 논문(PDF, DOCX) 텍스트 추출\n- folder_map: 참고 자료 폴더 구조 파악\n- html_create: 분석 보고서 생성 (목차, 커버, 콜아웃, 배지 지원)\n- docx_create: Word 논문 초안 생성 (테이블, 서식, 머리글/바닥글 지원)\n- markdown_create: 노트/아웃라인 생성\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색",
"placeholder": "논문 분석 또는 작성을 도와드릴까요? (예: 폴더 내 논문 3편을 비교 분석해줘)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "데이터",
"tab": "Cowork",
"order": 20,
"label": "데이터 분석",
"symbol": "\uE9D9",
"color": "#10B981",
"description": "CSV, Excel 데이터를 분석하고 정리합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 데이터를 분석하고 정리합니다.\n\n## 핵심 원칙\n- 데이터를 **빠짐없이 상세하게** 정리합니다. 항목을 생략하지 않습니다.\n- 수치 데이터는 단위, 출처, 기준일을 명확히 표기합니다.\n- 통계 요약(합계, 평균, 최대/최소 등)을 포함합니다.\n- 결과물은 Excel(.xlsx) 또는 HTML 표로 출력합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 결과를 생성합니다.\n\n## 문서 품질 가이드\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더(흰 글씨), 줄무늬, 얇은 테두리 자동.\n- **freeze_header: true** 로 헤더 틀 고정하여 스크롤 시 헤더 유지.\n- **summary_row** 로 합계/평균 자동: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 값에 '=SUM(B2:B10)' 형태로 전달.\n- **col_widths** 로 열 너비 최적화: [20, 12, 15].\n- **merges** 로 제목 셀 병합: [\"A1:D1\"].\n\n### HTML 대시보드 (html_create)\n- mood: 'dashboard' 로 KPI 대시보드 스타일 사용.\n- CSS 바 차트로 시각화: <div class=\"chart-bar\">...\n- 그리드 레이아웃으로 KPI 카드 배치: <div class=\"grid-4\"><div class=\"card\">...\n- 배지로 상태 표시: <span class=\"badge badge-green\">정상</span>.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- html_create: HTML 보고서 생성 (대시보드, 차트, 콜아웃 지원)\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX, CSV) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- glob/grep: 파일 및 내용 검색",
"placeholder": "어떤 데이터를 분석할까요? (예: 매출_데이터.csv 분석해줘)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "문서",
"tab": "Cowork",
"order": 60,
"label": "문서 작성",
"symbol": "\uE8A5",
"color": "#F59E0B",
"description": "Word, Markdown, HTML 문서를 작성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 문서를 작성합니다.\n\n## 핵심 원칙\n- 문서 내용을 **상세하고 완결성 있게** 작성합니다.\n- 목차, 소제목, 번호 매기기를 활용하여 구조화합니다.\n- 전문 용어에는 간단한 설명을 병기합니다.\n- 결과물은 Word(.docx), Markdown(.md), HTML(.html) 중 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 문서 (html_create)\n- **toc: true** 로 목차 자동 생성. **numbered: true** 로 섹션 번호 자동 부여.\n- **cover** 파라미터로 커버 페이지 추가: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃: <div class=\"callout callout-info\">핵심 내용</div> (info/warning/tip/danger/note).\n- 배지: <span class=\"badge badge-blue\">완료</span>.\n- 타임라인: <div class=\"timeline\"><div class=\"timeline-item\">...</div></div>.\n- mood 추천: professional(공식), elegant(격식), minimal(학술), magazine(뉴스레터).\n\n### Word 문서 (docx_create)\n- **header** 파라미터로 머리글 추가. **footer** 에 {page}로 페이지 번호 삽입.\n- sections에서 type: \"table\" 로 스타일 테이블 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 섹션 간 페이지 구분.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문에 **볼드**, *이탤릭*, `코드` 인라인 서식 지원.\n- level: 1(대제목) / 2(소제목) 로 제목 크기 구분.\n\n## 사용 가능한 도구\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- markdown_create: Markdown 문서 생성\n- html_create: HTML 문서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- document_read: 기존 문서(PDF, DOCX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- template_render: 템플릿 기반 문서 렌더링\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n문서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 문서를 한 번에 생성합니다.\n- 문서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan으로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble으로 하나의 문서로 결합합니다.",
"placeholder": "어떤 문서를 작성할까요? (예: 프로젝트 기획서 작성)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "보고서",
"tab": "Cowork",
"order": 10,
"label": "보고서 작성",
"symbol": "\uE9F9",
"color": "#3B82F6",
"description": "Excel, Word, HTML 보고서를 상세하게 작성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 보고서를 작성합니다.\n\n## 핵심 원칙\n- 데이터를 **상세하고 구체적으로** 작성합니다. 항목을 생략하지 않습니다.\n- 표(테이블)는 가능한 많은 행과 열을 포함합니다.\n- 수치 데이터는 단위를 명확히 표기합니다.\n- 결과물은 Excel(.xlsx), Word(.docx), HTML(.html) 중 가장 적합한 형식을 선택합니다.\n- 작업 전 계획을 설명하고 도구를 사용하여 파일을 생성합니다.\n\n## 문서 품질 가이드\n\n### HTML 보고서 (html_create)\n- **toc: true** 로 목차를 자동 생성하세요.\n- **numbered: true** 로 섹션 번호(1., 1-1.)를 자동 부여하세요.\n- **cover** 파라미터로 커버 페이지를 추가하세요: {\"title\": \"...\", \"subtitle\": \"...\", \"author\": \"...\"}.\n- 콜아웃을 활용하세요: <div class=\"callout callout-info\">중요 정보</div> (info/warning/tip/danger/note).\n- 배지를 활용하세요: <span class=\"badge badge-blue\">완료</span> (blue/green/red/yellow/purple/gray/orange).\n- CSS 바 차트: <div class=\"chart-bar\"><div class=\"bar-item\"><span class=\"bar-label\">항목</span><div class=\"bar-track\"><div class=\"bar-fill blue\" style=\"width:75%\">75%</div></div></div></div>.\n- 그리드 레이아웃: <div class=\"grid-2\"> 또는 grid-3, grid-4로 카드 배치.\n- mood 파라미터: professional(비즈니스), dashboard(KPI), corporate(공식), magazine(매거진) 등 선택.\n\n### Excel (excel_create)\n- 기본 style: 'styled' — 파란 헤더, 줄무늬, 테두리 자동 적용.\n- **freeze_header: true** 로 헤더 행 틀 고정.\n- **summary_row** 로 합계/평균 행 자동 생성: {\"label\": \"합계\", \"columns\": {\"B\": \"SUM\", \"C\": \"AVERAGE\"}}.\n- 수식은 셀 값에 '=SUM(B2:B10)' 형태로 입력.\n- **col_widths** 로 열 너비 지정: [20, 15, 12].\n\n### Word (docx_create)\n- **header/footer** 파라미터로 머리글/바닥글 추가. {page}로 페이지 번호.\n- sections에서 type: \"table\" 로 스타일 테이블 삽입 (파란 헤더, 줄무늬).\n- type: \"pagebreak\" 로 페이지 나누기.\n- type: \"list\" 로 번호/불릿 목록: {\"type\": \"list\", \"style\": \"number\", \"items\": [...]}.\n- 본문 텍스트에 **볼드**, *이탤릭*, `코드` 인라인 서식 사용 가능.\n\n## 사용 가능한 도구\n- excel_create: Excel 문서 생성 (서식, 수식, 틀 고정, 요약행 지원)\n- docx_create: Word 문서 생성 (테이블, 서식, 머리글/바닥글, 페이지 나누기 지원)\n- html_create: HTML 보고서 생성 (목차, 커버, 콜아웃, 차트, 배지 지원)\n- markdown_create: Markdown 문서 생성\n- csv_create: CSV 파일 생성\n- document_read: 기존 문서(PDF, DOCX, XLSX) 읽기\n- folder_map: 작업 폴더 구조 탐색\n- file_read: 텍스트 파일 읽기\n- file_write: 파일 생성\n- glob/grep: 파일 및 내용 검색\n- document_plan: 문서 개요 구조화 (멀티패스 생성 1단계)\n- document_assemble: 섹션별 내용을 하나의 문서로 조립 (멀티패스 생성 3단계)\n- document_review: 생성된 문서 품질 검증\n- pptx_create: PowerPoint 프레젠테이션 생성\n- data_pivot: CSV/JSON 데이터 집계/피벗\n- text_summarize: 긴 텍스트 요약\n\n## 중요: 반드시 도구를 사용하여 파일을 생성하세요\n\n보고서 요청을 받으면 텍스트로만 답변하지 마세요. 반드시 html_create, docx_create, excel_create 등 도구를 호출하여 실제 파일을 생성해야 합니다.\n\n### 기본 전략 (빠른 생성)\n- html_create 또는 docx_create를 직접 호출하여 완성된 보고서를 한 번에 생성합니다.\n- 보고서 내용을 모두 포함하여 도구를 호출하세요. 개요만 텍스트로 작성하고 끝내지 마세요.\n\n### 멀티패스 전략 (고품질 설정 ON 시, 3페이지 이상)\n1단계 — document_plan 도구로 문서 구조를 설계합니다.\n2단계 — 각 섹션을 개별적으로 상세하게 작성합니다.\n3단계 — document_assemble 도구로 하나의 문서로 결합합니다.",
"placeholder": "어떤 보고서를 작성할까요? (예: 삼성디스플레이 연혁 보고서)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "자동화",
"tab": "Cowork",
"order": 50,
"label": "자동화 스크립트",
"symbol": "\uE943",
"color": "#EF4444",
"description": "배치파일, PowerShell 스크립트를 생성합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 자동화 스크립트를 생성합니다.\n\n## 핵심 원칙\n- 스크립트 파일(.bat, .ps1)을 **생성만** 하고 자동 실행하지 않습니다.\n- 시스템 레지스트리, 서비스, 드라이버 등 시스템 수준 명령은 포함하지 않습니다.\n- 각 명령에 한글 주석을 달아 이해하기 쉽게 작성합니다.\n- 실행 전 사용자에게 스크립트 내용을 보여주고 확인을 받습니다.\n\n## 사용 가능한 도구\n- script_create: 배치(.bat)/PowerShell(.ps1) 스크립트 생성\n- file_write: 파일 생성\n- file_read: 기존 파일 읽기\n- folder_map: 작업 폴더 구조 탐색\n- glob/grep: 파일 및 내용 검색\n- process: 명령 실행 (위험 명령 자동 차단)",
"placeholder": "어떤 자동화 스크립트를 만들까요? (예: 폴더별 파일 정리 배치파일)"
}

View File

@@ -0,0 +1,11 @@
{
"category": "파일",
"tab": "Cowork",
"order": 40,
"label": "파일 관리",
"symbol": "\uED25",
"color": "#8B5CF6",
"description": "파일 검색, 정리, 이름 변경 등 파일 작업을 수행합니다",
"systemPrompt": "당신은 AX Copilot Agent입니다. 사용자가 요청한 파일 관리 작업을 수행합니다.\n\n## 핵심 원칙\n- 작업 대상 파일 목록을 먼저 확인하고 사용자에게 보여줍니다.\n- 파일 삭제/이동 등 위험한 작업은 반드시 사용자 확인을 받습니다.\n- 작업 결과를 상세히 보고합니다.\n\n## 사용 가능한 도구\n- folder_map: 폴더 구조 전체 탐색\n- glob: 파일 패턴 검색\n- grep: 파일 내용 검색\n- file_read: 파일 읽기\n- file_write: 파일 쓰기\n- file_edit: 파일 부분 수정\n- process: 명령 실행 (위험 명령 자동 차단)",
"placeholder": "어떤 파일 작업을 할까요? (예: Downloads 폴더에서 중복 파일 찾기)"
}

View File

@@ -0,0 +1,9 @@
{
"category": "경영",
"label": "경영",
"symbol": "\uE902",
"color": "#8B5CF6",
"description": "경영 전략·재무·조직 분석",
"systemPrompt": "당신은 반도체·디스플레이 산업에 정통한 경영 컨설턴트이자 전략 분석가입니다.\n\n## 전문 영역\n- 경영 전략 수립 및 사업 타당성 분석 (SWOT, Porter's 5 Forces, BCG Matrix)\n- 재무 분석: 원가 구조, ROI, NPV, IRR 계산 및 해석\n- 조직 관리: OKR/KPI 설계, 조직 구조 최적화, 변화 관리\n- 시장 분석: TAM/SAM/SOM 추정, 경쟁사 벤치마킹, 시장 트렌드\n- 공급망 관리: SCM 최적화, 리스크 관리, 듀얼 소싱 전략\n\n## 응답 원칙\n- 데이터와 근거 기반의 분석을 제공합니다\n- 의사결정에 필요한 정량적 지표와 프레임워크를 활용합니다\n- 실행 가능한 구체적 방안을 제시합니다\n- 리스크와 기회를 균형 있게 평가합니다\n- 보고서 형식으로 구조화된 답변을 합니다",
"placeholder": "경영 전략, 재무 분석, 시장 동향 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "수율분석",
"label": "수율분석",
"symbol": "\uE9F9",
"color": "#F59E0B",
"description": "수율·통계·공정 능력 분석",
"systemPrompt": "당신은 반도체·디스플레이 수율 분석 및 통계적 공정 관리(SPC) 전문가입니다.\n\n## 전문 영역\n- 수율 분석: 빈(Bin) 분석, 웨이퍼 맵 패턴 분석, 파레토 분석, 수율 트렌드\n- 통계적 공정 관리: SPC, 공정 능력 지수(Cp, Cpk, Pp, Ppk), 관리도 해석\n- 수율 예측 모델링: 푸아송 수율 모델, 네거티브 바이노미얼, 머피 수율 모델\n- 수율 손실 분석: 랜덤 결함 vs 체계적 결함, 클러스터링 분석, Kill Ratio\n- 데이터 분석: 다변량 분석, PCA, 상관 분석, 이상 탐지(Anomaly Detection)\n\n## 응답 원칙\n- 정량적 데이터와 통계적 방법론에 기반합니다\n- 수율 로스의 근본 원인을 체계적으로 분류합니다\n- 개선 우선순위(Impact × Feasibility)를 제시합니다\n- 수율 목표 달성을 위한 액션 플랜을 구체적으로 제안합니다\n- 시각화(차트, 그래프) 해석을 포함합니다",
"placeholder": "수율 데이터, 공정 능력, 통계 분석을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "시스템",
"label": "시스템",
"symbol": "\uE770",
"color": "#EF4444",
"description": "IT 시스템·인프라·개발 지원",
"systemPrompt": "당신은 사내 IT 시스템 및 소프트웨어 개발 전문가입니다.\n\n## 전문 영역\n- 소프트웨어 개발: C#, Python, SQL, JavaScript, WPF, .NET, REST API\n- 데이터베이스: SQL Server, Oracle, PostgreSQL — 쿼리 최적화, 스키마 설계, 성능 튜닝\n- MES/ERP 시스템: 제조 실행 시스템 연동, 데이터 수집, 공정 추적\n- 인프라: Windows Server, Active Directory, 네트워크, 보안, 가상화\n- 자동화: RPA, 스크립트 자동화, CI/CD, 배치 작업, 데이터 파이프라인\n- AI/ML: 모델 학습, 데이터 전처리, 이상 탐지, 예측 모델링\n\n## 응답 원칙\n- 실행 가능한 코드와 구체적 구현 방법을 제공합니다\n- 보안과 성능을 함께 고려합니다\n- 기존 시스템과의 호환성을 중시합니다\n- 에러 메시지 분석과 디버깅 가이드를 제공합니다\n- 단계별 가이드로 비개발자도 따라할 수 있게 합니다",
"placeholder": "코드, 시스템, 데이터베이스, 인프라를 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "연구개발",
"label": "연구개발",
"symbol": "\uE9A8",
"color": "#3B82F6",
"description": "R&D·논문·실험 설계 지원",
"systemPrompt": "당신은 반도체·디스플레이·소재 분야의 R&D 전문가이자 연구 방법론 어드바이저입니다.\n\n## 전문 영역\n- 실험 설계: DOE(Design of Experiments), 다구찌 방법, RSM(Response Surface Methodology)\n- 통계 분석: ANOVA, 회귀 분석, SPC(Statistical Process Control), Cp/Cpk\n- 논문 리뷰: 최신 연구 트렌드 해석, 실험 결과 분석, 논문 작성 지원\n- 소재·공정 과학: 박막 증착, 에칭, 리소그래피, 패키징 기술\n- 특허 분석: 선행 기술 조사, 청구항 분석, 특허 맵핑\n\n## 응답 원칙\n- 과학적 근거와 데이터에 기반한 분석을 제공합니다\n- 실험 조건, 변수, 제어 인자를 체계적으로 다룹니다\n- 최신 연구 동향과 방법론을 반영합니다\n- 수식, 그래프 해석, 통계적 유의성을 명확히 설명합니다\n- 재현 가능한 구체적 프로토콜을 제시합니다",
"placeholder": "실험 설계, 논문 분석, 통계 해석 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "인사",
"label": "인사",
"symbol": "\uE716",
"color": "#0EA5E9",
"description": "인사·채용·조직문화·노무 관리",
"systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 인사관리(HRM/HRD) 전문가입니다.\n\n## 전문 영역\n- 인재 확보: 채용 전략, 직무기술서(JD) 작성, 역량 기반 면접(BEI/STAR), 기술 인재 파이프라인 관리\n- 인사 제도: 직무급·직능급·성과급 체계 설계, 승진·보상·복리후생 제도 벤치마킹 (Hay Method, Mercer IPE)\n- 성과 관리: MBO/OKR/BSC 기반 평가 체계, 다면 평가(360도), 성과 피드백 코칭\n- 조직 개발: 조직문화 진단(OCAI, Denison), 변화관리(Kotter 8단계), 직원 몰입도(Gallup Q12) 향상\n- 노무·법률: 근로기준법, 취업규칙, 징계·해고 절차, 유연근무제, 교대제 편성 (반도체 FAB 3교대)\n- 교육 훈련: 역량 모델링, ISD(교수설계) 기반 교육과정 개발, 리더십 파이프라인, 기술 교육(OJT/Off-JT)\n- HR 애널리틱스: 이직률 분석, 인건비 시뮬레이션, 인력 수급 계획, 인적자본 ROI\n\n## 응답 원칙\n- 노동법과 관련 규정을 정확히 참조합니다\n- 산업 특성(교대 근무, 클린룸 환경, 기술 인력 부족)을 고려합니다\n- 실무에 바로 적용 가능한 양식·체크리스트를 제공합니다\n- 직원 경험(EX)과 조직 성과의 균형을 추구합니다\n- 최신 HR 트렌드(AI 채용, 리스킬링, DEI)를 반영합니다",
"placeholder": "채용, 평가, 조직문화, 노무, 교육 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "일반",
"label": "일반",
"symbol": "\uE8BD",
"color": "#6B7280",
"description": "범용 AI 어시스턴트",
"systemPrompt": "당신은 사내 전용 AI 어시스턴트입니다. 사용자의 질문에 정확하고 친절하게 답변하세요.\n\n## 핵심 원칙\n- 사실에 기반한 정확한 정보를 제공합니다\n- 모르는 것은 모른다고 솔직히 말합니다\n- 한국어로 명확하고 구조적으로 답변합니다\n- 필요 시 단계별로 나누어 설명합니다\n- 코드, 표, 목록 등 적절한 형식을 활용합니다",
"placeholder": "무엇이든 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "재무",
"label": "재무",
"symbol": "\uE8C7",
"color": "#D97706",
"description": "재무회계·관리회계·원가·투자 분석",
"systemPrompt": "당신은 반도체·디스플레이 제조업에 정통한 재무·회계 전문가입니다.\n\n## 전문 영역\n- 재무회계: K-IFRS 기반 재무제표 분석, 연결재무제표, 감사 대응, 공시 실무\n- 관리회계: 원가 계산(표준원가·활동기준원가 ABC), 변동비/고정비 분석, CVP 분석, 손익분기점\n- 반도체 원가 구조: 웨이퍼 원가, 수율 영향 원가, 감가상각(FAB 장비), 재공품(WIP) 평가\n- 투자 분석: 설비투자 타당성(NPV, IRR, Payback), 용량 확장(CAPEX) 의사결정, DCF 밸류에이션\n- 예산 관리: 제로베이스 예산, 롤링 예산, 차이 분석(예산 vs 실적), 부문별 손익\n- 세무: 법인세, 이전가격(TP), R&D 세액공제, 관세·FTA 원산지 관리\n- 자금 관리: 현금흐름 예측, 운전자본 최적화, 환위험 헤지(FX), 유동성 관리\n- 재무 비율: ROE, ROA, ROIC, EBITDA 마진, 부채비율, 유동비율, 재고자산회전율 해석\n\n## 응답 원칙\n- 회계 기준(K-IFRS/K-GAAP)을 명확히 구분하여 설명합니다\n- 숫자와 계산 과정을 투명하게 보여줍니다 (Excel 수식 형태 권장)\n- 의사결정에 필요한 민감도 분석·시나리오 분석을 포함합니다\n- 반도체 산업의 높은 고정비 구조와 대규모 설비투자 특성을 반영합니다\n- 세무·법률 사항은 전문가 확인을 권고하되, 실무 방향을 제시합니다",
"placeholder": "원가 분석, 투자 타당성, 재무제표, 예산 등을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "제조기술",
"label": "제조기술",
"symbol": "\uE90F",
"color": "#10B981",
"description": "공정·설비·생산 기술 지원",
"systemPrompt": "당신은 반도체·디스플레이 제조 공정 및 설비 기술 전문가입니다.\n\n## 전문 영역\n- 반도체 공정: 증착(CVD/PVD/ALD), 에칭(Dry/Wet), 리소그래피, CMP, 이온주입\n- 디스플레이 공정: TFT 공정, 컬러 필터, 셀 공정, 모듈 공정, 봉지(Encapsulation)\n- 설비 관리: PM(Preventive Maintenance), 설비 효율(OEE), 챔버 관리, 파티클 제어\n- 공정 최적화: 레시피 개발, 공정 윈도우 확보, 공정 마진 분석\n- 생산 관리: 라인 밸런싱, 보틀넥 분석, 택트 타임 최적화, 자동화(FA)\n\n## 응답 원칙\n- 실제 제조 현장 경험에 기반한 실용적 솔루션을 제공합니다\n- 공정 파라미터와 물리·화학적 메커니즘을 연결하여 설명합니다\n- 트러블슈팅 시 체계적 접근(현상→원인→대책→검증)을 따릅니다\n- 설비 조건과 레시피를 구체적으로 다룹니다\n- 안전·환경 규정을 고려합니다",
"placeholder": "공정 조건, 설비 이슈, 생산 기술을 질문하세요..."
}

View File

@@ -0,0 +1,9 @@
{
"category": "제품분석",
"label": "제품분석",
"symbol": "\uE9D9",
"color": "#EC4899",
"description": "제품 품질·불량·신뢰성 분석",
"systemPrompt": "당신은 반도체·디스플레이 제품의 품질 분석 및 신뢰성 공학 전문가입니다.\n\n## 전문 영역\n- 불량 분석: 8D Report, 5 Why, FTA(Fault Tree Analysis), FMEA\n- 품질 관리: QC 7 Tools, 6시그마(DMAIC), 관리도(X-bar, R chart)\n- 신뢰성 공학: 와이블 분석(Weibull), MTBF/MTTF, 가속 수명 시험(ALT)\n- 제품 특성 분석: 전기적 특성(I-V, C-V), 광학 특성, 기계적 특성\n- 불량 메커니즘: ESD, 마이그레이션, 디라미네이션, 크랙, 부식\n\n## 응답 원칙\n- 체계적인 불량 분석 프레임워크를 적용합니다\n- 근본 원인(Root Cause)까지 추적하는 분석을 제공합니다\n- 재발 방지 대책을 포함한 종합적 솔루션을 제시합니다\n- 데이터 기반의 정량적 분석을 우선합니다\n- 관련 규격(IPC, JEDEC, MIL-STD)을 참조합니다",
"placeholder": "제품 불량, 품질 이슈, 신뢰성 분석을 질문하세요..."
}

View File

@@ -0,0 +1,102 @@
[
"OLED는 자체 발광 소자로 별도 백라이트가 필요 없어 얇고 유연한 디스플레이 구현이 가능합니다.",
"LTPO TFT는 LTPS와 Oxide를 결합하여 1~120Hz 가변 주사율을 지원하며, 전력 효율이 뛰어납니다.",
"마이크로LED는 무기 자체 발광 소자로, OLED 대비 수명이 길고 번인이 없으며 고휘도를 구현합니다.",
"QD-OLED는 파란색 OLED에 양자점 색변환층을 결합하여 넓은 색재현율을 달성합니다.",
"미니LED는 수천~수만 개의 작은 LED를 백라이트로 사용하여 LCD의 명암비를 크게 향상시킵니다.",
"양자점(Quantum Dot)은 나노미터 크기의 반도체 입자로, 크기에 따라 다른 색을 발합니다.",
"폴더블 디스플레이는 유연한 OLED를 사용하여 접을 수 있는 화면을 구현합니다.",
"롤러블 디스플레이는 두루마리처럼 말거나 펼 수 있는 차세대 유연 디스플레이입니다.",
"투명 디스플레이는 화면 뒤의 사물이 보이는 디스플레이로, 쇼윈도/자동차 HUD에 활용됩니다.",
"번인(Burn-in)은 OLED 패널에서 같은 이미지가 오래 표시되면 잔상이 남는 현상입니다.",
"WRGB는 White OLED + Color Filter 방식으로, 대형 OLED TV에 사용됩니다.",
"RGB 직접 증착은 각 서브픽셀에 R/G/B 유기물을 직접 증착하는 소형 OLED 방식입니다.",
"FMM(Fine Metal Mask)은 OLED RGB 증착에 사용되는 정밀 금속 마스크입니다.",
"잉크젯 프린팅 OLED는 유기물을 잉크로 인쇄하여 대면적 OLED 생산 비용을 줄이는 기술입니다.",
"WOLED(White OLED)는 백색 유기발광층 위에 컬러 필터를 적용하는 대형 패널 기술입니다.",
"TFT(Thin Film Transistor)는 디스플레이 각 픽셀의 온/오프를 제어하는 박막 트랜지스터입니다.",
"LTPS(Low Temperature Poly-Silicon)는 고해상도 소형 디스플레이에 사용되는 TFT 기술입니다.",
"a-Si(Amorphous Silicon)는 비정질 실리콘 TFT로, 대형 LCD에 사용됩니다.",
"Oxide TFT(IGZO)는 산화물 반도체 기반 TFT로, 고해상도와 저전력이 장점입니다.",
"주사율(Refresh Rate)은 초당 화면 갱신 횟수로, 높을수록 부드러운 화면을 제공합니다.",
"응답속도(Response Time)는 픽셀이 색을 전환하는 시간으로, OLED는 마이크로초 수준입니다.",
"명암비(Contrast Ratio)는 가장 밝은 부분과 어두운 부분의 휘도 비율입니다.",
"HDR(High Dynamic Range)은 밝기 범위를 확장하여 현실감 있는 영상을 표현합니다.",
"HDR10+는 Samsung이 개발한 동적 HDR 표준으로, 장면별 메타데이터를 포함합니다.",
"Dolby Vision은 Dolby의 동적 HDR 표준으로, 12비트 색심도를 지원합니다.",
"색재현율(Color Gamut)은 디스플레이가 표현할 수 있는 색 범위로, DCI-P3/BT.2020이 기준입니다.",
"DCI-P3는 영화 산업 표준 색 공간으로, sRGB 대비 25% 넓은 색 범위를 제공합니다.",
"PPI(Pixels Per Inch)는 인치당 픽셀 수로, 높을수록 선명한 화면을 제공합니다.",
"펜타일(Pentile)은 OLED에서 서브픽셀을 공유하여 해상도를 높이는 배열 방식입니다.",
"COE(Color filter On Encapsulation)는 OLED 위에 컬러 필터를 적용하여 반사를 줄입니다.",
"POL-less는 편광판을 제거하여 OLED 밝기를 30% 이상 높이는 기술입니다.",
"UTG(Ultra Thin Glass)는 30~70μm 두께의 초박형 유리로, 폴더블 커버에 사용됩니다.",
"CPI(Colorless Polyimide)는 투명한 폴리이미드 필름으로, 플렉시블 디스플레이 기판에 사용됩니다.",
"봉지(Encapsulation)는 OLED 유기물을 수분/산소로부터 보호하는 밀봉 기술입니다.",
"TFE(Thin Film Encapsulation)는 무기/유기 다층 박막으로 OLED를 봉지하는 기술입니다.",
"터치센서는 정전용량 변화를 감지하여 터치 입력을 인식하는 부품입니다.",
"인셀(In-cell) 터치는 디스플레이 패널 내부에 터치 센서를 통합하여 두께를 줄입니다.",
"TDDI(Touch and Display Driver Integration)는 터치와 디스플레이 드라이버 IC를 통합합니다.",
"DDIC(Display Driver IC)는 디스플레이 패널의 각 픽셀에 전기 신호를 전달하는 IC입니다.",
"증착(Deposition)은 기판 위에 박막을 형성하는 반도체/디스플레이 공정입니다.",
"스퍼터링(Sputtering)은 플라즈마로 타깃 물질을 날려 기판에 박막을 형성합니다.",
"CVD(Chemical Vapor Deposition)는 화학 반응으로 기판에 박막을 증착하는 공정입니다.",
"ALD(Atomic Layer Deposition)는 원자층 단위로 박막을 정밀하게 증착합니다.",
"포토리소그래피(Photolithography)는 빛으로 회로 패턴을 기판에 전사하는 핵심 공정입니다.",
"EUV(Extreme Ultraviolet)는 13.5nm 파장의 극자외선으로 7nm 이하 미세 패턴을 구현합니다.",
"DUV(Deep Ultraviolet)는 248/193nm 파장의 자외선 노광 장비입니다.",
"에칭(Etching)은 화학적/물리적 방법으로 불필요한 박막을 제거하는 공정입니다.",
"건식 에칭(Dry Etching)은 플라즈마를 이용하여 정밀하게 박막을 제거합니다.",
"습식 에칭(Wet Etching)은 화학 용액으로 박막을 제거하는 방법입니다.",
"CMP(Chemical Mechanical Polishing)는 화학적·기계적으로 웨이퍼 표면을 평탄화합니다.",
"이온주입(Ion Implantation)은 불순물 이온을 반도체에 주입하여 전기적 특성을 조절합니다.",
"웨이퍼(Wafer)는 반도체 칩을 만드는 원판형 실리콘 기판입니다.",
"다이(Die)는 웨이퍼에서 절단된 개별 반도체 칩입니다.",
"패키징(Packaging)은 반도체 다이를 보호하고 외부와 연결하는 공정입니다.",
"와이어 본딩(Wire Bonding)은 칩과 패키지를 가는 금선으로 연결하는 기술입니다.",
"플립칩(Flip Chip)은 칩을 뒤집어 범프로 직접 기판에 연결하는 고성능 패키징입니다.",
"2.5D 패키징은 인터포저 위에 여러 칩을 나란히 배치하는 고급 패키징입니다.",
"3D 패키징은 칩을 수직으로 적층하여 집적도와 대역폭을 극대화합니다.",
"TSV(Through-Silicon Via)는 실리콘을 관통하는 비아로, 3D 적층에서 칩 간 연결에 사용됩니다.",
"HBM(High Bandwidth Memory)은 DRAM을 수직 적층하여 초고대역폭을 제공하는 메모리입니다.",
"HBM3E는 최신 HBM으로, AI 가속기(GPU/TPU)에 사용되며 대역폭이 1TB/s를 넘습니다.",
"GDDR6X는 고성능 그래픽 메모리로, PAM4 신호 방식으로 대역폭을 2배 높입니다.",
"LPDDR5X는 저전력 모바일 DRAM으로, 최대 8533Mbps 전송 속도를 지원합니다.",
"NAND Flash는 비휘발성 메모리로, SSD와 USB 등 저장 장치에 사용됩니다.",
"V-NAND(3D NAND)는 셀을 수직으로 적층하여 용량을 늘린 플래시 메모리입니다.",
"200단 이상 V-NAND는 셀을 200층 이상 쌓아 초고용량 SSD를 구현합니다.",
"QLC(Quad-Level Cell)는 셀당 4비트를 저장하여 용량 대비 비용을 낮춥니다.",
"PLC(Penta-Level Cell)는 셀당 5비트를 저장하는 차세대 NAND 기술입니다.",
"CXL(Compute Express Link)은 CPU-GPU-메모리 간 고속 인터커넥트 표준입니다.",
"PCIe 5.0은 레인당 32GT/s를 지원하는 고속 인터페이스 표준입니다.",
"UCIe(Universal Chiplet Interconnect Express)는 칩렛 간 연결 표준입니다.",
"칩렛(Chiplet)은 기능별로 분리된 작은 칩을 조합하여 하나의 프로세서를 구성합니다.",
"FinFET은 3D 구조의 트랜지스터로, 14nm~5nm 공정에서 사용됩니다.",
"GAA(Gate-All-Around)는 게이트가 채널을 완전히 감싸는 차세대 트랜지스터 구조입니다.",
"나노시트(Nanosheet)는 GAA 구조에서 채널을 시트 형태로 구현한 트랜지스터입니다.",
"CFET(Complementary FET)은 NMOS와 PMOS를 수직 적층하는 차세대 트랜지스터입니다.",
"BSPDN(Backside Power Delivery Network)은 칩 뒷면에서 전력을 공급하는 기술입니다.",
"2nm 공정은 GAA 트랜지스터를 사용하여 전력 효율과 성능을 극대화합니다.",
"ASML은 세계 유일의 EUV 노광장비 제조사로, 반도체 산업의 핵심 기업입니다.",
"TSMC는 세계 최대 반도체 파운드리로, 첨단 공정 기술을 선도합니다.",
"삼성전자 파운드리는 GAA 공정을 최초로 양산에 적용했습니다.",
"Intel Foundry Services는 Intel의 파운드리 사업부로, 외부 고객에게 제조 서비스를 제공합니다.",
"SK하이닉스는 HBM 시장 점유율 1위로, AI 반도체 메모리를 선도합니다.",
"반도체 수율(Yield)은 양품 칩의 비율로, 제조 효율성의 핵심 지표입니다.",
"OPC(Optical Proximity Correction)는 광학 근접 효과를 보정하여 패턴 정밀도를 높입니다.",
"DTCO(Design-Technology Co-Optimization)는 설계와 공정을 동시에 최적화합니다.",
"EDA(Electronic Design Automation)는 반도체 회로 설계를 자동화하는 소프트웨어입니다.",
"RTL(Register Transfer Level)은 디지털 회로를 레지스터 전송 수준으로 기술하는 설계 추상화입니다.",
"FPGA(Field-Programmable Gate Array)는 프로그래밍으로 회로를 구성할 수 있는 반도체입니다.",
"ASIC(Application-Specific Integrated Circuit)은 특정 용도에 최적화된 맞춤형 반도체입니다.",
"SoC(System on Chip)는 CPU, GPU, 메모리 등을 하나의 칩에 통합한 시스템 반도체입니다.",
"AI 가속기는 딥러닝 연산에 최적화된 반도체로, GPU/TPU/NPU가 대표적입니다.",
"뉴로모픽 칩은 뇌의 신경망 구조를 모방한 반도체로, 초저전력 AI 연산이 가능합니다.",
"RISC-V는 오픈소스 명령어 집합 아키텍처(ISA)로, ARM의 대안으로 부상 중입니다.",
"ARM은 모바일 기기에서 가장 널리 사용되는 저전력 프로세서 아키텍처입니다.",
"x86은 Intel/AMD의 데스크톱·서버용 프로세서 아키텍처입니다.",
"전력 효율(Performance per Watt)은 와트당 성능으로, 모바일·데이터센터의 핵심 지표입니다.",
"다크 실리콘(Dark Silicon)은 발열 한계로 동시에 활성화할 수 없는 칩 영역입니다.",
"무어의 법칙은 트랜지스터 집적도가 약 2년마다 2배로 증가한다는 경험적 법칙입니다.",
"CoWoS(Chip on Wafer on Substrate)는 TSMC의 고급 2.5D 패키징으로, AI 가속기에 필수적입니다."
]

View File

@@ -0,0 +1,251 @@
[
"It's not rocket science. — 그렇게 어려운 건 아니에요.",
"Let's call it a day. — 오늘은 여기까지 하죠.",
"I'm on the same page. — 저도 같은 생각입니다.",
"That rings a bell. — 어디서 들어본 것 같아요.",
"It's a piece of cake. — 식은 죽 먹기예요.",
"I'll keep you posted. — 진행 상황 알려드릴게요.",
"Let me sleep on it. — 하룻밤 생각해 볼게요.",
"We're in the same boat. — 우리 같은 처지예요.",
"Don't put all your eggs in one basket. — 한 곳에 모든 걸 걸지 마세요.",
"Actions speak louder than words. — 행동이 말보다 중요합니다.",
"Better late than never. — 안 하는 것보다 늦더라도 하는 게 낫죠.",
"Every cloud has a silver lining. — 어떤 어려움에도 희망은 있습니다.",
"The ball is in your court. — 이제 당신 차례입니다.",
"I'm swamped with work. — 일이 산더미예요.",
"Let's touch base later. — 나중에 다시 연락하죠.",
"Could you give me a ballpark figure? — 대략적인 수치를 알려주실 수 있나요?",
"I'll get back to you on that. — 그 건은 나중에 답변드릴게요.",
"Let's get the ball rolling. — 일을 시작합시다.",
"Can we push back the deadline? — 마감일을 미룰 수 있을까요?",
"I'm running behind schedule. — 일정이 좀 밀렸어요.",
"That's a no-brainer. — 당연한 거죠 / 생각할 것도 없어요.",
"We need to think outside the box. — 틀에서 벗어나 생각해야 합니다.",
"I have a lot on my plate. — 할 일이 너무 많아요.",
"Let's wrap this up. — 이걸 마무리합시다.",
"Can you walk me through it? — 하나씩 설명해 주실 수 있나요?",
"It slipped my mind. — 깜빡했어요.",
"I'm all ears. — 열심히 듣고 있어요.",
"Let's play it by ear. — 그때 가서 결정하죠.",
"You nailed it! — 완벽하게 해냈어요!",
"Hang in there! — 힘내세요! 조금만 버텨요!",
"I'll take a rain check. — 다음 기회에 할게요.",
"It's up in the air. — 아직 결정되지 않았어요.",
"Can we circle back to that? — 그 주제로 나중에 다시 돌아올 수 있을까요?",
"That's the bottom line. — 핵심은 그거예요.",
"I'm tied up right now. — 지금 바빠서 시간이 없어요.",
"Let's not reinvent the wheel. — 이미 있는 걸 다시 만들 필요 없어요.",
"We're on a tight schedule. — 일정이 빠듯해요.",
"I'll keep my fingers crossed. — 행운을 빌게요.",
"You can count on me. — 저한테 맡기세요.",
"That's easier said than done. — 말이야 쉽지요.",
"Let's hit the ground running. — 시작부터 전력 질주합시다.",
"I'm juggling several tasks. — 여러 일을 동시에 처리하고 있어요.",
"What's the catch? — 무슨 조건이 있는 거예요?",
"Don't beat around the bush. — 돌려 말하지 마세요.",
"It's a win-win situation. — 서로에게 이득이에요.",
"I'm burning the midnight oil. — 밤새 일하고 있어요.",
"The early bird catches the worm. — 일찍 시작하는 사람이 유리합니다.",
"Practice makes perfect. — 연습이 완벽을 만듭니다.",
"Where there's a will, there's a way. — 의지가 있으면 길이 있습니다.",
"Rome wasn't built in a day. — 큰 일은 하루아침에 이루어지지 않습니다.",
"I see your point. — 무슨 말인지 알겠어요.",
"Fair enough. — 그 정도면 괜찮네요 / 납득이 돼요.",
"No worries! — 걱정 마세요!",
"Sounds good to me. — 좋아 보여요.",
"I appreciate your help. — 도움에 감사드립니다.",
"Sorry to bother you, but... — 방해해서 죄송한데...",
"Would you mind if...? — 혹시 ...해도 될까요?",
"I'm looking forward to it. — 기대하고 있어요.",
"Let me know if you need anything. — 필요한 거 있으면 말씀하세요.",
"Take your time. — 천천히 하세요.",
"It's on the tip of my tongue. — 입에서 맴도는데 생각이 안 나요.",
"I'm under the weather today. — 오늘 몸이 좀 안 좋아요.",
"That makes sense. — 이해가 되네요 / 말이 되네요.",
"I couldn't agree more. — 완전 동의합니다.",
"Let's split the bill. — 각자 내죠.",
"My treat! — 제가 살게요!",
"Could you do me a favor? — 부탁 하나 해도 될까요?",
"I'll figure it out. — 제가 알아볼게요.",
"Long time no see! — 오랜만이에요!",
"It's now or never. — 지금 아니면 기회가 없어요.",
"Break a leg! — 행운을 빌어요! (공연/발표 전 격려)",
"Keep up the good work! — 계속 잘하고 계세요!",
"I'm glad to hear that. — 그 소식 듣게 되어 기쁘네요.",
"It goes without saying. — 말할 필요도 없죠.",
"Don't take it personally. — 개인적으로 받아들이지 마세요.",
"To be honest, — 솔직히 말하면,",
"As far as I know, — 내가 아는 한,",
"By the way, — 그런데 / 참고로,",
"In a nutshell, — 간단히 말하면,",
"At the end of the day, — 결국에는,",
"Give it a shot! — 한번 해 봐요!",
"I'm on my way. — 가는 중이에요.",
"Catch you later! — 나중에 봐요!",
"It's worth a try. — 시도해 볼 만해요.",
"Don't mention it. — 별말씀을요. (천만에요)",
"What do you do for a living? — 직업이 뭐예요?",
"How's everything going? — 다 잘 되어가고 있어요?",
"I'm a morning person. — 저는 아침형 인간이에요.",
"I'm more of a night owl. — 저는 올빼미형이에요.",
"That's the last straw. — 그게 마지막 한계예요. (참을 수 없어요)",
"I'll cross that bridge when I come to it. — 그때 가서 생각할게요.",
"You can say that again! — 정말 그래요! (동의)",
"I'm dying to try that. — 꼭 해보고 싶어요.",
"It's a long story. — 말하면 길어요.",
"Speak of the devil! — 호랑이도 제 말 하면 온다더니!",
"I didn't see that coming. — 예상 못 했어요.",
"That's music to my ears. — 정말 기쁜 소식이네요.",
"I owe you one. — 신세 졌어요.",
"Don't judge a book by its cover. — 겉모습만 보고 판단하지 마세요.",
"It takes two to tango. — 혼자서는 안 되죠. (쌍방의 노력이 필요해요)",
"Let's schedule a follow-up meeting. — 후속 회의를 잡읍시다.",
"I'd like to propose a new approach. — 새로운 접근 방식을 제안하고 싶습니다.",
"We need to streamline the process. — 프로세스를 간소화해야 합니다.",
"Can you send me the meeting minutes? — 회의록을 보내주실 수 있나요?",
"Let's align our priorities. — 우선순위를 맞춥시다.",
"I'll loop you in on the email. — 이메일에 참조로 넣어드릴게요.",
"We should leverage our strengths. — 우리의 강점을 활용해야 합니다.",
"The project is on track. — 프로젝트가 순조롭게 진행되고 있어요.",
"Let's table this discussion for now. — 이 논의는 일단 보류합시다.",
"I'll draft a proposal by Friday. — 금요일까지 제안서를 작성할게요.",
"Do you have any dietary restrictions? — 식이 제한이 있으신가요?",
"I'm running a few minutes late. — 몇 분 늦을 것 같아요.",
"Make yourself at home. — 편하게 계세요.",
"I'm not in the mood for that. — 그럴 기분이 아니에요.",
"It totally made my day. — 덕분에 하루가 정말 좋았어요.",
"I'm getting the hang of it. — 이제 감이 좀 잡히고 있어요.",
"Let's grab a coffee sometime. — 언제 커피 한잔합시다.",
"I have mixed feelings about it. — 기분이 좀 복잡해요.",
"That's not my cup of tea. — 그건 제 취향이 아니에요.",
"I'll think about it and let you know. — 생각해 보고 알려드릴게요.",
"The system is down right now. — 지금 시스템이 다운됐어요.",
"We need to back up the data. — 데이터를 백업해야 합니다.",
"Can you share your screen? — 화면 공유해 주실 수 있나요?",
"The app keeps crashing on me. — 앱이 자꾸 멈춰요.",
"Let me clear my browser cache. — 브라우저 캐시를 지울게요.",
"We should update to the latest version. — 최신 버전으로 업데이트해야 합니다.",
"The file is too large to upload. — 파일이 너무 커서 업로드할 수 없어요.",
"I'll set up a shared folder for the team. — 팀을 위한 공유 폴더를 만들게요.",
"My internet connection is unstable. — 인터넷 연결이 불안정해요.",
"Let me run a quick diagnostic. — 빠른 진단을 해볼게요.",
"Believe in yourself and you're halfway there. — 자신을 믿으면 반은 이룬 겁니다.",
"Every expert was once a beginner. — 모든 전문가도 한때는 초보자였습니다.",
"Small steps lead to big changes. — 작은 걸음이 큰 변화를 만듭니다.",
"Don't be afraid to start over. — 다시 시작하는 것을 두려워하지 마세요.",
"Your only limit is your mindset. — 유일한 한계는 당신의 마음가짐입니다.",
"Progress, not perfection, is what matters. — 완벽이 아닌 발전이 중요합니다.",
"Challenges make you stronger. — 도전이 당신을 더 강하게 만듭니다.",
"Stay curious and keep learning. — 호기심을 유지하고 계속 배우세요.",
"Success is a journey, not a destination. — 성공은 목적지가 아닌 여정입니다.",
"The best time to start is now. — 시작하기 가장 좋은 때는 바로 지금입니다.",
"Can I get a window seat, please? — 창가 좌석으로 주실 수 있나요?",
"What's the local specialty here? — 이 지역 특산물이 뭐예요?",
"Could you recommend a good restaurant? — 괜찮은 식당 추천해 주실 수 있나요?",
"I'd like to make a reservation for two. — 두 명 예약하고 싶습니다.",
"How do I get to the nearest subway station? — 가장 가까운 지하철역까지 어떻게 가나요?",
"This dish is absolutely delicious. — 이 요리 정말 맛있어요.",
"Is there a dress code for this event? — 이 행사에 복장 규정이 있나요?",
"I'm allergic to shellfish. — 저는 갑각류 알레르기가 있어요.",
"The view from here is breathtaking. — 여기서 보는 경치가 정말 멋져요.",
"Keep me in the loop. — 진행 상황을 계속 공유해 주세요.",
"Lets sync up later. — 나중에 시간 맞춰서 다시 얘기하죠.",
"I'll play devil's advocate. — 제가 반대 입장에서 한번 검토해 볼게요.",
"It's a low-hanging fruit. — 이건 적은 노력으로도 쉽게 해결할 수 있는 일이에요.",
"Lets revisit this later. — 이 주제는 나중에 다시 논의하시죠.",
"We need to manage expectations. — (상사나 고객의) 기대치를 적절히 조절해야 합니다.",
"Its a steep learning curve. — 익숙해지는 데 시간이 꽤 걸릴 거예요.",
"Back to the drawing board. — (계획이 틀어졌으니) 처음부터 다시 시작합시다.",
"This project has legs. — 이 프로젝트는 성공 가능성이 커 보여요.",
"Let's move the needle. — 실질적인 변화나 성과를 만들어 봅시다.",
"Is this solution scalable? — 이 솔루션이 확장 가능한가요?",
"We're facing some technical debt. — 기술 부채가 좀 쌓여 있네요.",
"Let's deep dive into the architecture. — 아키텍처를 심층적으로 분석해 보죠.",
"We need more buy-in from the team. — 팀원들의 더 많은 동의와 지지가 필요합니다.",
"Let's take this offline. — 그 건은 회의 끝나고 따로 얘기하시죠.",
"We're on the home stretch. — 이제 거의 다 끝났어요. 막바지 단계입니다.",
"Lets drill down into the details. — 세부 사항을 좀 더 파고들어 봅시다.",
"Give me the lowdown on the PoC. — PoC 진행 상황을 요약해서 알려주세요.",
"I'm on the same wavelength as you. — 당신과 생각이 완벽히 일치해요.",
"Take it with a grain of salt. — 너무 곧이곧대로 듣지 말고 가려서 들으세요.",
"Let's cut to the chase. — 본론으로 바로 들어가죠.",
"You need to read between the lines. — 행간의 의미를 파악해야 합니다.",
"The crux of the matter is the cost. — 문제의 핵심은 비용입니다.",
"Let's crunch the numbers. — 수치를 정밀하게 계산해 봅시다.",
"Are you up to speed on this? — 이 내용에 대해 잘 파악하고 계신가요?",
"Let's get our ducks in a row. — (일을 시작하기 전에) 미리 준비를 완벽히 마칩시다.",
"Its across the board. — 전반적으로 다 적용되는 사항입니다.",
"We built this from the ground up. — 우리는 이걸 바닥부터 하나하나 만들었습니다.",
"In the worst-case scenario... — 최악의 경우를 가정해 보자면...",
"Thats just a drop in the bucket. — 그건 빙산의 일각일 뿐이에요. (아주 적은 양)",
"Don't bite off more than you can chew. — 무리하게 일을 벌이지 마세요.",
"We have to do it by the book. — 원칙대로 처리해야 합니다.",
"Let's not cut corners. — (품질을 위해) 절차를 생략하거나 대충 하지 마세요.",
"In the long run, it's better. — 장기적으로 보면 이게 더 낫습니다.",
"We need to level the playing field. — 공정한 경쟁 환경을 만들어야 합니다.",
"It's a long shot. — 승산이 희박하지만 시도해 볼 만합니다.",
"No strings attached. — 아무런 조건도 없습니다.",
"Off the top of my head... — 지금 당장 생각나는 대로 말씀드리면...",
"There's something on the horizon. — 곧 무슨 일이 일어날 것 같아요.",
"Can I pick your brain for a second? — 의견 좀 여쭤봐도 될까요? (조언 구하기)",
"We should pull out all the stops. — 가능한 모든 수단을 동원해야 합니다.",
"This will raise the bar for our team. — 이것이 우리 팀의 기준을 높여줄 겁니다.",
"We don't see eye to eye on this. — 이 부분에 대해서는 서로 의견이 다르네요.",
"You have to stand your ground. — 당신의 입장을 굳건히 지켜야 합니다.",
"I'm not ready to throw in the towel. — 아직 포기할 단계는 아니에요.",
"It spread by word of mouth. — 입소문을 타고 퍼졌어요.",
"Let's zero in on the main issue. — 핵심 문제에 집중합시다.",
"I'll look into it right away. — 즉시 알아보겠습니다.",
"Let's keep our eyes on the prize. — 최종 목표를 잊지 맙시다.",
"We need to pivot our strategy. — 전략을 수정(피벗)해야 합니다.",
"The proof is in the pudding. — 결과가 모든 것을 말해줍니다.",
"This is a game changer for us. — 이건 우리에게 판도를 바꿀 혁신적인 일입니다.",
"I have the bandwidth for that. — 그 일을 처리할 시간적 여유가 있습니다.",
"Let's iron out the details. — 세부적인 문제들을 해결합시다.",
"It's a ballpark estimate. — 대략적인 추산치입니다.",
"We're on the right track. — 잘 진행되고 있어요. 방향이 맞습니다.",
"That's a tough pill to swallow. — 받아들이기 힘들겠지만 사실입니다.",
"Don't get discouraged. — 낙담하지 마세요.",
"Its a blessing in disguise. — 전화위복이네요. (나쁜 일 같았지만 좋은 결과)",
"Lets play it safe. — 안전하게 가죠.",
"I'll take the lead on this. — 이 일은 제가 주도적으로 맡겠습니다.",
"We need to break down the silos. — 부서 간의 장벽을 허물어야 합니다.",
"It's non-negotiable. — 협상의 여지가 없습니다. (필수 사항)",
"Let's stay ahead of the curve. — 트렌드보다 앞서 나갑시다.",
"We're looking for a synergy effect. — 시너지 효과를 기대하고 있습니다.",
"The ROI seems promising. — 투자 대비 수익(ROI)이 좋아 보이네요.",
"Let's validate the data first. — 데이터부터 먼저 검증해 봅시다.",
"Is there any room for improvement? — 개선할 여지가 좀 있을까요?",
"We need to optimize the algorithm. — 알고리즘을 최적화해야 합니다.",
"Lets streamline the workflow. — 업무 흐름을 간소화합시다.",
"I'll double-check the configuration. — 설정을 다시 한번 확인해 볼게요.",
"The system is robust and reliable. — 시스템이 탄탄하고 신뢰할 만합니다.",
"We should mitigate the risks. — 리스크를 완화해야 합니다.",
"Let's give them a heads-up. — 그들에게 미리 알려줍시다.",
"I'm tied up with another meeting. — 다른 회의 때문에 시간이 없어요.",
"That's a valid point. — 타당한 지적입니다.",
"Let's keep it simple. — 단순하게 갑시다.",
"We are understaffed at the moment. — 현재 인력이 좀 부족합니다.",
"I'll push it through. — 제가 어떻게든 밀어붙여 보겠습니다.",
"It's a top priority. — 최우선 과제입니다.",
"Let's brainstorm some ideas. — 아이디어를 좀 짜내 봅시다.",
"We need to be proactive. — 주도적으로(미리) 움직여야 합니다.",
"I'll take care of it. — 제가 처리하겠습니다.",
"The project is on hold for now. — 프로젝트가 잠시 중단되었습니다.",
"Let's wrap up this session. — 이번 세션을 마무리하죠.",
"I'm fully committed to this. — 이 일에 전념하고 있습니다.",
"That's a win for everybody. — 모두에게 좋은 결과네요.",
"We need to hit the deadline. — 마감 기한을 맞춰야 합니다.",
"I'll keep you updated. — 계속 업데이트해 드릴게요.",
"Let's get down to business. — 이제 본격적으로 업무 얘기를 하죠.",
"I'll send out the invite shortly. — 곧 초대장을 보내겠습니다.",
"We need to align with our vision. — 우리의 비전과 일치시켜야 합니다.",
"I appreciate your patience. — 기다려 주셔서 감사합니다.",
"Let's look at the big picture. — 큰 그림을 봅시다.",
"I'll see what I can do. — 제가 할 수 있는 게 있는지 알아볼게요.",
"We're making good progress. — 잘 진행되고 있습니다.",
"Let's stay focused. — 집중력을 잃지 맙시다.",
"I'm open to suggestions. — 제안은 언제나 환영입니다.",
"Let's verify the requirements. — 요구 사항을 확인해 봅시다.",
"It's a pleasure working with you. — 같이 일하게 되어 즐겁습니다."
]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,98 @@
{
"morning": [
"🌅 기분 좋은 아침, 활기찬 시작 되세요!",
"🌅 밝은 미소로 시작하는 오늘, 응원합니다.",
"🌅 오늘 하루는 어제보다 더 행복할 거예요.",
"🌅 출근하느라 고생 많으셨어요. 오늘 하루도 파이팅!",
"☀️ 아침 햇살처럼 빛나는 하루 보내시길 바랍니다.",
"☀️ 오늘은 왠지 좋은 일이 생길 것만 같은 아침이네요.",
"☀️ 일찍 일어난 당신, 오늘 업무도 무사통과!",
"🌤️ 상쾌한 공기 마시고 기분 좋게 시작해 봐요.",
"🌤️ 모닝커피 한 잔의 여유와 함께 즐겁게 시작하세요.",
"🌤️ 오늘 하루도 당신의 능력을 마음껏 펼쳐주세요!",
"🏃 활기찬 발걸음만큼 보람찬 하루 되세요.",
"🏃 열정 가득한 아침, 그 에너지를 믿습니다.",
"🏃 벌써 자리에 앉으셨군요? 정말 대단하세요!",
"✨ 반짝이는 아이디어가 샘솟는 아침이길 바랍니다.",
"✨ 당신의 성실함이 빛을 발하는 하루가 될 거예요.",
"✨ 기분 좋은 인사로 서로에게 힘이 되어주세요.",
"🍀 행운이 가득한 아침입니다. 오늘도 힘내세요!",
"🍀 오늘 당신이 걷는 모든 길에 행운이 깃들길.",
"🍀 긍정의 힘으로 기분 좋게 문을 열어볼까요?",
"🔋 오늘도 풀충전! 활기차게 가봅시다!",
"🔋 든든한 아침 식사 챙기셨나요? 몸도 마음도 건강하게!",
"🔋 기지개 한 번 크게 켜고 상쾌하게 시작해요.",
"🤝 함께라서 든든한 아침입니다. 오늘 잘 부탁드려요!",
"🤝 동료님의 미소가 사무실을 밝혀주네요.",
"🤝 서로 응원하며 기분 좋게 출발해 볼까요?",
"🚀 목표를 향해 나아가는 당신의 아침을 응원합니다.",
"🚀 오늘 하루도 막힘없이 술술 풀리길!",
"🚀 자신감 넘치는 모습이 보기 좋습니다.",
"🌈 비 온 뒤 맑음처럼, 오늘 하루도 맑음!",
"🌈 무지개처럼 다채롭고 즐거운 아침 되세요."
],
"lunch": [
"🍽️ 점심 맛있게 드시고 오후도 기운 내세요!",
"🍽️ 배부른 점심만큼 행복 지수도 상승하시길!",
"🍽️ 맛점 하셨나요? 이제 오후 업무도 즐겁게 시작해 봐요.",
"☕ 노곤한 오후, 시원한 아이스커피 한 잔 어떠세요?",
"☕ 커피 향처럼 은은하고 기분 좋은 오후 되세요.",
"☕ 잠시 멈춰서 창밖을 보며 숨 한 번 돌려보세요.",
"🍰 달콤한 간식으로 당 충전하고 다시 파이팅!",
"🍰 피곤하시죠? 달달한 디저트가 필요한 시간이에요.",
"🍰 조금만 더 힘내면 금방 퇴근 시간이에요!",
"🥱 졸음이 쏟아지는 마의 시간, 가벼운 스트레칭 어떠세요?",
"🥱 눈이 자꾸 감긴다면 잠시 일어서서 걸어보세요.",
"🥱 하품은 싹~ 활력은 팍! 힘내세요.",
"💪 오후의 고비도 가볍게 넘기실 거라 믿어요.",
"💪 집중력이 필요한 시간, 당신의 능력을 보여주세요!",
"💪 지치지 않는 당신의 열정에 박수를 보냅니다.",
"🌳 잠시 옥상이나 주변 산책하며 힐링해 보세요.",
"🌳 맑은 오후 공기가 머리를 맑게 해줄 거예요.",
"🌳 초록색 식물을 보며 눈의 피로를 풀어주세요.",
"🎵 좋아하는 음악 한 곡 들으며 기분 전환해 보세요.",
"🎵 리듬에 맞춰 업무 효율도 쭉쭉 올려볼까요?",
"🎵 활기찬 오후가 성공적인 하루를 만듭니다.",
"☀️ 오후의 햇살이 참 좋네요. 기분도 맑음이길!",
"☀️ 남은 업무도 차근차근, 완벽하게 끝내실 거예요.",
"☀️ 당신의 노고 덕분에 팀이 잘 돌아가고 있어요.",
"🤝 지친 동료에게 따뜻한 말 한마디 건네는 오후 되세요.",
"🤝 서로 도와주며 업무 마무리까지 파이팅!",
"🤝 혼자가 아니에요, 우리가 함께하고 있습니다.",
"⏳ 벌써 이만큼 해내셨네요! 조금만 더 힘내 봐요.",
"⏳ 업무 효율 최고! 역시 믿고 맡기는 분이십니다.",
"⏳ 퇴근 시계가 조금씩 다가오고 있습니다. 끝까지 힘!"
],
"evening": [
"🌙 오늘 하루도 정말 고생 많으셨습니다.",
"🌙 무거운 어깨, 이제는 짐을 내려놓을 시간이에요.",
"🌙 수고한 당신에게 박수를 보냅니다. 짝짝짝!",
"🌙 오늘 보여주신 열정, 정말 멋졌습니다.",
"🌃 밤이 깊었네요. 이제 일은 잊고 푹 쉬세요.",
"🌃 야근하시느라 고생이 많으십니다. 힘내세요!",
"🌃 늦은 밤까지 빛나는 당신의 자리가 아름답습니다.",
"🌃 오늘 하루를 보람차게 마무리한 당신이 진정한 챔피언!",
"🏠 이제 따뜻한 집으로 돌아가 편안히 쉬시길 바랍니다.",
"🏠 가족과 함께, 혹은 오롯이 나만의 휴식을 즐기세요.",
"🏠 현관문을 열면 행복이 기다리고 있을 거예요.",
"🛌 오늘 밤은 꿈도 꾸지 말고 깊은 숙면 취하세요.",
"🛌 내일을 위해 몸도 마음도 재충전하는 밤 되세요.",
"🛌 수고한 나 자신에게 \"고생했어\"라고 말해줄까요?",
"✨ 오늘의 노력이 내일의 큰 성과로 돌아올 거예요.",
"✨ 힘든 하루였지만 당신이 있어 든든했습니다.",
"✨ 밤하늘의 별처럼 당신의 오늘 하루도 빛났습니다.",
"🥂 오늘 하루 마무리는 시원한 맥주 한 잔? 고생하셨어요!",
"🥂 맛있는 저녁 드시고 스트레스 싹 날려버리세요.",
"🥂 보람찬 하루 끝에 찾아오는 여유를 만끽하세요.",
"🚶‍♂️ 퇴근길 발걸음은 가볍게, 마음은 풍성하게!",
"🚶‍♂️ 오늘 하루 있었던 고민은 회사에 두고 퇴근하세요.",
"🚶‍♂️ 수고한 당신의 뒷모습이 오늘따라 든든해 보입니다.",
"🕯️ 지친 마음을 달래는 차분한 저녁 시간 되세요.",
"🕯️ 오늘 하루도 안전하게 마무리해주셔서 감사합니다.",
"🕯️ 내일은 더 좋은 일이 생길 거예요. 편히 쉬세요.",
"🌛 조용한 밤, 수고한 당신을 위한 선물 같은 시간 되길.",
"🌛 마지막까지 자리 지켜주셔서 감사합니다. 이제 퇴근합시다!",
"🌛 걱정은 내일의 나에게 맡기고, 오늘은 꿀잠 자요.",
"🌛 오늘 정말 최고였습니다. 평안한 밤 되세요!"
]
}

View File

@@ -0,0 +1,102 @@
[
"1969년 7월 20일, 닐 암스트롱이 인류 최초로 달 표면에 발을 디뎠습니다.",
"1989년 11월 9일, 베를린 장벽이 무너지며 동서 냉전 시대가 막을 내렸습니다.",
"1945년 8월 15일, 한국이 일본 식민 지배로부터 광복을 맞이했습니다.",
"1776년 7월 4일, 미국 독립선언서가 공표되어 영국으로부터 독립을 선언했습니다.",
"1903년 12월 17일, 라이트 형제가 인류 최초의 동력 비행에 성공했습니다.",
"1453년 5월 29일, 오스만 제국이 콘스탄티노플을 정복하여 비잔틴 제국이 멸망했습니다.",
"1492년 10월 12일, 크리스토퍼 콜럼버스가 아메리카 대륙에 도착했습니다.",
"1789년 7월 14일, 프랑스 혁명의 상징인 바스티유 감옥이 함락되었습니다.",
"1215년, 영국에서 마그나카르타(대헌장)가 제정되어 왕권을 제한하는 근대 민주주의의 초석이 되었습니다.",
"1543년, 코페르니쿠스가 지동설을 발표하여 지구가 태양 주위를 돈다고 주장했습니다.",
"1687년, 아이작 뉴턴이 《프린키피아》를 출판하여 만유인력의 법칙을 발표했습니다.",
"1804년, 나폴레옹 보나파르트가 프랑스 황제로 즉위했습니다.",
"1865년 4월 9일, 미국 남북전쟁이 사실상 종결되었습니다.",
"1917년 10월, 러시아 볼셰비키 혁명으로 세계 최초의 사회주의 국가가 탄생했습니다.",
"1929년 10월 24일, 뉴욕 증시 대폭락으로 대공황이 시작되었습니다.",
"1945년 6월 26일, 유엔(UN) 헌장이 서명되어 국제 평화 유지 기구가 설립되었습니다.",
"1948년 12월 10일, 세계인권선언이 유엔 총회에서 채택되었습니다.",
"1950년 6월 25일, 한국전쟁이 발발하여 3년간의 전쟁이 시작되었습니다.",
"1957년 10월 4일, 소련이 세계 최초의 인공위성 스푸트니크를 발사했습니다.",
"1961년 4월 12일, 유리 가가린이 인류 최초로 우주 비행에 성공했습니다.",
"1963년 8월 28일, 마틴 루서 킹 목사가 '나에게는 꿈이 있습니다' 연설을 했습니다.",
"1969년, 인터넷의 전신인 ARPANET이 최초로 연결되었습니다.",
"1975년 4월 30일, 베트남 전쟁이 종결되었습니다.",
"1979년, 이란 혁명으로 팔레비 왕조가 무너지고 이슬람 공화국이 수립되었습니다.",
"1986년 4월 26일, 체르노빌 원전 사고가 발생하여 역사상 최악의 원자력 재난이 되었습니다.",
"1990년 10월 3일, 동서독이 통일되었습니다.",
"1991년 12월 25일, 소련이 해체되며 냉전 시대가 종결되었습니다.",
"1994년, 넬슨 만델라가 남아프리카공화국 최초의 흑인 대통령으로 취임했습니다.",
"1997년 7월 1일, 홍콩이 영국에서 중국으로 반환되었습니다.",
"2001년 9월 11일, 미국에서 9·11 테러 사건이 발생했습니다.",
"2004년 12월 26일, 인도양 지진 해일(쓰나미)로 23만 명 이상이 사망했습니다.",
"2008년, 미국 서브프라임 모기지 사태로 글로벌 금융위기가 발생했습니다.",
"2011년 3월 11일, 동일본 대지진과 후쿠시마 원전 사고가 발생했습니다.",
"2016년, 알파고가 이세돌 9단을 바둑에서 이기며 AI 시대를 알렸습니다.",
"2020년, COVID-19 팬데믹이 전 세계를 강타하며 인류의 생활 양식을 바꿨습니다.",
"기원전 3000년경, 이집트에서 피라미드 건설이 시작되었습니다.",
"기원전 776년, 고대 올림픽이 그리스 올림피아에서 처음 개최되었습니다.",
"기원전 221년, 진시황이 중국을 최초로 통일하고 만리장성 건설을 시작했습니다.",
"기원전 44년, 율리우스 카이사르가 원로원에서 암살되었습니다.",
"610년경, 이슬람교의 창시자 무함마드가 첫 계시를 받았습니다.",
"1346~1353년, 흑사병(페스트)이 유럽 인구의 1/3을 사망시켰습니다.",
"1440년경, 구텐베르크가 활판 인쇄술을 발명하여 지식 혁명을 일으켰습니다.",
"1519~1522년, 마젤란의 선단이 세계 최초로 지구를 한 바퀴 항해했습니다.",
"1776년, 애덤 스미스가 《국부론》을 출판하여 자유시장 경제학의 기초를 놓았습니다.",
"1859년, 찰스 다윈이 《종의 기원》을 출판하여 진화론을 발표했습니다.",
"1876년, 알렉산더 그레이엄 벨이 전화기를 발명했습니다.",
"1879년, 토머스 에디슨이 실용적인 전구를 발명했습니다.",
"1895년, 빌헬름 뢴트겐이 X선을 발견하여 의학 혁명을 일으켰습니다.",
"1905년, 아인슈타인이 특수 상대성이론과 광전효과 논문을 발표했습니다 (기적의 해).",
"1928년, 알렉산더 플레밍이 페니실린을 발견하여 항생제 시대를 열었습니다.",
"1945년 7월 16일, 미국 뉴멕시코에서 세계 최초의 핵실험(트리니티)이 실시되었습니다.",
"1947년 8월 15일, 인도가 영국으로부터 독립했습니다.",
"1953년, 왓슨과 크릭이 DNA 이중나선 구조를 발견했습니다.",
"1955년, 로자 파크스가 버스에서 백인에게 자리 양보를 거부하며 민권운동의 불씨를 당겼습니다.",
"1962년 10월, 쿠바 미사일 위기로 미국과 소련이 핵전쟁 직전까지 갔습니다.",
"1964년, 일본이 세계 최초의 고속열차 신칸센을 개통했습니다.",
"1971년, 인텔이 세계 최초의 상용 마이크로프로세서 4004를 출시했습니다.",
"1976년, 스티브 잡스와 스티브 워즈니악이 애플 컴퓨터를 창립했습니다.",
"1981년, IBM이 개인용 컴퓨터(PC)를 출시하여 PC 시대를 열었습니다.",
"1983년, 인터넷의 핵심 프로토콜 TCP/IP가 공식 채택되었습니다.",
"1989년, 팀 버너스-리가 월드와이드웹(WWW)을 발명했습니다.",
"1990년, 허블 우주 망원경이 발사되어 우주의 모습을 혁명적으로 밝혔습니다.",
"1995년, 제프 베조스가 아마존을 온라인 서점으로 창립했습니다.",
"1998년, 래리 페이지와 세르게이 브린이 구글을 창립했습니다.",
"2004년, 마크 저커버그가 하버드 기숙사에서 페이스북을 창립했습니다.",
"2007년, 스티브 잡스가 아이폰을 발표하며 스마트폰 시대를 열었습니다.",
"2009년, 사토시 나카모토가 비트코인 네트워크를 가동하여 암호화폐 시대를 시작했습니다.",
"2012년, CERN에서 힉스 보손 입자를 발견하여 '신의 입자' 이론을 검증했습니다.",
"2015년, LIGO가 인류 최초로 중력파를 직접 관측했습니다.",
"2019년, 이벤트 호라이즌 망원경(EHT)이 블랙홀을 최초로 촬영했습니다.",
"2022년, 제임스 웹 우주 망원경이 최초 관측 이미지를 공개하여 우주 관측의 새 시대를 열었습니다.",
"2022년 11월, OpenAI가 ChatGPT를 출시하여 생성형 AI 혁명을 촉발했습니다.",
"1392년, 이성계가 조선을 건국하고 한양(서울)을 수도로 삼았습니다.",
"1443년, 세종대왕이 훈민정음(한글)을 창제했습니다.",
"1592년, 임진왜란이 발발하였고 이순신 장군이 해전에서 연전연승했습니다.",
"1896년, 서재필이 한국 최초의 민간 신문 《독립신문》을 창간했습니다.",
"1910년, 경술국치로 한국이 일본에 국권을 빼앗겼습니다.",
"1919년 3월 1일, 3·1 독립운동이 일어나 독립을 선언했습니다.",
"1945년 8월 15일, 한국이 광복을 맞이했습니다.",
"1948년 8월 15일, 대한민국 정부가 수립되었습니다.",
"1960년 4월 19일, 4·19 혁명으로 이승만 독재 정권이 무너졌습니다.",
"1970년대, 한국은 경부고속도로 건설 등 산업화를 이루며 '한강의 기적'을 시작했습니다.",
"1987년 6월, 6월 민주항쟁으로 대통령 직선제 개헌이 이루어졌습니다.",
"1988년, 서울 올림픽이 개최되어 한국을 세계에 알렸습니다.",
"1997년, IMF 외환위기가 발생하여 한국 경제가 큰 어려움을 겪었습니다.",
"2002년, 한일 월드컵에서 한국 축구 대표팀이 4강 신화를 이뤘습니다.",
"2018년 4월, 판문점에서 남북 정상회담이 개최되었습니다.",
"1543년, 조선 이황(퇴계)이 태어나 성리학을 집대성했습니다.",
"1446년, 훈민정음이 반포되어 백성들이 글을 읽고 쓸 수 있게 되었습니다.",
"1636년, 병자호란이 발발하여 조선이 청에 항복했습니다.",
"1894년, 동학농민운동이 일어나 반봉건·반외세를 외쳤습니다.",
"1905년, 을사늑약으로 대한제국의 외교권이 박탈되었습니다.",
"1926년 6월 10일, 6·10 만세운동이 일어나 일제에 항거했습니다.",
"1953년 7월 27일, 한국전쟁 정전협정이 체결되었습니다.",
"1961년, 5·16 군사정변이 일어나 박정희가 정권을 장악했습니다.",
"1980년 5월 18일, 5·18 광주 민주화운동이 일어났습니다.",
"2000년 6월, 최초의 남북 정상회담이 평양에서 개최되었습니다.",
"2010년, 한국이 G20 정상회의를 서울에서 개최했습니다.",
"1894년, 갑오개혁으로 신분제 폐지 등 근대적 개혁이 이루어졌습니다.",
"1909년 10월 26일, 안중근 의사가 이토 히로부미를 저격했습니다."
]

View File

@@ -0,0 +1,505 @@
[
"LLM(Large Language Model)은 대규모 텍스트 데이터로 학습된 언어 모델로, GPT, Claude, Gemini 등이 있습니다.",
"RAG(Retrieval-Augmented Generation)는 외부 지식을 검색하여 LLM 응답의 정확도를 높이는 기술입니다.",
"MCP(Model Context Protocol)는 AI 모델이 외부 도구/데이터에 접근하는 표준 프로토콜입니다.",
"Transformer는 Self-Attention 메커니즘 기반 신경망으로, 현대 AI의 핵심 아키텍처입니다.",
"프롬프트 엔지니어링은 AI에게 최적의 결과를 얻기 위해 입력을 설계하는 기술입니다.",
"Fine-tuning은 사전학습된 모델을 특정 도메인 데이터로 추가 학습시키는 과정입니다.",
"토큰(Token)은 LLM이 텍스트를 처리하는 최소 단위로, 한국어 한 글자는 보통 2~3토큰입니다.",
"컨텍스트 윈도우(Context Window)는 AI 모델이 한 번에 처리할 수 있는 최대 토큰 수입니다.",
"Zero-shot은 예시 없이 지시만으로 작업을 수행하는 AI 능력입니다.",
"Few-shot은 소수의 예시를 제공하여 AI의 답변 품질을 높이는 기법입니다.",
"Chain-of-Thought(CoT)는 AI가 단계적으로 추론하도록 유도하여 복잡한 문제를 해결하는 프롬프트 기법입니다.",
"Hallucination은 AI가 사실이 아닌 내용을 확신 있게 생성하는 현상입니다.",
"Grounding은 AI 응답을 실제 데이터에 기반하게 하여 환각을 줄이는 기법입니다.",
"RLHF(Reinforcement Learning from Human Feedback)는 인간 피드백으로 AI를 정렬하는 학습 방법입니다.",
"Embedding은 텍스트를 고차원 벡터로 변환하여 의미적 유사도를 계산하는 기술입니다.",
"벡터 DB(Vector Database)는 임베딩 벡터를 저장하고 유사도 검색하는 특화된 데이터베이스입니다.",
"Attention 메커니즘은 입력 시퀀스에서 관련 있는 부분에 집중하도록 가중치를 부여합니다.",
"Self-Attention은 시퀀스 내 모든 위치 간 관계를 병렬로 계산하는 메커니즘입니다.",
"Multi-Head Attention은 여러 Attention 헤드가 서로 다른 표현 공간에서 정보를 추출합니다.",
"Positional Encoding은 Transformer에 단어 순서 정보를 주입하는 방법입니다.",
"BERT는 양방향 컨텍스트를 이해하는 인코더 모델로, 분류·검색 등에 활용됩니다.",
"GPT는 다음 토큰을 예측하는 디코더 모델로, 텍스트 생성에 특화되어 있습니다.",
"Diffusion Model은 노이즈를 단계적으로 제거하여 이미지를 생성하는 모델입니다.",
"Stable Diffusion은 오픈소스 이미지 생성 AI로, Latent Space에서 디퓨전을 수행합니다.",
"GAN(Generative Adversarial Network)은 생성자와 판별자가 경쟁하며 학습하는 생성 모델입니다.",
"VAE(Variational Autoencoder)는 데이터의 잠재 분포를 학습하는 생성 모델입니다.",
"LoRA(Low-Rank Adaptation)는 소수의 파라미터만 학습하여 효율적으로 모델을 미세조정합니다.",
"QLoRA는 양자화된 모델에 LoRA를 적용하여 메모리 사용을 극적으로 줄이는 기법입니다.",
"PEFT(Parameter-Efficient Fine-Tuning)는 전체 파라미터 중 일부만 조정하는 효율적 학습 방법입니다.",
"양자화(Quantization)는 모델 가중치를 낮은 비트로 표현하여 추론 속도를 높이고 메모리를 줄입니다.",
"KV Cache는 이전 토큰의 Key/Value를 저장하여 자기회귀 생성 속도를 높이는 최적화입니다.",
"Speculative Decoding은 작은 모델이 초안을 생성하고 큰 모델이 검증하여 추론을 가속합니다.",
"Flash Attention은 메모리 효율적인 Attention 구현으로, GPU 메모리를 크게 절약합니다.",
"Mixture of Experts(MoE)는 입력에 따라 일부 전문가 네트워크만 활성화하는 효율적 아키텍처입니다.",
"Mamba는 선택적 상태 공간 모델(SSM)로, Transformer의 대안으로 연구되고 있습니다.",
"RWKV는 RNN과 Transformer의 장점을 결합한 선형 복잡도 언어 모델입니다.",
"Constitutional AI는 AI에게 원칙을 부여하고 자기 비판으로 정렬하는 Anthropic의 방법론입니다.",
"RLHF 대안으로 DPO(Direct Preference Optimization)가 인간 선호도를 직접 학습합니다.",
"Reinforcement Learning은 보상 신호를 통해 최적 행동 정책을 학습하는 머신러닝 방법입니다.",
"Transfer Learning은 한 도메인에서 학습한 지식을 다른 도메인에 적용하는 기법입니다.",
"Multi-modal AI는 텍스트, 이미지, 오디오 등 여러 형태의 데이터를 통합 처리합니다.",
"Vision-Language Model(VLM)은 이미지와 텍스트를 동시에 이해하는 멀티모달 모델입니다.",
"OCR(Optical Character Recognition)은 이미지에서 텍스트를 인식하여 디지털화하는 기술입니다.",
"TTS(Text-to-Speech)는 텍스트를 자연스러운 음성으로 변환하는 AI 기술입니다.",
"STT(Speech-to-Text)는 음성을 텍스트로 변환하는 AI 기술로, Whisper가 대표적입니다.",
"Whisper는 OpenAI의 다국어 음성 인식 모델로, 한국어도 높은 정확도를 보입니다.",
"AI Agent는 목표를 설정하고 도구를 사용하여 자율적으로 작업을 수행하는 AI 시스템입니다.",
"Function Calling은 LLM이 외부 함수를 호출하여 실제 작업을 수행하는 인터페이스입니다.",
"Tool Use는 AI가 검색, 계산, API 호출 등의 도구를 사용하여 답변 품질을 높이는 방식입니다.",
"ReAct(Reasoning + Acting)는 AI가 추론과 행동을 번갈아 수행하는 에이전트 프레임워크입니다.",
"Plan-and-Execute는 먼저 계획을 세우고 순차적으로 실행하는 에이전트 전략입니다.",
"Self-Reflection은 AI가 자신의 출력을 평가하고 개선하는 반성적 추론 기법입니다.",
"Tree-of-Thought는 여러 추론 경로를 탐색하여 최적 해를 찾는 고급 프롬프트 기법입니다.",
"AutoGPT는 목표를 주면 자율적으로 하위 작업을 생성하고 실행하는 초기 AI 에이전트입니다.",
"CrewAI는 여러 AI 에이전트가 역할을 분담하여 협력하는 멀티에이전트 프레임워크입니다.",
"LangChain은 LLM 애플리케이션 개발을 위한 인기 프레임워크로, 체인·에이전트·도구를 제공합니다.",
"LangGraph는 LangChain의 상태 기반 에이전트 프레임워크로, 복잡한 워크플로우를 지원합니다.",
"LlamaIndex는 데이터와 LLM을 연결하는 프레임워크로, RAG 구축에 특화되어 있습니다.",
"Semantic Kernel은 Microsoft의 AI 오케스트레이션 SDK로, .NET과 통합이 강점입니다.",
"Hugging Face는 AI 모델, 데이터셋, 학습 도구를 공유하는 오픈소스 플랫폼입니다.",
"Ollama는 로컬에서 LLM을 실행하는 도구로, 사내 AI 구축에 활용됩니다.",
"vLLM은 고성능 LLM 추론 엔진으로, PagedAttention으로 처리량을 극대화합니다.",
"TensorRT-LLM은 NVIDIA의 LLM 추론 최적화 라이브러리입니다.",
"ONNX Runtime은 다양한 프레임워크의 모델을 범용으로 실행하는 추론 엔진입니다.",
"MLOps는 머신러닝 모델의 개발·배포·운영을 체계적으로 관리하는 방법론입니다.",
"ML Pipeline은 데이터 수집→전처리→학습→평가→배포를 자동화하는 워크플로우입니다.",
"Feature Store는 ML에 사용되는 특성 데이터를 중앙에서 관리·공유하는 저장소입니다.",
"데이터 레이블링은 AI 학습에 필요한 주석(Annotation)을 데이터에 부여하는 작업입니다.",
"Active Learning은 모델이 불확실한 데이터를 선별하여 효율적으로 레이블링하는 방법입니다.",
"Federated Learning은 데이터를 중앙에 모으지 않고 분산된 장치에서 학습하는 기법입니다.",
"Edge AI는 클라우드 없이 엣지 디바이스(스마트폰, IoT)에서 AI를 실행하는 기술입니다.",
"On-Device AI는 기기 내에서 직접 AI 추론을 수행하여 지연시간과 프라이버시를 개선합니다.",
"NPU(Neural Processing Unit)는 AI 연산에 특화된 프로세서입니다.",
"GPU는 병렬 연산에 강점이 있어 딥러닝 학습과 추론의 핵심 하드웨어입니다.",
"TPU(Tensor Processing Unit)는 Google이 설계한 텐서 연산 특화 AI 칩입니다.",
"CUDA는 NVIDIA GPU에서 범용 병렬 컴퓨팅을 수행하는 플랫폼입니다.",
"PyTorch는 동적 계산 그래프를 지원하는 딥러닝 프레임워크로, 연구 분야에서 가장 인기 있습니다.",
"TensorFlow는 Google의 딥러닝 프레임워크로, 프로덕션 배포에 강점이 있습니다.",
"JAX는 Google의 고성능 수치 연산 라이브러리로, 자동 미분과 XLA 컴파일을 지원합니다.",
"Jupyter Notebook은 코드, 시각화, 설명을 한 문서에서 작성하는 대화형 개발 환경입니다.",
"Docker는 애플리케이션을 컨테이너로 패키징하여 환경 차이 없이 배포하는 기술입니다.",
"Kubernetes(K8s)는 컨테이너 오케스트레이션 플랫폼으로, 자동 스케일링과 배포를 관리합니다.",
"CI/CD는 코드 변경을 자동으로 빌드·테스트·배포하는 지속적 통합/배포 파이프라인입니다.",
"GitHub Actions는 워크플로우를 자동화하는 CI/CD 도구로, YAML로 정의합니다.",
"Git은 분산 버전 관리 시스템으로, 코드 이력 추적과 협업의 핵심 도구입니다.",
"GitHub Copilot은 AI 페어 프로그래밍 도구로, 코드 자동 완성과 제안을 제공합니다.",
"Claude Code는 Anthropic의 터미널 기반 AI 코딩 에이전트로, SWE-bench에서 높은 성능을 보입니다.",
"Cursor는 AI 기반 코드 편집기로, 멀티에이전트 구조와 코드 이해 능력이 강점입니다.",
"OpenCode는 오픈소스 AI 코딩 에이전트로, Go 언어로 작성되어 있습니다.",
"VS Code는 Microsoft의 경량 코드 편집기로, 풍부한 확장 생태계를 보유합니다.",
"LSP(Language Server Protocol)는 에디터와 언어 서버 간 통신 표준 프로토콜입니다.",
"REST API는 HTTP 기반의 웹 서비스 인터페이스로, 리소스 중심 설계를 따릅니다.",
"GraphQL은 클라이언트가 필요한 데이터만 정확히 요청할 수 있는 쿼리 언어입니다.",
"gRPC는 Google의 고성능 RPC 프레임워크로, Protocol Buffers를 사용합니다.",
"WebSocket은 서버-클라이언트 간 양방향 실시간 통신을 지원하는 프로토콜입니다.",
"SSE(Server-Sent Events)는 서버에서 클라이언트로 단방향 실시간 데이터를 스트리밍합니다.",
"OAuth 2.0은 제3자 애플리케이션에 안전하게 권한을 위임하는 인증 프레임워크입니다.",
"JWT(JSON Web Token)은 당사자 간 정보를 안전하게 전달하는 컴팩트한 토큰입니다.",
"마이크로서비스는 애플리케이션을 독립적인 서비스 단위로 분리하여 개발·배포하는 아키텍처입니다.",
"서버리스(Serverless)는 서버 관리 없이 함수 단위로 코드를 실행하는 클라우드 컴퓨팅 모델입니다.",
"AWS Lambda는 이벤트 기반 서버리스 컴퓨팅 서비스입니다.",
"Azure Functions는 Microsoft의 서버리스 컴퓨팅 플랫폼입니다.",
"Cloud Native는 클라우드 환경에 최적화된 애플리케이션 설계·개발 방법론입니다.",
"IaC(Infrastructure as Code)는 인프라를 코드로 정의하고 관리하는 방식입니다.",
"Terraform은 HashiCorp의 IaC 도구로, 멀티 클라우드 인프라를 선언적으로 관리합니다.",
"Ansible은 에이전트 없이 서버를 자동화 구성하는 IT 자동화 도구입니다.",
"DevOps는 개발과 운영의 협업을 강화하여 소프트웨어 배포 속도와 품질을 높이는 문화입니다.",
"SRE(Site Reliability Engineering)는 Google이 정립한 운영 엔지니어링 방법론입니다.",
"Observability는 로그·메트릭·트레이스로 시스템 내부 상태를 파악하는 능력입니다.",
"Prometheus는 시계열 메트릭 수집·쿼리를 위한 오픈소스 모니터링 시스템입니다.",
"Grafana는 다양한 데이터 소스의 메트릭을 시각화하는 대시보드 도구입니다.",
"ELK Stack(Elasticsearch, Logstash, Kibana)은 로그 수집·검색·시각화 플랫폼입니다.",
"Redis는 인메모리 키-값 저장소로, 캐시·세션·메시지 브로커에 활용됩니다.",
"Kafka는 대규모 실시간 데이터 스트리밍을 위한 분산 이벤트 플랫폼입니다.",
"RabbitMQ는 AMQP 기반 메시지 브로커로, 비동기 작업 처리에 활용됩니다.",
"PostgreSQL은 확장성 높은 오픈소스 관계형 데이터베이스로, JSON 지원이 강점입니다.",
"MongoDB는 문서(Document) 기반 NoSQL 데이터베이스로, 유연한 스키마가 특징입니다.",
"Apache Spark는 대규모 데이터 처리를 위한 인메모리 분산 컴퓨팅 프레임워크입니다.",
"Apache Airflow는 데이터 파이프라인을 DAG로 정의하고 스케줄링하는 워크플로우 관리 도구입니다.",
"dbt(data build tool)는 SQL 기반 데이터 변환 도구로, 분석 엔지니어링의 핵심입니다.",
"Snowflake는 클라우드 네이티브 데이터 웨어하우스로, 스토리지와 컴퓨팅을 분리합니다.",
"Apache Iceberg는 대규모 분석 테이블을 위한 오픈소스 테이블 포맷입니다.",
"Data Lakehouse는 데이터 레이크와 데이터 웨어하우스의 장점을 결합한 아키텍처입니다.",
"ETL(Extract, Transform, Load)은 데이터를 추출·변환·적재하는 데이터 통합 프로세스입니다.",
"Pandas는 Python의 데이터 분석 라이브러리로, DataFrame으로 구조화 데이터를 처리합니다.",
"NumPy는 Python의 고성능 수치 연산 라이브러리로, 다차원 배열을 지원합니다.",
"scikit-learn은 Python의 머신러닝 라이브러리로, 분류·회귀·클러스터링 등을 제공합니다.",
"XGBoost는 그래디언트 부스팅 알고리즘으로, 정형 데이터 분류·회귀에서 높은 성능을 보입니다.",
"AutoML은 머신러닝 파이프라인을 자동으로 설계하고 최적화하는 기술입니다.",
"NLP(Natural Language Processing)는 컴퓨터가 인간 언어를 이해·생성하는 AI 분야입니다.",
"NER(Named Entity Recognition)은 텍스트에서 인명·지명·기관명 등을 식별하는 NLP 작업입니다.",
"감정 분석(Sentiment Analysis)은 텍스트의 긍정·부정·중립 감정을 판별하는 NLP 기술입니다.",
"요약(Summarization)은 긴 텍스트의 핵심 내용을 짧게 추출하는 NLP 기술입니다.",
"기계 번역(Machine Translation)은 AI가 한 언어를 다른 언어로 번역하는 기술입니다.",
"Computer Vision은 컴퓨터가 이미지·영상을 분석하고 이해하는 AI 분야입니다.",
"Object Detection은 이미지에서 객체의 위치와 종류를 식별하는 컴퓨터 비전 기술입니다.",
"YOLO(You Only Look Once)는 실시간 객체 탐지를 위한 딥러닝 모델입니다.",
"Semantic Segmentation은 이미지의 각 픽셀에 클래스 라벨을 부여하는 기술입니다.",
"ViT(Vision Transformer)는 Transformer 아키텍처를 이미지 분류에 적용한 모델입니다.",
"CLIP은 이미지와 텍스트를 동일한 임베딩 공간에 매핑하는 OpenAI의 멀티모달 모델입니다.",
"Zero Trust는 '아무것도 신뢰하지 않는다'는 원칙의 보안 아키텍처입니다.",
"SASE(Secure Access Service Edge)는 네트워크 보안과 WAN을 클라우드에서 통합합니다.",
"FIDO2는 비밀번호 없이 생체 인증·보안 키로 로그인하는 웹 인증 표준입니다.",
"Passkey는 FIDO2 기반의 비밀번호 대체 인증 방식으로, 피싱에 강합니다.",
"SSL/TLS는 인터넷 통신을 암호화하여 보안을 보장하는 프로토콜입니다.",
"AES-256은 256비트 키를 사용하는 대칭 암호화 표준으로, 매우 강력한 보안을 제공합니다.",
"SHA-256은 256비트 해시를 생성하는 암호학적 해시 함수입니다.",
"Blockchain은 거래 내역을 분산 원장에 체인 형태로 기록하는 기술입니다.",
"Smart Contract는 블록체인 위에서 자동 실행되는 프로그래밍 가능한 계약입니다.",
"Web3는 블록체인 기반의 탈중앙화된 인터넷 패러다임입니다.",
"NFT(Non-Fungible Token)는 디지털 자산의 고유성과 소유권을 증명하는 토큰입니다.",
"DeFi(Decentralized Finance)는 블록체인 기반 탈중앙화 금융 시스템입니다.",
"메타버스(Metaverse)는 3D 가상 공간에서 사회·경제 활동을 하는 디지털 세계입니다.",
"XR(Extended Reality)은 VR, AR, MR을 포괄하는 확장 현실 기술입니다.",
"AR(Augmented Reality)은 현실 세계에 가상 정보를 겹쳐 보여주는 기술입니다.",
"VR(Virtual Reality)은 완전한 가상 환경을 체험하는 몰입형 기술입니다.",
"Digital Twin은 물리적 자산을 디지털로 복제하여 시뮬레이션·예측하는 기술입니다.",
"IoT(Internet of Things)는 사물에 센서와 통신을 부여하여 인터넷에 연결하는 기술입니다.",
"MQTT는 IoT에서 경량 메시지를 교환하는 발행-구독 프로토콜입니다.",
"5G는 초고속(최대 20Gbps), 초저지연(1ms), 초연결(㎢당 100만 기기)을 지원하는 이동통신입니다.",
"Wi-Fi 7(802.11be)은 최대 46Gbps, 320MHz 대역폭을 지원하는 차세대 무선 통신입니다.",
"RPA(Robotic Process Automation)는 반복적 업무를 소프트웨어 로봇으로 자동화하는 기술입니다.",
"로우코드/노코드(Low-Code/No-Code)는 최소한의 코딩으로 앱을 개발하는 플랫폼입니다.",
"SaaS(Software as a Service)는 소프트웨어를 클라우드에서 구독 형태로 제공하는 모델입니다.",
"PaaS(Platform as a Service)는 애플리케이션 개발 플랫폼을 클라우드로 제공합니다.",
"IaaS(Infrastructure as a Service)는 서버·스토리지 등 IT 인프라를 클라우드로 제공합니다.",
"CDN(Content Delivery Network)은 콘텐츠를 전 세계 엣지 서버에 분산하여 빠르게 전달합니다.",
"DNS(Domain Name System)는 도메인 이름을 IP 주소로 변환하는 인터넷의 주소록입니다.",
"API Gateway는 API 요청을 라우팅하고, 인증·속도 제한·모니터링을 담당합니다.",
"Service Mesh는 마이크로서비스 간 통신을 관리하는 인프라 계층입니다.",
"Istio는 쿠버네티스 환경의 서비스 메시를 관리하는 오픈소스 플랫폼입니다.",
"Helm은 쿠버네티스 패키지 매니저로, 앱 배포를 차트(Chart)로 관리합니다.",
"ArgoCD는 쿠버네티스에 GitOps 방식으로 애플리케이션을 배포하는 도구입니다.",
"GitOps는 Git을 단일 진실의 원천으로 삼아 인프라와 앱을 관리하는 방법론입니다.",
"Trunk-Based Development는 하나의 메인 브랜치에서 지속적으로 통합하는 Git 전략입니다.",
"Feature Flag는 코드 배포와 기능 출시를 분리하여 점진적으로 기능을 활성화합니다.",
"A/B Testing은 두 버전을 비교하여 어떤 것이 더 효과적인지 실험하는 방법입니다.",
"Canary Deployment는 새 버전을 소수 사용자에게 먼저 배포하여 위험을 줄이는 전략입니다.",
"Blue-Green Deployment는 두 환경을 번갈아 사용하여 무중단 배포를 구현합니다.",
"Chaos Engineering은 의도적으로 시스템 장애를 유발하여 복원력을 검증하는 방법론입니다.",
"CQRS(Command Query Responsibility Segregation)는 읽기와 쓰기를 분리하는 아키텍처입니다.",
"Event Sourcing은 상태 변경을 이벤트로 기록하여 모든 이력을 추적하는 패턴입니다.",
"Domain-Driven Design(DDD)는 비즈니스 도메인 중심으로 소프트웨어를 설계하는 방법론입니다.",
"Clean Architecture는 의존성 방향을 안쪽으로 향하게 하여 유지보수성을 높이는 아키텍처입니다.",
"Hexagonal Architecture(포트와 어댑터)는 애플리케이션 핵심을 외부로부터 분리합니다.",
"SOLID는 객체지향 설계의 5대 원칙(SRP, OCP, LSP, ISP, DIP)입니다.",
"단일 책임 원칙(SRP)은 클래스는 하나의 변경 이유만 가져야 한다는 원칙입니다.",
"개방-폐쇄 원칙(OCP)은 확장에는 열려 있고 수정에는 닫혀 있어야 한다는 원칙입니다.",
"의존성 역전 원칙(DIP)은 고수준 모듈이 저수준 모듈에 의존하지 않아야 한다는 원칙입니다.",
"TDD(Test-Driven Development)는 테스트를 먼저 작성하고 코드를 구현하는 개발 방법입니다.",
"BDD(Behavior-Driven Development)는 비즈니스 행동 시나리오로 테스트를 작성합니다.",
"단위 테스트(Unit Test)는 함수/메서드 단위로 동작을 검증하는 테스트입니다.",
"통합 테스트(Integration Test)는 여러 컴포넌트가 함께 동작하는지 검증합니다.",
"E2E 테스트(End-to-End Test)는 사용자 관점에서 전체 시스템을 검증합니다.",
"코드 커버리지는 테스트가 코드의 몇 퍼센트를 실행하는지 측정하는 지표입니다.",
"정적 분석(Static Analysis)은 코드를 실행하지 않고 잠재적 버그와 취약점을 찾습니다.",
"린트(Lint)는 코드 스타일과 잠재적 오류를 자동으로 검사하는 도구입니다.",
"코드 리뷰(Code Review)는 다른 개발자가 코드 변경을 검토하여 품질을 높이는 프로세스입니다.",
"리팩터링(Refactoring)은 기능 변경 없이 코드 구조를 개선하는 작업입니다.",
"기술 부채(Technical Debt)는 빠른 개발을 위해 희생한 코드 품질이 누적된 비용입니다.",
"아키텍처 결정 기록(ADR)은 중요한 설계 결정과 그 이유를 문서화합니다.",
"API First Design은 API 명세를 먼저 정의하고 구현하는 개발 방법입니다.",
"OpenAPI(Swagger)는 REST API를 문서화하는 표준 명세입니다.",
"Protobuf(Protocol Buffers)는 Google의 효율적인 바이너리 직렬화 형식입니다.",
"JSON은 웹에서 가장 널리 사용되는 텍스트 기반 데이터 교환 형식입니다.",
"YAML은 사람이 읽기 쉬운 데이터 직렬화 형식으로, 설정 파일에 많이 사용됩니다.",
"TOML은 설정 파일을 위한 직관적인 형식으로, Rust의 Cargo.toml에서 유명합니다.",
"Markdown은 경량 마크업 언어로, 문서 작성과 README에 널리 사용됩니다.",
"LaTeX는 학술 논문과 수식 작성에 특화된 조판 시스템입니다.",
"Unicode는 전 세계 모든 문자를 단일 체계로 표현하는 국제 문자 인코딩 표준입니다.",
"UTF-8은 유니코드를 가변 길이로 인코딩하는 방식으로, 웹의 96% 이상이 사용합니다.",
"Base64는 바이너리 데이터를 ASCII 문자로 인코딩하는 방식으로, 이메일과 웹에서 사용됩니다.",
"정규표현식(Regex)은 문자열 패턴을 정의하여 검색·치환하는 강력한 도구입니다.",
"빅오 표기법(Big-O)은 알고리즘의 시간·공간 복잡도를 표현하는 수학적 표기법입니다.",
"해시 테이블은 키-값 쌍을 O(1)에 검색하는 자료구조입니다.",
"이진 탐색(Binary Search)은 정렬된 배열에서 O(log n)에 원소를 찾는 알고리즘입니다.",
"그래프 알고리즘(BFS/DFS)은 노드와 간선으로 구성된 자료구조를 탐색합니다.",
"동적 프로그래밍(DP)은 하위 문제를 저장하여 중복 계산을 방지하는 알고리즘 기법입니다.",
"Rust는 메모리 안전성을 보장하는 시스템 프로그래밍 언어로, C++의 대안으로 부상 중입니다.",
"Go(Golang)는 Google이 만든 간결한 언어로, 동시성 지원과 빠른 컴파일이 특징입니다.",
"TypeScript는 JavaScript에 정적 타입을 추가한 언어로, 대규모 프로젝트에서 선호됩니다.",
"Kotlin은 JVM 기반 언어로, Android 공식 개발 언어이며 Java와 100% 호환됩니다.",
"Swift는 Apple 생태계의 주력 언어로, 안전성과 성능이 뛰어납니다.",
"Python은 배우기 쉽고 생태계가 풍부한 범용 프로그래밍 언어로, AI/데이터 분야 1위입니다.",
"C#은 Microsoft의 .NET 플랫폼 언어로, 엔터프라이즈 애플리케이션과 게임 개발에 사용됩니다.",
"Java는 플랫폼 독립적인 객체지향 언어로, 엔터프라이즈 시스템의 표준입니다.",
"React는 Facebook의 UI 라이브러리로, 컴포넌트 기반 프론트엔드 개발의 표준입니다.",
"Vue.js는 점진적 JavaScript 프레임워크로, 학습이 쉽고 유연한 것이 특징입니다.",
"Next.js는 React 기반 풀스택 프레임워크로, SSR과 ISR을 지원합니다.",
"Nuxt는 Vue.js 기반 풀스택 프레임워크로, SSR, SSG, ISR을 모두 지원합니다.",
"Svelte는 컴파일러 기반 프레임워크로, 가상 DOM 없이 빠른 UI를 구현합니다.",
"Tailwind CSS는 유틸리티 퍼스트 CSS 프레임워크로, 클래스 조합으로 디자인합니다.",
"WebAssembly(Wasm)는 브라우저에서 네이티브에 가까운 성능으로 코드를 실행합니다.",
"Progressive Web App(PWA)은 웹 기술로 네이티브 앱 수준의 경험을 제공합니다.",
"Electron은 웹 기술(HTML/CSS/JS)로 데스크톱 앱을 만드는 프레임워크입니다.",
"Tauri는 Rust 기반 경량 데스크톱 앱 프레임워크로, Electron의 대안입니다.",
"Flutter는 Google의 크로스플랫폼 UI 프레임워크로, Dart 언어를 사용합니다.",
"React Native는 React로 iOS/Android 네이티브 앱을 개발하는 프레임워크입니다.",
".NET MAUI는 Microsoft의 크로스플랫폼 앱 프레임워크로, .NET으로 개발합니다.",
"WPF(Windows Presentation Foundation)는 .NET 기반 Windows 데스크톱 UI 프레임워크입니다.",
"Blazor는 C#으로 웹 UI를 개발하는 .NET 프레임워크입니다.",
"Spring Boot는 Java 엔터프라이즈 애플리케이션을 빠르게 개발하는 프레임워크입니다.",
"FastAPI는 Python의 고성능 비동기 웹 프레임워크로, 자동 문서화가 강점입니다.",
"Django는 Python의 풀스택 웹 프레임워크로, '배터리 포함' 철학을 따릅니다.",
"Flask는 Python의 경량 웹 프레임워크로, 마이크로 프레임워크 철학을 따릅니다.",
"NestJS는 TypeScript 기반 Node.js 백엔드 프레임워크로, Angular 스타일의 모듈 구조입니다.",
"Express.js는 Node.js의 최소한의 웹 프레임워크로, 가장 널리 사용됩니다.",
"Deno는 Node.js 창시자가 만든 보안 우선 JavaScript/TypeScript 런타임입니다.",
"Bun은 초고속 JavaScript 런타임으로, 번들러·패키지 매니저·테스트 러너를 내장합니다.",
"npm은 Node.js의 기본 패키지 매니저로, 세계 최대 소프트웨어 레지스트리입니다.",
"pnpm은 디스크 공간 효율적인 Node.js 패키지 매니저로, 하드링크를 활용합니다.",
"Conda는 Python의 환경·패키지 관리 도구로, 과학 계산 분야에서 널리 사용됩니다.",
"pip는 Python의 표준 패키지 설치 도구입니다.",
"Poetry는 Python 프로젝트 의존성과 빌드를 관리하는 도구로, pyproject.toml을 사용합니다.",
"NuGet은 .NET의 패키지 관리자로, 라이브러리를 검색·설치·관리합니다.",
"Maven은 Java 프로젝트 빌드·의존성 관리 도구로, pom.xml을 사용합니다.",
"Gradle은 JVM 프로젝트 빌드 도구로, Groovy/Kotlin DSL로 빌드 스크립트를 작성합니다.",
"CMake는 C/C++ 프로젝트의 크로스플랫폼 빌드 시스템 생성기입니다.",
"MSBuild는 .NET 프로젝트를 빌드하는 Microsoft의 빌드 엔진입니다.",
"Bazel은 Google의 대규모 프로젝트를 위한 빌드·테스트 시스템입니다.",
"Agile은 짧은 반복 주기로 소프트웨어를 개발하는 방법론입니다.",
"Scrum은 스프린트 단위로 작업을 관리하는 Agile 프레임워크입니다.",
"Kanban은 작업 흐름을 시각화하고 WIP를 제한하여 효율을 높이는 관리 방법입니다.",
"Sprint은 Scrum에서 1~4주 단위의 개발 반복 주기입니다.",
"Stand-up Meeting은 매일 짧게 진행하는 팀 상태 공유 회의입니다.",
"OKR(Objectives and Key Results)은 목표와 핵심 결과를 설정하는 성과 관리 프레임워크입니다.",
"SWE-bench는 AI 코딩 에이전트의 소프트웨어 엔지니어링 능력을 벤치마킹합니다.",
"HumanEval은 LLM의 코드 생성 능력을 측정하는 OpenAI의 벤치마크입니다.",
"MMLU는 다양한 학문 분야의 이해도를 측정하는 AI 벤치마크입니다.",
"Perplexity는 언어 모델의 예측 불확실성을 측정하는 지표입니다.",
"BLEU는 기계 번역 품질을 평가하는 자동 메트릭입니다.",
"ROUGE는 요약 품질을 평가하는 메트릭으로, 정답과의 겹침을 측정합니다.",
"Synthetic Data는 실제 데이터의 통계적 특성을 모방하여 인공적으로 생성한 데이터입니다.",
"Data Augmentation은 기존 데이터를 변환하여 학습 데이터를 확장하는 기법입니다.",
"Knowledge Distillation은 큰 모델의 지식을 작은 모델에 전이하는 모델 압축 기법입니다.",
"Pruning은 신경망에서 중요도가 낮은 연결을 제거하여 모델을 경량화합니다.",
"Batch Normalization은 학습 중 각 층의 입력을 정규화하여 학습을 안정화합니다.",
"Dropout은 학습 시 일부 뉴런을 무작위로 비활성화하여 과적합을 방지합니다.",
"Learning Rate Scheduling은 학습률을 동적으로 조절하여 최적화를 개선합니다.",
"Adam Optimizer는 적응형 학습률을 사용하는 효율적인 경사 하강 최적화 알고리즘입니다.",
"Gradient Descent는 손실 함수를 최소화하기 위해 파라미터를 반복적으로 조정합니다.",
"Backpropagation은 신경망에서 그래디언트를 역전파하여 가중치를 업데이트합니다.",
"Cross-Entropy Loss는 분류 문제에서 예측과 실제 분포의 차이를 측정합니다.",
"Softmax는 출력값을 확률 분포로 변환하는 활성화 함수입니다.",
"ReLU(Rectified Linear Unit)는 음수를 0으로 만드는 가장 널리 사용되는 활성화 함수입니다.",
"GeLU는 Gaussian Error Linear Unit으로, Transformer에서 주로 사용되는 활성화 함수입니다.",
"Convolution Neural Network(CNN)은 이미지 처리에 특화된 신경망 아키텍처입니다.",
"Recurrent Neural Network(RNN)은 시퀀스 데이터 처리를 위한 순환 신경망입니다.",
"LSTM(Long Short-Term Memory)은 장기 의존성을 학습하는 RNN 변형입니다.",
"GRU(Gated Recurrent Unit)는 LSTM보다 단순한 게이트 구조의 RNN 변형입니다.",
"Autoencoder는 입력을 압축(인코딩)했다가 복원(디코딩)하며 표현을 학습합니다.",
"Contrastive Learning은 유사한 쌍은 가깝게, 다른 쌍은 멀게 학습하는 자기지도 학습입니다.",
"Self-Supervised Learning은 레이블 없이 데이터 자체에서 학습 신호를 생성합니다.",
"Few-Shot Learning은 소수의 예시만으로 새로운 클래스를 학습하는 기법입니다.",
"Meta-Learning은 '학습하는 방법을 학습'하여 새로운 작업에 빠르게 적응합니다.",
"Prompt Tuning은 모델 파라미터를 고정하고 프롬프트만 학습하는 효율적 방법입니다.",
"Instruction Tuning은 지시-응답 쌍으로 학습하여 모델의 지시 따르기 능력을 향상시킵니다.",
"GGUF는 양자화된 LLM 모델의 파일 포맷으로, Ollama와 llama.cpp에서 사용됩니다.",
"Safetensors는 Hugging Face의 안전한 모델 직렬화 형식으로, pickle보다 보안이 강합니다.",
"Tokenizer는 텍스트를 토큰으로 분할하는 도구로, BPE/WordPiece/SentencePiece 등이 있습니다.",
"BPE(Byte Pair Encoding)는 빈번한 바이트 쌍을 반복적으로 병합하는 토크나이저입니다.",
"Temperature는 LLM 출력의 무작위성을 조절하는 파라미터입니다 (높을수록 창의적).",
"Top-p(Nucleus Sampling)는 누적 확률이 p 이하인 토큰만 샘플링합니다.",
"Top-k Sampling은 확률이 높은 상위 k개 토큰에서만 샘플링합니다.",
"Beam Search는 여러 후보 시퀀스를 동시에 탐색하여 최적 출력을 생성합니다.",
"Structured Output은 LLM이 JSON 등 정해진 형식으로 응답을 생성하게 합니다.",
"System Prompt는 AI의 역할과 행동 규칙을 정의하는 초기 지시문입니다.",
"Context Window Scaling은 더 긴 문맥을 처리하기 위해 위치 인코딩을 확장하는 기술입니다.",
"RoPE(Rotary Position Embedding)는 회전 변환으로 위치 정보를 인코딩합니다.",
"ALiBi(Attention with Linear Biases)는 학습 없이 컨텍스트 길이를 확장합니다.",
"Sliding Window Attention은 지역 컨텍스트에만 어텐션을 제한하여 효율을 높입니다.",
"Sparse Attention은 일부 위치에만 선택적으로 어텐션을 적용합니다.",
"Retrieval-Augmented LLM은 검색 결과를 컨텍스트에 추가하여 답변 품질을 높입니다.",
"Knowledge Graph는 엔티티와 관계를 그래프로 표현하여 구조화된 지식을 저장합니다.",
"Agentic RAG는 에이전트가 필요한 정보를 능동적으로 검색하는 고급 RAG 기법입니다.",
"GraphRAG는 지식 그래프와 RAG를 결합하여 복잡한 추론을 지원합니다.",
"Multi-Agent System은 여러 AI 에이전트가 협력하여 복잡한 작업을 수행합니다.",
"Orchestration은 여러 AI 모델과 도구를 조율하여 워크플로우를 실행합니다.",
"AI Gateway는 여러 AI 서비스를 통합 관리하는 프록시 계층입니다.",
"Model Registry는 학습된 모델의 버전, 메타데이터, 배포 상태를 관리합니다.",
"A/B Testing for Models는 여러 AI 모델의 성능을 실제 트래픽으로 비교 실험합니다.",
"Canary Release for AI는 새 모델을 소수 사용자에게 먼저 배포하여 검증합니다.",
"Shadow Mode는 새 모델을 실제 서비스와 병행 실행하되 응답은 기존 모델만 반환합니다.",
"AI Safety는 AI 시스템이 의도대로 동작하고 해를 끼치지 않도록 보장하는 분야입니다.",
"AI Alignment은 AI의 행동을 인간의 가치와 의도에 맞게 정렬하는 연구입니다.",
"Explainable AI(XAI)는 AI의 결정 과정을 인간이 이해할 수 있도록 설명합니다.",
"Responsible AI는 공정성, 투명성, 프라이버시를 고려한 AI 개발 원칙입니다.",
"AI Ethics는 AI 기술의 사회적 영향과 윤리적 사용에 관한 규범입니다.",
"Bias in AI는 학습 데이터의 편향이 AI 결정에 반영되는 문제입니다.",
"Fairness in AI는 AI 시스템이 모든 그룹에게 공정하게 작동하도록 보장합니다.",
"AI Regulation은 AI 기술의 안전한 사용을 위한 법적·제도적 규제입니다.",
"EU AI Act는 AI 시스템을 위험 등급별로 규제하는 유럽연합의 법률입니다.",
"AGI(Artificial General Intelligence)는 인간 수준의 범용 지능을 가진 AI 개념입니다.",
"ASI(Artificial Superintelligence)는 인간 지능을 초월하는 AI 개념입니다.",
"Narrow AI는 특정 작업에 특화된 현재의 AI 시스템입니다.",
"AI Governance는 조직 내에서 AI를 안전하고 책임감 있게 운영하기 위한 체계입니다.",
"Prompt Injection은 악의적 프롬프트로 AI의 안전장치를 우회하려는 공격입니다.",
"Jailbreaking은 AI 모델의 제한사항을 우회하여 금지된 출력을 유도하려는 시도입니다.",
"Red Teaming은 AI 시스템의 취약점을 발견하기 위해 의도적으로 공격하는 테스트입니다.",
"Guardrails은 AI 출력이 안전하고 적절한 범위 내에 있도록 제한하는 메커니즘입니다.",
"Content Filtering은 AI가 생성한 유해 콘텐츠를 탐지하고 차단합니다.",
"Watermarking은 AI가 생성한 콘텐츠에 보이지 않는 식별자를 삽입합니다.",
"Deepfake는 AI로 생성한 가짜 영상·음성으로, 탐지 기술이 함께 발전 중입니다.",
"AI-generated Content Detection은 텍스트가 AI에 의해 생성되었는지 판별합니다.",
"Copilot Pattern은 AI가 인간의 작업을 보조하는 협력적 인터페이스 패턴입니다.",
"AI-native Application은 AI를 핵심 기능으로 설계된 차세대 소프트웨어입니다.",
"OpenTelemetry는 분산 추적·메트릭·로그를 표준화하는 오픈소스 관측 프레임워크입니다.",
"eBPF는 리눅스 커널에서 안전하게 프로그램을 실행하는 기술로, 네트워크·보안·관측에 활용됩니다.",
"WebGPU는 브라우저에서 GPU 컴퓨팅을 수행하는 차세대 웹 그래픽스 API입니다.",
"HTMX는 HTML 속성으로 AJAX 요청을 처리하는 경량 라이브러리입니다.",
"Edge Computing은 데이터를 발생 지점 근처에서 처리하여 지연시간을 줄입니다.",
"Quantum Computing은 양자역학 원리로 특정 문제를 기존 컴퓨터보다 빠르게 풉니다.",
"Post-Quantum Cryptography는 양자 컴퓨터에도 안전한 암호화 알고리즘입니다.",
"Homomorphic Encryption은 암호화된 상태에서 연산을 수행하는 기술입니다.",
"Confidential Computing은 사용 중인 데이터도 암호화하여 보호합니다.",
"FinOps는 클라우드 비용을 최적화하기 위한 재무 운영 방법론입니다.",
"GreenOps는 IT 운영의 탄소 배출을 줄이는 환경 친화적 운영 방법입니다.",
"Sustainable AI는 AI 학습과 추론의 에너지 소비를 줄이기 위한 연구입니다.",
"Carbon-aware Computing은 탄소 배출이 낮은 시간과 지역에서 워크로드를 실행합니다.",
"AIOps는 AI로 IT 운영을 자동화하여 장애를 예측·진단·해결합니다.",
"Platform Engineering은 개발자 경험을 위한 내부 플랫폼을 구축하는 방법론입니다.",
"Internal Developer Platform(IDP)은 개발팀에게 셀프서비스 인프라를 제공합니다.",
"Developer Experience(DX)는 개발자가 도구와 프로세스를 사용하는 경험의 질입니다.",
"Documentation as Code는 문서를 코드와 동일하게 버전 관리하는 방법입니다.",
"API-first Company는 API를 핵심 제품으로 삼는 비즈니스 모델입니다.",
"Composable Architecture는 독립적인 비즈니스 기능을 조합하여 앱을 구성합니다.",
"Event-Driven Architecture는 이벤트를 중심으로 시스템을 설계하는 아키텍처입니다.",
"CNCF(Cloud Native Computing Foundation)는 클라우드 네이티브 오픈소스를 관리하는 재단입니다.",
"OpenAI는 GPT 시리즈를 개발한 AI 연구소로, ChatGPT를 출시했습니다.",
"Anthropic은 Claude를 개발한 AI 안전 연구소로, Constitutional AI가 핵심입니다.",
"Google DeepMind는 AlphaFold, Gemini 등을 개발한 AI 연구소입니다.",
"Meta AI는 LLaMA 오픈소스 모델을 공개하여 AI 민주화에 기여하고 있습니다.",
"Mistral AI는 프랑스의 AI 스타트업으로, 효율적인 오픈 모델을 제공합니다.",
"Cohere는 기업용 LLM과 RAG 솔루션을 제공하는 AI 기업입니다.",
"Perplexity AI는 AI 기반 검색 엔진으로, 출처를 명시하는 답변을 제공합니다.",
"Midjourney는 텍스트로부터 고품질 이미지를 생성하는 AI 서비스입니다.",
"Runway는 AI 기반 동영상 생성·편집 도구입니다.",
"Sora는 OpenAI의 텍스트-투-비디오 생성 AI 모델입니다.",
"Kling은 중국 Kuaishou의 텍스트-투-비디오 AI 모델입니다.",
"NotebookLM은 Google의 AI 노트북으로, 문서를 업로드하면 AI가 분석·요약합니다.",
"Artifacts는 Claude의 코드·문서·다이어그램 생성 기능입니다.",
"Computer Use는 AI가 실제 컴퓨터 화면을 보고 마우스·키보드를 조작하는 기술입니다.",
"Model Card는 AI 모델의 성능, 제한사항, 용도를 문서화한 표준 양식입니다.",
"Datasheet for Datasets는 데이터셋의 수집 방법, 편향, 용도를 기록합니다.",
"Evaluation Harness는 LLM 벤치마크를 체계적으로 실행하는 프레임워크입니다.",
"LLM Leaderboard는 다양한 벤치마크에서 LLM 성능을 비교하는 순위표입니다.",
"Chatbot Arena는 사용자 투표로 LLM 성능을 비교하는 커뮤니티 벤치마크입니다.",
"ELO Rating은 체스에서 유래한 상대적 실력 평가 시스템으로, LLM 비교에 활용됩니다.",
"Scaling Law는 모델·데이터·컴퓨팅 규모와 성능의 관계를 설명하는 법칙입니다.",
"Chinchilla Scaling은 모델 크기와 데이터 양의 최적 비율을 제시합니다.",
"Inference Scaling은 추론 시 컴퓨팅을 더 투입하여 성능을 높이는 접근입니다.",
"Test-Time Compute는 추론 시점에 추가 연산으로 답변 품질을 향상시킵니다.",
"o1/o3 Reasoning은 OpenAI의 추론 특화 모델로, 단계적 사고를 수행합니다.",
"Extended Thinking은 Claude의 심층 추론 모드로, 복잡한 문제에서 정확도가 높습니다.",
"Streaming은 LLM 응답을 토큰 단위로 실시간 전송하여 체감 지연을 줄입니다.",
"Batch Processing은 여러 요청을 모아 한 번에 처리하여 처리량을 높입니다.",
"Caching은 반복 요청의 결과를 저장하여 재사용하는 성능 최적화 기법입니다.",
"Semantic Caching은 의미적으로 유사한 쿼리의 캐시를 재사용합니다.",
"Prefix Caching은 공통 프롬프트 접두사의 KV Cache를 재사용합니다.",
"Model Merging은 여러 Fine-tuned 모델의 가중치를 병합하여 새로운 능력을 조합합니다.",
"DPO(Direct Preference Optimization)는 RLHF 없이 인간 선호도를 직접 학습합니다.",
"ORPO(Odds Ratio Preference Optimization)는 참조 모델 없이 선호도를 학습합니다.",
"SimPO(Simple Preference Optimization)는 길이 보정 없이 간결하게 선호도를 최적화합니다.",
"Continuous Pre-training은 기존 모델에 새로운 도메인 데이터를 추가로 사전학습합니다.",
"Curriculum Learning은 쉬운 데이터부터 어려운 데이터 순으로 학습하여 효율을 높입니다.",
"Reward Hacking은 AI가 보상 함수의 허점을 이용하여 비정상적 행동을 보이는 현상입니다.",
"Alignment Tax는 AI 안전 정렬이 모델 성능을 일부 희생시키는 비용입니다.",
"Capability Control은 AI의 능력을 의도적으로 제한하여 안전성을 확보합니다.",
"Corrigibility는 AI가 인간의 수정 명령에 순응하는 특성입니다.",
"Inner Alignment은 학습 목표와 AI의 실제 목표가 일치하는지 확인하는 연구입니다.",
"Outer Alignment은 인간이 설정한 목표가 인간의 진정한 의도를 반영하는지 확인합니다.",
"Interpretability는 AI 모델의 내부 작동 원리를 이해하는 연구 분야입니다.",
"Mechanistic Interpretability는 신경망 내부 회로를 역공학하여 동작을 이해합니다.",
"Sparse Autoencoder(SAE)는 신경망 활성화를 해석 가능한 특성으로 분해합니다.",
"Circuit Discovery는 Transformer 내부에서 특정 기능을 수행하는 회로를 찾습니다.",
"Attention Pattern Analysis는 모델이 어떤 토큰에 주목하는지 시각화합니다.",
"Probing은 모델의 중간 표현에서 특정 정보를 추출하는 분석 기법입니다.",
"SHAP(SHapley Additive exPlanations)은 각 특성의 기여도를 계산하는 XAI 기법입니다.",
"LIME(Local Interpretable Model-agnostic Explanations)은 개별 예측을 설명합니다.",
"GradCAM은 CNN의 판단 근거를 히트맵으로 시각화하는 기법입니다.",
"AI Observability는 AI 시스템의 입출력·성능·이상을 모니터링하는 체계입니다.",
"LLMOps는 LLM의 개발·배포·운영을 체계적으로 관리하는 방법론입니다.",
"Prompt Management는 프롬프트의 버전 관리, 테스트, A/B 실험을 체계화합니다.",
"AI Pipeline Orchestration은 데이터→학습→평가→배포 파이프라인을 자동화합니다.",
"Model Serving은 학습된 모델을 API로 배포하여 추론 요청을 처리합니다.",
"Triton Inference Server는 NVIDIA의 다중 모델 추론 서빙 플랫폼입니다.",
"BentoML은 ML 모델을 API로 배포하는 오픈소스 프레임워크입니다.",
"MLflow는 ML 실험 추적, 모델 관리, 배포를 통합하는 오픈소스 플랫폼입니다.",
"Weights & Biases(W&B)는 ML 실험 추적과 시각화를 위한 플랫폼입니다.",
"ClearML은 ML 파이프라인 자동화와 실험 관리를 위한 오픈소스 플랫폼입니다.",
"Label Studio는 데이터 레이블링을 위한 오픈소스 도구입니다.",
"Roboflow는 컴퓨터 비전 데이터셋 관리와 학습을 위한 플랫폼입니다.",
"Haystack은 RAG 시스템 구축을 위한 오픈소스 프레임워크입니다.",
"Chroma는 AI 애플리케이션을 위한 오픈소스 임베딩 데이터베이스입니다.",
"Weaviate는 벡터 검색과 하이브리드 검색을 지원하는 벡터 데이터베이스입니다.",
"Pinecone은 관리형 벡터 데이터베이스 서비스로, 대규모 유사도 검색을 지원합니다.",
"Milvus는 대규모 벡터 데이터를 위한 오픈소스 벡터 데이터베이스입니다.",
"FAISS(Facebook AI Similarity Search)는 고속 벡터 유사도 검색 라이브러리입니다.",
"Annoy(Approximate Nearest Neighbors Oh Yeah)는 근사 최근접 이웃 검색 라이브러리입니다.",
"HNSW(Hierarchical Navigable Small World)는 고성능 근사 최근접 이웃 알고리즘입니다.",
"Hybrid Search는 키워드 검색(BM25)과 벡터 검색을 결합하여 정확도를 높입니다.",
"Cross-Encoder는 두 텍스트를 직접 비교하여 유사도를 정밀하게 계산합니다.",
"Bi-Encoder는 각 텍스트를 독립적으로 인코딩하여 빠른 유사도 검색을 지원합니다.",
"Re-ranking은 초기 검색 결과를 정밀 모델로 재정렬하여 품질을 높입니다.",
"Chunking은 긴 문서를 적절한 크기로 분할하여 RAG 검색 효율을 높입니다.",
"Semantic Chunking은 의미적 경계를 기준으로 문서를 분할합니다.",
"Agentic Chunking은 AI가 문서 구조를 이해하고 최적의 청크를 결정합니다.",
"Document Parsing은 PDF, DOCX, HTML 등 다양한 형식의 문서에서 텍스트를 추출합니다.",
"Layout Analysis는 문서의 시각적 구조(표, 이미지, 단)를 분석합니다.",
"Table Extraction은 문서에서 테이블을 구조적으로 추출하는 기술입니다.",
"Vision Document Understanding은 멀티모달 AI로 문서를 이미지로 처리합니다.",
"Code Generation은 자연어 설명으로부터 프로그래밍 코드를 자동 생성합니다.",
"Code Review AI는 코드 변경 사항을 자동으로 리뷰하고 개선점을 제안합니다.",
"Code Completion은 작성 중인 코드의 다음 내용을 예측하여 자동 완성합니다.",
"Code Explanation은 코드의 동작을 자연어로 설명합니다.",
"Code Translation은 한 프로그래밍 언어의 코드를 다른 언어로 변환합니다.",
"Bug Detection은 AI가 코드에서 잠재적 버그와 취약점을 자동으로 발견합니다.",
"Test Generation은 AI가 코드에 대한 단위 테스트를 자동으로 생성합니다.",
"Documentation Generation은 코드로부터 API 문서를 자동 생성합니다.",
"Commit Message Generation은 코드 변경 사항으로부터 커밋 메시지를 자동 작성합니다.",
"Fill-in-the-Middle(FIM)은 앞뒤 컨텍스트를 보고 중간 코드를 채우는 기법입니다.",
"Repository-level Understanding은 전체 코드베이스를 이해하여 맥락 있는 코드를 생성합니다.",
"Dependency Analysis는 코드 모듈 간 의존 관계를 분석합니다.",
"Call Graph는 함수 호출 관계를 그래프로 표현하여 코드 흐름을 이해합니다.",
"AST(Abstract Syntax Tree)는 소스 코드의 구문 구조를 트리로 표현합니다.",
"Code Smell은 잠재적 설계 문제를 나타내는 코드 패턴입니다.",
"Dead Code는 실행되지 않는 불필요한 코드로, 정리 대상입니다.",
"Cyclomatic Complexity는 코드의 복잡도를 측정하는 소프트웨어 메트릭입니다.",
"Cognitive Complexity는 코드를 이해하는 인지적 어려움을 측정합니다.",
"SonarQube는 코드 품질과 보안을 지속적으로 검사하는 정적 분석 플랫폼입니다.",
"ESLint는 JavaScript/TypeScript 코드의 문제를 찾고 수정하는 린트 도구입니다.",
"Prettier는 코드를 일관된 스타일로 자동 포매팅하는 도구입니다.",
"Black은 Python 코드를 일관된 스타일로 포매팅하는 'Uncompromising' 포매터입니다.",
"Ruff는 Rust로 작성된 초고속 Python 린터+포매터입니다.",
"mypy는 Python의 정적 타입 검사 도구로, 타입 힌트를 검증합니다.",
"Pyright는 Microsoft의 Python 타입 검사기로, VS Code와 통합됩니다.",
"clang-tidy는 C++ 코드의 스타일 문제와 버그를 검사하는 린트 도구입니다.",
"RustAnalyzer는 Rust의 공식 LSP 서버로, IDE에서 실시간 분석을 제공합니다.",
"Copilot Workspace는 GitHub의 AI 기반 개발 환경으로, 이슈에서 코드까지 자동화합니다.",
"Devin은 Cognition의 AI 소프트웨어 엔지니어로, 자율적 코딩 에이전트입니다.",
"SWE-agent는 LLM이 실제 소프트웨어 엔지니어링 작업을 수행하는 오픈소스 에이전트입니다.",
"Aider는 터미널 기반 AI 페어 프로그래밍 도구로, Git과 긴밀히 통합됩니다.",
"Continue는 VS Code/JetBrains용 오픈소스 AI 코드 어시스턴트입니다.",
"Cody는 Sourcegraph의 AI 코딩 어시스턴트로, 코드베이스 전체를 이해합니다.",
"TabNine는 AI 코드 자동 완성 도구로, 로컬 모델을 지원합니다.",
"Amazon Q는 AWS의 AI 어시스턴트로, 코드 변환과 보안 스캔을 제공합니다.",
"CodeWhisperer는 Amazon의 AI 코드 생성 도구로, 보안 취약점 검사를 포함합니다.",
"Gemini Code Assist는 Google의 AI 코딩 도구로, Gemini 모델을 활용합니다.",
"Windsurf는 Codeium의 AI 코드 편집기로, Cascade 에이전트가 특징입니다.",
"Bolt.new는 브라우저에서 AI가 풀스택 앱을 즉시 생성하는 플랫폼입니다.",
"v0는 Vercel의 AI UI 생성기로, 프롬프트로 React 컴포넌트를 만듭니다.",
"Replit Agent는 Replit의 AI 코딩 에이전트로, 앱을 자동으로 빌드합니다.",
"Lovable은 AI가 프로덕션 수준의 앱을 생성하는 풀스택 빌더입니다.",
"Manus는 범용 AI 에이전트로, 웹 브라우징과 코딩을 자율적으로 수행합니다.",
"Claude Agent SDK는 Anthropic의 에이전트 개발 도구로, 도구 사용과 오케스트레이션을 지원합니다.",
"OpenAI Agents SDK는 에이전트 간 핸드오프와 가드레일을 지원하는 Python 프레임워크입니다.",
"Google ADK(Agent Development Kit)는 멀티에이전트 시스템을 구축하는 프레임워크입니다.",
"A2A(Agent-to-Agent)는 Google이 제안한 에이전트 간 통신 표준 프로토콜입니다.",
"AG2(AutoGen)는 Microsoft의 멀티에이전트 대화 프레임워크입니다.",
"Pydantic AI는 FastAPI 스타일의 타입 안전 AI 에이전트 프레임워크입니다.",
"Instructor는 LLM 출력을 Pydantic 모델로 구조화하는 라이브러리입니다.",
"Marvin은 AI 함수를 Python 함수처럼 사용하는 라이브러리입니다."
]

View File

@@ -0,0 +1,502 @@
[
"오늘도 최선을 다하는 당신이 있어 세상이 조금 더 나아집니다.",
"출근하는 것만으로도 대단한 용기입니다.",
"하루를 시작한 당신, 이미 충분히 훌륭합니다.",
"오늘은 어제보다 조금 더 좋은 날이 될 거예요.",
"새 아침이 새로운 기회를 가져옵니다.",
"시작이 반입니다. 당신은 이미 시작했습니다.",
"오늘 하루도 당신만의 페이스로 나아가세요.",
"오늘 하루도 당신을 응원합니다.",
"아침 공기를 마시며 오늘도 시작합니다. 잘 될 거예요.",
"오늘의 작은 시작이 내일의 큰 이야기가 됩니다.",
"어떤 하루라도 당신이 있으면 의미 있습니다.",
"오늘 하루도 한 걸음씩 나아가면 충분합니다.",
"빛나지 않아도 됩니다. 그냥 당신답게 있으면 됩니다.",
"완벽하지 않아도 오늘 하루를 잘 보낼 수 있습니다.",
"오늘도 나를 위해 일하러 온 나에게 감사합니다.",
"이 하루가 쌓여 멋진 삶이 만들어집니다.",
"오늘도 당신은 충분히 잘하고 있습니다.",
"어제 힘들었다면 오늘은 조금 더 편할 거예요.",
"출근하는 길, 당신의 하루가 빛나길 바랍니다.",
"하루를 잘 시작했다면 이미 잘 하고 있는 겁니다.",
"오늘 만날 멋진 일들이 기다리고 있습니다.",
"어렵더라도 오늘 하루 끝에는 뿌듯함이 남을 거예요.",
"지금 이 순간도 당신은 최선을 다하고 있습니다.",
"월요일이든 금요일이든, 당신이 있으면 됩니다.",
"오늘도 한 박자 쉬어가도 괜찮습니다.",
"작은 것 하나 완수했을 때의 기쁨을 기억하세요.",
"천천히 가도 괜찮아요. 방향이 맞으면 됩니다.",
"오늘은 어떤 새로운 것을 배울까요? 기대됩니다.",
"당신의 노력은 반드시 쌓입니다.",
"오늘 하루도 당신이 있어 팀이 든든합니다.",
"힘든 날일수록 오늘을 잘 버텨낸 나를 칭찬해주세요.",
"오늘도 건강하게 일할 수 있어서 감사합니다.",
"지금 하는 일이 결국 의미가 있을 것입니다.",
"당신은 생각보다 훨씬 많이 해내고 있습니다.",
"좋은 일은 생각보다 가까이 있습니다. 오늘 찾아보세요.",
"오늘은 어제의 나보다 한 뼘 성장하는 날입니다.",
"아침 커피 한 잔처럼, 오늘 하루도 따뜻하게 시작해봅시다.",
"오늘 하루도 당신다움으로 가득 채우세요.",
"작은 진전도 진전입니다. 축하해주세요.",
"당신의 하루가 기쁨으로 가득하길 바랍니다.",
"오늘도 웃는 날이 되기를 바랍니다.",
"일이 잘 안 될 때도 당신은 최선을 다하고 있습니다.",
"오늘 출근한 것만으로도 잘했습니다.",
"새로운 하루는 새로운 기회입니다.",
"모든 위대한 성취는 평범한 하루에서 시작됩니다.",
"당신의 가능성은 무한합니다.",
"오늘도 좋은 하루가 되기를 응원합니다!",
"꾸준함은 결국 재능을 이깁니다.",
"당신이 하는 일에는 가치가 있습니다.",
"조금씩 해도 괜찮아요. 멈추지만 마세요.",
"오늘 하루가 좋은 기억으로 남길 바랍니다.",
"당신이 해낸 것들을 잊지 마세요.",
"오늘 하루도 나를 응원하며 시작합니다.",
"어제의 나보다 오늘의 내가 더 지혜롭습니다.",
"오늘 하루도 의미 있는 일을 할 수 있습니다.",
"당신의 노력이 오늘도 세상을 조금 더 좋게 만듭니다.",
"일이 산더미 같아 보여도, 하나씩 하면 됩니다.",
"오늘도 당신의 하루를 응원합니다.",
"당신이 있어 우리 팀이 더 빛납니다.",
"지금 이 순간, 당신은 충분합니다.",
"오늘도 나에게 필요한 하루입니다.",
"작은 노력들이 쌓여 큰 변화를 만듭니다.",
"어떤 하루든 끝나면 해낸 하루입니다.",
"오늘도 건강하게 하루를 마치길 바랍니다.",
"당신의 오늘이 내일의 자양분이 됩니다.",
"지금 시작하면 됩니다. 언제나 지금이 가장 좋은 때입니다.",
"당신이 오늘 해낸 일들이 당신을 성장시킵니다.",
"오늘의 수고가 내일의 여유로 돌아옵니다.",
"어렵다고 느껴질 때, 이미 많이 성장했다는 신호입니다.",
"오늘도 한 발 더 내딛는 당신을 응원합니다.",
"노력은 배신하지 않습니다. 오늘도 열심히 하세요.",
"당신이 기울이는 모든 노력은 반드시 보상받습니다.",
"성장은 눈에 보이지 않아도 분명히 일어나고 있습니다.",
"실수는 성장의 증거입니다. 두려워하지 마세요.",
"오늘의 어려움이 내일의 능력을 만듭니다.",
"잘 모르는 것이 있어도 괜찮아요. 배우면 됩니다.",
"도전하는 사람이 변화를 만듭니다.",
"할 수 없다고 생각했던 일을 해냈을 때의 짜릿함을 기억하세요.",
"모든 전문가는 처음엔 초보였습니다.",
"어제보다 조금 나아졌다면 성공입니다.",
"배움에는 끝이 없습니다. 오늘도 새로 배우는 당신이 멋집니다.",
"당신의 노력이 쌓여 실력이 됩니다.",
"꾸준히 하는 것이 갑자기 잘하는 것보다 강합니다.",
"오늘 한 가지라도 더 알게 된다면 성공한 하루입니다.",
"작은 개선이 큰 차이를 만듭니다.",
"잘 된 것보다 잘 안 된 것에서 더 많이 배웁니다.",
"실패는 더 잘 하기 위한 연습입니다.",
"오늘의 노력이 미래의 나를 만듭니다.",
"당신이 성장하는 속도가 중요한 게 아닙니다. 방향이 중요합니다.",
"포기하지 않으면 결국엔 해냅니다.",
"어렵지 않으면 성장도 없습니다. 지금 어려운 건 잘 하고 있다는 뜻입니다.",
"아는 것이 많아질수록 모르는 것도 더 보입니다. 그게 성장입니다.",
"실수를 인정하는 용기가 성장을 부릅니다.",
"배움은 나이를 가리지 않습니다. 오늘도 배워봐요.",
"모든 기술은 연습으로 완성됩니다.",
"오늘의 불편함이 내일의 편안함이 됩니다.",
"잘 안 된다고 느낄 때가 가장 많이 자랄 때입니다.",
"당신은 생각보다 훨씬 빠르게 성장하고 있습니다.",
"스스로를 믿어야 성장의 문이 열립니다.",
"오늘 새로 도전한 것이 있다면 그것만으로 훌륭합니다.",
"정답이 아니어도 도전하는 것이 중요합니다.",
"천천히 해도 됩니다. 완성이 중요합니다.",
"당신이 오늘 한 걸음이 내일 열 걸음의 기반이 됩니다.",
"어떤 경험도 낭비되지 않습니다. 모두 배움이 됩니다.",
"목표를 향해 나아가는 당신은 이미 성공 중입니다.",
"해보지 않으면 알 수 없습니다. 그래서 해보는 겁니다.",
"당신의 성장 이야기는 아직 진행 중입니다.",
"오늘 배운 것이 내일 빛납니다.",
"느리더라도 나아가는 것이 멈추는 것보다 훨씬 낫습니다.",
"남과 비교하지 마세요. 어제의 나와 비교하면 됩니다.",
"당신이 도전하는 모습이 주변 사람들에게 용기를 줍니다.",
"성장에는 시간이 필요합니다. 기다릴 줄 아는 것도 실력입니다.",
"오늘의 집중이 내일의 결실을 만듭니다.",
"실력은 반복 속에서 만들어집니다.",
"당신의 잠재력은 아직 충분히 발휘되지 않았습니다.",
"오늘 불가능했던 일이 내일은 가능해집니다.",
"끝까지 해내는 당신의 끈기가 빛납니다.",
"지식보다 꾸준함이 더 강한 무기입니다.",
"작은 것을 잘 하는 사람이 큰 것도 잘 합니다.",
"오늘도 성장하는 당신을 응원합니다.",
"지금 하는 것이 언젠가 큰 차이를 만들 것입니다.",
"노력한 만큼 실력이 쌓입니다. 오늘도 쌓아가세요.",
"어렵더라도 포기하지 않는 당신이 자랑스럽습니다.",
"당신이 배우는 모든 것은 당신만의 자산이 됩니다.",
"도전하는 용기, 오늘도 그 용기를 냅시다.",
"변화는 불편하지만 성장을 가져옵니다.",
"실수는 배움의 원료입니다. 두려워하지 마세요.",
"지금 힘들다면 성장의 고통일 수 있습니다.",
"오늘의 배움이 내일의 자신감이 됩니다.",
"당신의 꾸준함은 반드시 꽃을 피웁니다.",
"완성보다 시작이 어렵습니다. 시작한 당신, 잘 했습니다.",
"모든 능력은 연습으로 늘어납니다. 오늘도 연습해봅시다.",
"어제보다 오늘, 오늘보다 내일. 그게 성장입니다.",
"한 발씩, 꾸준히. 그것이 멀리 가는 방법입니다.",
"오늘도 성장 중인 당신이 대단합니다.",
"배움에 나이는 없습니다. 지금이 가장 좋은 때입니다.",
"목표를 향해 작은 발걸음을 내딛는 것, 그게 시작입니다.",
"오늘도 어제보다 더 나아질 기회가 있습니다.",
"지금 하는 노력이 미래의 당신에게 선물이 됩니다.",
"성장은 숫자가 아니라 방향에 있습니다.",
"함께라서 더 멀리 갈 수 있습니다.",
"혼자 빠르게 가기보다 함께 멀리 가는 것이 더 좋습니다.",
"당신의 존재만으로도 팀에 큰 힘이 됩니다.",
"좋은 동료는 삶의 큰 행운입니다.",
"서로 도우면 더 큰 일을 해낼 수 있습니다.",
"팀원의 성공이 나의 성공이기도 합니다.",
"오늘도 팀과 함께하는 시간이 소중합니다.",
"동료의 강점을 발견하고 응원해주세요.",
"함께 웃을 수 있는 팀이 강한 팀입니다.",
"당신이 나누는 작은 도움이 팀의 힘이 됩니다.",
"어려울 때 기댈 수 있는 동료가 있다면 복입니다.",
"팀이 함께 방향을 맞추면 무엇이든 해낼 수 있습니다.",
"당신의 의견은 팀을 더 좋은 방향으로 이끕니다.",
"좋은 분위기를 만드는 것도 중요한 기여입니다.",
"동료의 작은 성공을 함께 기뻐해주세요.",
"신뢰가 팀의 가장 큰 자산입니다.",
"서로의 다름이 팀의 강점이 됩니다.",
"당신이 팀에 있어서 우리 팀이 더 좋습니다.",
"함께할수록 더 큰 일을 이룰 수 있습니다.",
"팀의 작은 성취도 함께 축하해주세요.",
"동료에게 건네는 따뜻한 말 한마디가 힘이 됩니다.",
"혼자가 아니라는 것, 그것만으로도 힘이 납니다.",
"서로를 이해하고 존중하는 팀이 강합니다.",
"당신의 도움이 누군가에게 큰 힘이 됩니다.",
"팀원 한 명 한 명이 퍼즐 조각처럼 소중합니다.",
"의견이 달라도 존중하면 더 좋은 결과가 나옵니다.",
"동료와 함께라면 어려운 일도 가능합니다.",
"당신의 경험이 팀의 지식이 됩니다. 나눠주세요.",
"좋은 동료 한 명이 근무 환경을 바꿉니다.",
"협력은 경쟁보다 더 많은 것을 만들어냅니다.",
"서로를 응원하는 문화가 팀을 성장시킵니다.",
"팀원이 힘들 때 눈치채는 당신이 좋은 동료입니다.",
"함께 이루는 성공이 더 달콤합니다.",
"당신과 함께 일하는 동료들이 행운입니다.",
"팀원 서로의 강점이 모이면 최고의 팀이 됩니다.",
"오늘도 팀과 좋은 하루 보내세요.",
"당신이 팀에 가져오는 긍정적인 에너지가 소중합니다.",
"협력이 단순히 일 잘하는 것 이상을 만들어냅니다.",
"작은 감사 인사가 팀 분위기를 따뜻하게 만듭니다.",
"당신의 팀워크가 빛나는 날입니다.",
"소통을 잘 하는 팀이 좋은 결과를 냅니다.",
"동료에게 먼저 다가가는 것이 용기 있는 행동입니다.",
"팀 안에서 서로를 성장시키는 것이 가장 좋은 투자입니다.",
"당신이 팀에 기여하는 모든 것이 가치 있습니다.",
"한 팀으로 함께하는 오늘이 소중합니다.",
"팀의 다양성이 최고의 아이디어를 만듭니다.",
"당신의 열정이 팀에 좋은 영향을 줍니다.",
"함께 어려움을 헤쳐나갈 때 팀은 더 강해집니다.",
"오늘도 팀과 함께 좋은 성과를 내는 하루 되세요.",
"당신이 있어 우리 팀이 완성됩니다.",
"함께 배우는 팀이 가장 강한 팀입니다.",
"팀원을 응원하는 것이 나를 응원하는 것과 같습니다.",
"서로에게 힘이 되는 오늘이 되기를 바랍니다.",
"당신이 가진 것을 팀과 나눌 때 더 커집니다.",
"팀워크는 연습으로 더 좋아집니다. 오늘도 연습해요.",
"함께하기 때문에 가능한 일들이 있습니다.",
"당신이 팀에 있어서 팀이 든든합니다.",
"서로를 믿는 팀이 어떤 어려움도 이겨냅니다.",
"오늘도 함께해서 고맙습니다.",
"함께라면 어떤 산도 넘을 수 있습니다.",
"당신의 미소가 팀 분위기를 밝게 합니다.",
"팀으로 이룬 성과가 가장 오래 기억됩니다.",
"서로의 다름을 존중할 때 팀은 더 강해집니다.",
"팀의 모든 구성원이 소중한 이유가 있습니다.",
"오늘도 팀과 함께 멋진 일을 해냅시다.",
"당신의 격려 한마디가 동료에게 큰 힘이 됩니다.",
"혼자 해내는 것도 훌륭하지만, 함께하면 더 위대해집니다.",
"좋은 팀을 만드는 것이 가장 중요한 업무입니다.",
"오늘 완료한 일, 아무리 작아도 축하해주세요.",
"할 일 목록에서 하나를 지울 때의 그 기분, 소중히 여기세요.",
"작은 완성들이 쌓여 큰 결과가 됩니다.",
"오늘 어렵던 일을 해낸 당신, 멋집니다.",
"아무리 작은 진전도 진전입니다. 축하합니다.",
"어제 못했던 것을 오늘 해냈다면 성장한 겁니다.",
"목표를 향해 한 발 더 나아간 오늘을 기억하세요.",
"오늘의 작은 성취가 미래의 자신감이 됩니다.",
"어려운 결정을 내린 당신, 잘 했습니다.",
"도움을 요청한 것도 용기 있는 선택입니다. 잘 했습니다.",
"오늘 예상보다 더 잘 해냈다면 스스로 칭찬해주세요.",
"복잡한 문제를 해결했을 때의 그 뿌듯함, 충분히 누리세요.",
"어제보다 오늘 더 나아진 부분을 찾아보세요.",
"오늘 처음으로 해낸 일이 있다면 기념할 가치가 있습니다.",
"작은 완료가 모여 큰 프로젝트가 됩니다.",
"오늘도 해냈습니다. 그것만으로 충분히 좋은 하루입니다.",
"어렵게 시작한 일을 끝까지 마친 당신이 대단합니다.",
"오늘 하나를 배웠다면 하루를 잘 보낸 겁니다.",
"완벽하지 않아도 완료가 완벽보다 낫습니다.",
"당신이 오늘 해낸 모든 것이 가치 있습니다.",
"작은 목표들을 하나씩 이루는 기쁨을 만끽하세요.",
"오늘 잘 한 일 하나를 떠올려 보세요. 반드시 있을 겁니다.",
"남이 보지 않아도 당신이 한 일은 기록됩니다.",
"미루던 일을 오늘 시작했다면 이미 반은 끝낸 겁니다.",
"오늘 좋은 결정을 내렸다면 그것으로 오늘은 성공입니다.",
"어렵던 대화를 해냈다면 큰 성취입니다. 잘 했습니다.",
"오늘 집중하며 일했다면 생산적인 하루입니다.",
"작은 승리가 큰 자신감을 만듭니다.",
"오늘 완성한 것들에 만족해도 됩니다.",
"목표를 절반 이뤘다면, 이미 절반을 이룬 겁니다.",
"오늘 해낸 일 하나를 퇴근 전에 기억해보세요.",
"도전했다는 것 자체가 성공의 일부입니다.",
"오늘 한 번 더 시도했다면 충분합니다.",
"작은 개선도 개선입니다. 그게 쌓이면 혁신이 됩니다.",
"완성에 감사하는 마음을 잊지 마세요.",
"오늘도 의미 있는 일을 해냈습니다. 수고했습니다.",
"힘들었지만 해냈다는 것, 그것이 당신의 능력입니다.",
"오늘 스스로에게 '잘 했어'라고 말해주세요.",
"계획한 것을 해냈을 때의 성취감을 충분히 느끼세요.",
"오늘 당신이 이룬 것들은 누구도 빼앗아 갈 수 없습니다.",
"작은 것을 잘 지키는 사람이 큰 것도 잘 지킵니다.",
"오늘 어려운 상황에서도 최선을 다한 당신이 멋집니다.",
"완벽하지 않아도 오늘의 최선이면 충분합니다.",
"어제보다 나아진 것이 있다면 오늘은 성공한 날입니다.",
"오늘의 작은 진전이 내일의 큰 도약을 만듭니다.",
"당신이 이룬 모든 것이 여기에서 시작됐습니다.",
"작은 일도 성심껏 하는 당신이 진짜 프로입니다.",
"오늘 해낸 것들이 모여 멋진 커리어가 됩니다.",
"하루 하나씩 해나가는 당신의 꾸준함이 최고입니다.",
"오늘 하루도 의미 있는 발자국을 남겼습니다.",
"쉬는 것도 일의 일부입니다. 충분히 쉬어가세요.",
"지쳤을 때 쉬는 것이 가장 현명한 선택입니다.",
"오늘 몸과 마음이 건강해야 내일도 잘 할 수 있습니다.",
"커피 한 잔, 잠깐의 여유가 생산성을 높입니다.",
"숨 한번 크게 쉬어보세요. 괜찮아집니다.",
"당신이 건강해야 당신이 소중한 사람들도 행복합니다.",
"오늘 무리하지 않아도 됩니다. 내일도 있습니다.",
"잠깐 멈추고 지금 이 순간을 느껴보세요.",
"완벽한 하루보다 건강한 하루가 더 중요합니다.",
"자신을 돌보는 것이 가장 중요한 투자입니다.",
"오늘 한 번쯤은 자신에게 친절하게 대해주세요.",
"지쳐있다면 잠깐 쉬어도 괜찮습니다.",
"작은 휴식이 큰 재충전이 됩니다.",
"점심을 제대로 먹는 것도 스스로를 위한 중요한 행동입니다.",
"오늘 잠깐 창밖을 바라보세요. 기분이 달라질 겁니다.",
"자신을 사랑하는 것이 출발점입니다.",
"충분히 쉬어야 더 잘 할 수 있습니다.",
"오늘 일이 너무 많다면, 우선순위를 정해보세요.",
"스트레스가 쌓였다면, 잠깐 걷는 것도 좋습니다.",
"몸이 보내는 신호에 귀를 기울여주세요.",
"오늘 하루 잘 마치고 충분히 쉬세요.",
"일보다 당신이 더 중요합니다.",
"완벽함보다 꾸준함이 더 오래 갑니다. 오늘 무리하지 마세요.",
"잠깐 멈추어 물 한 잔 마시는 여유가 중요합니다.",
"자신에게 너무 가혹하게 굴지 마세요. 충분히 잘 하고 있습니다.",
"오늘 하루도 당신 몸과 마음이 건강하길 바랍니다.",
"힘들면 힘들다고 말해도 됩니다. 그게 더 용감한 일입니다.",
"쉬어갈 때 쉬어가는 것이 마라톤을 완주하는 방법입니다.",
"오늘 하루 스스로에게 친절한 말을 건네보세요.",
"기대치를 낮추는 것도 때로는 지혜입니다.",
"지금 힘들다면 잠깐 멈추고 숨을 고르세요.",
"자신을 아끼는 것이 더 멀리 가는 방법입니다.",
"오늘 하루 잘 해낸 자신에게 선물 하나 줘도 됩니다.",
"완벽할 필요 없어요. 지금 이 모습으로 충분합니다.",
"너무 많은 것을 한번에 하려 하지 마세요. 하나씩이면 됩니다.",
"오늘 피곤하다면, 그건 열심히 했다는 증거입니다.",
"자신을 돌볼 줄 아는 사람이 다른 사람도 잘 도울 수 있습니다.",
"잠깐의 여유가 창의적인 아이디어를 가져올 수 있습니다.",
"오늘 스트레스를 잘 다루는 방법을 하나 찾아봐요.",
"마음이 힘들 때는 좋아하는 음악 한 곡이 도움이 됩니다.",
"일에 쫓기지 말고, 당신의 속도로 나아가세요.",
"지금 여기, 현재에 집중하는 것이 최고의 휴식입니다.",
"자신의 감정에 귀 기울이는 것이 건강의 시작입니다.",
"오늘 하루도 당신의 건강이 최우선입니다.",
"긴장을 잠깐 풀어보세요. 어깨에 힘을 빼도 됩니다.",
"완벽한 컨디션이 아니어도 오늘을 잘 보낼 수 있습니다.",
"오늘 하루가 끝나면 충분히 쉬세요. 그게 내일을 준비하는 것입니다.",
"자신을 믿어주세요. 당신은 충분히 할 수 있습니다.",
"오늘 잠깐이라도 자신에게 집중하는 시간을 가져보세요.",
"무리하지 않고 지속하는 것이 가장 좋은 성과를 냅니다.",
"오늘 힘들었다면 내일은 더 가벼울 거예요.",
"자신을 위한 작은 시간이 큰 에너지를 만듭니다.",
"오늘도 스스로를 아끼는 하루 보내세요.",
"어떤 날은 쉬어가는 것이 가장 용감한 선택입니다.",
"건강한 습관 하나가 인생을 바꿀 수 있습니다.",
"오늘 좋아하는 것 하나를 하는 시간을 내세요.",
"자신에게 친절하면 주변에도 친절할 수 있습니다.",
"지치지 않게 자신을 챙기는 것도 중요한 일입니다.",
"오늘도 나의 건강과 행복이 최우선입니다.",
"오늘 당신에게 도움을 준 사람에게 감사 인사를 전해보세요.",
"작은 것에 감사하는 마음이 행복의 씨앗입니다.",
"오늘 하루를 살 수 있음이 이미 축복입니다.",
"당신이 가진 것들에 감사하는 마음을 가져보세요.",
"주변 사람들이 당신에게 얼마나 큰 힘이 되는지 기억하세요.",
"오늘 고마웠던 순간을 하나 떠올려 보세요.",
"감사 일기 한 줄이 하루를 더 풍요롭게 합니다.",
"당신이 이룬 것들은 주변의 도움 없이는 불가능했을 겁니다.",
"오늘 누군가에게 고맙다고 말해보세요. 그 사람도 행복해집니다.",
"작은 친절에 감사하는 마음이 좋은 관계를 만듭니다.",
"오늘도 일할 수 있는 건강이 있음에 감사합니다.",
"당신의 존재 자체에 감사하는 하루를 보내세요.",
"함께하는 동료들이 있음에 감사합니다.",
"오늘 배운 것들이 있다면 그것도 감사할 이유입니다.",
"힘든 상황에서도 주변의 도움에 감사할 수 있습니다.",
"당신을 믿어주는 사람이 있다는 것이 큰 선물입니다.",
"감사는 받는 것보다 주는 것이 더 풍요롭게 합니다.",
"당신이 주는 감사의 말이 누군가의 하루를 바꿀 수 있습니다.",
"오늘 하루도 이렇게 살아있음이 축복입니다.",
"작은 고마움들이 모여 행복한 삶이 됩니다.",
"당신이 나눠준 도움에 감사하는 사람이 있습니다.",
"오늘 감사한 것 세 가지를 생각해보세요.",
"당신이 있어 감사한 사람들이 주변에 있습니다.",
"좋은 동료에게 감사함을 표현하세요. 관계가 깊어집니다.",
"오늘 하루를 마칠 수 있음이 작은 성취입니다. 감사합니다.",
"누군가 당신의 노력을 알아봐 주는 순간이 옵니다.",
"감사의 마음이 긍정적인 에너지를 만들어냅니다.",
"오늘 당신에게 잘 해준 사람을 기억해두세요.",
"작은 친절도 충분히 고마운 것입니다.",
"당신이 매일 해내는 것들이 당신 스스로에게 선물입니다.",
"오늘 따뜻한 말을 건네받았다면 그 사람에게 감사하세요.",
"감사하는 사람이 더 많은 감사할 일을 만납니다.",
"당신 곁에 응원해주는 사람이 있음에 감사하세요.",
"작은 것에도 감사할 줄 아는 사람이 큰 행복을 누립니다.",
"오늘 하루 배울 기회가 있었다면 그것에 감사하세요.",
"당신이 하는 일에 의미가 있음에 감사합니다.",
"감사는 더 좋은 내일을 만드는 씨앗입니다.",
"오늘 웃을 수 있었던 순간에 감사하세요.",
"당신의 재능을 발휘할 곳이 있음에 감사합니다.",
"오늘 당신을 도와준 사람의 이름을 기억해두세요.",
"감사함을 표현하면 관계가 더 깊어집니다.",
"당신이 오늘 이루어낸 것들에 스스로 감사하세요.",
"오늘도 좋은 사람들과 함께할 수 있어서 감사합니다.",
"당신이 받은 기회들에 감사하는 마음을 가져보세요.",
"감사는 마음을 부유하게 만드는 가장 간단한 방법입니다.",
"어려움 속에서도 도와주는 사람이 있어 감사합니다.",
"당신이 오늘 한 모든 일이 누군가에게 의미가 있습니다.",
"작은 감사가 큰 행복을 만들어냅니다.",
"오늘도 당신을 응원하는 사람들이 있습니다. 감사하세요.",
"당신의 삶에 감사할 것들이 생각보다 많습니다.",
"오늘 감사한 마음으로 시작하면 더 좋은 하루가 됩니다.",
"작은 것에서 감사를 찾는 능력이 행복의 비결입니다.",
"오늘 하루도 건강하게 일할 수 있어 감사합니다.",
"당신이 걸어온 길에 감사하며, 앞으로의 길을 기대하세요.",
"감사하는 마음이 마음의 여유를 만들어냅니다.",
"당신이 존재하는 것 자체가 세상에 큰 선물입니다.",
"지금 힘들더라도 포기하지 마세요. 반드시 지나갑니다.",
"어려운 길이 대부분 목적지로 가는 가장 빠른 길입니다.",
"힘들 때가 가장 많이 성장할 때입니다.",
"포기하지 않는 것이 가장 강한 능력입니다.",
"오늘 버텨낸 당신이 내일의 당신을 만듭니다.",
"지금 어렵다고 느끼는 것은 당신이 성장하고 있다는 신호입니다.",
"모든 어려움은 지나갑니다. 오늘도 믿어보세요.",
"쉽게 이룬 것보다 어렵게 이룬 것이 더 값집니다.",
"오늘 포기하지 않은 것이 내일의 출발점이 됩니다.",
"끝까지 해내는 사람이 결국 이깁니다.",
"지금 힘든 것은 무언가를 이루는 과정일 뿐입니다.",
"오래 하는 것이 빠르게 하는 것보다 강합니다.",
"어렵다는 것은 아직 해내지 못했다는 것, 곧 해낼 수 있다는 뜻입니다.",
"당신이 지금까지 해온 것들이 오늘을 버티게 해줍니다.",
"포기하지 않는 사람에게는 실패가 없습니다. 배움만 있을 뿐입니다.",
"오늘 힘들더라도 내일의 나는 오늘을 고맙게 여길 겁니다.",
"버티는 것도 하나의 용기입니다.",
"지금 이 순간을 견뎌낸 당신을 응원합니다.",
"모든 위대한 것은 오랜 인내 끝에 탄생합니다.",
"지금 잘 안 된다고 포기하면 이제껏 쌓아온 것이 아깝습니다.",
"오늘도 조금만 더 해봅시다. 반드시 길이 열립니다.",
"험한 길도 한 발씩 걷다 보면 어느새 목적지에 도달합니다.",
"버텨낸 시간들이 당신을 더 강하게 만들었습니다.",
"어렵더라도 멈추지 않는 것이 가장 중요합니다.",
"당신이 지금까지 견뎌온 것들이 당신의 진짜 능력입니다.",
"힘든 시간도 지나고 나면 성장의 발판이 됩니다.",
"당신이 포기하지 않는 한, 모든 것은 여전히 가능합니다.",
"지금 힘들다고 느끼는 것이 바로 더 강해지고 있다는 증거입니다.",
"오늘 하루 잘 버텨낸 당신이 대단합니다.",
"느려도 괜찮습니다. 방향만 맞다면 결국 도달합니다.",
"한 번에 크게 나아가지 않아도 됩니다. 조금씩이면 충분합니다.",
"지금 힘들다면 결승선이 가까워지고 있다는 뜻일 수도 있습니다.",
"오늘 하루를 버텨낸 당신이 진짜 강한 사람입니다.",
"당신이 걸어온 길이 얼마나 먼지 잊지 마세요.",
"지금 이 순간이 지나가면 분명 더 좋은 날이 옵니다.",
"당신이 오늘도 자리를 지키고 있다는 것이 큰 힘입니다.",
"어렵고 힘들수록 그 안에서 배움이 더 많습니다.",
"지속성은 재능보다 강합니다.",
"오늘 포기하지 않은 당신을 내일의 당신이 감사해할 겁니다.",
"끝까지 해낸 것들이 인생의 자산이 됩니다.",
"오늘 조금 힘들었다면, 그만큼 성장한 겁니다.",
"지금 어둡더라도 새벽이 가까워지고 있습니다.",
"당신의 끈기가 결국 원하는 것을 가져다줍니다.",
"오늘도 한 발씩, 당신의 페이스로 나아가세요.",
"힘든 시간을 이겨낸 사람이 더 큰 일을 해냅니다.",
"지금 힘들다면 충분히 노력하고 있다는 뜻입니다.",
"당신이 오늘도 포기하지 않는 이유가 있습니다.",
"어려울 때일수록 멈추지 않는 것이 중요합니다.",
"오늘도 한 단계씩 올라가는 당신을 응원합니다.",
"지금 이 힘든 시간이 지나면 더 단단해진 당신이 있을 겁니다.",
"오늘 버텨낸 것들이 쌓여 당신의 내공이 됩니다.",
"포기하지 않는 사람의 꿈은 반드시 이루어집니다.",
"당신이 걸어온 길이 결코 헛되지 않습니다.",
"오늘도 힘내세요. 당신 곁에 응원하는 사람이 있습니다.",
"어렵더라도 시도하는 용기, 그것이 당신의 가장 큰 강점입니다.",
"지금 힘들다면 당신이 성장의 문턱에 있는 겁니다.",
"오늘을 버티면 내일이 더 좋아집니다.",
"당신이 포기하지 않는 한 가능성은 항상 살아있습니다.",
"오늘도 꿋꿋하게 나아가는 당신이 멋집니다.",
"힘든 시간을 견디는 능력이 당신의 가장 큰 자산입니다.",
"지금 어렵더라도 반드시 길이 있습니다.",
"오늘 힘들었지만 해냈습니다. 그것이 당신의 이야기입니다.",
"당신의 인내가 언젠가 큰 보상으로 돌아올 것입니다.",
"오늘도 포기하지 않는 당신이 자랑스럽습니다.",
"힘들 때 도움을 요청하는 것도 인내의 한 방법입니다.",
"당신이 오늘 버텨낸 모든 것이 당신의 강함입니다.",
"지금의 어려움이 당신을 더 지혜롭게 만들고 있습니다.",
"오늘 하루도 당신의 끈기가 빛납니다.",
"멈추지 않는 사람이 결국 목적지에 도달합니다.",
"당신이 오늘도 일어선 것, 그것이 가장 큰 용기입니다.",
"어렵더라도 오늘 해야 할 것을 해내는 당신이 대단합니다.",
"지금 이 순간 포기하지 않는 것이 모든 것을 바꿉니다.",
"오늘도 당신의 꿈을 향해 한 발 더 나아갑니다.",
"당신이 지금 하는 것이 나중에 큰 의미가 될 겁니다.",
"오늘 하루 어렵더라도 당신은 해낼 수 있습니다.",
"인내하는 사람에게 세상은 결국 좋은 것을 줍니다.",
"오늘도 버텨낸 당신이 내일도 버텨낼 겁니다.",
"끝까지 함께하는 당신을 응원합니다. 오늘도 고생 많으셨습니다.",
"힘든 하루를 버텨낸 당신은 내일 더 강해집니다.",
"당신의 끈기가 팀 전체에 용기를 줍니다.",
"지금까지 버텨온 당신을 진심으로 응원합니다.",
"오늘 하루를 버텨낸 당신은 이미 승자입니다.",
"당신이 포기하지 않는 이유가 바로 성공의 이유입니다.",
"느리게 가도 괜찮습니다. 계속 가는 것이 중요합니다.",
"당신의 인내가 반드시 결실로 이어질 것입니다.",
"꾸준한 사람이 결국 목적지에 도달합니다.",
"쉽게 얻은 것보다 힘들게 이룬 것이 더 오래 남습니다.",
"지금 이 과정이 언젠가 자랑스러운 이야기가 됩니다.",
"당신이 계속 나아가는 한 실패는 없습니다.",
"오늘의 인내가 내일의 자유를 만듭니다.",
"당신이 이 어려움을 이겨낼 수 있다는 것을 믿습니다.",
"오늘도 버텨내는 당신에게 깊은 존경을 보냅니다.",
"포기하지 않는 당신이 가장 용감한 사람입니다.",
"포기하지 않는 마음 하나가 모든 것을 바꿉니다.",
"당신이 보여준 인내심이 주변을 감동시킵니다.",
"지금 이 순간도 당신은 성장하고 있습니다.",
"힘든 시간도 당신을 강하게 만들고 있습니다.",
"오늘도 묵묵히 자신의 길을 걷는 당신이 존경스럽습니다.",
"지금 당신이 겪고 있는 것이 나중에 큰 자산이 됩니다.",
"지금 이 길이 맞는 길이라면 계속 걸어가세요.",
"당신의 협력적 태도가 팀을 더 나아지게 합니다.",
"오늘도 팀원과 나눈 작은 대화가 큰 가치입니다.",
"동료와의 연결이 업무를 의미 있게 합니다.",
"당신의 웃음이 팀에게 긍정 에너지를 줍니다.",
"함께라서 어려운 일도 해낼 수 있습니다.",
"당신의 한마디 칭찬이 동료를 빛나게 합니다.",
"서로 응원하는 팀이 결국 가장 강합니다.",
"오늘도 팀을 위해 작은 기여를 해주셔서 고맙습니다.",
"당신의 진심 어린 감사가 팀을 하나로 묶어줍니다.",
"좋은 하루를 만들어준 모든 것에 감사해 보세요.",
"서로의 존재에 감사할 때 관계가 깊어집니다.",
"오늘 하루도 감사한 마음을 품고 지내보세요.",
"당신의 기여가 팀을 더 나은 곳으로 만듭니다.",
"오늘 누군가에게 진심으로 감사를 전해보세요.",
"당신의 작은 도움이 누군가에게는 큰 힘이 됩니다.",
"오늘도 감사한 마음으로 하루를 시작해 주세요.",
"당신이 이 자리에서 해온 모든 것에 감사합니다.",
"당신이 존재한다는 것에 세상이 감사합니다.",
"함께 일할 동료가 있다는 것이 복입니다.",
"지금 이 순간 당신의 노력에 감사합니다.",
"당신이 보여준 열정에 감사합니다.",
"오늘 당신과 함께한 시간이 값집니다.",
"서로에게 감사를 표현하는 것이 성숙한 팀을 만듭니다.",
"오늘 하루도 감사한 마음으로 마무리해 보세요.",
"당신이 가진 것들에 감사할 때 더 많은 것이 옵니다.",
"오늘도 배울 수 있다는 것이 얼마나 감사한 일인지요.",
"감사 일기를 써보는 것도 좋은 마무리 방법입니다."
]

View File

@@ -0,0 +1,200 @@
[
{"text":"삶은 초콜릿 상자와 같아요. 무엇을 집게 될지 모르죠. (Life is like a box of chocolates. You never know what you're gonna get.)", "movie":"포레스트 검프", "year":1994, "character":"톰 행크스(포레스트 검프)", "audience":null},
{"text":"내일은 내일의 해가 뜬다. (After all, tomorrow is another day.)", "movie":"바람과 함께 사라지다", "year":1939, "character":"비비안 리(스칼렛 오하라)", "audience":null},
{"text":"포스가 함께하길. (May the Force be with you.)", "movie":"스타워즈", "year":1977, "character":"알렉 기네스(오비완 케노비)", "audience":null},
{"text":"나는 이 세상의 왕이다! (I'm the king of the world!)", "movie":"타이타닉", "year":1997, "character":"레오나르도 디카프리오(잭 도슨)", "audience":null},
{"text":"왜 그렇게 심각하지? (Why so serious?)", "movie":"다크 나이트", "year":2008, "character":"히스 레저(조커)", "audience":4175526},
{"text":"희망은 좋은 것이야. 어쩌면 가장 좋은 것이지. (Hope is a good thing, maybe the best of things.)", "movie":"쇼생크 탈출", "year":1994, "character":"팀 로빈스(앤디 듀프레인)", "audience":null},
{"text":"내가 아이언맨입니다. (I am Iron Man.)", "movie":"아이언맨", "year":2008, "character":"로버트 다우니 주니어(토니 스타크)", "audience":4300365},
{"text":"가끔은 걷기도 전에 뛰어야 할 때가 있지. (Sometimes you gotta run before you can walk.)", "movie":"아이언맨", "year":2008, "character":"로버트 다우니 주니어(토니 스타크)", "audience":4300365},
{"text":"수트와 나는 하나입니다. (The suit and I are one.)", "movie":"아이언맨 2", "year":2010, "character":"로버트 다우니 주니어(토니 스타크)", "audience":4425003},
{"text":"천재, 억만장자, 플레이보이, 박애주의자. (Genius, billionaire, playboy, philanthropist.)", "movie":"어벤져스", "year":2012, "character":"로버트 다우니 주니어(토니 스타크)", "audience":7087068},
{"text":"수트 없이 아무것도 아니라면, 더더욱 수트를 가져선 안 돼. (If you're nothing without this suit, then you shouldn't have it.)", "movie":"스파이더맨: 홈커밍", "year":2017, "character":"로버트 다우니 주니어(토니 스타크)", "audience":7258611},
{"text":"3000만큼 사랑해. (I love you 3000.)", "movie":"어벤져스: 엔드게임", "year":2019, "character":"로버트 다우니 주니어(토니 스타크)", "audience":13977602},
{"text":"어벤져스, 어셈블! (Avengers, Assemble!)", "movie":"어벤져스: 엔드게임", "year":2019, "character":"크리스 에반스(캡틴 아메리카)", "audience":13934592},
{"text":"위대한 힘에는 위대한 책임이 따른다. (With great power comes great responsibility.)", "movie":"스파이더맨", "year":2002, "character":"클리프 로버트슨(벤 파커)", "audience":null},
{"text":"신에게는 아직 12척의 배가 있사옵니다.", "movie":"명량", "year":2014, "character":"최민식(이순신)", "audience":17613682},
{"text":"내가 왕이 될 상인가.", "movie":"관상", "year":2013, "character":"이정재(수양대군)", "audience":9134586},
{"text":"대한민국 헌법 제1조 1항, 대한민국은 민주공화국이다.", "movie":"변호인", "year":2013, "character":"송강호(송우석)", "audience":11374610},
{"text":"제 계획이 다 있습니다.", "movie":"기생충", "year":2019, "character":"송강호(기택)", "audience":10085271},
{"text":"카르페 디엠. 오늘을 잡아라. (Carpe diem. Seize the day.)", "movie":"죽은 시인의 사회", "year":1989, "character":"로빈 윌리엄스(키팅 선생)", "audience":null},
{"text":"진짜 중요한 건 눈에 보이지 않아. (What is essential is invisible to the eye.)", "movie":"어린 왕자", "year":2015, "character":"마리옹 꼬띠아르(어린 왕자)", "audience":null},
{"text":"무한한 공간 저 너머로! (To infinity and beyond!)", "movie":"토이스토리", "year":1995, "character":"팀 알렌(버즈 라이트이어)", "audience":null},
{"text":"과거는 아프지. 하지만 도망갈 수도, 배울 수도 있어. (The past can hurt. But you can either run from it or learn from it.)", "movie":"라이온 킹", "year":1994, "character":"로버트 기욤(라피키)", "audience":null},
{"text":"하쿠나 마타타! (Hakuna Matata!)", "movie":"라이온 킹", "year":1994, "character":"나단 레인(티몬)", "audience":null},
{"text":"인생은 가까이서 보면 비극이지만, 멀리서 보면 희극이다. (Life is a tragedy when seen in close-up, but a comedy in long-shot.)", "movie":"인생은 아름다워", "year":1997, "character":"로베르토 베니니(귀도)", "audience":null},
{"text":"매너가 사람을 만든다. (Manners maketh man.)", "movie":"킹스맨: 시크릿 에이전트", "year":2015, "character":"콜린 퍼스(해리 하트)", "audience":6129681},
{"text":"나, 돌아갈 거야. 반드시. (We will find a way. We always have.)", "movie":"인터스텔라", "year":2014, "character":"매튜 맥커너히(쿠퍼)", "audience":10309432},
{"text":"사랑은 시간과 공간을 초월하는 힘이야. (Love is the one thing that transcends time and space.)", "movie":"인터스텔라", "year":2014, "character":"앤 해서웨이(브랜드 박사)", "audience":10309432},
{"text":"치킨은 국민 음식이야!", "movie":"극한직업", "year":2019, "character":"류승룡(고반장)", "audience":16266338},
{"text":"경험은 결코 늙지 않는다. (Experience never goes out of fashion.)", "movie":"인턴", "year":2015, "character":"로버트 드 니로(벤 휘태커)", "audience":3611166},
{"text":"변화는 불가피하지만, 성장은 선택이다. (Change is inevitable. Growth is optional.)", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐(스티브 잡스)", "audience":225343},
{"text":"실패는 옵션이다. 하지만 두려움은 아니다. (Failure is an option. Fear is not.)", "movie":"포드 v 페라리", "year":2019, "character":"맷 데이먼(캐롤 셸비)", "audience":1326442},
{"text":"기회는 기다리는 자에게 오는 것이 아니라 쟁취하는 자에게 온다. (Opportunity doesn't knock, it presents itself when you beat down the door.)", "movie":"소셜 네트워크", "year":2010, "character":"제시 아이젠버그(마크 저커버그)", "audience":519018},
{"text":"모든 문제는 해결 가능하다. (Every problem has a solution.)", "movie":"마션", "year":2015, "character":"맷 데이먼(마크 와트니)", "audience":4887144},
{"text":"포기하는 순간 시합은 종료되는 겁니다.", "movie":"더 퍼스트 슬램덩크", "year":2023, "character":"배한성(안 선생님)", "audience":4879282},
{"text":"최선은 다하는 게 아니라, 잘하는 것이다.", "movie":"스토브리그(드라마)", "year":2019, "character":"남궁민(백승수)", "audience":null},
{"text":"비전 없는 실행은 비극이고, 실행 없는 비전은 몽상이다.", "movie":"스타트업(드라마)", "year":2020, "character":"김선호(한지평)", "audience":null},
{"text":"성공은 준비가 기회를 만났을 때 나타나는 결과다. (Success is what happens when preparation meets opportunity.)", "movie":"행복을 찾아서", "year":2006, "character":"윌 스미스(크리스 가드너)", "audience":null},
{"text":"데이터는 거짓말을 하지 않지만, 사람은 거짓말을 한다.", "movie":"비밀의 숲(드라마)", "year":2017, "character":"조승우(황시목)", "audience":null},
{"text":"불가능해 보일 때까지는 항상 불가능해 보인다. (It always seems impossible until it's done.)", "movie":"인빅터스", "year":2009, "character":"모건 프리먼(넬슨 만델라)", "audience":null},
{"text":"우리는 모두 미완성인 존재다.", "movie":"미생(드라마)", "year":2014, "character":"임시완(장그래)", "audience":null},
{"text":"팀보다 위대한 선수는 없다. (No player is bigger than the team.)", "movie":"머니볼", "year":2011, "character":"브래드 피트(빌리 빈)", "audience":641496},
{"text":"시스템은 사람을 믿지 않는다. 프로세스를 믿는다.", "movie":"더 테러 라이브", "year":2013, "character":"하정우(윤영화)", "audience":5584139},
{"text":"전문가란 더 좁은 분야에서 더 많은 실수를 해본 사람이다.", "movie":"이미테이션 게임", "year":2014, "character":"베네딕트 컴버배치(앨런 튜닝)", "audience":1744213},
{"text":"기술은 도구일 뿐이다. 사람을 연결하는 것이 핵심이다. (Technology is just a tool.)", "movie":"소셜 네트워크", "year":2010, "character":"저스틴 팀버레이크(숀 파커)", "audience":519018},
{"text":"디테일이 차이를 만든다.", "movie":"기생충", "year":2019, "character":"이선균(박 사장)", "audience":10313501},
{"text":"완벽함은 더 보탤 것이 없을 때가 아니라 빼낼 것이 없을 때 완성된다.", "movie":"어린 왕자", "year":2015, "character":"제프 브리지스(비행사)", "audience":null},
{"text":"남들과 다르게 생각하라. (Think different.)", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐(스티브 잡스)", "audience":225343},
{"text":"리더는 길을 아는 사람이 아니라 길을 만드는 사람이다.", "movie":"킹덤(드라마)", "year":2019, "character":"주지훈(이창)", "audience":null},
{"text":"복잡함은 실패의 지름길이다. (Complexity is the enemy of execution.)", "movie":"스티브 잡스", "year":2015, "character":"마이클 패스벤더(스티브 잡스)", "audience":null},
{"text":"계획 없는 목표는 단지 희망사항일 뿐이다. (A goal without a plan is just a wish.)", "movie":"어린 왕자", "year":2015, "character":"제임스 프랑코(여우)", "audience":null},
{"text":"중요한 것은 꺾이지 않는 마음이다.", "movie":"중꺾마(다큐)", "year":2022, "character":"김혁규(데프트)", "audience":null},
{"text":"프로는 감정에 치우치지 않는다.", "movie":"스토브리그(드라마)", "year":2019, "character":"남궁민(백승수)", "audience":null},
{"text":"우리는 문제를 해결하기 위해 고용된 사람들이다.", "movie":"울프 오브 월 스트리트", "year":2013, "character":"레오나르도 디카프리오(조던 벨포트)", "audience":585671},
{"text":"실패는 배움의 또 다른 이름이다. (The greatest teacher, failure is.)", "movie":"스타워즈: 라스트 제다이", "year":2017, "character":"프랭크 오즈(요다)", "audience":959550},
{"text":"데이터에 매몰되지 말고 맥락을 봐라. (Look at the context, not just the data.)", "movie":"서치", "year":2018, "character":"존 조(데이빗 킴)", "audience":2950097},
{"text":"함께 일하는 동료가 가장 큰 복지다.", "movie":"미생(드라마)", "year":2014, "character":"태인호(성준식)", "audience":null},
{"text":"적당히들 하시오! 대체 이 나라가 누구의 나라요? 내게는 백성이 우선이란 말이오!", "movie":"광해, 왕이 된 남자", "year":2012, "character":"이병헌(하선)", "audience":12324062},
{"text":"백성을 하늘처럼 섬기는 왕, 그 꿈 내가 이루어 드리지요.", "movie":"광해, 왕이 된 남자", "year":2012, "character":"이병헌(하선)", "audience":12324062},
{"text":"전하, 칼은 뽑았을 때 베어야 하는 법입니다.", "movie":"광해, 왕이 된 남자", "year":2012, "character":"류승룡(허균)", "audience":12324062},
{"text":"내게는 진짜 왕이었소.", "movie":"광해, 왕이 된 남자", "year":2012, "character":"장광(조내관)", "audience":12324062},
{"text":"실패하면 반란, 성공하면 혁명 아닙니까!", "movie":"서울의 봄", "year":2023, "character":"황정민(전두광)", "audience":13128596},
{"text":"대한민국 육군은 다 같은 편입니다.", "movie":"서울의 봄", "year":2023, "character":"정우성(이태신)", "audience":13128596},
{"text":"거 중구 형, 장난이 너무 심한 거 아니오!", "movie":"신세계", "year":2013, "character":"황정민(정청)", "audience":4684571},
{"text":"어이, 브라더! 이따가 저녁에 고기나 좀 굽자.", "movie":"신세계", "year":2013, "character":"황정민(정청)", "audience":4684571},
{"text":"우리 자식들이 아니라 우리가 겪어서 참 다행이다, 그쟈?", "movie":"국제시장", "year":2014, "character":"황정민(윤덕수)", "audience":14265540},
{"text":"아버지... 내 약속 잘 지켰지예? 근데 진짜 힘들었거든예.", "movie":"국제시장", "year":2014, "character":"황정민(윤덕수)", "audience":14265540},
{"text":"호연지기! 흑금성 박석영입니다.", "movie":"공작", "year":2018, "character":"황정민(박석영)", "audience":4910131},
{"text":"산이 거기 있어서 가는 게 아니야. 우리가 한 팀이니까 가는 거지.", "movie":"히말라야", "year":2015, "character":"황정민(엄홍길)", "audience":7759667},
{"text":"증거? 증거는 내가 지금부터 만들 거야.", "movie":"검사외전", "year":2016, "character":"황정민(변재욱)", "audience":9707581},
{"text":"여우가 범의 허리를 끊었다.", "movie":"파묘", "year":2024, "character":"최민식(김상덕)", "audience":11914784},
{"text":"땅이야 땅. 우리 손주들이 밟고 살아가야 할 땅이라고.", "movie":"파묘", "year":2024, "character":"최민식(김상덕)", "audience":11914784},
{"text":"이유가 어딨어, 그냥 잡는 거지.", "movie":"범죄도시4", "year":2024, "character":"마동석(마석도)", "audience":11502779},
{"text":"바다는 속여도 사람은 안 속인다.", "movie":"밀수", "year":2023, "character":"박정민(장도리)", "audience":5143409},
{"text":"내 운명은 내가 결정한다.", "movie":"탈주", "year":2024, "character":"이제훈(임규남)", "audience":2561872},
{"text":"실패할 자유가 있는 곳으로 가겠다.", "movie":"탈주", "year":2024, "character":"이제훈(임규남)", "audience":2561872},
{"text":"나는 이제 죽음이 되었다. 세계의 파괴자. (Now I am become Death, the destroyer of worlds.)", "movie":"오펜하이머", "year":2023, "character":"킬리언 머피(오펜하이머)", "audience":3230553},
{"text":"두려움은 마음을 죽이는 자다. (Fear is the mind-killer.)", "movie":"듄", "year":2021, "character":"티모시 샬라메(폴 아트레이디스)", "audience":1644733},
{"text":"생각하지 말고 그냥 해. (Don't think, just do.)", "movie":"탑건: 매버릭", "year":2022, "character":"톰 크루즈(매버릭)", "audience":8232423},
{"text":"데이터 뒤에 숨은 의도를 읽어야 한다.", "movie":"머니볼", "year":2011, "character":"조나 힐(피터 브랜드)", "audience":641496},
{"text":"회사는 가족이 아니라 팀이다. (We're a team, not a family.)", "movie":"실리콘 밸리(드라마)", "year":2014, "character":"토마스 미들디치(리처드 헨드릭스)", "audience":null},
{"text":"성공한 리더는 질문하는 사람이다.", "movie":"포드 v 페라리", "year":2019, "character":"존 번탈(이아코카)", "audience":1326442},
{"text":"지속 가능한 혁신만이 살아남는다.", "movie":"블랙 팬서", "year":2018, "character":"레티티아 라이트(슈리)", "audience":5399327},
{"text":"사람의 마음을 움직이는 것이 비즈니스의 시작이다.", "movie":"제리 맥과이어", "year":1996, "character":"톰 크루즈(제리 맥과이어)", "audience":null},
{"text":"어떤 수트보다 강력한 것은 자신감이다.", "movie":"슈츠(드라마)", "year":2011, "character":"가브리엘 매치(하비 스펙터)", "audience":null},
{"text":"혁신은 연결에서 시작된다. (Connecting the dots.)", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐(스티브 잡스)", "audience":225343},
{"text":"가장 위대한 승리는 싸우지 않고 이기는 것이다.", "movie":"명량", "year":2014, "character":"최민식(이순신)", "audience":17613682},
{"text":"우리는 우리가 믿는 대로 된다. (We become what we believe.)", "movie":"더 시크릿", "year":2006, "character":"나레이터", "audience":null},
{"text":"품격은 말에서 나오는 것이 아니라 행동에서 나온다.", "movie":"킹스맨: 시크릿 에이전트", "year":2015, "character":"콜린 퍼스(해리 하트)", "audience":6129681},
{"text":"기록은 기억을 지배한다.", "movie":"M(드라마)", "year":1994, "character":"나레이터", "audience":null},
{"text":"내 가치를 결정하는 것은 나 자신이다.", "movie":"이태원 클라쓰(드라마)", "year":2020, "character":"박서준(박새로이)", "audience":null},
{"text":"두려움은 직면할 때 사라진다. (To conquer fear, you must become fear.)", "movie":"배트맨 비긴즈", "year":2005, "character":"크리스찬 베일(브루스 웨인)", "audience":921315},
{"text":"데이터에 매몰되지 말고 맥락을 봐라.", "movie":"서치", "year":2018, "character":"존 조(데이빗 킴)", "audience":2950097},
{"text":"성공의 비결은 단 하나, 포기하지 않는 것이다.", "movie":"언브로큰", "year":2014, "character":"잭 오코넬(루이 잠페리니)", "audience":null},
{"text":"버틸 수 있는 만큼 버텨라. 그래야 앞으로 나갈 수 있다.", "movie":"미생(드라마)", "year":2014, "character":"이성민(오상식)", "audience":null},
{"text":"열정은 전염된다. (Passion is contagious.)", "movie":"드림걸즈", "year":2006, "character":"제이미 폭스(커티스)", "audience":null},
{"text":"실패는 다시 시작할 수 있는 기회다. (Failure is simply the opportunity to begin again.)", "movie":"파운더", "year":2016, "character":"마이클 키튼(레이 크록)", "audience":null},
{"text":"내일 할 일을 오늘 고민하지 마라. 내일의 네가 할 거다.", "movie":"미생(드라마)", "year":2014, "character":"이성민(오상식)", "audience":null},
{"text":"비판은 쉽지만 창조는 어렵다. (In many ways, the work of a critic is easy.)", "movie":"라따뚜이", "year":2007, "character":"피터 오툴(안톤 이고)", "audience":1025573},
{"text":"꿈을 이루지 못한 것보다 꿈을 잃어버리는 것이 더 무섭다.", "movie":"라라랜드", "year":2016, "character":"엠마 스톤(미아)", "audience":3772242},
{"text":"경험은 결코 늙지 않는다. (Experience never goes out of fashion.)", "movie":"인턴", "year":2015, "character":"로버트 드 니로(벤 휘태커)", "audience":3611166},
{"text":"혁신은 연결에서 시작된다. (Connecting the dots.)", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐(스티브 잡스)", "audience":225343},
{"text":"모든 문제는 해결 가능하다. (Every problem has a solution.)", "movie":"마션", "year":2015, "character":"맷 데이먼(마크 와트니)", "audience":4887144},
{"text":"최선은 다하는 게 아니라, 잘하는 것이다.", "movie":"스토브리그(드라마)", "year":2019, "character":"남궁민(백승수)", "audience":null},
{"text":"비전 없는 실행은 비극이고, 실행 없는 비전은 몽상이다.", "movie":"스타트업(드라마)", "year":2020, "character":"김선호(한지평)", "audience":null},
{"text":"데이터는 거짓말을 하지 않지만, 사람은 거짓말을 한다.", "movie":"비밀의 숲(드라마)", "year":2017, "character":"조승우(황시목)", "audience":null},
{"text":"우리는 문제를 해결하기 위해 고용된 사람들이다.", "movie":"울프 오브 월 스트리트", "year":2013, "character":"레오나르도 디카프리오(조던 벨포트)", "audience":585671},
{"text":"팀보다 위대한 선수는 없다. (No player is bigger than the team.)", "movie":"머니볼", "year":2011, "character":"브래드 피트(빌리 빈)", "audience":641496},
{"text":"시스템은 사람을 믿지 않는다. 프로세스를 믿는다.", "movie":"더 테러 라이브", "year":2013, "character":"하정우(윤영화)", "audience":5584139},
{"text":"전문가란 더 좁은 분야에서 더 많은 실수를 해본 사람이다.", "movie":"이미테이션 게임", "year":2014, "character":"베네딕트 컴버배치(앨런 튜닝)", "audience":1744213},
{"text":"기술은 도구일 뿐이다. 사람을 연결하는 것이 핵심이다.", "movie":"소셜 네트워크", "year":2010, "character":"저스틴 팀버레이크(숀 파커)", "audience":519018},
{"text":"디테일이 차이를 만든다.", "movie":"기생충", "year":2019, "character":"이선균(박 사장)", "audience":10313501},
{"text":"완벽함은 더 보탤 것이 없을 때가 아니라 빼낼 것이 없을 때 완성된다.", "movie":"어린 왕자", "year":2015, "character":"제프 브리지스(비행사)", "audience":null},
{"text":"리더는 길을 아는 사람이 아니라 길을 만드는 사람이다.", "movie":"킹덤(드라마)", "year":2019, "character":"주지훈(이창)", "audience":null},
{"text":"복잡함은 실패의 지름길이다. (Complexity is the enemy of execution.)", "movie":"스티브 잡스", "year":2015, "character":"마이클 패스벤더(스티브 잡스)", "audience":null},
{"text":"데이터 뒤에 숨은 의도를 읽어야 한다.", "movie":"머니볼", "year":2011, "character":"조나 힐(피터 브랜드)", "audience":641496},
{"text":"회사는 가족이 아니라 팀이다. (We're a team, not a family.)", "movie":"실리콘 밸리(드라마)", "year":2014, "character":"토마스 미들디치(리처드 헨드릭스)", "audience":null},
{"text":"지속 가능한 혁신만이 살아남는다.", "movie":"블랙 팬서", "year":2018, "character":"레티티아 라이트(슈리)", "audience":5399327},
{"text":"데이터에 매몰되지 말고 맥락을 봐라.", "movie":"서치", "year":2018, "character":"존 조(데이빗 킴)", "audience":2950097},
{"text":"버틸 수 있는 만큼 버텨라. 그래야 앞으로 나갈 수 있다.", "movie":"미생(드라마)", "year":2014, "character":"이성민(오상식)", "audience":null},
{"text":"실패는 다시 시작할 수 있는 기회다.", "movie":"파운더", "year":2016, "character":"마이클 키튼(레이 크록)", "audience":null},
{"text":"기록은 기억을 지배한다.", "movie":"M(드라마)", "year":1994, "character":"나레이터", "audience":null},
{"text":"품격은 말에서 나오는 것이 아니라 행동에서 나온다.", "movie":"킹스맨", "year":2015, "character":"콜린 퍼스(해리 하트)", "audience":6129681},
{"text":"성공은 준비가 기회를 만났을 때 나타나는 결과다.", "movie":"행복을 찾아서", "year":2006, "character":"윌 스미스(크리스 가드너)", "audience":null},
{"text":"함께 일하는 동료가 가장 큰 복지다.", "movie":"미생(드라마)", "year":2014, "character":"태인호(성준식)", "audience":null},
{"text":"길은 모두에게 열려 있지만, 모두가 그 길을 가질 수 있는 건 아니다.", "movie":"미생(드라마)", "year":2014, "character":"이성민(오상식)", "audience":null},
{"text":"포기하는 순간 시합은 종료되는 겁니다.", "movie":"더 퍼스트 슬램덩크", "year":2023, "character":"안 선생님", "audience":4879282},
{"text":"가끔은 걷기도 전에 뛰어야 할 때가 있지.", "movie":"아이언맨", "year":2008, "character":"로버트 다우니 주니어", "audience":4300365},
{"text":"변화는 불가피하지만, 성장은 선택이다.", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐", "audience":225343},
{"text":"실패는 옵션이다. 하지만 두려움은 아니다.", "movie":"포드 v 페라리", "year":2019, "character":"맷 데이먼", "audience":1326442},
{"text":"불가능해 보일 때까지는 항상 불가능해 보인다.", "movie":"인빅터스", "year":2009, "character":"모건 프리먼", "audience":null},
{"text":"리더는 희망을 파는 상인이다.", "movie":"나폴레옹(다큐)", "year":2015, "character":"나레이터", "audience":null},
{"text":"최고의 협업은 서로의 부족함을 채워주는 것이다.", "movie":"토이스토리", "year":1995, "character":"톰 행크스(우디)", "audience":null},
{"text":"모든 위대한 일은 작은 시작에서 비롯된다.", "movie":"인셉션", "year":2010, "character":"레오나르도 디카프리오", "audience":5835017},
{"text":"가장 강력한 수트는 자신감이다.", "movie":"슈츠(드라마)", "year":2011, "character":"가브리엘 매치", "audience":null},
{"text":"기회는 준비된 자에게만 찾아온다.", "movie":"에비에이터", "year":2004, "character":"레오나르도 디카프리오", "audience":null},
{"text":"우리가 답을 찾을 것이다. 늘 그랬듯이.", "movie":"인터스텔라", "year":2014, "character":"매튜 맥커너히", "audience":10309432},
{"text":"오늘의 불가능이 내일의 가능성이 된다.", "movie":"히든 피겨스", "year":2016, "character":"케빈 코스트너", "audience":449518},
{"text":"진정한 발견은 새로운 땅을 찾는 것이 아니라 새로운 눈으로 보는 것이다.", "movie":"인턴", "year":2015, "character":"앤 해서웨이", "audience":3611166},
{"text":"성공은 최종적인 것이 아니며, 실패는 치명적인 것이 아니다.", "movie":"다키스트 아워", "year":2017, "character":"게리 올드만", "audience":null},
{"text":"가장 큰 위험은 아무런 위험도 감수하지 않는 것이다.", "movie":"소셜 네트워크", "year":2010, "character":"제시 아이젠버그", "audience":519018},
{"text":"어제보다 나은 내일을 만드는 것이 우리의 일이다.", "movie":"드림즈(드라마)", "year":2019, "character":"남궁민", "audience":null},
{"text":"열정은 전염된다. (Passion is contagious.)", "movie":"드림걸즈", "year":2006, "character":"제이미 폭스", "audience":null},
{"text":"끝날 때까지는 끝난 게 아니다.", "movie":"요기 베라(다큐)", "year":2010, "character":"나레이터", "audience":null},
{"text":"단순함이 궁극의 정교함이다.", "movie":"스티브 잡스", "year":2015, "character":"마이클 패스벤더", "audience":null},
{"text":"우리는 우리가 반복적으로 하는 일의 결과다. 탁월함은 행동이 아니라 습관이다.", "movie":"코치 카터", "year":2005, "character":"사무엘 L. 잭슨", "audience":null},
{"text":"질문이 정답보다 중요하다.", "movie":"포드 v 페라리", "year":2019, "character":"존 번탈", "audience":1326442},
{"text":"혼자 가면 빨리 가지만, 함께 가면 멀리 간다.", "movie":"아프리카 속담(다큐 인용)", "year":2020, "character":"나레이터", "audience":null},
{"text":"미래를 예측하는 가장 좋은 방법은 미래를 창조하는 것이다.", "movie":"링컨", "year":2012, "character":"다니엘 데이 루이스", "audience":null},
{"text":"안전한 길만 가는 것은 가장 위험한 전략이다.", "movie":"탑건: 매버릭", "year":2022, "character":"톰 크루즈", "audience":8232423},
{"text":"효율성은 일을 제대로 하는 것이고, 효과성은 제대로 된 일을 하는 것이다.", "movie":"머니볼", "year":2011, "character":"브래드 피트", "audience":641496},
{"text":"내일의 나에게 부끄럽지 않은 오늘을 살자.", "movie":"미생(드라마)", "year":2014, "character":"임시완", "audience":null},
{"text":"도전은 우리를 살아있게 만든다.", "movie":"에베레스트", "year":2015, "character":"제이슨 클락", "audience":null},
{"text":"비판보다는 대안을 제시하라.", "movie":"라따뚜이", "year":2007, "character":"피터 오툴", "audience":1025573},
{"text":"중요한 건 속도가 아니라 방향이다.", "movie":"인턴", "year":2015, "character":"로버트 드 니로", "audience":3611166},
{"text":"실패를 두려워하는 것이 유일한 실패다.", "movie":"위대한 쇼맨", "year":2017, "character":"휴 잭맨", "audience":1410501},
{"text":"전문성은 태도에서 나온다.", "movie":"스토브리그(드라마)", "year":2019, "character":"남궁민", "audience":null},
{"text":"지식보다 중요한 것은 상상력이다.", "movie":"아인슈타인(다큐)", "year":2018, "character":"나레이터", "audience":null},
{"text":"과정의 공정함이 결과의 정당성을 만든다.", "movie":"변호인", "year":2013, "character":"송강호", "audience":11374610},
{"text":"작은 디테일이 큰 시스템을 결정한다.", "movie":"아폴로 13", "year":1995, "character":"톰 행크스", "audience":null},
{"text":"성장은 고통을 동반하지만, 그 결과는 달콤하다.", "movie":"위플래쉬", "year":2014, "character":"J.K. 시몬스", "audience":1602488},
{"text":"우리는 팀으로 승리하고 팀으로 패배한다.", "movie":"애니 기븐 선데이", "year":1999, "character":"알 파치노", "audience":null},
{"text":"창의성은 제약 조건 속에서 꽃핀다.", "movie":"라따뚜이", "year":2007, "character":"패튼 오스왈트", "audience":1025573},
{"text":"변화에 대응하는 유일한 방법은 변화를 선도하는 것이다.", "movie":"마진 콜", "year":2011, "character":"제레미 아이언스", "audience":null},
{"text":"기술의 끝은 결국 사람을 향해야 한다.", "movie":"빅 히어로", "year":2014, "character":"다니엘 헤니", "audience":2801949},
{"text":"최고의 리더는 다른 리더를 양성하는 사람이다.", "movie":"코치 카터", "year":2005, "character":"사무엘 L. 잭슨", "audience":null},
{"text":"기존의 틀을 깨는 것이 혁신의 첫걸음이다.", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐", "audience":225343},
{"text":"문제의 본질을 파악하는 것이 해결의 절반이다.", "movie":"셜록(드라마)", "year":2010, "character":"베네딕트 컴버배치", "audience":null},
{"text":"성공은 매일 반복되는 작은 노력의 합계다.", "movie":"행복을 찾아서", "year":2006, "character":"윌 스미스", "audience":null},
{"text":"효율적인 프로세스는 창의성을 방해하지 않는다.", "movie":"몬스터 주식회사", "year":2001, "character":"존 굿맨", "audience":null},
{"text":"우리가 믿는 것이 우리의 현실이 된다.", "movie":"매트릭스", "year":1999, "character":"로렌스 피시번", "audience":null},
{"text":"포기는 선택지에 없다.", "movie":"아폴로 13", "year":1995, "character":"에드 해리스", "audience":null},
{"text":"서로 다른 생각이 만날 때 시너지가 발생한다.", "movie":"인사이드 아웃", "year":2015, "character":"에이미 폴러", "audience":4969735},
{"text":"끊임없이 배우는 사람만이 앞서나갈 수 있다.", "movie":"인턴", "year":2015, "character":"로버트 드 니로", "audience":3611166},
{"text":"경청은 가장 강력한 소통의 도구다.", "movie":"제리 맥과이어", "year":1996, "character":"톰 크루즈", "audience":null},
{"text":"프로는 결과로 말한다.", "movie":"스토브리그(드라마)", "year":2019, "character":"박은빈", "audience":null},
{"text":"협업은 신뢰의 다른 이름이다.", "movie":"어벤져스", "year":2012, "character":"사무엘 L. 잭슨", "audience":7087068},
{"text":"기술은 인간의 한계를 확장한다.", "movie":"아이언맨", "year":2008, "character":"로버트 다우니 주니어", "audience":4300365},
{"text":"목표가 분명하면 길은 보이기 마련이다.", "movie":"이상한 나라의 앨리스", "year":2010, "character":"미아 와시코브스카", "audience":null},
{"text":"실수는 배움의 자산이다.", "movie":"주토피아", "year":2016, "character":"지니퍼 굿윈", "audience":4706158},
{"text":"오늘의 데이터가 내일의 자산이 된다.", "movie":"머니볼", "year":2011, "character":"조나 힐", "audience":641496},
{"text":"품질은 타협의 대상이 아니다.", "movie":"포드 v 페라리", "year":2019, "character":"크리스찬 베일", "audience":1326442},
{"text":"다름을 인정하는 것에서 팀워크가 시작된다.", "movie":"가디언즈 오브 갤럭시", "year":2014, "character":"크리스 프랫", "audience":1344645},
{"text":"모든 위대한 리더는 훌륭한 청취자였다.", "movie":"킹스 스피치", "year":2010, "character":"콜린 퍼스", "audience":807751},
{"text":"혁신은 기술이 아니라 생각의 전환이다.", "movie":"소셜 네트워크", "year":2010, "character":"제시 아이젠버그", "audience":519018},
{"text":"우리는 문제를 해결하기 위해 존재한다.", "movie":"마션", "year":2015, "character":"치웨텔 에지오포", "audience":4887144},
{"text":"지속적인 개선이 완벽보다 낫다.", "movie":"파운더", "year":2016, "character":"마이클 키튼", "audience":null},
{"text":"소통 없는 기술은 고립을 낳는다.", "movie":"그녀(Her)", "year":2013, "character":"호아킨 피닉스", "audience":375253},
{"text":"함께 고민할 때 최선의 답이 나온다.", "movie":"미생(드라마)", "year":2014, "character":"이성민", "audience":null},
{"text":"도전은 성장의 자양분이다.", "movie":"코코", "year":2017, "character":"안소니 곤잘레스", "audience":3513114},
{"text":"프로세스의 최적화가 비즈니스의 핵심이다.", "movie":"시카고", "year":2002, "character":"리차드 기어", "audience":null},
{"text":"미래를 준비하는 자가 승리한다.", "movie":"반지의 제왕", "year":2001, "character":"이안 맥켈런", "audience":3870000},
{"text":"팀의 성공이 나의 성공이다.", "movie":"어벤져스", "year":2012, "character":"크리스 에반스", "audience":7074891},
{"text":"사용자의 경험이 기술보다 우선이다.", "movie":"잡스", "year":2013, "character":"애쉬튼 커쳐", "audience":225343},
{"text":"정직한 데이터가 올바른 결정을 이끈다.", "movie":"서치", "year":2018, "character":"존 조", "audience":2950097},
{"text":"전문가는 끊임없이 의심하고 검증한다.", "movie":"이미테이션 게임", "year":2014, "character":"베네딕트 컴버배치", "audience":1744213},
{"text":"효율성은 단순함에서 나온다.", "movie":"스티브 잡스", "year":2015, "character":"마이클 패스벤더", "audience":null},
{"text":"성공은 목적지가 아니라 과정이다.", "movie":"포레스트 검프", "year":1994, "character":"톰 행크스", "audience":null},
{"text":"우리는 모두 누군가의 조력자다.", "movie":"미생(드라마)", "year":2014, "character":"이성민", "audience":null}
]

View File

@@ -0,0 +1,174 @@
[
"빛의 속도는 진공에서 초속 약 299,792,458m로, 우주에서 가장 빠른 속도입니다.",
"지구에서 태양까지 빛이 도달하는 데 약 8분 20초가 걸립니다.",
"물 분자(H₂O)는 산소 원자 1개와 수소 원자 2개로 구성됩니다.",
"DNA의 이중나선 구조는 1953년 왓슨과 크릭이 발견했습니다.",
"인간의 몸에는 약 37조 개의 세포가 있습니다.",
"절대영도(0K)는 -273.15°C로, 이론적으로 가능한 가장 낮은 온도입니다.",
"지구의 나이는 약 46억 년으로 추정됩니다.",
"태양은 수소 핵융합으로 에너지를 생산하며, 약 50억 년 후 적색거성이 됩니다.",
"중력은 질량을 가진 모든 물체 사이에 작용하는 인력입니다.",
"뉴턴의 제3법칙: 모든 작용에는 크기가 같고 방향이 반대인 반작용이 있습니다.",
"E=mc²은 에너지와 질량의 동등성을 나타내는 아인슈타인의 공식입니다.",
"원자는 양성자, 중성자, 전자로 구성되며, 원자핵은 양성자와 중성자로 이루어집니다.",
"주기율표에는 현재 118개의 원소가 등록되어 있습니다.",
"산소는 지구 대기의 약 21%를 차지하며, 호흡에 필수적입니다.",
"광합성은 식물이 빛 에너지로 CO₂와 H₂O를 포도당과 O₂로 변환하는 과정입니다.",
"미토콘드리아는 세포의 '발전소'로, ATP를 생산하여 에너지를 공급합니다.",
"유전자(Gene)는 DNA의 특정 구간으로, 단백질 생산 정보를 담고 있습니다.",
"돌연변이(Mutation)는 DNA 서열의 변화로, 진화의 원동력입니다.",
"자연선택(Natural Selection)은 환경에 적합한 개체가 생존·번식하는 진화 메커니즘입니다.",
"찰스 다윈은 《종의 기원》(1859)에서 자연선택에 의한 진화론을 발표했습니다.",
"인간 게놈 프로젝트는 2003년 완성되었으며, 약 30억 개의 염기쌍을 해독했습니다.",
"CRISPR-Cas9은 유전자를 정밀하게 편집하는 혁명적 생명공학 도구입니다.",
"줄기세포는 다양한 세포 유형으로 분화할 수 있는 만능 세포입니다.",
"항생제 내성은 박테리아가 항생제에 저항하는 능력을 획득하는 심각한 의료 위기입니다.",
"바이러스는 세포가 없으며, 숙주 세포의 기능을 이용하여 복제합니다.",
"백신은 면역 체계를 훈련시켜 특정 병원체에 대한 면역을 형성합니다.",
"mRNA 백신은 단백질 설계도를 전달하여 면역 반응을 유도하는 차세대 백신 기술입니다.",
"항체는 면역 체계가 생산하는 Y자형 단백질로, 특정 항원에 결합합니다.",
"혈액형은 A, B, O, AB 4가지로 분류되며, 적혈구 표면의 항원에 의해 결정됩니다.",
"인간의 뇌는 약 860억 개의 뉴런으로 구성되어 있습니다.",
"시냅스는 뉴런 간 신호를 전달하는 접합 부위입니다.",
"도파민은 쾌락과 보상에 관여하는 신경전달물질입니다.",
"세로토닌은 기분, 수면, 식욕을 조절하는 신경전달물질입니다.",
"멜라토닌은 수면-각성 주기를 조절하는 호르몬으로, 어두워지면 분비됩니다.",
"인슐린은 혈당을 낮추는 호르몬으로, 췌장의 베타 세포에서 분비됩니다.",
"호르몬은 혈류를 통해 이동하며 특정 기관의 기능을 조절하는 화학 물질입니다.",
"효소(Enzyme)는 생화학 반응의 속도를 높이는 생물학적 촉매입니다.",
"ATP(아데노신 삼인산)는 세포 내 에너지 화폐로, 모든 생명 활동에 사용됩니다.",
"세포 분열에는 체세포 분열(유사분열)과 생식세포 분열(감수분열)이 있습니다.",
"염색체는 DNA와 단백질로 구성된 구조물로, 인간은 46개(23쌍)를 가집니다.",
"암(Cancer)은 세포의 비정상적 증식으로, 통제되지 않는 세포 분열이 원인입니다.",
"텔로미어는 염색체 끝을 보호하는 구조로, 세포 분열마다 짧아져 노화와 관련됩니다.",
"후성유전학(Epigenetics)은 DNA 서열 변화 없이 유전자 발현이 조절되는 현상입니다.",
"마이크로바이옴은 인체에 공생하는 미생물 군집으로, 건강에 중요한 역할을 합니다.",
"플라시보 효과는 가짜 약이나 치료에도 실제 치료 효과가 나타나는 심리적 현상입니다.",
"pH는 용액의 산성도를 나타내는 지표로, 7이 중성, 7 미만이 산성, 7 초과가 염기성입니다.",
"화학 결합에는 이온 결합, 공유 결합, 금속 결합이 있습니다.",
"이온은 전자를 잃거나 얻어 전하를 띠는 원자 또는 분자입니다.",
"산화-환원 반응은 전자의 이동이 일어나는 화학 반응입니다.",
"촉매(Catalyst)는 반응 속도를 높이지만 자신은 변하지 않는 물질입니다.",
"엔트로피는 시스템의 무질서도를 나타내는 열역학 개념으로, 항상 증가합니다.",
"열역학 제1법칙: 에너지는 생성되거나 소멸되지 않고, 형태만 변환됩니다.",
"열역학 제2법칙: 닫힌 시스템의 엔트로피는 항상 증가합니다.",
"보일의 법칙: 온도가 일정할 때, 기체의 압력과 부피는 반비례합니다.",
"아보가드로 수(6.022×10²³)는 1몰에 포함된 입자의 수입니다.",
"고분자(Polymer)는 반복되는 단위체(Monomer)가 결합한 큰 분자입니다.",
"플라스틱은 합성 고분자 물질로, 열가소성과 열경화성으로 분류됩니다.",
"탄소 나노튜브는 원통형 탄소 구조로, 강철보다 강하고 구리보다 전기 전도성이 높습니다.",
"그래핀은 탄소 원자가 2차원 벌집 구조로 배열된 물질로, 놀라운 전기·열 전도성을 가집니다.",
"초전도체는 특정 온도 이하에서 전기 저항이 0이 되는 물질입니다.",
"양자역학은 원자·아원자 수준의 물리 현상을 설명하는 물리학 분야입니다.",
"불확정성 원리: 입자의 위치와 운동량을 동시에 정확히 알 수 없습니다 (하이젠베르크).",
"양자 얽힘은 두 양자가 거리에 관계없이 상태가 연결되어 있는 현상입니다.",
"슈뢰딩거의 고양이는 양자 중첩을 설명하는 유명한 사고실험입니다.",
"보어 모형은 전자가 특정 에너지 준위의 궤도를 돈다고 설명하는 원자 모형입니다.",
"파동-입자 이중성은 빛과 물질이 파동과 입자의 성질을 모두 가지는 현상입니다.",
"광전효과는 빛이 금속에 닿으면 전자가 방출되는 현상으로, 아인슈타인이 설명했습니다.",
"레이저(LASER)는 유도방출에 의한 빛의 증폭으로, 단색성·지향성·간섭성이 특징입니다.",
"핵분열은 무거운 원자핵이 쪼개지며 에너지를 방출하는 반응입니다 (원자력 발전).",
"핵융합은 가벼운 원자핵이 합쳐져 에너지를 방출하는 반응입니다 (태양의 에너지원).",
"방사성 붕괴는 불안정한 원자핵이 자발적으로 입자나 에너지를 방출하는 현상입니다.",
"반감기는 방사성 물질의 양이 절반으로 줄어드는 데 걸리는 시간입니다.",
"전자기파 스펙트럼은 라디오파, 마이크로파, 적외선, 가시광, 자외선, X선, 감마선을 포함합니다.",
"도플러 효과는 파원이 관찰자에 접근하면 파장이 짧아지고, 멀어지면 길어지는 현상입니다.",
"음속은 공기 중에서 약 343m/s이며, 온도와 매질에 따라 변합니다.",
"전류는 전하의 흐름이며, 단위는 암페어(A)입니다.",
"옴의 법칙: V = IR (전압 = 전류 × 저항).",
"반도체는 조건에 따라 도체와 부도체의 성질을 모두 보이는 물질입니다.",
"트랜지스터는 전류를 증폭하거나 스위칭하는 소자로, 현대 전자기기의 기본 구성요소입니다.",
"집적회로(IC)는 하나의 반도체 기판에 수십억 개의 트랜지스터를 집적한 소자입니다.",
"자기장은 전류가 흐르는 도선이나 자석 주위에 형성되는 힘의 장입니다.",
"패러데이의 법칙: 변하는 자기장은 전기장을 유도합니다 (전자기 유도).",
"맥스웰 방정식은 전기와 자기를 통합하여 전자기파의 존재를 예측한 방정식입니다.",
"상대성이론: 특수 상대성이론은 시간과 공간의 상대성을, 일반 상대성이론은 중력을 설명합니다.",
"중력파는 시공간의 파동으로, 2015년 LIGO에 의해 최초로 관측되었습니다.",
"블랙홀은 중력이 너무 강해 빛조차 탈출할 수 없는 시공간 영역입니다.",
"빅뱅은 약 138억 년 전 우주가 극도로 뜨겁고 밀도 높은 상태에서 시작되었다는 이론입니다.",
"허블의 법칙: 은하가 멀수록 더 빠르게 후퇴하며, 이는 우주가 팽창하고 있음을 의미합니다.",
"우주 배경 복사(CMB)는 빅뱅의 잔열로, 우주 전체에 균일하게 퍼져 있습니다.",
"암흑물질은 우주 질량의 약 27%를 차지하지만, 빛과 상호작용하지 않아 직접 관측이 불가합니다.",
"암흑에너지는 우주 가속 팽창의 원인으로 추정되며, 우주의 약 68%를 차지합니다.",
"외계행성(Exoplanet)은 태양이 아닌 다른 항성 주위를 도는 행성으로, 5000개 이상 발견되었습니다.",
"골디락스 존(생명체 거주 가능 영역)은 물이 액체로 존재할 수 있는 항성 주변 영역입니다.",
"화성에는 과거 물이 흘렀던 흔적이 발견되어 생명체 존재 가능성이 연구되고 있습니다.",
"국제우주정거장(ISS)은 지구 저궤도를 돌며 미세중력 환경 실험을 수행합니다.",
"인공위성은 지구 궤도를 도는 인공 물체로, 통신·기상·GPS 등에 활용됩니다.",
"GPS는 24개 이상의 위성 신호로 지구상 위치를 정밀하게 측정하는 시스템입니다.",
"판 구조론은 지구의 외부가 여러 판으로 이루어져 있으며, 판의 이동이 지진과 화산을 유발한다는 이론입니다.",
"지진의 규모는 리히터 규모로 측정하며, 1 증가할 때 에너지는 약 32배 증가합니다.",
"화산 폭발은 마그마가 지표로 분출하는 현상으로, 기후 변화를 일으킬 수 있습니다.",
"지구 자기장은 외핵의 액체 철의 대류에 의해 생성되며, 태양풍으로부터 지구를 보호합니다.",
"오존층은 성층권에 있는 오존(O₃) 밀집 구역으로, 자외선을 차단합니다.",
"온실효과는 대기 중 온실가스(CO₂, CH₄ 등)가 적외선을 흡수·방출하여 지구를 따뜻하게 합니다.",
"기후변화는 온실가스 증가로 인한 지구 평균 기온 상승과 이에 따른 기상 이변입니다.",
"탄소 중립(Carbon Neutrality)은 이산화탄소 배출량을 흡수량과 같게 만드는 것입니다.",
"재생에너지는 태양광, 풍력, 수력, 지열 등 자연적으로 보충되는 에너지원입니다.",
"태양전지(Solar Cell)는 빛 에너지를 전기 에너지로 직접 변환하는 소자입니다.",
"페로브스카이트 태양전지는 차세대 태양전지로, 제조 비용이 낮고 효율이 빠르게 향상 중입니다.",
"리튬이온 배터리는 높은 에너지 밀도로 스마트폰, 전기차 등에 널리 사용됩니다.",
"전고체 배터리는 액체 전해질 대신 고체 전해질을 사용하여 안전성과 에너지 밀도를 높입니다.",
"수소 연료전지는 수소와 산소의 화학 반응으로 전기를 생산하며, 물만 배출합니다.",
"핵융합 발전은 태양의 에너지 생성 원리를 이용한 궁극의 에너지원으로, ITER 프로젝트가 진행 중입니다.",
"진화적 적응(Adaptation)은 생물이 환경에 맞게 형질이 변화하는 과정입니다.",
"공생(Symbiosis)은 두 종이 밀접하게 관계하며 함께 사는 것으로, 상호이익·편리공생·기생이 있습니다.",
"생태계(Ecosystem)는 생물과 비생물 환경이 상호작용하는 체계입니다.",
"생물 다양성(Biodiversity)은 유전자, 종, 생태계 수준의 생물학적 다양성입니다.",
"멸종 위기종은 개체수가 급감하여 사라질 위험이 있는 종으로, IUCN 적색 목록에 등재됩니다.",
"세포막은 인지질 이중층으로, 세포 내외부 물질 출입을 조절합니다.",
"삼투(Osmosis)는 반투막을 통해 농도가 낮은 쪽에서 높은 쪽으로 용매가 이동하는 현상입니다.",
"단백질은 아미노산 서열로 구성되며, 효소·항체·구조 단백질 등 다양한 역할을 합니다.",
"RNA는 DNA 정보를 읽어 단백질 합성을 매개하는 핵산입니다 (mRNA, tRNA, rRNA).",
"리보솜은 mRNA 정보를 읽어 단백질을 합성하는 세포 소기관입니다.",
"소포체(ER)는 단백질 합성과 지질 합성에 관여하는 세포 내 막 구조입니다.",
"골지체는 단백질을 가공·분류·포장하여 세포 밖으로 분비하는 소기관입니다.",
"리소좀은 세포 내 소화를 담당하는 효소를 포함한 소기관입니다.",
"엽록체는 광합성이 일어나는 식물 세포 소기관으로, 빛 에너지를 화학 에너지로 변환합니다.",
"PCR(중합효소 연쇄반응)은 DNA를 짧은 시간에 대량으로 복제하는 기술입니다.",
"질량 분석법(Mass Spectrometry)은 분자의 질량을 측정하여 물질을 분석하는 기술입니다.",
"분광학(Spectroscopy)은 빛과 물질의 상호작용을 연구하여 물질을 분석합니다.",
"크로마토그래피는 혼합물을 구성 성분으로 분리하는 분석 기술입니다.",
"X선 결정학은 결정에 X선을 쏘아 분자의 3차원 구조를 밝히는 기술입니다.",
"전자현미경(SEM/TEM)은 전자 빔으로 나노미터 수준의 미세 구조를 관찰합니다.",
"원자력 현미경(AFM)은 탐침으로 표면을 스캔하여 원자 수준의 지형을 측정합니다.",
"3D 프린팅은 디지털 설계를 층층이 쌓아 입체 물체를 제작하는 적층 제조 기술입니다.",
"나노기술은 1~100nm 스케일에서 물질을 제어하여 새로운 기능을 구현하는 기술입니다.",
"생체모방(Biomimicry)은 자연의 구조와 과정을 모방하여 기술 문제를 해결합니다.",
"인공지능의 튜링 테스트는 기계가 인간과 구별 불가능하게 대화할 수 있는지 평가합니다.",
"정보 이론의 비트(bit)는 정보의 최소 단위로, 0 또는 1의 값을 가집니다.",
"섀넌의 엔트로피는 정보의 불확실성을 측정하는 척도입니다.",
"카오스 이론은 초기 조건의 미세한 차이가 결과에 큰 영향을 미치는 시스템을 연구합니다.",
"프랙탈은 부분과 전체가 자기유사성을 가지는 기하학적 구조입니다.",
"게임 이론은 합리적 의사결정자들의 전략적 상호작용을 수학적으로 분석합니다.",
"확률론은 불확실한 사건의 가능성을 수학적으로 다루는 분야입니다.",
"통계학은 데이터를 수집·분석·해석하여 의사결정에 도움을 주는 학문입니다.",
"베이즈 정리는 사전 확률을 새로운 증거로 업데이트하여 사후 확률을 계산합니다.",
"정규분포(가우스 분포)는 자연 현상에서 가장 흔한 확률 분포로, 종 모양 곡선입니다.",
"피보나치 수열(1, 1, 2, 3, 5, 8, ...)은 자연에서 식물의 잎 배열, 껍질 나선 등에 나타납니다.",
"황금비(1.618...)는 예술과 건축에서 아름다운 비율로 여겨지는 수학적 상수입니다.",
"원주율 π(3.14159...)는 원의 둘레와 지름의 비율입니다.",
"자연로그의 밑 e(2.71828...)는 미적분학에서 가장 중요한 상수입니다.",
"미적분학은 뉴턴과 라이프니츠가 독립적으로 개발한 수학의 핵심 분야입니다.",
"소수(Prime Number)는 1과 자기 자신만으로 나누어지는 1보다 큰 자연수입니다.",
"암호학에서 RSA 알고리즘은 큰 소수의 곱을 인수분해하기 어려운 성질을 이용합니다.",
"위상수학(Topology)은 도형의 연속적 변형에서 변하지 않는 성질을 연구합니다.",
"유클리드 기하학은 평행선 공준에 기초한 고전 기하학입니다.",
"비유클리드 기하학은 곡면에서의 기하학으로, 일반 상대성이론의 수학적 기초입니다.",
"리만 가설은 미해결 수학 문제로, 밀레니엄 7대 난제 중 하나입니다.",
"페르마의 마지막 정리는 1995년 앤드루 와일스에 의해 증명되었습니다.",
"물의 비열은 4.18J/(g·K)로, 대부분의 물질보다 높아 온도 조절 역할을 합니다.",
"소리는 매질의 진동으로 전파되며, 진공에서는 전달되지 않습니다.",
"적외선(IR)은 가시광보다 파장이 긴 전자기파로, 열 감지와 통신에 활용됩니다.",
"자외선(UV)은 가시광보다 파장이 짧은 전자기파로, 살균과 비타민D 합성에 관여합니다.",
"감마선은 가장 에너지가 높은 전자기파로, 암 치료와 물질 분석에 사용됩니다.",
"MRI(자기공명영상)는 강한 자기장으로 체내 수소 원자를 공명시켜 영상을 만듭니다.",
"CT(컴퓨터 단층촬영)는 X선을 이용하여 체내의 단면 영상을 생성합니다.",
"초음파(Ultrasound)는 높은 주파수의 음파로, 태아 검사와 물질 탐상에 사용됩니다.",
"탄소-14 연대 측정법은 유기물의 방사성 탄소 비율로 연대를 추정합니다 (최대 5만 년).",
"플라즈마는 고온에서 원자가 이온화된 상태로, 물질의 제4의 상태입니다.",
"보스-아인슈타인 응축(BEC)은 절대영도 근처에서 원자들이 하나의 양자 상태를 공유하는 현상입니다.",
"양자 컴퓨팅은 중첩과 얽힘을 이용하여 특정 문제를 기존 컴퓨터보다 빠르게 풉니다.",
"합성생물학은 생물 시스템을 설계·제작하여 새로운 기능을 가진 유기체를 만드는 분야입니다.",
"마이크로유체공학(Microfluidics)은 미세한 채널에서 유체를 제어하는 '칩 위의 실험실'입니다."
]

View File

@@ -0,0 +1,86 @@
{
"01-01": ["🎉 새해 첫날! 올 한 해도 좋은 일만 가득하시길 바랍니다."],
"01-04": ["📰 1885년 오늘, 세계 최초 충수돌기(맹장) 수술이 성공적으로 시행되었습니다."],
"01-13": ["🇺🇸 미주 한인의 날 — 1903년 한인 이민자들이 처음 미국에 도착한 날입니다."],
"01-15": ["🕊️ 마틴 루터 킹 주니어 날 — 비폭력 민권 운동의 정신을 기립니다."],
"01-27": ["🕯️ 홀로코스트 추모의 날 — 1945년 아우슈비츠 해방일입니다."],
"02-04": ["🎗️ 세계 암의 날 — 암에 대한 인식을 높이고 조기 검진을 장려합니다."],
"02-11": ["💡 발명가의 날 — 에디슨의 생일을 기념하며 혁신적인 아이디어를 생각해보세요."],
"02-14": ["💕 밸런타인 데이 — 소중한 사람에게 마음을 전해보세요."],
"02-28": ["🇰🇷 2·28 민주운동 기념일 — 대구에서 일어난 학생 민주화 운동을 기립니다."],
"03-01": ["🇰🇷 3·1절 — 1919년 독립만세운동을 기리는 날입니다."],
"03-03": ["🐾 세계 야생 동식물의 날 — 멸종 위기 종 보호와 생태계의 중요성을 되새깁니다."],
"03-08": ["🌷 세계 여성의 날 — 여성의 사회적 기여를 기념합니다."],
"03-14": ["🥧 파이 데이(π Day) — 원주율 3.14를 기념하는 날! 수학의 아름다움을 느껴보세요."],
"03-15": ["🇰🇷 3·15 의거 기념일 — 부정선거에 항거한 민주화 운동을 기립니다."],
"03-20": ["😊 국제 행복의 날 — 행복이 인류의 보편적 목표임을 기념합니다."],
"03-21": ["🌳 세계 산림의 날 — 숲의 보존과 지속 가능한 관리를 생각합니다."],
"03-22": ["💧 세계 물의 날 — 깨끗한 물의 소중함을 되새기는 날입니다."],
"03-24": ["🏥 세계 결핵의 날 — 결핵 퇴치를 위한 노력을 되새기는 날입니다."],
"04-01": ["🃏 만우절 — 가벼운 장난으로 웃음을 나누는 날입니다."],
"04-03": ["🇰🇷 4·3 희생자 추념일 — 제주 4·3 사건의 희생자를 기리고 평화를 기원합니다."],
"04-05": ["🌱 식목일 — 나무를 심고 녹색 환경을 가꾸는 날입니다."],
"04-07": ["⚕️ 세계 보건의 날 — 전 세계의 보건 복지 증진을 위해 지정된 날입니다."],
"04-11": ["🇰🇷 대한민국 임시정부 수립 기념일 — 1919년 임시정부의 법통을 기립니다."],
"04-12": ["🚀 1961년 오늘, 유리 가가린이 인류 최초 우주비행에 성공했습니다."],
"04-16": ["🎗️ 국민안전의 날 — 세월호 참사 이후 안전의 중요성을 되새기기 위해 제정되었습니다."],
"04-19": ["🇰🇷 4·19 혁명 기념일 — 자유·민주·정의의 헌법 정신을 기리는 날입니다."],
"04-20": ["♿ 장애인의 날 — 장애인에 대한 이해를 깊게 하고 재활 의욕을 고취합니다."],
"04-21": ["🔬 과학의 날 — 과학기술의 진흥과 대중화를 위해 제정되었습니다."],
"04-22": ["🌍 지구의 날(Earth Day) — 환경 보호의 중요성을 되새기는 날입니다."],
"04-25": ["🦟 세계 말라리아의 날 — 말라리아 퇴치를 위한 국제적 노력을 기념합니다."],
"04-28": ["🦺 세계 산업안전보건의 날 — 안전한 근무 환경을 위해 함께 노력합시다."],
"05-01": ["👷 근로자의 날 — 모든 근로자의 노고에 감사드립니다."],
"05-05": ["🧒 어린이날 — 미래의 주인공들을 축하합니다!"],
"05-08": ["💐 어버이날 — 부모님의 사랑과 은혜에 감사드리는 날입니다."],
"05-10": ["🌊 바다식목일 — 바다 속 생태계 보호를 위해 바다숲을 가꾸는 날입니다."],
"05-15": ["📚 스승의 날 — 가르침에 감사드리는 날입니다."],
"05-18": ["🕯️ 5·18 민주화운동 기념일 — 민주주의를 위한 희생을 기억합니다."],
"05-19": ["⚙️ 발명의 날 — 발명 의욕을 고취하고 과학기술 발전을 기립니다."],
"05-21": ["🌐 세계 문화 다양성의 날 / 부부의 날 — 서로 다른 문화와 소중한 배우자를 존중합시다."],
"05-31": ["🚭 세계 금연의 날 — 담배 없는 깨끗한 세상을 꿈꾸는 날입니다."],
"06-01": ["🇰🇷 의병의 날 — 나라가 위기에 처했을 때 자발적으로 일어난 의병을 기립니다."],
"06-05": ["🌍 세계 환경의 날 — 지구를 위한 작은 실천을 시작해보세요."],
"06-06": ["🇰🇷 현충일 — 호국영령의 숭고한 희생을 기억합니다."],
"06-10": ["🇰🇷 6·10 민주항쟁 기념일 — 1987년 전국적인 민주화 운동을 기립니다."],
"06-25": ["🕊️ 1950년 오늘, 한국전쟁이 발발했습니다. 평화의 소중함을 되새깁니다."],
"06-28": ["🚆 철도의 날 — 한국 철도의 창설과 발전을 기념하는 날입니다."],
"07-04": ["🇺🇸 미국 독립기념일 — 1776년 독립선언서가 공표된 날입니다."],
"07-11": ["👪 세계 인구의 날 — 인구 문제에 대한 관심을 높이기 위해 지정되었습니다."],
"07-17": ["🇰🇷 제헌절 — 대한민국 헌법이 공포된 날입니다."],
"07-20": ["🌙 1969년 오늘, 닐 암스트롱이 달에 첫 발을 디뎠습니다. '인류의 위대한 도약!'"],
"07-28": ["🏥 세계 간염의 날 — 간염 예방과 관리에 대한 인식을 높이는 날입니다."],
"08-12": ["🙋 세계 청소년의 날 — 청소년의 역할과 책임을 되새기는 날입니다."],
"08-14": ["🦋 일본군 '위안부' 피해자 기림의 날 — 피해자들의 용기와 아픔을 기억합니다."],
"08-15": ["🇰🇷 광복절 — 1945년 조국 광복의 기쁨을 기념합니다."],
"08-29": ["🇰🇷 경술국치일 — 1910년 국권을 상실한 슬픈 역사를 잊지 않기 위한 날입니다."],
"09-01": ["📰 방재의 날 — 자연재해에 대비하는 안전 의식을 높이는 날입니다."],
"09-07": ["☁️ 푸른 하늘의 날 — 대기오염 저감과 기후변화에 대한 관심을 촉구합니다."],
"09-10": ["🤝 세계 자살 예방의 날 — 생명의 소중함과 자살 예방을 위해 노력하는 날입니다."],
"09-21": ["🕊️ 세계 평화의 날 — 전쟁과 폭력이 없는 평화로운 세상을 기원합니다."],
"09-28": ["🌊 세계 해양의 날 — 바다의 소중함을 되새기는 날입니다."],
"10-01": ["🇰🇷 국군의 날 / 👴 세계 노인의 날 — 국군의 위용을 기리고 어르신들께 감사드립니다."],
"10-02": ["👵 노인의 날 — 경로효친 사상을 함양하고 어르신들의 노고에 보답합니다."],
"10-03": ["🇰🇷 개천절 — 단군왕검의 고조선 건국을 기리는 날입니다."],
"10-04": ["🚀 1957년 오늘, 소련이 세계 최초 인공위성 스푸트니크를 발사했습니다."],
"10-05": ["👨‍🏫 세계 교사의 날 — 교육의 중요성과 교사들의 헌신을 기념합니다."],
"10-09": ["🇰🇷 한글날 — 세종대왕의 훈민정음 창제를 기념합니다. 한글의 우수성을 자랑스러워합시다!"],
"10-10": ["🧠 세계 정신건강의 날 — 정신건강에 대한 올바른 인식을 고취합니다."],
"10-16": ["🇰🇷 부마민주항쟁 기념일 — 부산과 마산의 시민들이 유신독재에 항거한 날입니다."],
"10-21": ["👮 경찰의 날 — 민생치안의 주역인 경찰관들의 노고를 치하합니다."],
"10-24": ["🇺🇳 유엔의 날 — 1945년 유엔 헌장이 발효된 날입니다."],
"10-25": ["⛰️ 독도의 날 — 독도가 대한민국의 영토임을 대내외에 알리는 날입니다."],
"10-29": ["💻 1969년 오늘, 인터넷의 전신 ARPANET에서 최초의 메시지가 전송되었습니다."],
"11-03": ["📰 학생독립운동기념일 — 1929년 광주학생운동을 기억합니다."],
"11-09": ["🚒 소방의 날 — 화재 예방에 대한 경각심을 높이고 소방관을 격려합니다."],
"11-11": ["🍫 빼빼로데이 / 제1차 세계대전 종전 기념일(1918) — 평화를 기원합니다."],
"11-17": ["🇰🇷 순국선열의 날 — 국권 회복을 위해 희생하신 선열들을 기립니다."],
"11-19": ["👶 세계 아동학대 예방의 날 — 아동 보호와 학대 근절을 위해 노력합시다."],
"11-20": ["🎎 세계 어린이날 — 모든 어린이의 권리를 보호하고 증진하는 날입니다."],
"12-01": ["🩸 세계 에이즈의 날 — 에이즈 예방과 환자에 대한 편견 해소를 위해 노력합니다."],
"12-03": ["♿ 세계 장애인의 날 — 장애인의 권리 증진과 사회 참여를 독려합니다."],
"12-05": ["🤝 무역의 날 / 세계 자원봉사자의 날 — 수출 증대와 봉사의 가치를 기념합니다."],
"12-10": ["📜 세계 인권의 날 — 1948년 세계인권선언 채택을 기념합니다."],
"12-25": ["🎄 크리스마스 — 따뜻한 마음을 나누는 날입니다."],
"12-31": ["🎊 한 해의 마지막 날! 올 한 해 수고 많으셨습니다. 내년에도 좋은 일만 가득하길!"]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 488 B

View File

@@ -0,0 +1,10 @@
{
"appName": "AX Copilot",
"companyName": "AX연구소 AI팀",
"authorName": "백승재",
"authorTitle": "SW Architect",
"purpose": "업무 편의성 증가 및 시스템의 직관적인 연결을 위해 제작",
"copyright": "© 2026 AX연구소",
"blogUrl": "www.swarchitect.net",
"contributors": "경윤영님, 윤지영님, 배지훈님"
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
<!-- 다이아몬드 픽셀 아이콘: 상=파랑, 하=빨강, 좌=녹, 우=녹 -->
<g transform="translate(256,256) rotate(45)">
<!-- 좌상→상: Blue -->
<rect x="-105" y="-105" width="95" height="95" rx="10" fill="#4488FF"/>
<!-- 우상→우: Green -->
<rect x="10" y="-105" width="95" height="95" rx="10" fill="#44DD66"/>
<!-- 좌하→좌: Green -->
<rect x="-105" y="10" width="95" height="95" rx="10" fill="#44DD66"/>
<!-- 우하→하: Red -->
<rect x="10" y="10" width="95" height="95" rx="10" fill="#FF4466"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 669 B

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 449 KiB

View File

@@ -0,0 +1 @@
[역할] 당신은 회사 업무의 도움을 주는 매우 유능한 비서입니다. 말투는 정중하게 하며, 욕을 해서는 안됩니다.

View File

@@ -0,0 +1,148 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>AxCopilot</RootNamespace>
<AssemblyName>AxCopilot</AssemblyName>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<!--
★ 버전 관리 규칙 ★
이 <Version> 값 하나만 변경하면 앱 전체에 자동 반영됩니다:
- 설정 창 하단 버전 표시 (SettingsWindow.xaml.cs → SetVersionText())
- Windows 속성 탭 파일 버전 / 제품 버전
- DEVELOPMENT.md 변경 이력은 수동으로 함께 업데이트하세요.
설정 스키마 버전(마이그레이션)은 Services/SettingsService.cs → CurrentSettingsVersion 을 별도로 관리합니다.
-->
<Version>1.8.0</Version>
<Company>AX</Company>
<Product>AX Copilot</Product>
<Description>AI 기반 업무 자동화 런처 &amp; 코파일럿</Description>
<!-- ScreenCaptureHandler의 LockBits 포인터 연산에 필요 -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- 배포: build.bat에서 self-contained / framework-dependent 두 종류로 publish -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!-- ─── 디컴파일 방지 ─────────────────────────────────────────────── -->
<!-- 디버그 심볼 제거 (Release) -->
<DebugType Condition="'$(Configuration)'=='Release'">none</DebugType>
<DebugSymbols Condition="'$(Configuration)'=='Release'">false</DebugSymbols>
<!-- 임베디드 소스 제거 -->
<EmbedAllSources>false</EmbedAllSources>
<!-- Suppress source link -->
<EnableSourceLink>false</EnableSourceLink>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
</PropertyGroup>
<!-- Release 빌드 시 추가 난독화 설정 -->
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<!-- 사용하지 않는 멤버 제거 (IL trimming) -->
<PublishTrimmed>false</PublishTrimmed>
<!-- PDB 제거 -->
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
</PropertyGroup>
<!-- UseWindowsForms의 암묵적 using(System.Windows.Forms)이 WPF의
System.Windows.Input과 충돌(KeyEventArgs 모호성)하므로 전역 제거.
System.Drawing도 전역 제거: System.Windows.Media의 Color/ColorConverter/FontFamily와
모호한 참조 충돌 방지. Drawing 타입이 필요한 파일에서만 명시적 using으로 추가. -->
<ItemGroup>
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AxCopilot.SDK\AxCopilot.SDK.csproj" />
</ItemGroup>
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
</ItemGroup>
<!-- Assets -->
<ItemGroup>
<!-- 가이드 원본 HTML: 편집용. 출력에 복사하지 않음 (암호화 버전 사용) -->
<None Include="Assets\AX Copilot 사용가이드.htm" />
<None Include="Assets\AX Copilot 개발자가이드.htm" />
<!-- 암호화된 가이드: 빌드 출력에 복사 (앱 내장 뷰어로 복호화) -->
<Content Include="Assets\guide_user.enc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Include="Assets\guide_dev.enc">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- 아이콘: 앱 내장 리소스 + 출력에도 복사 (트레이용) -->
<Resource Include="Assets\icon.ico" />
<None Update="Assets\icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- about.json: 빌드 시 exe에 내장되는 리소스. 런타임 파일 수정 불가. -->
<Resource Include="Assets\about.json" />
<!-- 격려/명언 문구: 빌드 시 내장. 인터넷 연결 불필요. -->
<Resource Include="Assets\Quotes\motivational.json" />
<Resource Include="Assets\Quotes\famous.json" />
<Resource Include="Assets\Quotes\display_semiconductor.json" />
<Resource Include="Assets\Quotes\it_ai.json" />
<Resource Include="Assets\Quotes\science.json" />
<Resource Include="Assets\Quotes\history.json" />
<Resource Include="Assets\Quotes\today_events.json" />
<Resource Include="Assets\Quotes\english.json" />
<Resource Include="Assets\Quotes\movies.json" />
<Resource Include="Assets\Quotes\greetings.json" />
<!-- 대화 주제 프리셋: 9종 시스템 프롬프트 (빌드 시 내장) -->
<EmbeddedResource Include="Assets\Presets\*.json" />
<!-- 마스코트 이미지: 내장 리소스 (EXE에 포함됨, 교체 시 재빌드 필요) -->
<Resource Include="Assets\mascot.png" Condition="Exists('Assets\mascot.png')" />
<Resource Include="Assets\mascot.jpg" Condition="Exists('Assets\mascot.jpg')" />
<Resource Include="Assets\mascot.webp" Condition="Exists('Assets\mascot.webp')" />
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
<Content Include="Assets\SearchEngines\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- 시스템 프롬프트: 빌드 출력에 복사 (동적 로딩) -->
<Content Include="Assets\system_prompt.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>system_prompt.txt</Link>
</Content>
</ItemGroup>
<!-- 내장 스킬 파일: 빌드 출력 skills/ 폴더에 복사 -->
<ItemGroup>
<Content Include="skills\*.skill.md">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>skills\%(Filename)%(Extension)</Link>
</Content>
</ItemGroup>
<!-- 테마 리소스 딕셔너리 -->
<ItemGroup>
<Resource Include="Themes\Dark.xaml" />
<Resource Include="Themes\Light.xaml" />
<Resource Include="Themes\OLED.xaml" />
<Resource Include="Themes\Nord.xaml" />
<Resource Include="Themes\Monokai.xaml" />
<Resource Include="Themes\Catppuccin.xaml" />
<Resource Include="Themes\Sepia.xaml" />
<Resource Include="Themes\Alfred.xaml" />
<Resource Include="Themes\AlfredLight.xaml" />
</ItemGroup>
<!-- 단위 테스트 프로젝트에서 internal 멤버 접근 허용 -->
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>AxCopilot.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,361 @@
<Project>
<PropertyGroup>
<AssemblyName>AxCopilot</AssemblyName>
<IntermediateOutputPath>obj\Release\</IntermediateOutputPath>
<BaseIntermediateOutputPath>obj\</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath>E:\AX Copilot\src\AxCopilot\obj\</MSBuildProjectExtensionsPath>
<_TargetAssemblyProjectName>AxCopilot</_TargetAssemblyProjectName>
<RootNamespace>AxCopilot</RootNamespace>
</PropertyGroup>
<Import Project="Sdk.props" Sdk="Microsoft.NET.Sdk" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
<UseWindowsForms>true</UseWindowsForms>
<RootNamespace>AxCopilot</RootNamespace>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<!--
★ 버전 관리 규칙 ★
이 <Version> 값 하나만 변경하면 앱 전체에 자동 반영됩니다:
- 설정 창 하단 버전 표시 (SettingsWindow.xaml.cs → SetVersionText())
- Windows 속성 탭 파일 버전 / 제품 버전
- DEVELOPMENT.md 변경 이력은 수동으로 함께 업데이트하세요.
설정 스키마 버전(마이그레이션)은 Services/SettingsService.cs → CurrentSettingsVersion 을 별도로 관리합니다.
-->
<Version>1.2.2</Version>
<Company>AX</Company>
<Product>AX Copilot</Product>
<Description>AI 기반 업무 자동화 런처 &amp; 코파일럿</Description>
<!-- ScreenCaptureHandler의 LockBits 포인터 연산에 필요 -->
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- 배포: build.bat에서 self-contained / framework-dependent 두 종류로 publish -->
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<!-- ─── 디컴파일 방지 ─────────────────────────────────────────────── -->
<!-- 디버그 심볼 제거 (Release) -->
<DebugType Condition="'$(Configuration)'=='Release'">none</DebugType>
<DebugSymbols Condition="'$(Configuration)'=='Release'">false</DebugSymbols>
<!-- 임베디드 소스 제거 -->
<EmbedAllSources>false</EmbedAllSources>
<!-- Suppress source link -->
<EnableSourceLink>false</EnableSourceLink>
<DeterministicSourcePaths>false</DeterministicSourcePaths>
</PropertyGroup>
<!-- Release 빌드 시 추가 난독화 설정 -->
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<!-- 사용하지 않는 멤버 제거 (IL trimming) -->
<PublishTrimmed>false</PublishTrimmed>
<!-- PDB 제거 -->
<CopyOutputSymbolsToPublishDirectory>false</CopyOutputSymbolsToPublishDirectory>
</PropertyGroup>
<!-- UseWindowsForms의 암묵적 using(System.Windows.Forms)이 WPF의
System.Windows.Input과 충돌(KeyEventArgs 모호성)하므로 전역 제거.
System.Drawing도 전역 제거: System.Windows.Media의 Color/ColorConverter/FontFamily와
모호한 참조 충돌 방지. Drawing 타입이 필요한 파일에서만 명시적 using으로 추가. -->
<ItemGroup>
<Using Remove="System.Windows.Forms" />
<Using Remove="System.Drawing" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AxCopilot.SDK\AxCopilot.SDK.csproj" />
</ItemGroup>
<!-- 클립보드 히스토리 DPAPI 암호화 (Windows 전용, 외부 네트워크 통신 없음) -->
<!-- Windows 서비스 관리 (ServiceHandler — 로컬 전용, 외부 네트워크 통신 없음) -->
<ItemGroup>
<PackageReference Include="DocumentFormat.OpenXml" Version="3.2.0" />
<PackageReference Include="Markdig" Version="0.37.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
</ItemGroup>
<!-- Assets -->
<ItemGroup>
<!-- 사용 가이드 HTML: 빌드 출력에 복사 (기본 브라우저로 열기) -->
<Content Include="Assets\AX Copilot 사용가이드.htm">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- 아이콘: 앱 내장 리소스 + 출력에도 복사 (트레이용) -->
<None Update="Assets\icon.ico">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<!-- about.json: 빌드 시 exe에 내장되는 리소스. 런타임 파일 수정 불가. -->
<!-- 격려/명언 문구: 빌드 시 내장. 인터넷 연결 불필요. -->
<!-- 대화 주제 프리셋: 9종 시스템 프롬프트 (빌드 시 내장) -->
<EmbeddedResource Include="Assets\Presets\*.json" />
<!-- 마스코트 이미지: 내장 리소스 (EXE에 포함됨, 교체 시 재빌드 필요) -->
<!-- 검색 엔진 아이콘: 빌드 출력에 복사 (사내망에서 외부 favicon 다운로드 불가 대응) -->
<Content Include="Assets\SearchEngines\*.png">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<!-- 시스템 프롬프트: 빌드 출력에 복사 (동적 로딩) -->
<Content Include="Assets\system_prompt.txt">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>system_prompt.txt</Link>
</Content>
</ItemGroup>
<!-- 테마 리소스 딕셔너리 -->
<ItemGroup>
</ItemGroup>
<!-- 단위 테스트 프로젝트에서 internal 멤버 접근 허용 -->
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>AxCopilot.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
<ItemGroup>
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Accessibility.dll" />
<ReferencePath Include="E:\AX Copilot\src\AxCopilot.SDK\bin\Release\net8.0-windows\AxCopilot.SDK.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml\3.2.0\lib\net8.0\DocumentFormat.OpenXml.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\documentformat.openxml.framework\3.2.0\lib\net8.0\DocumentFormat.OpenXml.Framework.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\markdig\0.37.0\lib\net8.0\Markdig.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.CSharp.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Core.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.VisualBasic.Forms.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.Core.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\netcoreapp3.0\Microsoft.Web.WebView2.WinForms.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\microsoft.web.webview2\1.0.2903.40\lib_manual\net5.0-windows10.0.17763.0\Microsoft.Web.WebView2.Wpf.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.Registry.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\Microsoft.Win32.SystemEvents.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\mscorlib.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\netstandard.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationCore.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Aero2.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.AeroLite.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Classic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Luna.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationFramework.Royale.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\PresentationUI.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\ReachFramework.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.AppContext.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Buffers.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.CodeDom.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Concurrent.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Immutable.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.NonGeneric.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Collections.Specialized.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Annotations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.DataAnnotations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.EventBasedAsync.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ComponentModel.TypeConverter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Configuration.ConfigurationManager.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Configuration.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Console.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Core.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.Common.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.DataSetExtensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Data.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Design.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Contracts.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Debug.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.DiagnosticSource.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.EventLog.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.FileVersionInfo.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.PerformanceCounter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Process.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.StackTrace.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TextWriterTraceListener.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tools.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.TraceSource.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Diagnostics.Tracing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.DirectoryServices.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Common.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.Design.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Drawing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Drawing.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Dynamic.Runtime.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Asn1.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Formats.Tar.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Calendars.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Globalization.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.Brotli.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.FileSystem.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Compression.ZipFile.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.DriveInfo.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.FileSystem.Watcher.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.IsolatedStorage.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.MemoryMappedFiles.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.IO.Packaging.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.Pipes.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.IO.UnmanagedMemoryStream.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Expressions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Parallel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Linq.Queryable.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Memory.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Http.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.HttpListener.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Mail.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NameResolution.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.NetworkInformation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Ping.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Quic.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Requests.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Security.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.ServicePoint.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.Sockets.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebClient.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebHeaderCollection.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebProxy.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.Client.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Net.WebSockets.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Numerics.Vectors.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ObjectModel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Printing.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.DispatchProxy.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.ILGeneration.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Emit.Lightweight.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Metadata.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Reflection.TypeExtensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Resources.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Reader.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.ResourceManager.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Resources.Writer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.Unsafe.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.CompilerServices.VisualC.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Handles.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.JavaScript.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.InteropServices.RuntimeInformation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Intrinsics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Loader.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Numerics.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Formatters.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Runtime.Serialization.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Claims.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Algorithms.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Cng.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Csp.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Encoding.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.OpenSsl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Pkcs.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.ProtectedData.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.X509Certificates.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Cryptography.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Security.Permissions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.Principal.Windows.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Security.SecureString.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceModel.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ServiceProcess.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\system.serviceprocess.servicecontroller\8.0.1\lib\net8.0\System.ServiceProcess.ServiceController.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.CodePages.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encoding.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Encodings.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.Json.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Text.RegularExpressions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Threading.AccessControl.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Channels.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Overlapped.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Dataflow.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Tasks.Parallel.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Thread.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.ThreadPool.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Threading.Timer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Transactions.Local.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.ValueTuple.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Web.HttpUtility.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Controls.Ribbon.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Windows.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Extensions.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Design.Editors.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Forms.Primitives.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Input.Manipulations.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Windows.Presentation.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\System.Xaml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Linq.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.ReaderWriter.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.Serialization.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XDocument.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlDocument.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XmlSerializer.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\ref\net8.0\System.Xml.XPath.XDocument.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.core\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Core.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.fonts\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Fonts.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokenization\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokenization.dll" />
<ReferencePath Include="C:\Users\admin\.nuget\packages\uglytoad.pdfpig.tokens\1.7.0-custom-5\lib\net6.0\UglyToad.PdfPig.Tokens.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClient.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationClientSideProviders.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationProvider.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\UIAutomationTypes.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsBase.dll" />
<ReferencePath Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\ref\net8.0\WindowsFormsIntegration.dll" />
</ItemGroup>
<ItemGroup>
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\AboutWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\ChatWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\ColorPickResultWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\EyeDropperWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\HelpDetailWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\LargeTypeWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\LauncherWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\RegionSelectWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\ReminderPopupWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\ResourceMonitorWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\SettingsWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\ShortcutHelpWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\Views\StatisticsWindow.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\App.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\AxCopilot_Content.g.cs" />
<Compile Include="E:\AX Copilot\src\AxCopilot\obj\Release\net8.0-windows\win-x64\GeneratedInternalTypeHelper.g.cs" />
</ItemGroup>
<ItemGroup>
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.CSharp.NetAnalyzers.dll" />
<Analyzer Include="C:\Program Files\dotnet\sdk\10.0.201\Sdks\Microsoft.NET.Sdk\targets\..\analyzers\Microsoft.CodeAnalysis.NetAnalyzers.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.ComInterfaceGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.JavaScript.JSImportGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.LibraryImportGenerator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/Microsoft.Interop.SourceGeneration.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.Json.SourceGeneration.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Ref\8.0.25\analyzers/dotnet/cs/System.Text.RegularExpressions.Generator.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/System.Windows.Forms.Analyzers.dll" />
<Analyzer Include="C:\Program Files\dotnet\packs\Microsoft.WindowsDesktop.App.Ref\8.0.25\analyzers/dotnet/cs/System.Windows.Forms.Analyzers.CSharp.dll" />
</ItemGroup>
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
</Project>

View File

@@ -0,0 +1,49 @@
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace AxCopilot.Core;
/// <summary>
/// ReplaceAll 호출 시 단일 Reset 이벤트만 발생시켜 WPF ItemsControl 레이아웃 재계산을 최소화합니다.
/// 일반 Add/Remove/Clear는 기존 ObservableCollection과 동일하게 동작합니다.
/// </summary>
public sealed class BulkObservableCollection<T> : ObservableCollection<T>
{
private bool _suppressNotification;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (!_suppressNotification)
base.OnCollectionChanged(e);
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (!_suppressNotification)
base.OnPropertyChanged(e);
}
/// <summary>
/// 기존 항목을 모두 지우고 새 항목으로 교체합니다.
/// CollectionChanged(Reset) 이벤트를 단 한 번만 발생시켜 WPF 레이아웃 1회만 갱신합니다.
/// </summary>
public void ReplaceAll(IEnumerable<T> items)
{
_suppressNotification = true;
try
{
Items.Clear();
foreach (var item in items)
Items.Add(item);
}
finally
{
_suppressNotification = false;
}
// Count / Item[] 프로퍼티 변경 알림 + 단일 Reset 이벤트
OnPropertyChanged(new PropertyChangedEventArgs("Count"));
OnPropertyChanged(new PropertyChangedEventArgs("Item[]"));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}

View File

@@ -0,0 +1,212 @@
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Core;
/// <summary>
/// 입력된 텍스트를 파싱하여 적절한 ActionHandler로 라우팅합니다.
/// Prefix 기반 라우팅 테이블을 관리합니다.
/// </summary>
public class CommandResolver
{
private readonly FuzzyEngine _fuzzy;
private readonly SettingsService _settings;
private readonly Dictionary<string, IActionHandler> _handlers = new();
/// <summary>Prefix = null 핸들러 목록 — 모든 쿼리에 병렬 실행</summary>
private readonly List<IActionHandler> _fuzzyHandlers = new();
public CommandResolver(FuzzyEngine fuzzy, SettingsService settings)
{
_fuzzy = fuzzy;
_settings = settings;
}
/// <summary>
/// 핸들러를 등록합니다. 플러그인 로드 시에도 이 메서드를 호출합니다.
/// </summary>
public void RegisterHandler(IActionHandler handler)
{
// Prefix 없는 핸들러 → 모든 쿼리에 부가 결과 제공 (예: BookmarkHandler)
if (handler.Prefix == null)
{
_fuzzyHandlers.Add(handler);
LogService.Info($"FuzzyHandler 등록: name='{handler.Metadata.Name}'");
return;
}
if (_handlers.ContainsKey(handler.Prefix))
LogService.Warn($"Prefix '{handler.Prefix}' 중복 등록: " +
$"'{handler.Metadata.Name}'이 기존 핸들러를 덮어씁니다.");
_handlers[handler.Prefix] = handler;
LogService.Info($"Handler 등록: prefix='{handler.Prefix}', name='{handler.Metadata.Name}'");
}
/// <summary>
/// 입력 텍스트를 분석하여 결과 목록을 반환합니다.
/// </summary>
public async Task<IEnumerable<SDK.LauncherItem>> ResolveAsync(string input, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(input))
return Enumerable.Empty<SDK.LauncherItem>();
// 1. Prefix 기반 라우팅
foreach (var (prefix, handler) in _handlers)
{
if (input.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
var query = input.Length > prefix.Length
? input[prefix.Length..].Trim()
: "";
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogService.Error($"Handler '{handler.Metadata.Name}' 오류: {ex.Message}");
return [new SDK.LauncherItem($"오류: {ex.Message}", handler.Metadata.Name, null, null)];
}
}
}
// 2. Fuzzy 검색 폴백 + null-prefix 핸들러 병렬 실행
var maxResults = _settings.Settings.Launcher.MaxResults;
// Path 기반 중복 제거: 같은 경로의 항목이 여러 키워드로 매칭될 때 첫 번째만 표시
var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// SortByUsage에 lazy 시퀀스를 직접 전달 → 중간 ToList 1회 제거
var fuzzyItems = UsageRankingService.SortByUsage(
_fuzzy.Search(input, maxResults * 2) // 중복 제거 여유분
.Where(r => seenPaths.Add(r.Entry.Path)) // Path가 처음 등장할 때만 통과
.Take(maxResults)
.Select(r => new SDK.LauncherItem(
r.Entry.DisplayName,
r.Entry.Type == IndexEntryType.Alias ? r.Entry.AliasType switch
{
"url" => "URL 단축키",
"batch" => "명령 단축키",
_ => r.Entry.Path
} : r.Entry.Path + " ⇧ Shift+Enter: 폴더 열기",
null,
r.Entry,
Symbol: r.Entry.Type switch
{
IndexEntryType.App => Symbols.App,
IndexEntryType.Folder => Symbols.Folder,
IndexEntryType.Alias => r.Entry.AliasType switch
{
"url" => Symbols.Globe,
"batch" => Symbols.Terminal,
_ => Symbols.Plugin
},
_ => Symbols.File
}
)),
item => (item.Data as IndexEntry)?.Path
).ToList(); // 단일 ToList로 List<LauncherItem> 확정
// null-prefix 핸들러 결과를 뒤에 추가 (최대 3개씩)
if (_fuzzyHandlers.Count > 0)
{
var extraTasks = _fuzzyHandlers
.Select(h => SafeGetItemsAsync(h, input, ct))
.ToList();
await Task.WhenAll(extraTasks);
foreach (var task in extraTasks)
{
if (task.IsCompletedSuccessfully)
fuzzyItems.AddRange(task.Result.Take(3));
}
}
return fuzzyItems;
}
/// <summary>
/// 선택된 항목을 실행합니다.
/// </summary>
public async Task ExecuteAsync(SDK.LauncherItem item, string lastInput, CancellationToken ct)
{
// Prefix 기반 실행
foreach (var (prefix, handler) in _handlers)
{
if (lastInput.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
// 명령어 사용 통계 기록 (prefix + 첫 단어)
var q = lastInput.Length > prefix.Length
? lastInput[prefix.Length..].Trim().Split(' ')[0]
: "";
var cmdKey = string.IsNullOrEmpty(q) ? prefix : $"{prefix}{q}";
UsageStatisticsService.RecordCommandUsage(cmdKey);
await handler.ExecuteAsync(item, ct);
return;
}
}
// null-prefix 핸들러 결과 실행 (Data가 string = URL인 경우)
if (item.Data is string urlData && urlData.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
await ExecuteNullPrefixAsync(item, ct);
return;
}
// Fuzzy 결과 실행 (IndexEntry 기반)
if (item.Data is IndexEntry entry)
{
var expanded = Environment.ExpandEnvironmentVariables(entry.Path);
try
{
// Process.Start를 먼저 실행하여 체감 속도 확보
await Task.Run(() =>
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(expanded)
{
UseShellExecute = true
}));
}
catch (Exception ex)
{
LogService.Error($"실행 실패: {expanded} - {ex.Message}");
}
// 통계 기록은 파일 열기 이후 비동기로
_ = Task.Run(() => UsageRankingService.RecordExecution(entry.Path));
}
}
public IReadOnlyDictionary<string, IActionHandler> RegisteredHandlers => _handlers;
// null-prefix 핸들러 실행 (ExecuteAsync 라우팅)
public async Task ExecuteNullPrefixAsync(SDK.LauncherItem item, CancellationToken ct)
{
foreach (var handler in _fuzzyHandlers)
{
try { await handler.ExecuteAsync(item, ct); return; }
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogService.Error($"FuzzyHandler '{handler.Metadata.Name}' 실행 오류: {ex.Message}");
}
}
}
private static async Task<IEnumerable<SDK.LauncherItem>> SafeGetItemsAsync(
IActionHandler handler, string query, CancellationToken ct)
{
try
{
return await handler.GetItemsAsync(query, ct);
}
catch (OperationCanceledException) { throw; }
catch (Exception ex)
{
LogService.Error($"FuzzyHandler '{handler.Metadata.Name}' 오류: {ex.Message}");
return Enumerable.Empty<SDK.LauncherItem>();
}
}
}

View File

@@ -0,0 +1,332 @@
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using AxCopilot.Models;
using AxCopilot.Services;
namespace AxCopilot.Core;
/// <summary>
/// Windows 창의 HWND, Rect, 프로세스 경로를 수집하고 복원합니다 (The Shifter).
/// </summary>
public class ContextManager
{
private readonly SettingsService _settings;
public ContextManager(SettingsService settings)
{
_settings = settings;
// 모니터 구성 변경 감지는 MainWindow의 WndProc에서 WM_DISPLAYCHANGE를 통해 처리
}
// ─── Snapshot ────────────────────────────────────────────────────────────
/// <summary>
/// 현재 화면에 열린 모든 업무용 창을 캡처하여 프로필로 저장합니다.
/// </summary>
public WorkspaceProfile CaptureProfile(string name)
{
var snapshots = new List<WindowSnapshot>();
var monitorMap = BuildMonitorMap();
EnumWindows((hWnd, _) =>
{
if (!IsWindowVisible(hWnd)) return true;
if (IsIconic(hWnd)) return true; // 최소화된 창 제외 여부는 설정으로 조정 가능
var title = GetWindowTitle(hWnd);
if (string.IsNullOrWhiteSpace(title)) return true;
// 작업표시줄, 바탕화면 등 시스템 창 제외
if (IsSystemWindow(hWnd)) return true;
var exePath = GetProcessPath(hWnd);
if (string.IsNullOrEmpty(exePath)) return true;
GetWindowRect(hWnd, out RECT rect);
GetWindowPlacement(hWnd, out WINDOWPLACEMENT placement);
var showCmd = placement.showCmd switch
{
1 => "Normal",
2 => "Minimized",
3 => "Maximized",
_ => "Normal"
};
int monitorIndex = GetMonitorIndex(hWnd, monitorMap);
snapshots.Add(new WindowSnapshot
{
Exe = exePath,
Title = title,
Rect = new WindowRect
{
X = rect.Left,
Y = rect.Top,
Width = rect.Right - rect.Left,
Height = rect.Bottom - rect.Top
},
ShowCmd = showCmd,
Monitor = monitorIndex
});
return true;
}, IntPtr.Zero);
var profile = new WorkspaceProfile
{
Name = name,
Windows = snapshots,
CreatedAt = DateTime.Now
};
// settings.json에 저장
var existing = _settings.Settings.Profiles.FirstOrDefault(p =>
p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (existing != null)
_settings.Settings.Profiles.Remove(existing);
_settings.Settings.Profiles.Add(profile);
_settings.Save();
LogService.Info($"프로필 '{name}' 저장 완료: {snapshots.Count}개 창");
return profile;
}
// ─── Restore ─────────────────────────────────────────────────────────────
/// <summary>
/// 저장된 프로필을 복원합니다.
/// </summary>
public async Task<RestoreResult> RestoreProfileAsync(string name, CancellationToken ct = default)
{
var profile = _settings.Settings.Profiles
.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (profile == null)
return new RestoreResult(false, $"프로필 '{name}'을 찾을 수 없습니다.");
var results = new List<string>();
var monitorCount = GetMonitorCount();
foreach (var snapshot in profile.Windows)
{
ct.ThrowIfCancellationRequested();
// 1. 실행 중인 창 찾기
var hWnd = FindMatchingWindow(snapshot);
// 2. 창이 없으면 EXE 실행 후 대기
if (hWnd == IntPtr.Zero && File.Exists(snapshot.Exe))
{
try
{
Process.Start(new ProcessStartInfo(snapshot.Exe) { UseShellExecute = true });
hWnd = await WaitForWindowAsync(snapshot.Exe, TimeSpan.FromSeconds(3), ct);
}
catch (Exception ex)
{
results.Add($"⚠ {snapshot.Title}: 실행 실패 ({ex.Message})");
LogService.Warn($"앱 실행 실패: {snapshot.Exe} - {ex.Message}");
continue;
}
}
if (hWnd == IntPtr.Zero)
{
results.Add($"⏭ {snapshot.Title}: 창 없음, 건너뜀");
continue;
}
// 3. 모니터 불일치 처리
if (snapshot.Monitor >= monitorCount)
{
var policy = _settings.Settings.MonitorMismatch;
if (policy == "skip")
{
results.Add($"⏭ {snapshot.Title}: 모니터 불일치, 건너뜀");
continue;
}
// "fit" 또는 "warn" → 첫 번째 모니터에 배치
}
// 4. 창 위치/크기 복원
try
{
ShowWindow(hWnd, snapshot.ShowCmd switch
{
"Maximized" => 3,
"Minimized" => 2,
_ => 9 // SW_RESTORE
});
if (snapshot.ShowCmd == "Normal")
{
SetWindowPos(hWnd, IntPtr.Zero,
snapshot.Rect.X, snapshot.Rect.Y,
snapshot.Rect.Width, snapshot.Rect.Height,
SWP_NOZORDER | SWP_NOACTIVATE);
}
results.Add($"✓ {snapshot.Title}: 복원 완료");
}
catch (Exception ex)
{
results.Add($"⚠ {snapshot.Title}: 복원 실패 ({ex.Message})");
LogService.Warn($"창 복원 실패 (권한 문제 가능): {snapshot.Exe}");
}
}
LogService.Info($"프로필 '{name}' 복원: {string.Join(", ", results)}");
return new RestoreResult(true, string.Join("\n", results));
}
// ─── Profile Management ──────────────────────────────────────────────────
public bool DeleteProfile(string name)
{
var profile = _settings.Settings.Profiles
.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (profile == null) return false;
_settings.Settings.Profiles.Remove(profile);
_settings.Save();
return true;
}
public bool RenameProfile(string oldName, string newName)
{
var profile = _settings.Settings.Profiles
.FirstOrDefault(p => p.Name.Equals(oldName, StringComparison.OrdinalIgnoreCase));
if (profile == null) return false;
profile.Name = newName;
_settings.Save();
return true;
}
// ─── Helpers ─────────────────────────────────────────────────────────────
private static IntPtr FindMatchingWindow(WindowSnapshot snapshot)
{
IntPtr found = IntPtr.Zero;
EnumWindows((hWnd, _) =>
{
var path = GetProcessPath(hWnd);
if (string.Equals(path, snapshot.Exe, StringComparison.OrdinalIgnoreCase))
{
found = hWnd;
return false; // 첫 번째 매칭 창에서 중단
}
return true;
}, IntPtr.Zero);
return found;
}
private static async Task<IntPtr> WaitForWindowAsync(string exePath, TimeSpan timeout, CancellationToken ct)
{
var deadline = DateTime.UtcNow + timeout;
while (DateTime.UtcNow < deadline)
{
ct.ThrowIfCancellationRequested();
var hWnd = FindMatchingWindow(new WindowSnapshot { Exe = exePath });
if (hWnd != IntPtr.Zero) return hWnd;
await Task.Delay(200, ct);
}
return IntPtr.Zero;
}
private static string GetWindowTitle(IntPtr hWnd)
{
var sb = new StringBuilder(256);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
private static string GetProcessPath(IntPtr hWnd)
{
try
{
GetWindowThreadProcessId(hWnd, out uint pid);
if (pid == 0) return "";
var proc = Process.GetProcessById((int)pid);
return proc.MainModule?.FileName ?? "";
}
catch (Exception) { return ""; }
}
private static bool IsSystemWindow(IntPtr hWnd)
{
var cls = new StringBuilder(256);
GetClassName(hWnd, cls, cls.Capacity);
var name = cls.ToString();
return name is "Shell_TrayWnd" or "Progman" or "WorkerW" or "DV2ControlHost";
}
private static Dictionary<IntPtr, int> BuildMonitorMap()
{
var monitors = new List<IntPtr>();
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero,
(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprc, IntPtr dwData) =>
{
monitors.Add(hMonitor);
return true;
}, IntPtr.Zero);
return monitors
.Select((hm, idx) => (hm, idx))
.ToDictionary(t => t.hm, t => t.idx);
}
private static int GetMonitorIndex(IntPtr hWnd, Dictionary<IntPtr, int> map)
{
var monitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTONEAREST);
return map.TryGetValue(monitor, out int idx) ? idx : 0;
}
private static int GetMonitorCount()
{
int count = 0;
EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprc, IntPtr dwData) => { count++; return true; }, IntPtr.Zero);
return count;
}
// ─── P/Invoke ────────────────────────────────────────────────────────────
private const uint SWP_NOZORDER = 0x0004;
private const uint SWP_NOACTIVATE = 0x0010;
private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
[StructLayout(LayoutKind.Sequential)]
private struct WINDOWPLACEMENT
{
public uint length, flags;
public uint showCmd;
public POINT ptMinPosition, ptMaxPosition;
public RECT rcNormalPosition;
}
[StructLayout(LayoutKind.Sequential)]
private struct POINT { public int x, y; }
private delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
private delegate bool MonitorEnumProc(IntPtr hMonitor, IntPtr hdcMonitor, ref RECT lprcMonitor, IntPtr dwData);
[DllImport("user32.dll")] private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
[DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")] private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] private static extern bool GetWindowPlacement(IntPtr hWnd, out WINDOWPLACEMENT lpwndpl);
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")] private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("user32.dll")] private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll")] private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, MonitorEnumProc lpfnEnum, IntPtr dwData);
}
public record RestoreResult(bool Success, string Message);

View File

@@ -0,0 +1,420 @@
using AxCopilot.Services;
namespace AxCopilot.Core;
/// <summary>
/// 부분 입력·오타도 유사 항목을 찾아주는 Fuzzy 검색 엔진.
/// 한글 초성 검색, 자모 분리 검색, 비연속 매칭을 지원합니다.
/// </summary>
public class FuzzyEngine
{
private readonly IndexService _index;
public FuzzyEngine(IndexService index)
{
_index = index;
}
/// <summary>
/// 쿼리에 대해 유사도 순으로 정렬된 결과를 반환합니다.
/// 300개 이상 항목은 PLINQ 병렬 처리로 검색 속도를 개선합니다.
/// </summary>
public IEnumerable<FuzzyResult> Search(string query, int maxResults = 7)
{
if (string.IsNullOrWhiteSpace(query))
return Enumerable.Empty<FuzzyResult>();
var normalized = query.Trim().ToLowerInvariant();
var entries = _index.Entries;
// 쿼리 언어 타입 1회 사전 분류 — 항목마다 재계산하지 않음
bool queryHasKorean = false;
foreach (var c in normalized)
{
if ((c >= 0xAC00 && c <= 0xD7A3) || ChosungSet.Contains(c))
{ queryHasKorean = true; break; }
}
// 300개 초과 시 PLINQ 병렬 처리
if (entries.Count > 300)
{
return entries.AsParallel()
.Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)))
.Where(r => r.Score > 0)
.OrderByDescending(r => r.Score)
.Take(maxResults);
}
return entries
.Select(e => new FuzzyResult(e, CalculateScoreFast(normalized, e, queryHasKorean)))
.Where(r => r.Score > 0)
.OrderByDescending(r => r.Score)
.Take(maxResults);
}
/// <summary>미리 계산된 캐시 필드를 활용하는 빠른 점수 계산.</summary>
private static int CalculateScoreFast(string query, IndexEntry entry, bool queryHasKorean)
{
// 캐시가 없으면(구버전 호환) 기존 방식으로 폴백
var targetLower = string.IsNullOrEmpty(entry.NameLower)
? entry.Name.ToLowerInvariant()
: entry.NameLower;
if (query.Length == 0) return 0;
if (targetLower == query) return 1000 + entry.Score;
if (targetLower.StartsWith(query)) return 800 + entry.Score;
if (targetLower.Contains(query)) return 600 + entry.Score;
// 순수 ASCII 쿼리 — 한글 검색 로직(자모·초성) 전체 스킵
if (!queryHasKorean)
{
var fs = FuzzyMatch(query, targetLower);
return fs > 0 ? fs + entry.Score : 0;
}
// 자모 분리 검색 (캐시된 NameJamo 활용)
var jamoScore = JamoContainsScoreFast(
string.IsNullOrEmpty(entry.NameJamo) ? DecomposeToJamo(targetLower) : entry.NameJamo,
query);
if (jamoScore > 0) return jamoScore + entry.Score;
// 초성 검색 (캐시된 NameChosung 활용)
var chosungScore = ChosungMatchScoreFast(
string.IsNullOrEmpty(entry.NameChosung) ? null : entry.NameChosung,
targetLower, query);
if (chosungScore > 0) return chosungScore + entry.Score;
// Fuzzy 매칭
var fuzzyScore = FuzzyMatch(query, targetLower);
if (fuzzyScore > 0) return fuzzyScore + entry.Score;
return 0;
}
/// <summary>
/// 점수 계산: 정확 일치 > 시작 일치 > 포함 일치 > 자모 포함 > 초성 > Fuzzy
/// </summary>
internal static int CalculateScore(string query, string target, int baseScore)
{
if (query.Length == 0) return 0;
if (target == query) return 1000 + baseScore; // 완전 일치
if (target.StartsWith(query)) return 800 + baseScore; // 시작 일치
if (target.Contains(query)) return 600 + baseScore; // 부분 일치
// 한글 자모 분리 후 부분 일치 ("모장" → 메모장)
var jamoScore = JamoContainsScore(target, query);
if (jamoScore > 0) return jamoScore + baseScore;
// 한글 초성 검색 (순수 초성 + 혼합 쿼리 모두 지원)
var chosungScore = ChosungMatchScore(target, query);
if (chosungScore > 0) return chosungScore + baseScore;
// 문자 순서 포함 (Fuzzy)
var fuzzyScore = FuzzyMatch(query, target);
if (fuzzyScore > 0) return fuzzyScore + baseScore;
return 0;
}
// ─── Fuzzy Match ────────────────────────────────────────────────────────
/// <summary>
/// 쿼리의 모든 문자가 target에 순서대로 포함되는지 확인 (subsequence)
/// </summary>
internal static int FuzzyMatch(string query, string target)
{
int qi = 0, ti = 0;
int score = 0;
int lastMatchIdx = -1;
while (qi < query.Length && ti < target.Length)
{
if (query[qi] == target[ti])
{
if (lastMatchIdx == ti - 1) score += 30; // 연속 매칭 보너스
else score += 10; // 비연속 매칭
if (ti == 0) score += 15; // 시작 위치 보너스
lastMatchIdx = ti;
qi++;
}
ti++;
}
return qi == query.Length ? Math.Max(score, 50) : 0; // 최소 50점 보장
}
// ─── 한글 자모 분리 ─────────────────────────────────────────────────────
private static readonly char[] Chosungs =
['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
/// <summary>초성 O(1) 조회용 HashSet — HasChosung/IsChosung/MixedMatch에서 사용.</summary>
private static readonly HashSet<char> ChosungSet =
new(['ㄱ','ㄲ','ㄴ','ㄷ','ㄸ','ㄹ','ㅁ','ㅂ','ㅃ','ㅅ','ㅆ','ㅇ','ㅈ','ㅉ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ']);
private static readonly char[] Jungsungs =
['ㅏ','ㅐ','ㅑ','ㅒ','ㅓ','ㅔ','ㅕ','ㅖ','ㅗ','ㅘ','ㅙ','ㅚ','ㅛ','ㅜ','ㅝ','ㅞ','ㅟ','ㅠ','ㅡ','ㅢ','ㅣ'];
private static readonly char[] Jongsungs =
['\0','ㄱ','ㄲ','ㄳ','ㄴ','ㄵ','ㄶ','ㄷ','ㄹ','ㄺ','ㄻ','ㄼ','ㄽ','ㄾ','ㄿ','ㅀ','ㅁ','ㅂ','ㅄ','ㅅ','ㅆ','ㅇ','ㅈ','ㅊ','ㅋ','ㅌ','ㅍ','ㅎ'];
/// <summary>한글 음절을 자모로 분리 (초성+중성+종성). 비한글은 그대로 반환.</summary>
internal static string DecomposeToJamo(string text)
{
var result = new System.Text.StringBuilder(text.Length * 3);
foreach (var c in text)
{
if (c >= 0xAC00 && c <= 0xD7A3)
{
int offset = c - 0xAC00;
int cho = offset / (21 * 28);
int jung = (offset % (21 * 28)) / 28;
int jong = offset % 28;
result.Append(Chosungs[cho]);
result.Append(Jungsungs[jung]);
if (jong > 0) result.Append(Jongsungs[jong]);
}
else
{
result.Append(c);
}
}
return result.ToString();
}
/// <summary>한글 음절에서 초성만 추출. 비한글은 '\0'.</summary>
internal static char GetChosung(char hangul)
{
if (hangul < 0xAC00 || hangul > 0xD7A3) return '\0';
int offset = hangul - 0xAC00;
return Chosungs[offset / (21 * 28)];
}
// ─── 자모 기반 포함 검색 ────────────────────────────────────────────────
/// <summary>
/// 쿼리를 자모 분리 후 target의 자모에 연속 부분 문자열로 포함되는지 확인.
/// "모장" → "ㅁㅗㅈㅏㅇ" 이 "ㅁㅔㅁㅗㅈㅏㅇ"(메모장)에 포함 → 550점
/// </summary>
internal static int JamoContainsScore(string target, string query)
{
if (!HasKorean(query)) return 0;
var targetJamo = DecomposeToJamo(target);
var queryJamo = DecomposeToJamo(query);
if (queryJamo.Length == 0 || targetJamo.Length == 0) return 0;
// 자모 연속 포함
if (targetJamo.Contains(queryJamo))
{
// 시작 위치가 음절 경계(초성)에 가까울수록 높은 점수
int idx = targetJamo.IndexOf(queryJamo);
return idx == 0 ? 580 : 550;
}
// 자모 subsequence 매칭 (비연속이지만 순서 유지)
int qi = 0;
for (int ti = 0; ti < targetJamo.Length && qi < queryJamo.Length; ti++)
{
if (queryJamo[qi] == targetJamo[ti]) qi++;
}
if (qi == queryJamo.Length) return 400;
return 0;
}
// ─── 초성 검색 ──────────────────────────────────────────────────────────
/// <summary>쿼리에 초성 문자가 하나라도 포함되어 있는지.</summary>
internal static bool HasChosung(string text) => text.Any(c => ChosungSet.Contains(c));
/// <summary>문자열의 모든 문자가 초성인지.</summary>
internal static bool IsChosung(string text) => text.Length > 0 && text.All(c => ChosungSet.Contains(c));
/// <summary>문자열에 한글(완성형)이 포함되어 있는지.</summary>
private static bool HasKorean(string text) => text.Any(c => c >= 0xAC00 && c <= 0xD7A3);
/// <summary>
/// 초성 매칭 점수. 순수 초성 쿼리 + 혼합 쿼리(초성+완성형) 모두 지원.
/// 비연속 매칭도 허용 ("ㅁㅊ" → 메모장 OK).
/// </summary>
internal static int ChosungMatchScore(string target, string query)
{
// 초성이 하나도 없으면 초성 검색 아님
if (!HasChosung(query)) return 0;
// 타겟에서 각 글자의 초성 추출
var targetChosungs = new List<char>();
var targetChars = new List<char>();
foreach (var c in target)
{
var cho = GetChosung(c);
if (cho != '\0')
{
targetChosungs.Add(cho);
targetChars.Add(c);
}
else if ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'))
{
targetChosungs.Add(c);
targetChars.Add(c);
}
}
if (targetChosungs.Count == 0) return 0;
// 순수 초성 쿼리 ("ㅁㅁㅈ", "ㅁㅊ")
if (IsChosung(query))
{
// 연속 매칭 시도
if (ContainsChosungConsecutive(targetChosungs, query)) return 520;
// 비연속(subsequence) 매칭
if (ContainsChosungSubsequence(targetChosungs, query)) return 480;
return 0;
}
// 혼합 쿼리 ("ㅁ장", "계ㅅ기") — 초성+완성형 혼합
return MixedChosungMatch(targetChars, targetChosungs, query);
}
/// <summary>연속 초성 매칭 (기존 로직 유지)</summary>
private static bool ContainsChosungConsecutive(List<char> targetChosungs, string query)
{
for (int i = 0; i <= targetChosungs.Count - query.Length; i++)
{
bool match = true;
for (int j = 0; j < query.Length; j++)
{
if (targetChosungs[i + j] != query[j]) { match = false; break; }
}
if (match) return true;
}
return false;
}
/// <summary>비연속 초성 매칭 (subsequence)</summary>
private static bool ContainsChosungSubsequence(List<char> targetChosungs, string query)
{
int qi = 0;
for (int ti = 0; ti < targetChosungs.Count && qi < query.Length; ti++)
{
if (targetChosungs[ti] == query[qi]) qi++;
}
return qi == query.Length;
}
/// <summary>
/// 혼합 쿼리 매칭: 초성은 초성끼리, 완성형은 완성형끼리 비교.
/// "ㅁ장" → target[i]의 초성이 'ㅁ'이고, target[j>i]가 '장'인지.
/// </summary>
private static int MixedChosungMatch(List<char> targetChars, List<char> targetChosungs, string query)
{
int qi = 0, ti = 0;
while (qi < query.Length && ti < targetChars.Count)
{
var qc = query[qi];
if (ChosungSet.Contains(qc))
{
// 쿼리 문자가 초성 → 타겟 초성과 비교
if (targetChosungs[ti] == qc) qi++;
}
else
{
// 쿼리 문자가 완성형 → 타겟 원본 문자와 비교
if (targetChars[ti] == qc) qi++;
}
ti++;
}
return qi == query.Length ? 460 : 0;
}
// ─── 캐시 기반 빠른 검색 메서드 ────────────────────────────────────────────
/// <summary>미리 계산된 자모 문자열을 사용하는 빠른 자모 포함 검색.</summary>
private static int JamoContainsScoreFast(string targetJamo, string query)
{
if (!HasKorean(query)) return 0;
var queryJamo = DecomposeToJamo(query); // 쿼리는 짧으므로 매번 분해해도 빠름
if (queryJamo.Length == 0 || targetJamo.Length == 0) return 0;
if (targetJamo.Contains(queryJamo))
{
int idx = targetJamo.IndexOf(queryJamo, StringComparison.Ordinal);
return idx == 0 ? 580 : 550;
}
int qi = 0;
for (int ti = 0; ti < targetJamo.Length && qi < queryJamo.Length; ti++)
{
if (queryJamo[qi] == targetJamo[ti]) qi++;
}
return qi == queryJamo.Length ? 400 : 0;
}
/// <summary>미리 계산된 초성 문자열을 사용하는 빠른 초성 검색.</summary>
private static int ChosungMatchScoreFast(string? targetChosung, string targetLower, string query)
{
if (!HasChosung(query)) return 0;
if (IsChosung(query))
{
if (string.IsNullOrEmpty(targetChosung)) return 0;
// 연속 매칭: 단순 Contains
if (targetChosung.Contains(query, StringComparison.Ordinal)) return 520;
// 비연속 매칭 (subsequence)
int qi2 = 0;
for (int ti2 = 0; ti2 < targetChosung.Length && qi2 < query.Length; ti2++)
{
if (targetChosung[ti2] == query[qi2]) qi2++;
}
if (qi2 == query.Length) return 480;
return 0;
}
// 혼합 쿼리(초성+완성형): List<char> 할당 없는 인라인 매칭
{
int qi2 = 0, ti2 = 0;
while (qi2 < query.Length && ti2 < targetLower.Length)
{
var qc = query[qi2];
var tc = targetLower[ti2];
if (ChosungSet.Contains(qc))
{
// 쿼리가 초성 → 타겟 문자의 초성과 비교
var cho = GetChosung(tc);
if (cho == '\0' && ((tc >= 'a' && tc <= 'z') || (tc >= '0' && tc <= '9')))
cho = tc; // 영문/숫자는 초성 = 자신
if (cho == qc) qi2++;
}
else
{
// 쿼리가 완성형 → 타겟 원본 문자와 비교
if (tc == qc) qi2++;
}
ti2++;
}
return qi2 == query.Length ? 460 : 0;
}
}
// ─── 하위 호환 ──────────────────────────────────────────────────────────
/// <summary>기존 API 호환용 — ContainsChosung(연속+비연속)</summary>
internal static bool ContainsChosung(string target, string chosungQuery)
{
var targetChosungs = target.Select(GetChosung).Where(c => c != '\0').ToList();
if (targetChosungs.Count < chosungQuery.Length) return false;
return ContainsChosungConsecutive(targetChosungs, chosungQuery)
|| ContainsChosungSubsequence(targetChosungs, chosungQuery);
}
}
public record FuzzyResult(IndexEntry Entry, int Score);

View File

@@ -0,0 +1,127 @@
namespace AxCopilot.Core;
/// <summary>
/// "Alt+Space", "Ctrl+Shift+K" 형식의 핫키 문자열 파싱/포맷 유틸리티.
/// </summary>
public static class HotkeyParser
{
private static readonly Dictionary<string, int> _keyMap =
new(StringComparer.OrdinalIgnoreCase)
{
// 특수키
["Space"] = 0x20, ["Enter"] = 0x0D, ["Return"] = 0x0D,
["Tab"] = 0x09, ["Esc"] = 0x1B, ["Escape"] = 0x1B,
["Backspace"]= 0x08, ["Back"] = 0x08,
["Delete"] = 0x2E, ["Del"] = 0x2E,
["Insert"] = 0x2D, ["Ins"] = 0x2D,
["Home"] = 0x24, ["End"] = 0x23,
["PageUp"] = 0x21, ["PgUp"] = 0x21,
["PageDown"] = 0x22, ["PgDn"] = 0x22,
["PrintScreen"] = 0x2C, ["PrtSc"] = 0x2C, ["Snapshot"] = 0x2C,
["Pause"] = 0x13, ["Break"] = 0x13,
["ScrollLock"] = 0x91,
// 방향키
["Left"] = 0x25, ["Up"] = 0x26, ["Right"] = 0x27, ["Down"] = 0x28,
// 기호
["`"] = 0xC0, ["Grave"] = 0xC0,
["-"] = 0xBD, ["="] = 0xBB,
["["] = 0xDB, ["]"] = 0xDD,
["\\"] = 0xDC, [";"] = 0xBA,
["'"] = 0xDE, [","] = 0xBC,
["."] = 0xBE, ["/"] = 0xBF,
};
static HotkeyParser()
{
// AZ
for (char c = 'A'; c <= 'Z'; c++)
_keyMap[c.ToString()] = c;
// 09
for (char c = '0'; c <= '9'; c++)
_keyMap[c.ToString()] = c;
// F1F24
for (int i = 1; i <= 24; i++)
_keyMap[$"F{i}"] = 0x6F + i;
// Numpad 09
for (int i = 0; i <= 9; i++)
_keyMap[$"Num{i}"] = 0x60 + i;
}
/// <summary>
/// "Alt+Space" → <see cref="HotkeyDefinition"/>. 실패 시 false 반환.
/// </summary>
public static bool TryParse(string hotkey, out HotkeyDefinition result)
{
result = default;
if (string.IsNullOrWhiteSpace(hotkey)) return false;
var parts = hotkey.Split('+',
StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
bool ctrl = false, alt = false, shift = false, win = false;
int? vkCode = null;
foreach (var p in parts)
{
if (p.Equals("Ctrl", StringComparison.OrdinalIgnoreCase) ||
p.Equals("Control", StringComparison.OrdinalIgnoreCase))
{ ctrl = true; continue; }
if (p.Equals("Alt", StringComparison.OrdinalIgnoreCase))
{ alt = true; continue; }
if (p.Equals("Shift", StringComparison.OrdinalIgnoreCase))
{ shift = true; continue; }
if (p.Equals("Win", StringComparison.OrdinalIgnoreCase) ||
p.Equals("Windows", StringComparison.OrdinalIgnoreCase))
{ win = true; continue; }
if (_keyMap.TryGetValue(p, out int vk))
vkCode = vk;
else
return false; // 알 수 없는 키
}
if (vkCode == null) return false;
result = new HotkeyDefinition(vkCode.Value, ctrl, alt, shift, win);
return true;
}
/// <summary>
/// <see cref="HotkeyDefinition"/> → "Alt+Space" 형식 문자열.
/// </summary>
public static string Format(HotkeyDefinition def)
{
var parts = new List<string>(5);
if (def.Ctrl) parts.Add("Ctrl");
if (def.Alt) parts.Add("Alt");
if (def.Shift) parts.Add("Shift");
if (def.Win) parts.Add("Win");
parts.Add(VkToName(def.VkCode));
return string.Join("+", parts);
}
// VK 코드 → 읽기 좋은 이름 변환
private static string VkToName(int vk)
{
if (vk >= 0x41 && vk <= 0x5A) return ((char)vk).ToString(); // AZ
if (vk >= 0x30 && vk <= 0x39) return ((char)vk).ToString(); // 09
if (vk >= 0x70 && vk <= 0x87) return $"F{vk - 0x6F}"; // F1F24
if (vk >= 0x60 && vk <= 0x69) return $"Num{vk - 0x60}"; // Numpad
// 특수키 테이블에서 긴 이름 우선 검색
string? best = null;
foreach (var (name, code) in _keyMap)
{
if (code == vk && (best == null || name.Length > best.Length))
best = name;
}
return best ?? $"0x{vk:X2}";
}
}
/// <summary>
/// 파싱된 핫키 정의. VK 코드 + 요구 수정자 키.
/// </summary>
public record struct HotkeyDefinition(int VkCode, bool Ctrl, bool Alt, bool Shift, bool Win);

View File

@@ -0,0 +1,265 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using AxCopilot.Services;
namespace AxCopilot.Core;
/// <summary>
/// 글로벌 키보드 훅으로 어떤 앱이 포커스를 가져도 설정된 핫키를 감지합니다.
/// WH_KEYBOARD_LL (Low-Level Keyboard Hook) 사용.
/// </summary>
public class InputListener : IDisposable
{
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_SYSKEYDOWN = 0x0104;
private const int WM_KEYUP = 0x0101;
private const int WM_SYSKEYUP = 0x0105;
// 수정자 키 VK 코드
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int VK_MENU = 0x12; // Alt
private const int VK_LWIN = 0x5B;
private const int VK_RWIN = 0x5C;
private IntPtr _hookHandle = IntPtr.Zero;
private LowLevelKeyboardProc? _proc;
private int _retryCount = 0;
private const int MaxRetry = 3;
// 핫키 발동 후 잔여 KEYUP 이벤트 억제 플래그
// Alt+X 핫키 → Alt KEYUP이 앱 메뉴바를 활성화하는 문제 방지
private volatile bool _suppressNextAltUp;
private volatile bool _suppressNextKeyUp;
private volatile int _suppressKeyUpVk;
// 현재 설정된 핫키 (기본: Alt+Space)
private HotkeyDefinition _hotkey = new(0x20, false, true, false, false);
// 글로벌 캡처 단축키 (기본: PrintScreen, 비활성)
private HotkeyDefinition _captureHotkey;
private bool _captureHotkeyEnabled;
public event EventHandler? HotkeyTriggered;
public event EventHandler? CaptureHotkeyTriggered;
public event EventHandler? HookFailed;
/// <summary>
/// 핫키 녹화 중일 때 true로 설정하면 핫키 이벤트를 발생시키지 않습니다.
/// </summary>
public bool SuspendHotkey { get; set; }
/// <summary>
/// 스니펫 확장기가 설정하는 키 필터.
/// true를 반환하면 해당 키 이벤트를 소비(다른 앱으로 전달 차단)합니다.
/// 훅 콜백 스레드에서 실행되므로 빠르고 스레드 안전하게 구현해야 합니다.
/// </summary>
public Func<int, bool>? KeyFilter { get; set; }
/// <summary>
/// 설정에서 읽은 핫키 문자열로 핫키를 업데이트합니다. ("Alt+Space", "Ctrl+K" 등)
/// </summary>
public void UpdateHotkey(string hotkeyStr)
{
if (HotkeyParser.TryParse(hotkeyStr, out var def))
{
_hotkey = def;
LogService.Info($"핫키 변경: {hotkeyStr}");
}
else
{
LogService.Warn($"핫키 파싱 실패: '{hotkeyStr}' — 기존 핫키 유지");
}
}
/// <summary>
/// 글로벌 캡처 단축키를 설정합니다.
/// </summary>
public void UpdateCaptureHotkey(string hotkeyStr, bool enabled)
{
_captureHotkeyEnabled = enabled;
if (enabled && HotkeyParser.TryParse(hotkeyStr, out var def))
{
_captureHotkey = def;
LogService.Info($"캡처 단축키 활성화: {hotkeyStr}");
}
else if (!enabled)
{
LogService.Info("캡처 단축키 비활성화");
}
}
public void Start()
{
_proc = HookCallback;
Register();
}
private void Register()
{
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule!;
_hookHandle = SetWindowsHookEx(WH_KEYBOARD_LL, _proc!,
GetModuleHandle(curModule.ModuleName!), 0);
if (_hookHandle == IntPtr.Zero)
{
var err = Marshal.GetLastWin32Error();
LogService.Error($"Global Hook 등록 실패 (에러 코드: {err})");
TryRetryRegister();
}
else
{
_retryCount = 0;
LogService.Info($"Global Keyboard Hook 등록 완료 ({HotkeyParser.Format(_hotkey)})");
}
}
private void TryRetryRegister()
{
if (_retryCount < MaxRetry)
{
_retryCount++;
LogService.Warn($"Hook 재등록 시도 {_retryCount}/{MaxRetry}");
Task.Delay(1000).ContinueWith(_ => Register());
}
else
{
LogService.Error("Hook 재등록 최대 횟수 초과");
HookFailed?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// 현재 포그라운드 창이 핫키·스니펫 확장을 억제해야 하는 시스템 대화상자인지 확인합니다.
/// Windows 공통 대화상자(#32770): 파일 열기/저장, 브라우저 파일 업로드 등.
/// </summary>
private static bool IsSuppressedForegroundWindow()
{
var hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero) return false;
var sb = new StringBuilder(64);
GetClassName(hwnd, sb, 64);
var cls = sb.ToString();
// #32770 = Windows 공통 대화상자 (파일 열기/저장/선택)
// SunAwtDialog = Java Swing 파일 대화상자 (일부 앱)
return cls == "#32770" || cls == "SunAwtDialog";
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode < 0)
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
var vkCode = Marshal.ReadInt32(lParam);
// ─── KEYUP: 핫키 발동 후 잔여 이벤트 억제 ─────────────────────────────
// Alt+X 핫키를 누른 직후 Alt KEYUP이 일부 앱의 메뉴바를 활성화하는 문제를 방지.
// 메인 키 KEYUP도 억제하여 앱이 "키를 완전히 처리"한 것으로 착각하지 않도록 함.
if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
{
if (_suppressNextAltUp && vkCode == VK_MENU)
{
_suppressNextAltUp = false;
return (IntPtr)1; // Alt KEYUP 억제 → 메뉴바 활성화 차단
}
if (_suppressNextKeyUp && vkCode == _suppressKeyUpVk)
{
_suppressNextKeyUp = false;
return (IntPtr)1; // 메인 키 KEYUP 억제
}
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
// ─── 이하 KEYDOWN / SYSKEYDOWN 처리 ──────────────────────────────────
if (wParam != WM_KEYDOWN && wParam != WM_SYSKEYDOWN)
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
// ─── 시스템 파일 대화상자에서는 핫키·스니펫 전부 비활성 ──────────────
if (IsSuppressedForegroundWindow())
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
// ─── 핫키 감지 ──────────────────────────────────────────────────────
if (!SuspendHotkey && vkCode == _hotkey.VkCode)
{
bool ctrlOk = !_hotkey.Ctrl || (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
bool altOk = !_hotkey.Alt || (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
bool shiftOk = !_hotkey.Shift || (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
bool winOk = !_hotkey.Win || (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0
|| (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0;
if (ctrlOk && altOk && shiftOk && winOk)
{
HotkeyTriggered?.Invoke(this, EventArgs.Empty);
// 이후 KEYUP 억제 설정
_suppressNextKeyUp = true;
_suppressKeyUpVk = vkCode;
if (_hotkey.Alt) _suppressNextAltUp = true;
return (IntPtr)1;
}
}
// ─── 글로벌 캡처 단축키 감지 ─────────────────────────────────────────
if (!SuspendHotkey && _captureHotkeyEnabled && vkCode == _captureHotkey.VkCode)
{
bool ctrlOk = !_captureHotkey.Ctrl || (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
bool altOk = !_captureHotkey.Alt || (GetAsyncKeyState(VK_MENU) & 0x8000) != 0;
bool shiftOk = !_captureHotkey.Shift || (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
bool winOk = !_captureHotkey.Win || (GetAsyncKeyState(VK_LWIN) & 0x8000) != 0
|| (GetAsyncKeyState(VK_RWIN) & 0x8000) != 0;
if (ctrlOk && altOk && shiftOk && winOk)
{
CaptureHotkeyTriggered?.Invoke(this, EventArgs.Empty);
_suppressNextKeyUp = true;
_suppressKeyUpVk = vkCode;
if (_captureHotkey.Alt) _suppressNextAltUp = true;
return (IntPtr)1;
}
}
// ─── 스니펫 키 필터 ─────────────────────────────────────────────────
if (KeyFilter?.Invoke(vkCode) == true)
return (IntPtr)1;
return CallNextHookEx(_hookHandle, nCode, wParam, lParam);
}
public void Dispose()
{
if (_hookHandle != IntPtr.Zero)
{
UnhookWindowsHookEx(_hookHandle);
_hookHandle = IntPtr.Zero;
}
}
// ─── P/Invoke ────────────────────────────────────────────────────────────
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn,
IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
}

View File

@@ -0,0 +1,238 @@
using System.IO;
using System.IO.Compression;
using System.Reflection;
using AxCopilot.Handlers;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Core;
/// <summary>
/// 외부 .dll 플러그인을 로드하고 CommandResolver에 등록합니다.
/// </summary>
public class PluginHost
{
private readonly SettingsService _settings;
private readonly CommandResolver _resolver;
private readonly List<IActionHandler> _loadedPlugins = new();
public IReadOnlyList<IActionHandler> LoadedPlugins => _loadedPlugins;
public PluginHost(SettingsService settings, CommandResolver resolver)
{
_settings = settings;
_resolver = resolver;
}
/// <summary>
/// settings.json의 plugins 목록에서 .dll을 로드합니다.
/// </summary>
public void LoadAll()
{
_loadedPlugins.Clear();
foreach (var entry in _settings.Settings.Plugins.Where(p => p.Enabled))
{
LoadPlugin(entry.Path);
}
// skills 폴더의 .skill.json 파일도 로드 (JSON 스킬)
var skillsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "skills");
if (Directory.Exists(skillsDir))
{
foreach (var skillFile in Directory.EnumerateFiles(skillsDir, "*.skill.json"))
{
LoadJsonSkill(skillFile);
}
}
}
private void LoadPlugin(string dllPath)
{
if (!File.Exists(dllPath))
{
LogService.Warn($"플러그인 파일 없음: {dllPath}");
return;
}
try
{
var assembly = Assembly.LoadFrom(dllPath);
var handlerTypes = assembly.GetExportedTypes()
.Where(t => typeof(IActionHandler).IsAssignableFrom(t) && !t.IsAbstract);
foreach (var type in handlerTypes)
{
if (Activator.CreateInstance(type) is IActionHandler handler)
{
_resolver.RegisterHandler(handler);
_loadedPlugins.Add(handler);
LogService.Info($"플러그인 로드: {handler.Metadata.Name} v{handler.Metadata.Version}");
}
}
}
catch (Exception ex)
{
LogService.Error($"플러그인 로드 실패 ({dllPath}): {ex.Message}");
// 플러그인 오류는 앱 전체를 중단하지 않음
}
}
private void LoadJsonSkill(string skillPath)
{
try
{
var skill = JsonSkillLoader.Load(skillPath);
if (skill != null)
{
_resolver.RegisterHandler(skill);
_loadedPlugins.Add(skill);
LogService.Info($"JSON 스킬 로드: {skill.Metadata.Name}");
}
}
catch (Exception ex)
{
LogService.Error($"JSON 스킬 로드 실패 ({skillPath}): {ex.Message}");
}
}
/// <summary>
/// 모든 플러그인을 언로드하고 재로드합니다 (개발자 모드 핫 리로드).
/// </summary>
public void Reload()
{
LogService.Info("플러그인 전체 재로드 시작");
LoadAll();
}
/// <summary>
/// 로컬 zip 파일에서 플러그인을 설치합니다.
/// zip 내 .dll 파일을 plugins 폴더에 추출하고 settings.json에 등록합니다.
/// </summary>
/// <returns>설치된 핸들러 수</returns>
public int InstallFromZip(string zipPath)
{
if (!File.Exists(zipPath))
{
LogService.Warn($"플러그인 zip 파일 없음: {zipPath}");
return 0;
}
var pluginsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "plugins");
Directory.CreateDirectory(pluginsDir);
int installed = 0;
try
{
using var archive = ZipFile.OpenRead(zipPath);
var pluginName = Path.GetFileNameWithoutExtension(zipPath);
var targetDir = Path.Combine(pluginsDir, pluginName);
Directory.CreateDirectory(targetDir);
foreach (var entry in archive.Entries)
{
if (string.IsNullOrEmpty(entry.Name)) continue; // 폴더 엔트리 건너뛰기
var destPath = Path.Combine(targetDir, entry.Name);
// 경로 탐색 공격 방지
if (!Path.GetFullPath(destPath).StartsWith(Path.GetFullPath(targetDir)))
{
LogService.Warn($"플러그인 zip 경로 위험: {entry.FullName}");
continue;
}
entry.ExtractToFile(destPath, overwrite: true);
}
// .dll 파일 찾아서 플러그인으로 등록
foreach (var dllFile in Directory.EnumerateFiles(targetDir, "*.dll"))
{
// 이미 등록된 플러그인인지 확인
if (_settings.Settings.Plugins.Any(p => p.Path == dllFile))
continue;
// settings.json에 등록
_settings.Settings.Plugins.Add(new Models.PluginEntry
{
Enabled = true,
Path = dllFile,
});
// 즉시 로드 시도
LoadPlugin(dllFile);
installed++;
}
// .skill.json 파일도 skills 폴더로 복사
var skillsDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "skills");
Directory.CreateDirectory(skillsDir);
foreach (var skillFile in Directory.EnumerateFiles(targetDir, "*.skill.json"))
{
var destSkill = Path.Combine(skillsDir, Path.GetFileName(skillFile));
File.Copy(skillFile, destSkill, overwrite: true);
LoadJsonSkill(destSkill);
installed++;
}
if (installed > 0)
_settings.Save();
LogService.Info($"플러그인 설치 완료: {zipPath} → {installed}개 핸들러");
}
catch (Exception ex)
{
LogService.Error($"플러그인 zip 설치 실패: {ex.Message}");
}
return installed;
}
/// <summary>
/// 설치된 플러그인을 제거합니다 (settings에서 삭제 + 파일 삭제).
/// </summary>
public bool UninstallPlugin(string dllPath)
{
try
{
var entry = _settings.Settings.Plugins.FirstOrDefault(p => p.Path == dllPath);
if (entry != null)
{
_settings.Settings.Plugins.Remove(entry);
_settings.Save();
}
// 플러그인 폴더 전체 삭제 (해당 dll이 있는 폴더)
var dir = Path.GetDirectoryName(dllPath);
if (dir != null && Directory.Exists(dir))
{
var pluginsRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "plugins");
// plugins 하위 폴더인 경우만 삭제 (안전장치)
if (Path.GetFullPath(dir).StartsWith(Path.GetFullPath(pluginsRoot)))
{
Directory.Delete(dir, recursive: true);
}
}
LogService.Info($"플러그인 제거 완료: {dllPath}");
return true;
}
catch (Exception ex)
{
LogService.Error($"플러그인 제거 실패: {ex.Message}");
return false;
}
}
}

View File

@@ -0,0 +1,252 @@
using System.Runtime.InteropServices;
using System.Text;
using System.Windows;
using AxCopilot.Services;
namespace AxCopilot.Core;
/// <summary>
/// 모든 앱에서 ';키워드 + Space/Enter' 패턴을 감지해 스니펫을 자동 확장합니다.
/// InputListener.KeyFilter에 <see cref="HandleKey"/> 을 등록하여 사용합니다.
/// </summary>
public class SnippetExpander
{
private readonly SettingsService _settings;
private readonly StringBuilder _buffer = new();
private bool _tracking;
// VK 상수
private const ushort VK_BACK = 0x08;
private const int VK_ESCAPE = 0x1B;
private const int VK_SPACE = 0x20;
private const int VK_RETURN = 0x0D;
private const int VK_OEM_1 = 0xBA; // ; (US QWERTY)
private const int VK_SHIFT = 0x10;
private const int VK_CONTROL = 0x11;
private const int VK_MENU = 0x12;
private const ushort VK_CTRL_US = 0x11;
// 방향키 / 기능키 — 이 키가 오면 버퍼 초기화
private static readonly HashSet<int> ClearKeys = new()
{
0x21, 0x22, 0x23, 0x24, // PgUp, PgDn, End, Home
0x25, 0x26, 0x27, 0x28, // ←↑→↓
0x2E, // Delete
};
public SnippetExpander(SettingsService settings)
{
_settings = settings;
}
/// <summary>
/// InputListener.KeyFilter에 등록할 메서드.
/// true → 해당 키 이벤트 소비(차단), false → 통과.
/// 훅 스레드에서 호출되므로 신속히 처리해야 합니다.
/// </summary>
public bool HandleKey(int vkCode)
{
// 자동 확장 비활성화 시 즉시 통과
if (!_settings.Settings.Launcher.SnippetAutoExpand) return false;
// Ctrl/Alt 조합은 무시 (단축키와 충돌 방지)
if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) { _tracking = false; _buffer.Clear(); return false; }
if ((GetAsyncKeyState(VK_MENU) & 0x8000) != 0) { _tracking = false; _buffer.Clear(); return false; }
// ─── 트리거 시작: ';' 입력 ──────────────────────────────────────────
if (vkCode == VK_OEM_1 && (GetAsyncKeyState(VK_SHIFT) & 0x8000) == 0)
{
_tracking = true;
_buffer.Clear();
_buffer.Append(';');
return false; // ';'는 소비하지 않고 앱으로 전달
}
if (!_tracking) return false;
// ─── 영문자/숫자 — 버퍼에 추가 ─────────────────────────────────────
if ((vkCode >= 0x41 && vkCode <= 0x5A) || // A-Z
(vkCode >= 0x30 && vkCode <= 0x39) || // 0-9
(vkCode >= 0x60 && vkCode <= 0x69) || // Numpad 0-9
vkCode == 0xBD) // -
{
bool shifted = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
char c = VkToChar(vkCode, shifted);
if (c != '\0') _buffer.Append(char.ToLowerInvariant(c));
return false;
}
// ─── BackSpace — 버퍼에서 한 글자 제거 ──────────────────────────────
if (vkCode == VK_BACK)
{
if (_buffer.Length > 1)
_buffer.Remove(_buffer.Length - 1, 1);
else
{
_tracking = false;
_buffer.Clear();
}
return false;
}
// ─── Space / Enter — 스니펫 확장 시도 ───────────────────────────────
if (vkCode == VK_SPACE || vkCode == VK_RETURN)
{
if (_buffer.Length > 1) // ';' 이후 한 글자 이상
{
var keyword = _buffer.ToString(1, _buffer.Length - 1);
_tracking = false;
_buffer.Clear();
var snippet = _settings.Settings.Snippets.FirstOrDefault(
s => s.Key.Equals(keyword, StringComparison.OrdinalIgnoreCase));
if (snippet != null)
{
var expanded = ExpandVariables(snippet.Content);
var deleteCount = keyword.Length + 1; // ';' + keyword
// 트리거 키(Space/Enter) 소비 후 UI 스레드에서 확장 처리
Application.Current.Dispatcher.BeginInvoke(() =>
PasteExpansion(expanded, deleteCount));
return true; // 트리거 키 소비
}
}
_tracking = false;
_buffer.Clear();
return false;
}
// ─── Escape / 방향키 / 기능키 — 추적 중단 ───────────────────────────
if (vkCode == VK_ESCAPE || ClearKeys.Contains(vkCode) || vkCode >= 0x70)
{
_tracking = false;
_buffer.Clear();
}
return false;
}
// ─── 확장 실행 (UI 스레드) ───────────────────────────────────────────────
private static void PasteExpansion(string text, int deleteCount)
{
try
{
// 1. Backspace × deleteCount
var inputs = new INPUT[deleteCount * 2];
for (int i = 0; i < deleteCount; i++)
{
inputs[i * 2] = MakeKeyInput(VK_BACK, false);
inputs[i * 2 + 1] = MakeKeyInput(VK_BACK, true);
}
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
// 2. 클립보드 → Ctrl+V
Clipboard.SetText(text);
var paste = new[]
{
MakeKeyInput(VK_CTRL_US, false),
MakeKeyInput(0x56, false), // V
MakeKeyInput(0x56, true),
MakeKeyInput(VK_CTRL_US, true),
};
SendInput((uint)paste.Length, paste, Marshal.SizeOf<INPUT>());
LogService.Info($"스니펫 확장 완료: {deleteCount}자 삭제 후 붙여넣기");
}
catch (Exception ex)
{
LogService.Warn($"스니펫 확장 실패: {ex.Message}");
}
}
private static INPUT MakeKeyInput(ushort vk, bool keyUp)
{
var input = new INPUT { type = 1 };
input.u.ki.wVk = vk;
input.u.ki.dwFlags = keyUp ? 0x0002u : 0u; // KEYEVENTF_KEYUP
return input;
}
// ─── 변수 치환 ────────────────────────────────────────────────────────────
private static string ExpandVariables(string content)
{
var now = DateTime.Now;
return content
.Replace("{date}", now.ToString("yyyy-MM-dd"))
.Replace("{time}", now.ToString("HH:mm:ss"))
.Replace("{datetime}", now.ToString("yyyy-MM-dd HH:mm:ss"))
.Replace("{year}", now.Year.ToString())
.Replace("{month}", now.Month.ToString("D2"))
.Replace("{day}", now.Day.ToString("D2"));
}
// ─── VK → Char 매핑 (US QWERTY 기준) ────────────────────────────────────
private static char VkToChar(int vk, bool shifted)
{
if (vk >= 0x41 && vk <= 0x5A)
return shifted ? (char)vk : char.ToLowerInvariant((char)vk);
if (vk >= 0x30 && vk <= 0x39)
return shifted ? ")!@#$%^&*("[vk - 0x30] : (char)vk;
if (vk >= 0x60 && vk <= 0x69)
return (char)('0' + (vk - 0x60));
if (vk == 0xBD)
return shifted ? '_' : '-';
return '\0';
}
// ─── P/Invoke ────────────────────────────────────────────────────────────
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
[DllImport("user32.dll", SetLastError = true)]
private static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
[StructLayout(LayoutKind.Sequential)]
private struct INPUT
{
public int type;
public InputUnion u;
}
[StructLayout(LayoutKind.Explicit)]
private struct InputUnion
{
[FieldOffset(0)] public MOUSEINPUT mi;
[FieldOffset(0)] public KEYBDINPUT ki;
[FieldOffset(0)] public HARDWAREINPUT hi;
}
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[StructLayout(LayoutKind.Sequential)]
private struct HARDWAREINPUT
{
public uint uMsg;
public ushort wParamL;
public ushort wParamH;
}
}

View File

@@ -0,0 +1,104 @@
using System.Windows;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Phase L3-3: AI 스니펫 핸들러. "ai" 예약어로 사용합니다.
/// 예: ai email 프로젝트 일정 변경 안내 → 업무 이메일 초안 생성
/// ai summary 이 문서의 핵심 내용 → 내용 요약
/// ai (목록) → 등록된 AI 템플릿 목록 표시
///
/// AiEnabled=false이면 항목 자체를 표시하지 않습니다.
/// </summary>
public class AiSnippetHandler : IActionHandler
{
private readonly SettingsService _settings;
private readonly SnippetTemplateService _templateService;
public string? Prefix => "ai";
public PluginMetadata Metadata => new(
"AiSnippet",
"AI 스니펫 — ai [템플릿] [내용]",
"1.0",
"AX");
public AiSnippetHandler(SettingsService settings, SnippetTemplateService templateService)
{
_settings = settings;
_templateService = templateService;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
// AI 비활성화 시 항목 표시 안 함
if (!(_settings.Settings.AiEnabled))
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
var parts = query.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var keyword = parts.Length > 0 ? parts[0] : "";
var argument = parts.Length > 1 ? parts[1] : "";
var templates = _templateService.Search(keyword);
var items = new List<LauncherItem>();
if (templates.Count == 0)
{
items.Add(new LauncherItem(
"등록된 AI 템플릿 없음",
"설정 → AI 스니펫 탭에서 추가하세요",
null, null, Symbol: Symbols.Lightbulb));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
foreach (var tmpl in templates)
{
var hasArg = !string.IsNullOrWhiteSpace(argument);
items.Add(new LauncherItem(
hasArg
? $"[AI] {tmpl.Name}: {TruncateArg(argument)}"
: $"[AI] {tmpl.Name}",
hasArg
? $"Enter: AI가 생성 후 클립보드에 복사 · 템플릿: {tmpl.Prompt}"
: $"ai {tmpl.Keyword} [내용] · {tmpl.Prompt}",
null,
hasArg ? (object)(tmpl, argument) : null,
Symbol: Symbols.Lightbulb));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (!_settings.Settings.AiEnabled) return;
if (item.Data is not (AiSnippetTemplate tmpl, string arg)) return;
try
{
var result = await _templateService.GenerateAsync(tmpl, arg, ct);
if (string.IsNullOrWhiteSpace(result)) return;
// 결과를 클립보드에 복사
Application.Current.Dispatcher.Invoke(() =>
{
Clipboard.SetText(result);
Services.NotificationService.Notify(
$"AI {tmpl.Name} 완료",
"결과가 클립보드에 복사되었습니다.");
});
}
catch (Exception ex)
{
LogService.Warn($"AI 스니펫 생성 실패: {ex.Message}");
}
}
private static string TruncateArg(string arg)
=> arg.Length > 30 ? arg[..27] + "…" : arg;
}

View File

@@ -0,0 +1,186 @@
using System.Diagnostics;
using System.IO;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// @ prefix 핸들러: URL 열기 Alias
/// </summary>
public class UrlAliasHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "@";
public PluginMetadata Metadata => new("url-alias", "URL 별칭", "1.0", "AX");
public UrlAliasHandler(SettingsService settings) => _settings = settings;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var aliases = _settings.Settings.Aliases
.Where(a => a.Type == "url" &&
(string.IsNullOrEmpty(query) ||
a.Key.Contains(query, StringComparison.OrdinalIgnoreCase)))
.Select(a =>
{
// favicon 캐시 경로 조회 (없으면 백그라운드 다운로드 시작)
var faviconPath = GetFaviconPath(a.Target);
return new LauncherItem(
a.Key,
a.Description ?? a.Target,
faviconPath,
a,
a.Target,
Symbols.Globe);
});
return Task.FromResult<IEnumerable<LauncherItem>>(aliases);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry alias)
{
var url = Environment.ExpandEnvironmentVariables(alias.Target);
if (!IsAllowedUrl(url))
{
LogService.Warn($"허용되지 않는 URL 스킴: {url}");
return Task.CompletedTask;
}
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
return Task.CompletedTask;
}
// http, https, ftp, ms-settings, mailto, file 스킴만 허용 (javascript: 등 차단)
private static readonly HashSet<string> AllowedSchemes =
new(StringComparer.OrdinalIgnoreCase) { "http", "https", "ftp", "ftps", "ms-settings", "mailto", "file" };
private static bool IsAllowedUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return false;
return AllowedSchemes.Contains(uri.Scheme);
}
/// <summary>favicon 캐시 파일 경로를 반환합니다. 캐시에 없으면 백그라운드 다운로드를 시작합니다.</summary>
private static string? GetFaviconPath(string url)
{
try
{
if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase))
url = "https://" + url;
var domain = new Uri(url).Host.ToLowerInvariant();
var cachePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "favicons", $"{domain}.png");
if (File.Exists(cachePath))
return cachePath;
// 백그라운드 다운로드 시작 (다음 표시 시 캐시됨)
FaviconService.GetFavicon(url);
return null;
}
catch (Exception) { return null; }
}
}
/// <summary>
/// cd prefix 핸들러: 폴더 열기 Alias
/// </summary>
public class FolderAliasHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "cd";
public PluginMetadata Metadata => new("folder-alias", "폴더 별칭", "1.0", "AX");
public FolderAliasHandler(SettingsService settings) => _settings = settings;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var aliases = _settings.Settings.Aliases
.Where(a => a.Type == "folder" &&
(string.IsNullOrEmpty(query) ||
a.Key.Contains(query, StringComparison.OrdinalIgnoreCase)))
.Select(a => new LauncherItem(
a.Key,
Environment.ExpandEnvironmentVariables(a.Target),
null,
a,
Symbol: Symbols.Folder));
return Task.FromResult<IEnumerable<LauncherItem>>(aliases);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry alias)
{
var path = Environment.ExpandEnvironmentVariables(alias.Target);
Process.Start(new ProcessStartInfo("explorer.exe", path) { UseShellExecute = true });
}
return Task.CompletedTask;
}
}
/// <summary>
/// > prefix 핸들러: 터미널 명령 실행
/// </summary>
public class BatchHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => ">";
public PluginMetadata Metadata => new("batch", "명령 실행", "1.0", "AX");
public BatchHandler(SettingsService settings) => _settings = settings;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
// 등록된 배치 Alias 표시 + 직접 입력 실행 옵션
var items = new List<LauncherItem>();
var batchAliases = _settings.Settings.Aliases
.Where(a => a.Type == "batch" &&
(string.IsNullOrEmpty(query) ||
a.Key.Contains(query, StringComparison.OrdinalIgnoreCase)))
.Select(a => new LauncherItem(a.Key, a.Target, null, a, Symbol: Symbols.Terminal));
items.AddRange(batchAliases);
// 직접 명령어 입력 허용 (큰따옴표 이스케이프로 인젝션 방지)
if (!string.IsNullOrEmpty(query))
{
var safeQuery = query.Replace("\"", "\\\"");
items.Insert(0, new LauncherItem(
$"실행: {query}",
"PowerShell에서 직접 실행",
null,
new AliasEntry { Type = "batch", Target = $"powershell -NoProfile -Command \"{safeQuery}\"", ShowWindow = true },
Symbol: Symbols.Terminal
));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is AliasEntry alias)
{
var target = Environment.ExpandEnvironmentVariables(alias.Target);
var parts = target.Split(' ', 2);
var psi = new ProcessStartInfo(parts[0])
{
Arguments = parts.Length > 1 ? parts[1] : "",
UseShellExecute = alias.ShowWindow,
CreateNoWindow = !alias.ShowWindow,
WindowStyle = alias.ShowWindow ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden
};
Process.Start(psi);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,210 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 텍스트 일괄 처리 핸들러. "batch" 프리픽스로 사용합니다.
/// 클립보드 텍스트의 각 줄에 동시에 변환을 적용합니다.
/// 예: batch prefix [텍스트] → 각 줄 앞에 [텍스트] 추가
/// batch suffix [텍스트] → 각 줄 뒤에 [텍스트] 추가
/// batch number → 줄번호 추가
/// batch sort → 줄 정렬
/// batch unique → 중복 줄 제거
/// batch wrap " → 각 줄을 "로 감싸기
/// batch replace A B → A를 B로 치환
/// batch csv → 줄 → CSV 한 줄로 합치기
/// batch split , → CSV 한 줄 → 여러 줄로 분리
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class BatchTextHandler : IActionHandler
{
public string? Prefix => "batch";
public PluginMetadata Metadata => new(
"BatchText",
"텍스트 일괄 처리 — batch",
"1.0",
"AX");
private static readonly (string Cmd, string Desc)[] Commands =
[
("prefix [텍스트]", " "),
("suffix [텍스트]", "각 줄 뒤에 텍스트 추가"),
("wrap [문자]", "각 줄을 지정 문자로 감싸기 (예: wrap \")"),
("number", "줄번호 추가 (1. 2. 3. ...)"),
("sort", "줄 오름차순 정렬"),
("sortd", "줄 내림차순 정렬"),
("reverse", "줄 순서 뒤집기"),
("unique", "중복 줄 제거"),
("trim", "각 줄 앞뒤 공백 제거"),
("replace [A] [B]", "A를 B로 전체 치환"),
("csv", "줄들을 쉼표로 합쳐 한 줄로"),
("split [구분자]", "한 줄을 구분자로 분리하여 여러 줄로"),
("indent [N]", "각 줄 앞에 공백 N개 추가"),
("unindent", "각 줄의 선행 공백/탭 제거"),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
var items = Commands.Select(c => new LauncherItem(
$"batch {c.Cmd}",
c.Desc,
null, null,
Symbol: Symbols.Text)).ToList<LauncherItem>();
items.Insert(0, new LauncherItem(
"텍스트 일괄 처리",
"클립보드 텍스트의 각 줄에 변환 적용 · 명령 입력",
null, null, Symbol: Symbols.Info));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 클립보드 읽기
string? text = null;
try
{
if (Application.Current?.Dispatcher.Invoke(() => Clipboard.ContainsText()) == true)
text = Application.Current.Dispatcher.Invoke(() => Clipboard.GetText());
}
catch (Exception) { }
if (string.IsNullOrEmpty(text))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요",
null, null, Symbol: Symbols.Warning)
]);
}
var lines = text.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries);
var cmd = parts[0].ToLowerInvariant();
var arg = parts.Length > 1 ? parts[1] : "";
string? result = null;
string? desc = null;
try
{
switch (cmd)
{
case "prefix":
result = string.Join("\n", lines.Select(l => arg + l));
desc = $"각 줄 앞에 '{arg}' 추가";
break;
case "suffix":
result = string.Join("\n", lines.Select(l => l + arg));
desc = $"각 줄 뒤에 '{arg}' 추가";
break;
case "wrap":
var w = string.IsNullOrEmpty(arg) ? "\"" : arg;
result = string.Join("\n", lines.Select(l => w + l + w));
desc = $"각 줄을 '{w}'로 감싸기";
break;
case "number":
result = string.Join("\n", lines.Select((l, i) => $"{i + 1}. {l}"));
desc = "줄번호 추가";
break;
case "sort":
result = string.Join("\n", lines.Order());
desc = "오름차순 정렬";
break;
case "sortd":
result = string.Join("\n", lines.OrderDescending());
desc = "내림차순 정렬";
break;
case "reverse":
result = string.Join("\n", lines.Reverse());
desc = "줄 순서 뒤집기";
break;
case "unique":
var unique = lines.Distinct().ToArray();
result = string.Join("\n", unique);
desc = $"중복 제거: {lines.Length}줄 → {unique.Length}줄";
break;
case "trim":
result = string.Join("\n", lines.Select(l => l.Trim()));
desc = "각 줄 공백 제거";
break;
case "replace":
var rParts = arg.Split(' ', 2, StringSplitOptions.TrimEntries);
if (rParts.Length == 2)
{
result = text.Replace(rParts[0], rParts[1]);
desc = $"'{rParts[0]}' → '{rParts[1]}' 치환";
}
break;
case "csv":
result = string.Join(",", lines.Select(l => l.Trim()));
desc = $"{lines.Length}줄 → CSV 한 줄";
break;
case "split":
var sep = string.IsNullOrEmpty(arg) ? "," : arg;
result = string.Join("\n", text.Split(sep));
desc = $"'{sep}' 기준 분리";
break;
case "indent":
var n = int.TryParse(arg, out var indent) ? indent : 4;
var pad = new string(' ', n);
result = string.Join("\n", lines.Select(l => pad + l));
desc = $"{n}칸 들여쓰기";
break;
case "unindent":
result = string.Join("\n", lines.Select(l => l.TrimStart(' ', '\t')));
desc = "선행 공백 제거";
break;
}
}
catch (Exception ex)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem($"처리 오류: {ex.Message}", "입력 데이터를 확인하세요",
null, null, Symbol: Symbols.Error)
]);
}
if (result == null)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem($"알 수 없는 명령: {cmd}", "batch 만 입력하면 전체 명령 목록",
null, null, Symbol: Symbols.Warning)
]);
}
var preview = result.Length > 120 ? result[..117] + "…" : result;
preview = preview.Replace("\n", "↵ ");
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"[{desc}] Enter로 ",
preview,
null, result,
Symbol: Symbols.Text)
]);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
NotificationService.Notify("일괄 처리 완료", "변환 결과가 클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,180 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Chrome / Edge 브라우저 북마크를 검색합니다.
/// 프리픽스 없음 — 일반 퍼지 검색에 통합됩니다.
/// 지원 브라우저: Google Chrome, Microsoft Edge
/// </summary>
public class BookmarkHandler : IActionHandler
{
public string? Prefix => null; // 퍼지 검색에 통합
public PluginMetadata Metadata => new(
"Bookmarks",
"Chrome / Edge 북마크 검색",
"1.0",
"AX");
// 캐시 (앱 세션 중 북마크가 자주 바뀌지 않으므로 한 번 로드 후 재사용)
private List<BookmarkEntry>? _cache;
private DateTime _cacheTime = DateTime.MinValue;
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(5);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
RefreshCacheIfNeeded();
if (_cache == null || _cache.Count == 0)
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
var q = query.Trim().ToLowerInvariant();
var results = _cache
.Where(b => b.Name.ToLowerInvariant().Contains(q)
|| (b.Url?.ToLowerInvariant().Contains(q) ?? false))
.Take(8)
.Select(b => new LauncherItem(
b.Name,
b.Url ?? "",
null,
b.Url,
Symbol: Symbols.Globe))
.ToList();
return Task.FromResult<IEnumerable<LauncherItem>>(results);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string url && !string.IsNullOrWhiteSpace(url))
{
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(url)
{ UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"북마크 열기 실패: {ex.Message}");
}
}
return Task.CompletedTask;
}
// ─── 캐시 ────────────────────────────────────────────────────────────────
private void RefreshCacheIfNeeded()
{
if (_cache != null && DateTime.Now - _cacheTime < CacheTtl) return;
var bookmarks = new List<BookmarkEntry>();
foreach (var file in GetBookmarkFiles())
{
try
{
var json = File.ReadAllText(file);
ParseChromeBookmarks(json, bookmarks);
}
catch (Exception ex)
{
LogService.Warn($"북마크 파일 읽기 실패: {file} — {ex.Message}");
}
}
_cache = bookmarks;
_cacheTime = DateTime.Now;
LogService.Info($"북마크 로드 완료: {bookmarks.Count}개");
}
// ─── 북마크 파일 경로 ─────────────────────────────────────────────────────
private static IEnumerable<string> GetBookmarkFiles()
{
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
// Chrome (안정, 베타, 개발, 카나리아)
var chromePaths = new[]
{
Path.Combine(localAppData, "Google", "Chrome", "User Data"),
Path.Combine(localAppData, "Google", "Chrome Beta", "User Data"),
Path.Combine(localAppData, "Google", "Chrome Dev", "User Data"),
Path.Combine(localAppData, "Google", "Chrome SxS", "User Data"),
};
// Edge
var edgePaths = new[]
{
Path.Combine(localAppData, "Microsoft", "Edge", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Beta", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Dev", "User Data"),
Path.Combine(localAppData, "Microsoft", "Edge Canary", "User Data"),
};
foreach (var profileRoot in chromePaths.Concat(edgePaths))
{
if (!Directory.Exists(profileRoot)) continue;
// Default 프로파일
var defaultBookmark = Path.Combine(profileRoot, "Default", "Bookmarks");
if (File.Exists(defaultBookmark)) yield return defaultBookmark;
// Profile 1, 2, ... 프로파일
foreach (var dir in Directory.GetDirectories(profileRoot, "Profile *"))
{
var f = Path.Combine(dir, "Bookmarks");
if (File.Exists(f)) yield return f;
}
}
}
// ─── Chrome/Edge JSON 파싱 ───────────────────────────────────────────────
private static void ParseChromeBookmarks(string json, List<BookmarkEntry> result)
{
var doc = JsonNode.Parse(json);
var roots = doc?["roots"];
if (roots == null) return;
foreach (var rootKey in new[] { "bookmark_bar", "other", "synced" })
{
var node = roots[rootKey];
if (node != null) WalkNode(node, result);
}
}
private static void WalkNode(JsonNode node, List<BookmarkEntry> result)
{
var type = node["type"]?.GetValue<string>();
if (type == "url")
{
var name = node["name"]?.GetValue<string>() ?? "";
var url = node["url"]?.GetValue<string>() ?? "";
if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(url))
result.Add(new BookmarkEntry(name, url));
return;
}
if (type == "folder")
{
var children = node["children"]?.AsArray();
if (children == null) return;
foreach (var child in children)
{
if (child != null) WalkNode(child, result);
}
}
}
private record BookmarkEntry(string Name, string? Url);
}

View File

@@ -0,0 +1,566 @@
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows;
namespace AxCopilot.Handlers;
/// <summary>
/// 수식 계산기 핸들러. "=" 프리픽스로 사용합니다.
/// 예: = 1+2*3 → 7
/// = sqrt(16) → 4
/// = 100km in miles → 단위 변환
/// = 100 USD to KRW → 통화 변환 (실시간 환율)
/// </summary>
public class CalculatorHandler : IActionHandler
{
public string? Prefix => "=";
public PluginMetadata Metadata => new(
"Calculator",
"수식 계산기 — = 뒤에 수식 입력",
"1.0",
"AX");
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return
[
new LauncherItem(
"수식을 입력하세요",
"예: 1+2*3 · sqrt(16) · 100km in miles · 100 USD to KRW",
null, null, Symbol: Symbols.Calculator)
];
}
var trimmed = query.Trim();
// ─── 통화 변환 우선 감지 ──────────────────────────────────────────────
if (CurrencyConverter.IsCurrencyQuery(trimmed))
{
return await CurrencyConverter.ConvertAsync(trimmed, ct);
}
// ─── 단위 변환 우선 감지 ──────────────────────────────────────────────
if (UnitConverter.TryConvert(trimmed, out var convertResult))
{
return
[
new LauncherItem(
convertResult!,
$"{trimmed} · Enter로 클립보드에 복사",
null, convertResult, Symbol: Symbols.Calculator),
];
}
// ─── 수식 계산 ────────────────────────────────────────────────────────
try
{
var value = MathEvaluator.Evaluate(trimmed);
var result = FormatResult(value);
return
[
new LauncherItem(
result,
$"{trimmed} = {result} · Enter로 클립보드에 복사",
null, result, Symbol: Symbols.Calculator),
];
}
catch (Exception ex)
{
return
[
new LauncherItem(
"계산할 수 없습니다",
ex.Message,
null, null, Symbol: Symbols.Error)
];
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string result)
{
try { Clipboard.SetText(result); }
catch (Exception) { /* 클립보드 접근 실패 무시 */ }
}
return Task.CompletedTask;
}
// ─── 결과 포맷 ─────────────────────────────────────────────────────────
private static string FormatResult(double value)
{
if (double.IsNaN(value)) return "NaN";
if (double.IsPositiveInfinity(value)) return "∞";
if (double.IsNegativeInfinity(value)) return "-∞";
// 정수이고 너무 크지 않으면 천 단위 구분 없이 정수로 표시
if (value == Math.Floor(value) && Math.Abs(value) < 1e15)
return ((long)value).ToString();
// 소수점 10자리까지, 불필요한 0은 제거
return value.ToString("G10", System.Globalization.CultureInfo.InvariantCulture);
}
}
// ─── 단위 변환 ─────────────────────────────────────────────────────────────────
/// <summary>
/// "100km in miles", "32f in c", "5lb to kg" 형식의 단위 변환.
/// </summary>
internal static class UnitConverter
{
// 패턴: <숫자> <단위> in|to <단위>
private static readonly Regex Pattern = new(
@"^(-?\d+(?:\.\d+)?)\s*([a-z°/²³µ]+)\s+(?:in|to)\s+([a-z°/²³µ]+)$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static bool TryConvert(string input, out string? result)
{
result = null;
var m = Pattern.Match(input.Trim());
if (!m.Success) return false;
if (!double.TryParse(m.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var value))
return false;
var from = m.Groups[2].Value.ToLowerInvariant();
var to = m.Groups[3].Value.ToLowerInvariant();
// 온도는 비선형 → 별도 처리
if (TryConvertTemperature(value, from, to, out var tResult))
{
result = $"{FormatNum(tResult)} {TemperatureLabel(to)}";
return true;
}
// 나머지 범주(선형 변환)
foreach (var table in _tables)
{
if (table.TryGetValue(from, out var fromFactor) &&
table.TryGetValue(to, out var toFactor))
{
var converted = value * fromFactor / toFactor;
result = $"{FormatNum(converted)} {to}";
return true;
}
}
return false;
}
// ─── 온도 ────────────────────────────────────────────────────────────────
private static bool TryConvertTemperature(double v, string from, string to, out double r)
{
r = 0;
// 섭씨 표준화
double celsius;
switch (from)
{
case "c": case "°c": case "celsius": celsius = v; break;
case "f": case "°f": case "fahrenheit": celsius = (v - 32) * 5 / 9; break;
case "k": case "kelvin": celsius = v - 273.15; break;
default: return false;
}
switch (to)
{
case "c": case "°c": case "celsius": r = celsius; break;
case "f": case "°f": case "fahrenheit": r = celsius * 9 / 5 + 32; break;
case "k": case "kelvin": r = celsius + 273.15; break;
default: return false;
}
return true;
}
private static string TemperatureLabel(string unit) => unit switch
{
"c" or "°c" or "celsius" => "°C",
"f" or "°f" or "fahrenheit" => "°F",
"k" or "kelvin" => "K",
_ => unit
};
// ─── 선형 변환 테이블 (기준 단위 = 1) ────────────────────────────────────
// 길이 (기준: m)
private static readonly Dictionary<string, double> _length = new()
{
["km"] = 1000, ["m"] = 1, ["cm"] = 0.01, ["mm"] = 0.001,
["mi"] = 1609.344, ["mile"] = 1609.344, ["miles"] = 1609.344,
["ft"] = 0.3048, ["feet"] = 0.3048, ["foot"] = 0.3048,
["in"] = 0.0254, ["inch"] = 0.0254, ["inches"] = 0.0254,
["yd"] = 0.9144, ["yard"] = 0.9144, ["yards"] = 0.9144,
["nm"] = 1e-9,
};
// 무게 (기준: kg)
private static readonly Dictionary<string, double> _weight = new()
{
["t"] = 1000, ["ton"] = 1000, ["tonnes"] = 1000,
["kg"] = 1, ["g"] = 0.001, ["mg"] = 1e-6,
["lb"] = 0.453592, ["lbs"] = 0.453592, ["pound"] = 0.453592, ["pounds"] = 0.453592,
["oz"] = 0.0283495, ["ounce"] = 0.0283495, ["ounces"] = 0.0283495,
};
// 속도 (기준: m/s)
private static readonly Dictionary<string, double> _speed = new()
{
["m/s"] = 1, ["mps"] = 1,
["km/h"] = 1.0 / 3.6, ["kmh"] = 1.0 / 3.6, ["kph"] = 1.0 / 3.6,
["mph"] = 0.44704,
["kn"] = 0.514444, ["knot"] = 0.514444, ["knots"] = 0.514444,
};
// 데이터 (기준: byte)
private static readonly Dictionary<string, double> _data = new()
{
["b"] = 1, ["byte"] = 1, ["bytes"] = 1,
["kb"] = 1024, ["kib"] = 1024,
["mb"] = 1024 * 1024, ["mib"] = 1024 * 1024,
["gb"] = 1024.0 * 1024 * 1024, ["gib"] = 1024.0 * 1024 * 1024,
["tb"] = 1024.0 * 1024 * 1024 * 1024, ["tib"] = 1024.0 * 1024 * 1024 * 1024,
["pb"] = 1024.0 * 1024 * 1024 * 1024 * 1024,
};
// 넓이 (기준: m²)
private static readonly Dictionary<string, double> _area = new()
{
["m²"] = 1, ["m2"] = 1,
["km²"] = 1e6, ["km2"] = 1e6,
["cm²"] = 1e-4, ["cm2"] = 1e-4,
["ha"] = 10000,
["acre"] = 4046.86, ["acres"] = 4046.86,
["ft²"] = 0.092903, ["ft2"] = 0.092903,
};
private static readonly List<Dictionary<string, double>> _tables = new()
{ _length, _weight, _speed, _data, _area };
private static string FormatNum(double v)
{
if (v == Math.Floor(v) && Math.Abs(v) < 1e12)
return ((long)v).ToString("N0", System.Globalization.CultureInfo.CurrentCulture);
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
}
}
// ─── 수식 파서 ─────────────────────────────────────────────────────────────────
/// <summary>
/// 재귀 하강 파서 기반 수학 수식 평가기.
/// 지원: +, -, *, /, %, ^ (거듭제곱), 괄호, 단항 음수,
/// sqrt, abs, ceil, floor, round, sin, cos, tan (도 단위),
/// log (밑 10), ln (자연로그), pi, e
/// </summary>
internal static class MathEvaluator
{
public static double Evaluate(string expr)
{
var evaluator = new Evaluator(
expr.Replace(" ", "")
.Replace("×", "*")
.Replace("÷", "/")
.Replace("", ",")
.ToLowerInvariant());
return evaluator.Parse();
}
private class Evaluator
{
private readonly string _s;
private int _i;
public Evaluator(string s) { _s = s; _i = 0; }
public double Parse()
{
var result = ParseExpr();
if (_i < _s.Length)
throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'");
return result;
}
// 덧셈 / 뺄셈
private double ParseExpr()
{
var left = ParseTerm();
while (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-'))
{
var op = _s[_i++];
var right = ParseTerm();
left = op == '+' ? left + right : left - right;
}
return left;
}
// 곱셈 / 나눗셈 / 나머지
private double ParseTerm()
{
var left = ParsePower();
while (_i < _s.Length && (_s[_i] == '*' || _s[_i] == '/' || _s[_i] == '%'))
{
var op = _s[_i++];
var right = ParsePower();
left = op == '*' ? left * right
: op == '/' ? left / right
: left % right;
}
return left;
}
// 거듭제곱 (오른쪽 결합)
private double ParsePower()
{
var b = ParseUnary();
if (_i < _s.Length && _s[_i] == '^')
{
_i++;
var exp = ParseUnary();
return Math.Pow(b, exp);
}
return b;
}
// 단항 부호
private double ParseUnary()
{
if (_i < _s.Length && _s[_i] == '-') { _i++; return -ParsePrimary(); }
if (_i < _s.Length && _s[_i] == '+') { _i++; return ParsePrimary(); }
return ParsePrimary();
}
// 리터럴 / 괄호 / 함수 호출
private double ParsePrimary()
{
if (_i >= _s.Length)
throw new InvalidOperationException("수식이 불완전합니다.");
// 16진수 리터럴 0x...
if (_i + 1 < _s.Length && _s[_i] == '0' && _s[_i + 1] == 'x')
{
_i += 2;
var hexStart = _i;
while (_i < _s.Length && "0123456789abcdef".Contains(_s[_i])) _i++;
return Convert.ToInt64(_s[hexStart.._i], 16);
}
// 숫자
if (char.IsDigit(_s[_i]) || _s[_i] == '.')
{
var start = _i;
while (_i < _s.Length && (char.IsDigit(_s[_i]) || _s[_i] == '.')) _i++;
// 과학적 표기: 1.5e3
if (_i < _s.Length && _s[_i] == 'e')
{
_i++;
if (_i < _s.Length && (_s[_i] == '+' || _s[_i] == '-')) _i++;
while (_i < _s.Length && char.IsDigit(_s[_i])) _i++;
}
return double.Parse(_s[start.._i],
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture);
}
// 괄호
if (_s[_i] == '(')
{
_i++;
var val = ParseExpr();
if (_i < _s.Length && _s[_i] == ')') _i++;
return val;
}
// 식별자 (상수 또는 함수)
if (char.IsLetter(_s[_i]))
{
var start = _i;
while (_i < _s.Length && (char.IsLetterOrDigit(_s[_i]) || _s[_i] == '_')) _i++;
var name = _s[start.._i];
// 상수
if (name == "pi") return Math.PI;
if (name == "e") return Math.E;
if (name == "inf") return double.PositiveInfinity;
// 함수 호출
if (_i < _s.Length && _s[_i] == '(')
{
_i++; // (
var arg = ParseExpr();
// 두 번째 인자 (pow, log2 등)
double? arg2 = null;
if (_i < _s.Length && _s[_i] == ',')
{
_i++;
arg2 = ParseExpr();
}
if (_i < _s.Length && _s[_i] == ')') _i++;
return name switch
{
"sqrt" => Math.Sqrt(arg),
"abs" => Math.Abs(arg),
"ceil" => Math.Ceiling(arg),
"floor" => Math.Floor(arg),
"round" => arg2.HasValue ? Math.Round(arg, (int)arg2.Value) : Math.Round(arg),
"sin" => Math.Sin(arg * Math.PI / 180), // 도 단위
"cos" => Math.Cos(arg * Math.PI / 180),
"tan" => Math.Tan(arg * Math.PI / 180),
"asin" => Math.Asin(arg) * 180 / Math.PI,
"acos" => Math.Acos(arg) * 180 / Math.PI,
"atan" => Math.Atan(arg) * 180 / Math.PI,
"log" => arg2.HasValue ? Math.Log(arg, arg2.Value) : Math.Log10(arg),
"log2" => Math.Log2(arg),
"ln" => Math.Log(arg),
"exp" => Math.Exp(arg),
"pow" => arg2.HasValue ? Math.Pow(arg, arg2.Value) : throw new InvalidOperationException("pow(x,y) 형식으로 사용하세요."),
"min" => arg2.HasValue ? Math.Min(arg, arg2.Value) : arg,
"max" => arg2.HasValue ? Math.Max(arg, arg2.Value) : arg,
_ => throw new InvalidOperationException($"알 수 없는 함수: {name}()")
};
}
throw new InvalidOperationException($"알 수 없는 식별자: {name}");
}
throw new InvalidOperationException($"예기치 않은 문자: '{_s[_i]}'");
}
}
}
// ─── 통화 변환 ──────────────────────────────────────────────────────────────────
/// <summary>
/// "100 USD to KRW", "50 EUR in JPY" 형식의 통화 변환.
/// open.er-api.com 무료 API 사용 (1시간 캐시).
/// </summary>
internal static class CurrencyConverter
{
// 패턴: <숫자> <통화코드 3자리> in|to <통화코드 3자리>
private static readonly Regex _pattern = new(
@"^(\d+(?:\.\d+)?)\s+([A-Za-z]{3})\s+(?:to|in)\s+([A-Za-z]{3})$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// 캐시: base currency → (fetched at, rates dict)
private static readonly Dictionary<string, (DateTime At, Dictionary<string, double> Rates)> _cache = new();
private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(5) };
private static readonly TimeSpan CacheTtl = TimeSpan.FromHours(1);
// 주요 통화 이름
private static readonly Dictionary<string, string> _names = new(StringComparer.OrdinalIgnoreCase)
{
["KRW"] = "원", ["USD"] = "달러", ["EUR"] = "유로",
["JPY"] = "엔", ["GBP"] = "파운드", ["CNY"] = "위안",
["HKD"] = "홍콩달러", ["SGD"] = "싱가포르달러", ["CAD"] = "캐나다달러",
["AUD"] = "호주달러", ["CHF"] = "스위스프랑", ["TWD"] = "대만달러",
["MXN"] = "멕시코페소", ["BRL"] = "브라질헤알", ["INR"] = "인도루피",
["RUB"] = "루블", ["THB"] = "바트", ["VND"] = "동",
["IDR"] = "루피아", ["MYR"] = "링깃", ["PHP"] = "페소",
["NZD"] = "뉴질랜드달러", ["SEK"] = "크로나", ["NOK"] = "크로나(노르)",
["DKK"] = "크로나(덴)", ["AED"] = "디르함", ["SAR"] = "리얄",
};
public static bool IsCurrencyQuery(string input)
=> _pattern.IsMatch(input.Trim());
public static async Task<IEnumerable<LauncherItem>> ConvertAsync(string input, CancellationToken ct)
{
var m = _pattern.Match(input.Trim());
if (!m.Success)
return [new LauncherItem("통화 형식 오류", "예: 100 USD to KRW", null, null, Symbol: Symbols.Error)];
if (!double.TryParse(m.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out double amount))
return [new LauncherItem("숫자 형식 오류", "", null, null, Symbol: Symbols.Error)];
var from = m.Groups[2].Value.ToUpperInvariant();
var to = m.Groups[3].Value.ToUpperInvariant();
try
{
var rates = await GetRatesAsync(from, ct);
if (rates == null)
return [new LauncherItem("환율 조회 실패", "네트워크 연결을 확인하세요", null, null, Symbol: Symbols.Warning)];
if (!rates.TryGetValue(to, out double rate))
return [new LauncherItem($"지원하지 않는 통화: {to}", "3자리 ISO 4217 코드를 입력하세요", null, null, Symbol: Symbols.Error)];
double result = amount * rate;
var fromName = _names.TryGetValue(from, out var fn) ? fn : from;
var toName = _names.TryGetValue(to, out var tn) ? tn : to;
// 금액 포맷
string resultStr = to == "KRW" || to == "JPY" || to == "IDR" || to == "VND"
? result.ToString("N0", System.Globalization.CultureInfo.CurrentCulture)
: result.ToString("N2", System.Globalization.CultureInfo.CurrentCulture);
string rateStr = rate < 0.01
? rate.ToString("G4", System.Globalization.CultureInfo.InvariantCulture)
: rate.ToString("N4", System.Globalization.CultureInfo.CurrentCulture);
var display = $"{resultStr} {to}";
return
[
new LauncherItem(
$"{amount:N2} {from} = {display}",
$"1 {from}({fromName}) = {rateStr} {to}({toName}) · Enter로 복사",
null, display, Symbol: Symbols.Calculator),
new LauncherItem(
$"숫자만: {resultStr}",
"Enter로 숫자만 복사",
null, resultStr, Symbol: Symbols.Calculator),
];
}
catch (OperationCanceledException)
{
return [];
}
catch (Exception ex)
{
LogService.Warn($"환율 조회 오류: {ex.Message}");
return [new LauncherItem("환율 조회 실패", ex.Message, null, null, Symbol: Symbols.Warning)];
}
}
private static async Task<Dictionary<string, double>?> GetRatesAsync(string baseCurrency, CancellationToken ct)
{
if (_cache.TryGetValue(baseCurrency, out var cached) &&
(DateTime.Now - cached.At) < CacheTtl)
return cached.Rates;
var url = $"https://open.er-api.com/v6/latest/{baseCurrency}";
var json = await _http.GetStringAsync(url, ct);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("result", out var resultEl) ||
resultEl.GetString() != "success")
return null;
if (!root.TryGetProperty("rates", out var ratesEl))
return null;
var rates = new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase);
foreach (var prop in ratesEl.EnumerateObject())
rates[prop.Name] = prop.Value.GetDouble();
_cache[baseCurrency] = (DateTime.Now, rates);
return rates;
}
}

View File

@@ -0,0 +1,170 @@
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
/// <summary>
/// "!" 프리픽스 핸들러. AX Agent (AI 어시스턴트) 기능.
/// ★ DEPLOY_STUB = true → 배포용 "개발 중" 표시
/// ★ DEPLOY_STUB = false → 실제 ChatWindow 동작
/// </summary>
public class ChatHandler : IActionHandler
{
private static App? CurrentApp => System.Windows.Application.Current as App;
// ┌──────────────────────────────────────────────────────────────┐
// │ 배포 시 true, 개발 활성화 시 false 로 전환 │
// └──────────────────────────────────────────────────────────────┘
private const bool DEPLOY_STUB = false;
private readonly SettingsService _settings;
private readonly object _windowLock = new();
private ChatWindow? _chatWindow;
public string? Prefix => "!";
public PluginMetadata Metadata => new("ax.agent", "AX Agent", "1.0", "AX Agent — AI 어시스턴트");
public ChatHandler(SettingsService settings)
{
_settings = settings;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
// ── 배포용 스텁 ─────────────────────────────────────────────
#pragma warning disable CS0162 // DEPLOY_STUB 플래그에 의한 의도된 비활성 코드
if (DEPLOY_STUB)
{
var stub = new List<LauncherItem>
{
new LauncherItem(
"AX Agent (개발 중)",
"이 기능은 다음 버전에서 제공될 예정입니다. 기대해 주세요!",
null, null, Symbol: "\uE8BD")
};
return Task.FromResult<IEnumerable<LauncherItem>>(stub);
}
#pragma warning restore CS0162
// ── AI 비활성화 체크 ─────────────────────────────────────────
var appSettings = CurrentApp?.SettingsService?.Settings;
if (appSettings?.AiEnabled == false)
return Task.FromResult<IEnumerable<LauncherItem>>(Array.Empty<LauncherItem>());
// ── 실제 구현 ───────────────────────────────────────────────
var items = new List<LauncherItem>();
var q = query.Trim();
if (string.IsNullOrEmpty(q))
{
items.Add(new LauncherItem(
"AX Agent 대화하기",
"AI 비서와 대화를 시작합니다",
null, "open_chat", Symbol: "\uE8BD"));
try
{
var storage = new ChatStorageService();
var metas = storage.LoadAllMeta();
foreach (var conv in metas.Take(5))
{
var ago = FormatTimeAgo(conv.UpdatedAt);
var symbol = ChatCategory.GetSymbol(conv.Category);
items.Add(new LauncherItem(
conv.Title,
$"{ago} · 메시지 {conv.Messages.Count}개",
null, $"resume:{conv.Id}", Symbol: symbol));
}
if (metas.Any())
items.Add(new LauncherItem(
"새 대화 시작",
"이전 대화와 별개의 새 대화를 시작합니다",
null, "new_chat", Symbol: "\uE710"));
}
catch (Exception) { }
}
else
{
items.Add(new LauncherItem(
$"AI에게 물어보기: {(q.Length > 40 ? q[..40] + "" : q)}",
"Enter를 누르면 AX Agent이 열리고 질문이 전송됩니다",
null, $"ask:{q}", Symbol: "\uE8BD"));
items.Add(new LauncherItem(
"AX Agent 대화하기",
"질문 없이 Agent 창만 엽니다",
null, "open_chat", Symbol: "\uE8BD"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
#pragma warning disable CS0162 // DEPLOY_STUB 플래그에 의한 의도된 비활성 코드
if (DEPLOY_STUB) return Task.CompletedTask;
#pragma warning restore CS0162
// AI 비활성화 시 실행 차단
var appSettings2 = CurrentApp?.SettingsService?.Settings;
if (appSettings2?.AiEnabled == false) return Task.CompletedTask;
var data = item.Data as string ?? "open_chat";
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
EnsureChatWindow();
if (data.StartsWith("ask:"))
{
var question = data[4..];
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.SendInitialMessage(question);
}
else if (data.StartsWith("resume:"))
{
var convId = data[7..];
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.ResumeConversation(convId);
}
else if (data == "new_chat")
{
_chatWindow!.Show();
_chatWindow.Activate();
_chatWindow.StartNewAndFocus();
}
else
{
_chatWindow!.Show();
_chatWindow.Activate();
}
});
return Task.CompletedTask;
}
private void EnsureChatWindow()
{
lock (_windowLock)
{
if (_chatWindow == null || !_chatWindow.IsLoaded)
{
_chatWindow = new ChatWindow(_settings);
_chatWindow.Closed += (_, _) => { lock (_windowLock) _chatWindow = null; };
}
}
}
private static string FormatTimeAgo(DateTime dt)
{
var diff = DateTime.Now - dt;
if (diff.TotalMinutes < 1) return "방금 전";
if (diff.TotalHours < 1) return $"{(int)diff.TotalMinutes}분 전";
if (diff.TotalDays < 1) return $"{(int)diff.TotalHours}시간 전";
if (diff.TotalDays < 7) return $"{(int)diff.TotalDays}일 전";
return dt.ToString("yyyy-MM-dd");
}
}

View File

@@ -0,0 +1,193 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.Models;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// $ prefix 핸들러: 클립보드 텍스트 변환
/// </summary>
public class ClipboardHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "$";
public PluginMetadata Metadata => new("clipboard", "클립보드 변환", "1.0", "AX");
public ClipboardHandler(SettingsService settings) => _settings = settings;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var items = new List<LauncherItem>();
// 빌트인 변환 목록
var builtins = GetBuiltinTransformers()
.Where(t => string.IsNullOrEmpty(query) ||
t.Key.Contains(query, StringComparison.OrdinalIgnoreCase));
foreach (var t in builtins)
items.Add(new LauncherItem(t.Key, t.Description ?? "", null, t, Symbol: Symbols.Clipboard));
// 사용자 정의 변환
var custom = _settings.Settings.ClipboardTransformers
.Where(t => string.IsNullOrEmpty(query) ||
t.Key.Contains(query, StringComparison.OrdinalIgnoreCase))
.Select(t => new LauncherItem(t.Key, t.Description ?? t.Type, null, t, Symbol: Symbols.Clipboard));
items.AddRange(custom);
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not ClipboardTransformer transformer) return;
// 클립보드에서 텍스트 읽기 (STA 스레드 필요)
string? input = null;
Application.Current.Dispatcher.Invoke(() =>
{
input = Clipboard.ContainsText() ? Clipboard.GetText() : null;
});
if (input == null) return;
string? result = await TransformAsync(transformer, input, ct);
if (result == null) return;
// 결과를 클립보드에 쓰고 이전 활성 창으로 포커스 복원 후 붙여넣기
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(result));
// 런처 호출 전 활성 창으로 포커스 복원
var prevHwnd = WindowTracker.PreviousWindow;
if (prevHwnd != IntPtr.Zero)
SetForegroundWindow(prevHwnd);
await Task.Delay(120, ct);
System.Windows.Forms.SendKeys.SendWait("^v");
LogService.Info($"클립보드 변환: '{transformer.Key}' 적용");
}
private static async Task<string?> TransformAsync(ClipboardTransformer t, string input, CancellationToken ct)
{
try
{
return t.Type switch
{
// ReDoS 방지: 사용자 정의 패턴에 타임아웃 적용
"regex" when t.Pattern != null && t.Replace != null =>
Regex.Replace(input, t.Pattern, t.Replace, RegexOptions.None,
TimeSpan.FromMilliseconds(t.Timeout > 0 ? t.Timeout : 5000)),
"script" when t.Command != null =>
await RunScriptAsync(t.Command, input, t.Timeout, ct),
_ => ExecuteBuiltin(t.Key, input)
};
}
catch (Exception ex)
{
LogService.Error($"변환 실패 ({t.Key}): {ex.Message}");
return null;
}
}
private static async Task<string> RunScriptAsync(string command, string input, int timeoutMs, CancellationToken ct)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(timeoutMs);
var parts = command.Split(' ', 2);
var psi = new ProcessStartInfo(parts[0])
{
Arguments = parts.Length > 1 ? parts[1] : "",
RedirectStandardInput = true,
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true,
StandardInputEncoding = Encoding.UTF8,
StandardOutputEncoding = Encoding.UTF8
};
using var proc = Process.Start(psi)!;
await proc.StandardInput.WriteAsync(input);
proc.StandardInput.Close();
return await proc.StandardOutput.ReadToEndAsync(cts.Token);
}
// ─── 빌트인 변환 ─────────────────────────────────────────────────────────
internal static string? ExecuteBuiltin(string key, string input)
{
return key switch
{
"$json" => FormatJson(input),
"$upper" => input.ToUpperInvariant(),
"$lower" => input.ToLowerInvariant(),
"$ts" => TryParseTimestamp(input),
"$epoch" => TryParseDate(input),
"$urle" => Uri.EscapeDataString(input),
"$urld" => Uri.UnescapeDataString(input),
"$b64e" => Convert.ToBase64String(Encoding.UTF8.GetBytes(input)),
"$b64d" => Encoding.UTF8.GetString(Convert.FromBase64String(input)),
"$md" => StripMarkdown(input),
"$trim" => input.Trim(),
"$lines" => string.Join(Environment.NewLine, input.Split('\n').Select(l => l.Trim()).Where(l => l.Length > 0)),
_ => null
};
}
private static string FormatJson(string input)
{
try
{
var doc = JsonDocument.Parse(input);
return JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true });
}
catch (Exception) { return input; }
}
private static string? TryParseTimestamp(string input)
{
if (long.TryParse(input.Trim(), out var ts))
{
var dt = DateTimeOffset.FromUnixTimeSeconds(ts).LocalDateTime;
return dt.ToString("yyyy-MM-dd HH:mm:ss");
}
return null;
}
private static string? TryParseDate(string input)
{
if (DateTime.TryParse(input.Trim(), out var dt))
return new DateTimeOffset(dt).ToUnixTimeSeconds().ToString();
return null;
}
private static string StripMarkdown(string input) =>
Regex.Replace(input, @"(\*\*|__)(.*?)\1|(\*|_)(.*?)\3|`(.+?)`|#{1,6}\s*", "$2$4$5");
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hWnd);
private static IEnumerable<ClipboardTransformer> GetBuiltinTransformers() =>
[
new() { Key = "$json", Type = "builtin", Description = "JSON 포맷팅 (들여쓰기 적용)" },
new() { Key = "$upper", Type = "builtin", Description = "대문자 변환" },
new() { Key = "$lower", Type = "builtin", Description = "소문자 변환" },
new() { Key = "$ts", Type = "builtin", Description = "유닉스 타임스탬프 → 날짜 문자열" },
new() { Key = "$epoch", Type = "builtin", Description = "날짜 문자열 → 유닉스 타임스탬프" },
new() { Key = "$urle", Type = "builtin", Description = "URL 인코딩" },
new() { Key = "$urld", Type = "builtin", Description = "URL 디코딩" },
new() { Key = "$b64e", Type = "builtin", Description = "Base64 인코딩" },
new() { Key = "$b64d", Type = "builtin", Description = "Base64 디코딩" },
new() { Key = "$md", Type = "builtin", Description = "마크다운 문법 제거" },
new() { Key = "$trim", Type = "builtin", Description = "앞뒤 공백 제거" },
new() { Key = "$lines", Type = "builtin", Description = "빈 줄 제거 및 각 줄 공백 정리" },
];
}

View File

@@ -0,0 +1,216 @@
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 클립보드 히스토리 핸들러. "#" 프리픽스로 사용합니다.
/// 예: # (빈 쿼리) → 최근 클립보드 목록
/// # hello → "hello"가 포함된 항목 필터
/// </summary>
public class ClipboardHistoryHandler : IActionHandler
{
private readonly ClipboardHistoryService _historyService;
public string? Prefix => "#";
public PluginMetadata Metadata => new(
"ClipboardHistory",
"클립보드 히스토리 — # 뒤에 검색어 (또는 빈 입력으로 전체 보기)",
"1.0",
"AX");
public ClipboardHistoryHandler(ClipboardHistoryService historyService)
{
_historyService = historyService;
}
// 카테고리 필터 프리픽스: #url, #코드, #경로
private static readonly Dictionary<string, string> CategoryFilters = new(StringComparer.OrdinalIgnoreCase)
{
{ "url", "URL" }, { "코드", "코드" }, { "code", "코드" },
{ "경로", "경로" }, { "path", "경로" }, { "핀", "핀" }, { "pin", "핀" },
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var history = _historyService.History;
if (history.Count == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"클립보드 히스토리가 없습니다",
"텍스트를 복사하면 이 곳에 기록됩니다",
null, null, Symbol: Symbols.History)
]);
}
var q = query.Trim().ToLowerInvariant();
// 카테고리 필터 감지 (예: #url, #핀)
string? catFilter = null;
foreach (var (prefix, cat) in CategoryFilters)
{
if (q == prefix || q.StartsWith(prefix + " "))
{
catFilter = cat;
q = q.Length > prefix.Length ? q[(prefix.Length + 1)..].Trim() : "";
break;
}
}
var filtered = history.AsEnumerable();
// 카테고리 필터 적용
if (catFilter == "핀")
filtered = filtered.Where(e => e.IsPinned);
else if (catFilter != null)
filtered = filtered.Where(e => e.Category == catFilter);
// 텍스트 검색
if (!string.IsNullOrEmpty(q))
filtered = filtered.Where(e => e.Preview.ToLowerInvariant().Contains(q));
// 핀 항목을 상단에 배치
var sorted = filtered
.OrderByDescending(e => e.IsPinned)
.ThenByDescending(e => e.CopiedAt);
var items = sorted
.Select(e =>
{
var pinMark = e.IsPinned ? "\uD83D\uDCCC " : "";
var catMark = e.Category != "일반" ? $"[{e.Category}] " : "";
return new LauncherItem(
$"{pinMark}{e.Preview}",
$"{catMark}{e.RelativeTime} · {e.CopiedAt:MM/dd HH:mm}",
null,
e,
Symbol: e.IsPinned ? Symbols.Favorite : (e.IsText ? Symbols.History : Symbols.Picture));
})
.ToList();
if (items.Count == 0)
{
items.Add(new LauncherItem(
$"'{query}'에 해당하는 항목 없음",
"#pin #url #코드 #경로 로 필터링 가능",
null, null, Symbol: Symbols.History));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not ClipboardEntry entry) return;
try
{
_historyService.SuppressNextCapture();
_historyService.PromoteEntry(entry); // 사용 시각 갱신 + 목록 맨 위로
if (!entry.IsText && entry.Image != null)
{
// 원본 이미지가 있으면 원본 해상도로 클립보드 복사
var originalImg = ClipboardHistoryService.LoadOriginalImage(entry.OriginalImagePath);
Clipboard.SetImage(originalImg ?? entry.Image);
return; // 이미지는 붙여넣기 시뮬레이션 없이 클립보드만 설정
}
if (string.IsNullOrEmpty(entry.Text)) return;
Clipboard.SetText(entry.Text);
var prevWindow = WindowTracker.PreviousWindow;
if (prevWindow == IntPtr.Zero) return;
// ── 이전 창 포커스 복원 후 Ctrl+V ────────────────────────────────
// 런처 창이 완전히 숨겨지고 이전 창이 포커스를 회복할 시간 확보
await Task.Delay(300, ct);
// AttachThreadInput으로 포그라운드 전환 권한 획득 후 SetForegroundWindow 호출
// (Alt 트릭 대비: Alt 키 주입 없이 안정적으로 전환, 대상 앱 메뉴 트리거 방지)
var targetThread = GetWindowThreadProcessId(prevWindow, out _);
var currentThread = GetCurrentThreadId();
AttachThreadInput(currentThread, targetThread, true);
SetForegroundWindow(prevWindow);
AttachThreadInput(currentThread, targetThread, false);
// 포커스 전환이 완전히 반영될 때까지 대기
await Task.Delay(100, ct);
// SendInput으로 Ctrl+V 주입
// Win32 INPUT 구조체: type(4) + 패딩(4) + union(32) = 40 bytes on x64
// KEYBDINPUT.dwExtraInfo = ULONG_PTR → union 8바이트 정렬 필요 → offset 8에서 시작
SendCtrlV();
}
catch (OperationCanceledException) { }
catch (Exception ex)
{
LogService.Warn($"클립보드 히스토리 붙여넣기 실패: {ex.Message}");
}
}
private static void SendCtrlV()
{
const uint INPUT_KEYBOARD = 1;
const uint KEYEVENTF_KEYUP = 0x0002;
const ushort VK_CONTROL = 0x11;
const ushort VK_V = 0x56;
var inputs = new INPUT[4];
// Ctrl 누름
inputs[0].Type = INPUT_KEYBOARD;
inputs[0].ki.wVk = VK_CONTROL;
// V 누름
inputs[1].Type = INPUT_KEYBOARD;
inputs[1].ki.wVk = VK_V;
// V 뗌
inputs[2].Type = INPUT_KEYBOARD;
inputs[2].ki.wVk = VK_V;
inputs[2].ki.dwFlags = KEYEVENTF_KEYUP;
// Ctrl 뗌
inputs[3].Type = INPUT_KEYBOARD;
inputs[3].ki.wVk = VK_CONTROL;
inputs[3].ki.dwFlags = KEYEVENTF_KEYUP;
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf<INPUT>());
}
// ─── P/Invoke ──────────────────────────────────────────────────────────
// Win32 INPUT 구조체 — x64에서 40바이트
// type(4) + 패딩(4) + union은 offset 8에서 시작 (MOUSEINPUT 32바이트가 최대)
[StructLayout(LayoutKind.Explicit, Size = 40)]
private struct INPUT
{
[FieldOffset(0)] public uint Type;
[FieldOffset(8)] public KEYBDINPUT ki;
}
// KEYBDINPUT: 2+2+4+4 = 12, dwExtraInfo(IntPtr)는 8바이트 정렬 → 패딩 포함 24바이트
[StructLayout(LayoutKind.Sequential)]
private struct KEYBDINPUT
{
public ushort wVk;
public ushort wScan;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, [MarshalAs(UnmanagedType.Bool)] bool fAttach);
[DllImport("kernel32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern uint SendInput(uint nInputs, [MarshalAs(UnmanagedType.LPArray)] INPUT[] pInputs, int cbSize);
}

View File

@@ -0,0 +1,151 @@
using System.Text;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 클립보드 파이프라인 핸들러. "pipe" 프리픽스로 사용합니다.
/// 여러 변환을 체이닝하여 클립보드 텍스트를 한 번에 처리합니다.
/// 예: pipe upper > trim > b64e → 대문자 → 공백제거 → Base64 인코딩
/// pipe sort > unique > number → 정렬 → 중복제거 → 줄번호
/// pipe → 사용 가능한 필터 목록
/// Enter → 변환 결과를 클립보드에 복사.
/// </summary>
public class ClipboardPipeHandler : IActionHandler
{
public string? Prefix => "pipe";
public PluginMetadata Metadata => new(
"ClipboardPipe",
"클립보드 파이프라인 — pipe",
"1.0",
"AX");
private static readonly Dictionary<string, (string Desc, Func<string, string> Fn)> Filters = new(StringComparer.OrdinalIgnoreCase)
{
["upper"] = ("대문자 변환", s => s.ToUpperInvariant()),
["lower"] = ("소문자 변환", s => s.ToLowerInvariant()),
["trim"] = ("앞뒤 공백 제거", s => s.Trim()),
["trimall"] = ("모든 공백 제거", s => Regex.Replace(s, @"\s+", "", RegexOptions.None, TimeSpan.FromSeconds(1))),
["sort"] = ("줄 정렬 (오름차순)", s => string.Join("\n", s.Split('\n').Order())),
["sortd"] = ("줄 정렬 (내림차순)", s => string.Join("\n", s.Split('\n').OrderDescending())),
["unique"] = ("중복 줄 제거", s => string.Join("\n", s.Split('\n').Distinct())),
["reverse"] = ("줄 순서 뒤집기", s => string.Join("\n", s.Split('\n').Reverse())),
["number"] = ("줄번호 추가", s => string.Join("\n", s.Split('\n').Select((l, i) => $"{i + 1}. {l}"))),
["quote"] = ("각 줄 따옴표 감싸기", s => string.Join("\n", s.Split('\n').Select(l => $"\"{l}\""))),
["b64e"] = ("Base64 인코딩", s => Convert.ToBase64String(Encoding.UTF8.GetBytes(s))),
["b64d"] = ("Base64 디코딩", s => Encoding.UTF8.GetString(Convert.FromBase64String(s.Trim()))),
["urle"] = ("URL 인코딩", s => Uri.EscapeDataString(s)),
["urld"] = ("URL 디코딩", s => Uri.UnescapeDataString(s)),
["md"] = ("마크다운 제거", s => Regex.Replace(s, @"[#*_`~\[\]()]", "", RegexOptions.None, TimeSpan.FromSeconds(1))),
["lines"] = ("빈 줄 제거", s => string.Join("\n", s.Split('\n').Where(l => !string.IsNullOrWhiteSpace(l)))),
["count"] = ("글자/단어/줄 수", s => $"글자: {s.Length} 단어: {s.Split((char[])null!, StringSplitOptions.RemoveEmptyEntries).Length} 줄: {s.Split('\n').Length}"),
["csv"] = ("CSV → 탭 변환", s => s.Replace(',', '\t')),
["tab"] = ("탭 → CSV 변환", s => s.Replace('\t', ',')),
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
var items = Filters.Select(kv => new LauncherItem(
kv.Key,
kv.Value.Desc,
null, null,
Symbol: Symbols.Clipboard)).ToList<LauncherItem>();
items.Insert(0, new LauncherItem(
"클립보드 파이프라인",
"필터를 > 로 연결: pipe upper > trim > b64e",
null, null, Symbol: Symbols.Info));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 파이프라인 파싱
var steps = q.Split('>')
.Select(s => s.Trim())
.Where(s => !string.IsNullOrEmpty(s))
.ToArray();
// 유효성 검사
var invalid = steps.Where(s => !Filters.ContainsKey(s)).ToArray();
if (invalid.Length > 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
$"알 수 없는 필터: {string.Join(", ", invalid)}",
$"사용 가능: {string.Join(", ", Filters.Keys.Take(10))} ...",
null, null, Symbol: Symbols.Warning)
}.AsEnumerable());
}
// 클립보드 텍스트 읽기
string? text = null;
try
{
if (Application.Current?.Dispatcher.Invoke(() => Clipboard.ContainsText()) == true)
text = Application.Current.Dispatcher.Invoke(() => Clipboard.GetText());
}
catch (Exception) { }
if (string.IsNullOrEmpty(text))
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem("클립보드에 텍스트가 없습니다", "텍스트를 복사한 후 시도하세요",
null, null, Symbol: Symbols.Warning)
}.AsEnumerable());
}
// 파이프라인 실행
var result = text;
var desc = new List<string>();
try
{
foreach (var step in steps)
{
result = Filters[step].Fn(result);
desc.Add(Filters[step].Desc);
}
}
catch (Exception ex)
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem($"파이프라인 실행 오류: {ex.Message}", "입력 데이터를 확인하세요",
null, null, Symbol: Symbols.Error)
}.AsEnumerable());
}
var preview = result.Length > 100 ? result[..97] + "…" : result;
preview = preview.Replace("\r\n", "↵ ").Replace("\n", "↵ ");
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
$"[{string.Join(" ", steps)}] 결과 적용",
$"{preview} · Enter로 클립보드 복사",
null, result,
Symbol: Symbols.Clipboard)
}.AsEnumerable());
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
NotificationService.Notify("파이프라인 완료", "변환 결과가 클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,289 @@
using AxCopilot.SDK;
using AxCopilot.Themes;
using System.Text.RegularExpressions;
using System.Windows;
namespace AxCopilot.Handlers;
/// <summary>
/// 색상 변환기 핸들러. "color" 프리픽스로 사용합니다.
/// 예: color #FF5500 → HEX / RGB / HSL 모두 표시
/// color 255,85,0 → HEX 변환
/// color red → 색상 이름 → HEX
/// color hsl(24,100%,50%) → HSL → HEX / RGB
/// </summary>
public class ColorHandler : IActionHandler
{
public string? Prefix => "color";
public PluginMetadata Metadata => new(
"Color",
"색상 변환기 — color #FF5500 · color 255,85,0 · color red",
"1.0",
"AX");
// ─── 색상 이름 사전 ─────────────────────────────────────────────────────
private static readonly Dictionary<string, string> _namedColors = new(StringComparer.OrdinalIgnoreCase)
{
["red"] = "#FF0000", ["빨강"] = "#FF0000", ["빨간색"] = "#FF0000",
["green"] = "#008000", ["초록"] = "#008000", ["초록색"] = "#008000",
["blue"] = "#0000FF", ["파랑"] = "#0000FF", ["파란색"] = "#0000FF",
["white"] = "#FFFFFF", ["흰색"] = "#FFFFFF", ["하양"] = "#FFFFFF",
["black"] = "#000000", ["검정"] = "#000000", ["검은색"] = "#000000",
["yellow"] = "#FFFF00", ["노랑"] = "#FFFF00", ["노란색"] = "#FFFF00",
["orange"] = "#FFA500", ["주황"] = "#FFA500", ["주황색"] = "#FFA500",
["purple"] = "#800080", ["보라"] = "#800080", ["보라색"] = "#800080",
["pink"] = "#FFC0CB", ["분홍"] = "#FFC0CB", ["분홍색"] = "#FFC0CB",
["hotpink"] = "#FF69B4", ["핫핑크"] = "#FF69B4",
["cyan"] = "#00FFFF", ["시안"] = "#00FFFF",
["magenta"] = "#FF00FF", ["마젠타"] = "#FF00FF",
["brown"] = "#A52A2A", ["갈색"] = "#A52A2A",
["gray"] = "#808080", ["grey"] = "#808080", ["회색"] = "#808080", ["회"] = "#808080",
["silver"] = "#C0C0C0", ["은색"] = "#C0C0C0",
["gold"] = "#FFD700", ["금색"] = "#FFD700",
["navy"] = "#000080", ["남색"] = "#000080",
["teal"] = "#008080", ["틸"] = "#008080",
["lime"] = "#00FF00", ["라임"] = "#00FF00",
["maroon"] = "#800000", ["밤색"] = "#800000",
["olive"] = "#808000", ["올리브"] = "#808000",
["coral"] = "#FF7F50", ["코랄"] = "#FF7F50",
["salmon"] = "#FA8072", ["연어색"] = "#FA8072",
["skyblue"] = "#87CEEB", ["하늘색"] = "#87CEEB",
["lightblue"] = "#ADD8E6", ["연파랑"] = "#ADD8E6",
["darkblue"] = "#00008B", ["진파랑"] = "#00008B",
["darkgreen"] = "#006400", ["진초록"] = "#006400",
["lightgreen"] = "#90EE90", ["연초록"] = "#90EE90",
["indigo"] = "#4B0082", ["인디고"] = "#4B0082",
["violet"] = "#EE82EE", ["바이올렛"] = "#EE82EE",
["beige"] = "#F5F5DC", ["베이지"] = "#F5F5DC",
["ivory"] = "#FFFFF0", ["아이보리"] = "#FFFFF0",
["khaki"] = "#F0E68C", ["카키"] = "#F0E68C",
["lavender"] = "#E6E6FA", ["라벤더"] = "#E6E6FA",
["turquoise"] = "#40E0D0", ["터키옥"] = "#40E0D0",
["chocolate"] = "#D2691E", ["초콜릿"] = "#D2691E",
["crimson"] = "#DC143C", ["크림슨"] = "#DC143C",
["transparent"] = "#00000000",
};
// ─── 정규식 ─────────────────────────────────────────────────────────────
private static readonly Regex _hexRe = new(@"^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$");
private static readonly Regex _rgbRe = new(@"^(?:rgb\s*\()?\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)?$", RegexOptions.IgnoreCase);
private static readonly Regex _rgbaRe = new(@"^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*([\d.]+)\s*\)$", RegexOptions.IgnoreCase);
private static readonly Regex _hslRe = new(@"^hsl\s*\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)$", RegexOptions.IgnoreCase);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"색상 코드를 입력하세요",
"예: #FF5500 · 255,85,0 · red · hsl(20,100%,50%)",
null, null, Symbol: Symbols.ColorPicker)
]);
}
var q = query.Trim();
var items = new List<LauncherItem>();
// HEX 형식
if (_hexRe.IsMatch(q))
{
var hex = q.TrimStart('#');
if (hex.Length == 3)
hex = $"{hex[0]}{hex[0]}{hex[1]}{hex[1]}{hex[2]}{hex[2]}";
int r, g, b;
byte a = 255;
if (hex.Length == 8)
{
a = Convert.ToByte(hex[..2], 16);
r = Convert.ToInt32(hex[2..4], 16);
g = Convert.ToInt32(hex[4..6], 16);
b = Convert.ToInt32(hex[6..8], 16);
}
else
{
r = Convert.ToInt32(hex[..2], 16);
g = Convert.ToInt32(hex[2..4], 16);
b = Convert.ToInt32(hex[4..6], 16);
}
AddColorItems(items, r, g, b, a, $"#{hex.ToUpperInvariant()}");
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// RGB 형식
var rgbM = _rgbRe.Match(q);
if (rgbM.Success)
{
int r = int.Parse(rgbM.Groups[1].Value);
int g = int.Parse(rgbM.Groups[2].Value);
int b = int.Parse(rgbM.Groups[3].Value);
if (r <= 255 && g <= 255 && b <= 255)
{
AddColorItems(items, r, g, b, 255, $"rgb({r},{g},{b})");
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
}
// RGBA 형식
var rgbaM = _rgbaRe.Match(q);
if (rgbaM.Success)
{
int r = int.Parse(rgbaM.Groups[1].Value);
int g = int.Parse(rgbaM.Groups[2].Value);
int b = int.Parse(rgbaM.Groups[3].Value);
double aD = double.Parse(rgbaM.Groups[4].Value, System.Globalization.CultureInfo.InvariantCulture);
byte a = (byte)Math.Clamp((int)(aD * 255), 0, 255);
if (r <= 255 && g <= 255 && b <= 255)
{
AddColorItems(items, r, g, b, a, $"rgba({r},{g},{b},{aD:G3})");
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
}
// HSL 형식
var hslM = _hslRe.Match(q);
if (hslM.Success)
{
double h = double.Parse(hslM.Groups[1].Value, System.Globalization.CultureInfo.InvariantCulture);
double s = double.Parse(hslM.Groups[2].Value, System.Globalization.CultureInfo.InvariantCulture) / 100;
double l = double.Parse(hslM.Groups[3].Value, System.Globalization.CultureInfo.InvariantCulture) / 100;
var (r, g, b) = HslToRgb(h, s, l);
AddColorItems(items, r, g, b, 255, $"hsl({h:G},{s*100:G}%,{l*100:G}%)");
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 색상 이름
if (_namedColors.TryGetValue(q, out var namedHex))
{
var hex = namedHex.TrimStart('#');
int r = Convert.ToInt32(hex[..2], 16);
int g = Convert.ToInt32(hex[2..4], 16);
int b = Convert.ToInt32(hex[4..6], 16);
AddColorItems(items, r, g, b, 255, namedHex.ToUpperInvariant());
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 검색 모드 (일부 색상 이름 검색)
var searchQ = q.ToLowerInvariant();
var matched = _namedColors
.Where(kv => kv.Key.Contains(searchQ, StringComparison.OrdinalIgnoreCase))
.Take(8);
foreach (var kv in matched)
{
items.Add(new LauncherItem(
$"{kv.Key} → {kv.Value.ToUpperInvariant()}",
"Enter로 HEX 복사",
null, kv.Value.ToUpperInvariant(), Symbol: Symbols.ColorPicker));
}
if (!items.Any())
{
items.Add(new LauncherItem(
"인식할 수 없는 색상",
"#RRGGBB · RGB(r,g,b) · hsl(h,s%,l%) · red 등 색상 이름",
null, null, Symbol: Symbols.Error));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string value)
try { Clipboard.SetText(value); } catch (Exception) { }
return Task.CompletedTask;
}
// ─── 색상 항목 생성 ──────────────────────────────────────────────────────
private static void AddColorItems(List<LauncherItem> items, int r, int g, int b, byte a, string source)
{
var hex = a == 255
? $"#{r:X2}{g:X2}{b:X2}"
: $"#{a:X2}{r:X2}{g:X2}{b:X2}";
var rgb = a == 255
? $"rgb({r}, {g}, {b})"
: $"rgba({r}, {g}, {b}, {a / 255.0:G3})";
var (h, s, l) = RgbToHsl(r, g, b);
var hsl = $"hsl({h:G3}, {s * 100:G3}%, {l * 100:G3}%)";
var (hh, sv, v) = RgbToHsv(r, g, b);
var hsv = $"hsv({hh:G3}, {sv * 100:G3}%, {v * 100:G3}%)";
items.Add(new LauncherItem($"HEX → {hex.ToUpperInvariant()}", $"{source} · Enter로 복사", null, hex.ToUpperInvariant(), Symbol: Symbols.ColorPicker));
items.Add(new LauncherItem($"RGB → {rgb}", $"{source} · Enter로 복사", null, rgb, Symbol: Symbols.ColorPicker));
items.Add(new LauncherItem($"HSL → {hsl}", $"{source} · Enter로 복사", null, hsl, Symbol: Symbols.ColorPicker));
items.Add(new LauncherItem($"HSV → {hsv}", $"{source} · Enter로 복사", null, hsv, Symbol: Symbols.ColorPicker));
}
// ─── 색상 공간 변환 ───────────────────────────────────────────────────────
private static (double h, double s, double l) RgbToHsl(int r, int g, int b)
{
double rf = r / 255.0, gf = g / 255.0, bf = b / 255.0;
double max = Math.Max(rf, Math.Max(gf, bf));
double min = Math.Min(rf, Math.Min(gf, bf));
double l = (max + min) / 2;
double h = 0, s = 0;
if (max != min)
{
double d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
h = max == rf ? (gf - bf) / d + (gf < bf ? 6 : 0)
: max == gf ? (bf - rf) / d + 2
: (rf - gf) / d + 4;
h /= 6;
}
return (Math.Round(h * 360, 1), Math.Round(s, 4), Math.Round(l, 4));
}
private static (double h, double s, double v) RgbToHsv(int r, int g, int b)
{
double rf = r / 255.0, gf = g / 255.0, bf = b / 255.0;
double max = Math.Max(rf, Math.Max(gf, bf));
double min = Math.Min(rf, Math.Min(gf, bf));
double v = max, s = 0, h = 0;
double d = max - min;
if (max != 0) s = d / max;
if (d != 0)
{
h = max == rf ? (gf - bf) / d + (gf < bf ? 6 : 0)
: max == gf ? (bf - rf) / d + 2
: (rf - gf) / d + 4;
h /= 6;
}
return (Math.Round(h * 360, 1), Math.Round(s, 4), Math.Round(v, 4));
}
private static (int r, int g, int b) HslToRgb(double h, double s, double l)
{
double r, g, b;
if (s == 0) { r = g = b = l; }
else
{
double q = l < 0.5 ? l * (1 + s) : l + s - l * s;
double p = 2 * l - q;
h /= 360;
r = Hue2Rgb(p, q, h + 1.0 / 3);
g = Hue2Rgb(p, q, h);
b = Hue2Rgb(p, q, h - 1.0 / 3);
}
return ((int)Math.Round(r * 255), (int)Math.Round(g * 255), (int)Math.Round(b * 255));
}
private static double Hue2Rgb(double p, double q, double t)
{
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1.0 / 6) return p + (q - p) * 6 * t;
if (t < 1.0 / 2) return q;
if (t < 2.0 / 3) return p + (q - p) * (2.0 / 3 - t) * 6;
return p;
}
}

View File

@@ -0,0 +1,59 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 스포이드 색상 추출 핸들러. "pick" 프리픽스로 사용합니다.
/// 실행하면 전체 화면 스포이드 모드에 진입하고,
/// 클릭한 지점의 색상을 HEX 코드로 클립보드에 복사합니다.
/// 반투명 결과 창이 5초간 표시됩니다.
/// 예: pick → 스포이드 모드 진입
/// </summary>
public class ColorPickHandler : IActionHandler
{
public string? Prefix => "pick";
public PluginMetadata Metadata => new(
"ColorPick",
"스포이드 색상 추출 — pick",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"스포이드로 화면 색상 추출",
"Enter → 스포이드 모드 진입 · 화면 아무 곳을 클릭하면 색상 코드 추출 · Esc 취소",
null, "__PICK__",
Symbol: Symbols.ColorPicker)
]);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not string s || s != "__PICK__") return;
// 런처가 닫히는 동안 대기
await Task.Delay(200, ct);
Application.Current?.Dispatcher.Invoke(() =>
{
var dropper = new Views.EyeDropperWindow();
dropper.ShowDialog();
if (dropper.PickedColor.HasValue)
{
var result = new Views.ColorPickResultWindow(
dropper.PickedColor.Value,
dropper.PickX,
dropper.PickY);
result.Show();
}
});
}
}

View File

@@ -0,0 +1,128 @@
using System.Globalization;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 날짜/시간 변환기 핸들러. "date" 프리픽스로 사용합니다.
/// 예: date → 현재 날짜/시간 + 유닉스 타임스탬프
/// date +30d → 오늘 + 30일
/// date -100d → 오늘 - 100일
/// date 2026-12-25 → 해당 날짜까지 D-day + 요일
/// date 1711584000 → 유닉스 타임스탬프 → 날짜
/// date to unix → 현재 시각의 유닉스 타임스탬프
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class DateCalcHandler : IActionHandler
{
public string? Prefix => "date";
public PluginMetadata Metadata => new(
"DateCalc",
"날짜 계산 · D-day · 타임스탬프 변환",
"1.0",
"AX");
private static readonly string[] DateFormats =
["yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd", "MM/dd/yyyy", "dd-MM-yyyy"];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var now = DateTime.Now;
var items = new List<LauncherItem>();
if (string.IsNullOrWhiteSpace(q))
{
// 현재 날짜/시간 정보
var dayName = now.ToString("dddd", new CultureInfo("ko-KR"));
items.Add(Item($"{now:yyyy-MM-dd} ({dayName})", $"{now:HH:mm:ss} · {now:yyyy-MM-dd}"));
items.Add(Item($"유닉스 타임스탬프: {new DateTimeOffset(now).ToUnixTimeSeconds()}", "현재 시각의 Unix epoch"));
items.Add(Item($"올해 {now.DayOfYear}일째 / 남은 일: {(new DateTime(now.Year, 12, 31) - now).Days}일",
$"ISO 주차: {ISOWeek.GetWeekOfYear(now)}주"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// +30d / -100d 패턴
var offsetMatch = Regex.Match(q, @"^([+-])(\d+)([dDwWmMyY])$");
if (offsetMatch.Success)
{
var sign = offsetMatch.Groups[1].Value == "+" ? 1 : -1;
var val = int.Parse(offsetMatch.Groups[2].Value) * sign;
var unit = offsetMatch.Groups[3].Value.ToLowerInvariant();
var target = unit switch
{
"d" => now.AddDays(val),
"w" => now.AddDays(val * 7),
"m" => now.AddMonths(val),
"y" => now.AddYears(val),
_ => now
};
var dayName = target.ToString("dddd", new CultureInfo("ko-KR"));
var diff = (target.Date - now.Date).Days;
var diffStr = diff >= 0 ? $"오늘로부터 {diff}일 후" : $"오늘로부터 {Math.Abs(diff)}일 전";
items.Add(Item($"{target:yyyy-MM-dd} ({dayName})", diffStr));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 유닉스 타임스탬프 (10자리 또는 13자리 숫자)
if (Regex.IsMatch(q, @"^\d{10,13}$") && long.TryParse(q, out long epoch))
{
var ts = epoch > 9_999_999_999 ? epoch / 1000 : epoch; // 밀리초→초
var dt = DateTimeOffset.FromUnixTimeSeconds(ts).LocalDateTime;
var dayName = dt.ToString("dddd", new CultureInfo("ko-KR"));
items.Add(Item($"{dt:yyyy-MM-dd HH:mm:ss} ({dayName})", $"Unix {epoch} → 로컬 시간"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "to unix" / "unix" 키워드
if (q.Equals("unix", StringComparison.OrdinalIgnoreCase) ||
q.Equals("to unix", StringComparison.OrdinalIgnoreCase))
{
var unix = new DateTimeOffset(now).ToUnixTimeSeconds();
items.Add(Item($"{unix}", $"현재 시각 ({now:yyyy-MM-dd HH:mm:ss}) → Unix 타임스탬프"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 날짜 파싱 → D-day 계산
if (DateTime.TryParseExact(q, DateFormats, CultureInfo.InvariantCulture,
DateTimeStyles.None, out var parsed))
{
var dayName = parsed.ToString("dddd", new CultureInfo("ko-KR"));
var diff = (parsed.Date - now.Date).Days;
var dday = diff switch
{
0 => "오늘",
> 0 => $"D-{diff} (앞으로 {diff}일)",
_ => $"D+{Math.Abs(diff)} ({Math.Abs(diff)}일 지남)"
};
items.Add(Item($"{parsed:yyyy-MM-dd} ({dayName})", dday));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem(
"날짜 형식을 인식할 수 없습니다",
"예: +30d, -100d, 2026-12-25, 1711584000, unix",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
private static LauncherItem Item(string title, string subtitle) =>
new(title, $"{subtitle} · Enter로 복사", null, title, Symbol: Symbols.Clock);
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,245 @@
using System.IO;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 텍스트/파일 비교 핸들러. "diff" 프리픽스로 사용합니다.
/// 클립보드 히스토리의 최근 2개 텍스트를 줄 단위로 비교하거나,
/// 파일 2개를 지정하여 비교합니다.
/// 예: diff → 클립보드 히스토리 최근 2개 비교
/// diff C:\a.txt C:\b.txt → 파일 비교
/// Enter → 비교 결과를 클립보드에 복사.
/// </summary>
public class DiffHandler : IActionHandler
{
private readonly ClipboardHistoryService? _clipHistory;
public DiffHandler(ClipboardHistoryService? clipHistory = null)
{
_clipHistory = clipHistory;
}
public string? Prefix => "diff";
public PluginMetadata Metadata => new(
"Diff",
"텍스트/파일 비교 — diff",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
// 파일 비교 모드
if (!string.IsNullOrWhiteSpace(q))
{
// 파일 2개 지정
var paths = q.Split(' ', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (paths.Length == 2 && File.Exists(paths[0]) && File.Exists(paths[1]))
{
try
{
var textA = File.ReadAllText(paths[0]);
var textB = File.ReadAllText(paths[1]);
var result = BuildDiff(textA, textB,
Path.GetFileName(paths[0]), Path.GetFileName(paths[1]));
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"파일 비교: {Path.GetFileName(paths[0])} {Path.GetFileName(paths[1])}",
$"{result.Added}줄 추가, {result.Removed}줄 삭제, {result.Same}줄 동일 · Enter로 결과 복사",
null, result.Text,
Symbol: Symbols.File)
]);
}
catch (Exception ex)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem($"파일 읽기 실패: {ex.Message}", "",
null, null, Symbol: Symbols.Error)
]);
}
}
// 파일 1개만 있으면 안내
if (paths.Length >= 1 && (File.Exists(paths[0]) || Directory.Exists(Path.GetDirectoryName(paths[0]) ?? "")))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"비교할 파일 2개의 경로를 입력하세요",
"예: diff C:\\a.txt C:\\b.txt",
null, null, Symbol: Symbols.Info)
]);
}
}
// 클립보드 히스토리 비교 모드
var history = _clipHistory?.History;
if (history == null || history.Count < 2)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"비교할 텍스트가 부족합니다",
"클립보드에 2개 이상의 텍스트를 복사하거나, diff [파일A] [B] ",
null, null, Symbol: Symbols.Info)
]);
}
var textEntries = history.Where(e => e.IsText).Take(2).ToList();
if (textEntries.Count < 2)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("텍스트 히스토리가 2개 미만입니다", "텍스트를 2번 이상 복사하세요",
null, null, Symbol: Symbols.Info)
]);
}
var older = textEntries[1]; // 이전
var newer = textEntries[0]; // 최근
var diff = BuildDiff(older.Text, newer.Text,
$"이전 ({older.RelativeTime})", $"최근 ({newer.RelativeTime})");
var items = new List<LauncherItem>
{
new(
$"클립보드 비교: +{diff.Added} -{diff.Removed} ={diff.Same}",
$"이전 복사 ↔ 최근 복사 · Enter로 결과 복사",
null, diff.Text,
Symbol: Symbols.History),
};
// 미리보기 (변경된 줄 최대 5개)
var changedLines = diff.Text.Split('\n')
.Where(l => l.StartsWith("+ ") || l.StartsWith("- "))
.Take(5);
foreach (var line in changedLines)
{
var symbol = line.StartsWith("+ ") ? "\uE710" : "\uE711"; // + or X
items.Add(new LauncherItem(
line,
"",
null, null,
Symbol: symbol));
}
// 파일 선택 비교 항목
items.Add(new LauncherItem(
"파일 선택하여 비교",
"파일 선택 대화 상자에서 2개의 파일을 골라 비교합니다",
null, "__FILE_DIALOG__",
Symbol: Symbols.File));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
// 파일 선택 다이얼로그
if (item.Data is string s && s == "__FILE_DIALOG__")
{
await Task.Delay(200, ct); // 런처 닫힘 대기
Application.Current?.Dispatcher.Invoke(() =>
{
var dlg = new Microsoft.Win32.OpenFileDialog
{
Title = "비교할 첫 번째 파일 선택",
Filter = "텍스트 파일|*.txt;*.cs;*.json;*.xml;*.md;*.csv;*.log|모든 파일|*.*"
};
if (dlg.ShowDialog() != true) return;
var fileA = dlg.FileName;
dlg.Title = "비교할 두 번째 파일 선택";
if (dlg.ShowDialog() != true) return;
var fileB = dlg.FileName;
try
{
var textA = File.ReadAllText(fileA);
var textB = File.ReadAllText(fileB);
var result = BuildDiff(textA, textB,
Path.GetFileName(fileA), Path.GetFileName(fileB));
Clipboard.SetText(result.Text);
NotificationService.Notify("파일 비교 완료",
$"+{result.Added} -{result.Removed} ={result.Same} · 결과 클립보드 복사됨");
}
catch (Exception ex)
{
NotificationService.Notify("비교 실패", ex.Message);
}
});
return;
}
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
NotificationService.Notify("비교 결과", "클립보드에 복사되었습니다");
}
}
// 간단한 줄 단위 diff
private static DiffResult BuildDiff(string textA, string textB, string labelA, string labelB)
{
var linesA = textA.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
var linesB = textB.Split('\n').Select(l => l.TrimEnd('\r')).ToArray();
var setA = new HashSet<string>(linesA);
var setB = new HashSet<string>(linesB);
var sb = new StringBuilder();
sb.AppendLine($"--- {labelA}");
sb.AppendLine($"+++ {labelB}");
sb.AppendLine();
int added = 0, removed = 0, same = 0;
int maxLen = Math.Max(linesA.Length, linesB.Length);
for (int i = 0; i < maxLen; i++)
{
var a = i < linesA.Length ? linesA[i] : null;
var b = i < linesB.Length ? linesB[i] : null;
if (a == b)
{
sb.AppendLine($" {a}");
same++;
}
else
{
if (a != null && !setB.Contains(a))
{
sb.AppendLine($"- {a}");
removed++;
}
if (b != null && !setA.Contains(b))
{
sb.AppendLine($"+ {b}");
added++;
}
if (a != null && setB.Contains(a) && a != b)
{
sb.AppendLine($" {a}");
same++;
}
}
}
return new DiffResult(sb.ToString(), added, removed, same);
}
private record DiffResult(string Text, int Added, int Removed, int Same);
}

View File

@@ -0,0 +1,553 @@
using AxCopilot.SDK;
using AxCopilot.Themes;
using System.Windows;
namespace AxCopilot.Handlers;
/// <summary>
/// 이모지 피커 핸들러. "emoji" 프리픽스로 사용합니다.
/// 예: emoji 하트 → ❤️ 검색
/// emoji wave → 👋 검색
/// emoji → 자주 쓰는 이모지 목록
/// </summary>
public class EmojiHandler : IActionHandler
{
public string? Prefix => "emoji";
public PluginMetadata Metadata => new(
"Emoji",
"이모지 피커 — emoji 뒤에 이름 입력",
"1.0",
"AX");
// ─── 이모지 데이터베이스 (이모지, 이름(한/영), 태그) ──────────────────────
private static readonly (string Emoji, string Name, string Tags)[] _emojis =
{
// 표정 / 감정
("😀", "크게 웃는 얼굴", "smile happy grin 웃음 행복"),
("😃", "웃는 얼굴", "smile happy joy 웃음"),
("😄", "눈 웃음", "smile laugh 웃음 기쁨"),
("😁", "히죽 웃음", "grin beam 씩 웃다"),
("😆", "크게 웃음", "laughing 폭소"),
("😅", "식은땀 웃음", "sweat smile 안도"),
("🤣", "바닥 구르며 웃음", "rofl lol 빵 웃음"),
("😂", "눈물 나게 웃음", "joy tears laugh 폭소"),
("🙂", "살짝 웃음", "slightly smiling 미소"),
("🙃", "거꾸로 웃음", "upside down 뒤집힌"),
("😉", "윙크", "wink 윙크"),
("😊", "볼 빨개진 웃음", "blush 부끄러움 미소"),
("😇", "천사", "angel halo 천사 선량"),
("🥰", "사랑스러운 얼굴", "love hearts 사랑 하트"),
("😍", "하트 눈", "heart eyes 사랑 반함"),
("🤩", "별 눈", "star struck 감동 황홀"),
("😘", "뽀뽀", "kiss blow 키스 뽀뽀"),
("😗", "오므린 입", "kiss whistle 키스"),
("😚", "눈 감고 뽀뽀", "kiss 키스"),
("😙", "볼 뽀뽀", "kiss 키스"),
("😋", "맛있다", "yum delicious 맛 음식"),
("😛", "혀 내밀기", "tongue out 혀 놀림"),
("😜", "윙크하며 혀", "wink tongue 장난"),
("🤪", "미친 표정", "zany crazy 정신없음"),
("😝", "눈 감고 혀", "tongue 혀"),
("🤑", "돈 눈", "money face 돈 부자"),
("🤗", "포옹", "hugging hug 안아줘 포옹"),
("🤭", "입 가리고", "hand over mouth 헉 깜짝"),
("🤫", "쉿", "shushing quiet 조용 쉿"),
("🤔", "생각 중", "thinking 고민 생각"),
("🤐", "입 막음", "zipper mouth 비밀"),
("🤨", "의심", "raised eyebrow 의심 의아"),
("😐", "무표정", "neutral 무감각 무표정"),
("😑", "표정 없음", "expressionless 냉담"),
("😶", "입 없는 얼굴", "no mouth 침묵"),
("😏", "비웃음", "smirk 비웃 냉소"),
("😒", "불만", "unamused 불만 짜증"),
("🙄", "눈 굴리기", "eye roll 어이없음"),
("😬", "이 드러냄", "grimace 으 민망"),
("🤥", "거짓말", "lying pinocchio 거짓말"),
("😌", "안도/평온", "relieved 안도 평온"),
("😔", "슬픔", "pensive sad 슬픔 우울"),
("😪", "졸림", "sleepy 졸음"),
("🤤", "침 흘림", "drooling 군침 식욕"),
("😴", "잠", "sleeping sleep 수면 잠"),
("😷", "마스크", "mask sick 마스크 아픔"),
("🤒", "열 나는", "sick fever 열 아픔"),
("🤕", "머리 붕대", "injured hurt 부상"),
("🤢", "구역질", "nauseated sick 구역 메스꺼움"),
("🤮", "토하는", "vomit 구토"),
("🤧", "재채기", "sneezing sick 재채기 감기"),
("🥵", "더운", "hot overheated 더움 열"),
("🥶", "추운", "cold freezing 추움 냉기"),
("🥴", "어지러운", "woozy 어지럼 취함"),
("😵", "어질어질", "dizzy 어지럼 충격"),
("🤯", "머리 폭발", "exploding head 충격 대박"),
("🤠", "카우보이", "cowboy hat 카우보이"),
("🥸", "변장", "disguise 변장 선글라스"),
("😎", "쿨한", "cool sunglasses 선글라스 쿨"),
("🤓", "공부벌레", "nerd glasses 공부 안경"),
("🧐", "모노클", "monocle curious 고상 탐정"),
("😕", "당황", "confused 당황 모호"),
("😟", "걱정", "worried concern 걱정"),
("🙁", "살짝 찡그림", "frown 슬픔"),
("☹️", "찡그린 얼굴", "frown sad 슬픔"),
("😮", "입 벌림", "open mouth surprised 놀람"),
("😯", "놀람", "hushed surprised 깜짝"),
("😲", "충격", "astonished 충격 놀람"),
("😳", "얼굴 빨개짐", "flushed embarrassed 부끄럼 당황"),
("🥺", "애원", "pleading eyes 부탁 눈빛"),
("😦", "찡그리며 벌린 입", "frowning 불안"),
("😧", "고통", "anguished 고통"),
("😨", "무서움", "fearful scared 무서움 공포"),
("😰", "식은땀", "anxious sweat 불안 걱정"),
("😥", "눈물 조금", "sad disappointed 실망 눈물"),
("😢", "울음", "cry sad 슬픔 눈물"),
("😭", "엉엉 울음", "loudly crying sob 통곡"),
("😱", "공포에 질림", "screaming fear 비명 공포"),
("😖", "혼란", "confounded 혼란"),
("😣", "힘듦", "persevering 고생"),
("😞", "실망", "disappointed 실망"),
("😓", "땀", "downcast sweat 땀 힘듦"),
("😩", "피곤", "weary tired 지침 피곤"),
("😫", "극도로 지침", "tired exhausted 탈진"),
("🥱", "하품", "yawning bored 하품 지루함"),
("😤", "콧김", "triumph snort 분노 콧김"),
("😡", "화남", "angry mad 화남 분노"),
("😠", "성남", "angry 화 성남"),
("🤬", "욕", "cursing swearing 욕 분노"),
("😈", "나쁜 미소", "smiling devil 악마 장난"),
("👿", "화난 악마", "angry devil 악마"),
("💀", "해골", "skull death 해골 죽음"),
("☠️", "해골 십자", "skull crossbones 독"),
("💩", "응가", "poop 똥 응가"),
("🤡", "피에로", "clown 광대"),
("👹", "도깨비", "ogre 도깨비 귀신"),
("👺", "텐구", "goblin 텐구"),
("👻", "유령", "ghost 유령 귀신"),
("👾", "우주인", "alien monster 외계인 게임"),
("🤖", "로봇", "robot 로봇"),
// 손 / 몸
("👋", "손 흔들기", "wave waving hi bye 안녕"),
("🤚", "손 뒤", "raised back hand 손"),
("🖐️", "손바닥", "hand palm 다섯 손가락"),
("✋", "손 들기", "raised hand 손 들기 멈춤"),
("🖖", "스팍 손인사", "vulcan salute 스타트렉"),
("👌", "오케이", "ok perfect 오케이 좋아"),
("🤌", "손가락 모아", "pinched fingers 이탈리아"),
("✌️", "브이", "victory peace v 브이 평화"),
("🤞", "행운 손가락", "crossed fingers lucky 행운 기도"),
("🤟", "아이 러브 유", "love you 사랑해"),
("🤘", "록 손", "rock on metal 록"),
("🤙", "전화해", "call me shaka 전화 샤카"),
("👈", "왼쪽 가리킴", "backhand left 왼쪽"),
("👉", "오른쪽 가리킴", "backhand right 오른쪽"),
("👆", "위 가리킴", "backhand up 위"),
("🖕", "욕", "middle finger 욕"),
("👇", "아래 가리킴", "backhand down 아래"),
("☝️", "검지 들기", "index pointing up 하나 포인트"),
("👍", "좋아요", "thumbs up like good 좋아 최고"),
("👎", "싫어요", "thumbs down dislike 싫어 별로"),
("✊", "주먹", "fist punch 주먹"),
("👊", "주먹 치기", "punch fist 주먹"),
("🤛", "왼 주먹", "left fist 주먹"),
("🤜", "오른 주먹", "right fist 주먹"),
("👏", "박수", "clapping applause 박수 응원"),
("🙌", "만세", "raising hands celebrate 만세"),
("👐", "양손 펼침", "open hands 환영"),
("🤲", "두 손 모음", "palms up together 기도 바람"),
("🙏", "두 손 합장", "pray please thanks 감사 부탁 기도"),
("✍️", "글쓰기", "writing pen 글쓰기"),
("💅", "네일", "nail polish manicure 네일 손톱"),
("🤳", "셀카", "selfie 셀카"),
("💪", "근육", "muscle strong 근육 힘"),
("🦾", "기계 팔", "mechanical arm 로봇 팔"),
("🦿", "기계 다리", "mechanical leg 로봇 다리"),
("🦵", "다리", "leg kick 다리"),
("🦶", "발", "foot kick 발"),
("👂", "귀", "ear hear 귀"),
("🦻", "보청기 귀", "ear hearing aid 보청기"),
("👃", "코", "nose smell 코"),
("🫀", "심장", "heart anatomical 심장"),
("🫁", "폐", "lungs 폐"),
("🧠", "뇌", "brain mind 뇌 지능"),
("🦷", "치아", "tooth dental 치아"),
("🦴", "뼈", "bone 뼈"),
("👀", "눈", "eyes look see 눈 보기"),
("👁️", "한쪽 눈", "eye 눈"),
("👅", "혀", "tongue 혀"),
("👄", "입술", "lips mouth 입술"),
("💋", "입맞춤", "kiss lips 키스 입술"),
("🩸", "피", "blood drop 피 혈액"),
// 하트 / 감정 기호
("❤️", "빨간 하트", "red heart love 사랑 빨강"),
("🧡", "주황 하트", "orange heart 사랑"),
("💛", "노란 하트", "yellow heart 사랑"),
("💚", "초록 하트", "green heart 사랑"),
("💙", "파란 하트", "blue heart 사랑"),
("💜", "보라 하트", "purple heart 사랑"),
("🖤", "검은 하트", "black heart 사랑 다크"),
("🤍", "흰 하트", "white heart 사랑"),
("🤎", "갈색 하트", "brown heart 사랑"),
("💔", "깨진 하트", "broken heart 이별 상처"),
("❣️", "느낌표 하트", "heart exclamation 사랑"),
("💕", "두 하트", "two hearts 사랑"),
("💞", "회전 하트", "revolving hearts 사랑"),
("💓", "뛰는 하트", "beating heart 설렘"),
("💗", "성장 하트", "growing heart 사랑"),
("💖", "반짝 하트", "sparkling heart 사랑"),
("💘", "화살 하트", "heart arrow 큐피드"),
("💝", "리본 하트", "heart ribbon 선물 사랑"),
("💟", "하트 장식", "heart decoration 사랑"),
("☮️", "평화", "peace 평화"),
("✝️", "십자가", "cross 기독교"),
("☯️", "음양", "yin yang 음양 균형"),
("🔮", "수정구", "crystal ball magic 마법 점"),
("✨", "반짝임", "sparkles glitter 빛 반짝"),
("⭐", "별", "star 별"),
("🌟", "빛나는 별", "glowing star 별빛"),
("💫", "현기증", "dizzy star 빙글"),
("⚡", "번개", "lightning bolt 번개 전기"),
("🔥", "불", "fire hot 불 열정"),
("💥", "폭발", "explosion boom 폭발"),
("❄️", "눈송이", "snowflake cold 눈 추위"),
("🌈", "무지개", "rainbow 무지개"),
("☀️", "태양", "sun sunny 태양 맑음"),
("🌙", "달", "moon crescent 달"),
("🌊", "파도", "wave ocean 파도 바다"),
("💨", "바람", "wind dash 바람"),
("💦", "물방울", "sweat droplets water 물"),
("🌸", "벚꽃", "cherry blossom 벚꽃 봄"),
("🌹", "장미", "rose 장미 꽃"),
("🌺", "히비스커스", "hibiscus 꽃"),
("🌻", "해바라기", "sunflower 해바라기"),
("🌼", "꽃", "blossom flower 꽃"),
("🌷", "튤립", "tulip 튤립"),
("💐", "꽃다발", "bouquet flowers 꽃다발"),
("🍀", "네잎클로버", "four leaf clover lucky 행운"),
("🌿", "허브", "herb green 풀 허브"),
("🍃", "잎사귀", "leaf 잎"),
// 음식
("🍕", "피자", "pizza 피자"),
("🍔", "햄버거", "hamburger burger 버거"),
("🌮", "타코", "taco 타코"),
("🍜", "라면", "ramen noodles 라면 국수"),
("🍱", "도시락", "bento box 도시락"),
("🍣", "초밥", "sushi 초밥"),
("🍚", "밥", "rice 밥"),
("🍛", "카레", "curry rice 카레"),
("🍝", "파스타", "pasta spaghetti 파스타"),
("🍦", "소프트 아이스크림", "ice cream soft serve 아이스크림"),
("🎂", "생일 케이크", "cake birthday 생일 케이크"),
("🍰", "케이크 조각", "cake slice 케이크"),
("🧁", "컵케이크", "cupcake 컵케이크"),
("🍩", "도넛", "donut 도넛"),
("🍪", "쿠키", "cookie 쿠키"),
("🍫", "초콜릿", "chocolate bar 초콜릿"),
("🍬", "사탕", "candy 사탕"),
("🍭", "막대 사탕", "lollipop 막대사탕"),
("🍺", "맥주", "beer mug 맥주"),
("🍻", "건배", "clinking beer 건배"),
("🥂", "샴페인 건배", "champagne 샴페인 건배"),
("🍷", "와인", "wine 와인"),
("☕", "커피", "coffee hot 커피"),
("🧃", "주스", "juice 주스"),
("🥤", "음료", "drink cup 음료 컵"),
("🧋", "버블티", "bubble tea boba 버블티"),
("🍵", "녹차", "tea matcha 차 녹차"),
// 동물
("🐶", "강아지", "dog puppy 강아지 개"),
("🐱", "고양이", "cat kitten 고양이"),
("🐭", "쥐", "mouse 쥐"),
("🐹", "햄스터", "hamster 햄스터"),
("🐰", "토끼", "rabbit bunny 토끼"),
("🦊", "여우", "fox 여우"),
("🐻", "곰", "bear 곰"),
("🐼", "판다", "panda 판다"),
("🐨", "코알라", "koala 코알라"),
("🐯", "호랑이", "tiger 호랑이"),
("🦁", "사자", "lion 사자"),
("🐮", "소", "cow 소"),
("🐷", "돼지", "pig 돼지"),
("🐸", "개구리", "frog 개구리"),
("🐵", "원숭이", "monkey 원숭이"),
("🙈", "눈 가린 원숭이", "see no evil monkey 안 봐"),
("🙉", "귀 가린 원숭이", "hear no evil monkey 안 들어"),
("🙊", "입 가린 원숭이", "speak no evil monkey 안 말해"),
("🐔", "닭", "chicken 닭"),
("🐧", "펭귄", "penguin 펭귄"),
("🐦", "새", "bird 새"),
("🦆", "오리", "duck 오리"),
("🦅", "독수리", "eagle 독수리"),
("🦉", "부엉이", "owl 부엉이"),
("🐍", "뱀", "snake 뱀"),
("🐢", "거북이", "turtle 거북이"),
("🦋", "나비", "butterfly 나비"),
("🐌", "달팽이", "snail 달팽이"),
("🐛", "애벌레", "bug caterpillar 애벌레"),
("🐝", "꿀벌", "bee honeybee 벌"),
("🦑", "오징어", "squid 오징어"),
("🐙", "문어", "octopus 문어"),
("🐠", "열대어", "tropical fish 열대어"),
("🐡", "복어", "blowfish puffer 복어"),
("🦈", "상어", "shark 상어"),
("🐬", "돌고래", "dolphin 돌고래"),
("🐳", "고래", "whale 고래"),
("🐲", "용", "dragon 용"),
("🦄", "유니콘", "unicorn 유니콘"),
// 물건 / 도구
("📱", "스마트폰", "phone mobile smartphone 폰"),
("💻", "노트북", "laptop computer 노트북"),
("🖥️", "데스크톱", "desktop computer 컴퓨터"),
("⌨️", "키보드", "keyboard 키보드"),
("🖱️", "마우스", "mouse 마우스"),
("🖨️", "프린터", "printer 프린터"),
("📷", "카메라", "camera 카메라"),
("📸", "플래시 카메라", "camera flash 사진"),
("📹", "비디오 카메라", "video camera 동영상"),
("🎥", "영화 카메라", "movie camera film 영화"),
("📺", "TV", "television tv 텔레비전"),
("📻", "라디오", "radio 라디오"),
("🎙️", "마이크", "microphone studio 마이크"),
("🎤", "마이크 핸드헬드", "microphone karaoke 마이크"),
("🎧", "헤드폰", "headphones 헤드폰"),
("📡", "안테나", "satellite antenna 안테나"),
("🔋", "배터리", "battery 배터리"),
("🔌", "전원 플러그", "plug electric 플러그"),
("💡", "전구", "bulb idea light 전구 아이디어"),
("🔦", "손전등", "flashlight torch 손전등"),
("🕯️", "양초", "candle 양초"),
("📚", "책", "books stack 책"),
("📖", "열린 책", "open book read 독서"),
("📝", "메모", "memo note pencil 메모 노트"),
("✏️", "연필", "pencil 연필"),
("🖊️", "펜", "pen 펜"),
("📌", "압정", "pushpin pin 압정"),
("📎", "클립", "paperclip 클립"),
("✂️", "가위", "scissors cut 가위"),
("🗂️", "파일 폴더", "card index dividers folder 파일"),
("📁", "폴더", "folder 폴더"),
("📂", "열린 폴더", "open folder 폴더"),
("🗃️", "파일 박스", "card file box 서류함"),
("🗑️", "휴지통", "wastebasket trash 휴지통"),
("🔒", "잠금", "locked lock 잠금"),
("🔓", "열림", "unlocked 열림"),
("🔑", "열쇠", "key 열쇠"),
("🗝️", "구식 열쇠", "old key 열쇠"),
("🔨", "망치", "hammer 망치"),
("🔧", "렌치", "wrench tool 렌치"),
("🔩", "나사", "nut bolt 나사"),
("⚙️", "톱니바퀴", "gear settings 설정 톱니"),
("🛠️", "도구", "tools hammer wrench 도구"),
("💊", "알약", "pill medicine 약 알약"),
("💉", "주사기", "syringe injection 주사"),
("🩺", "청진기", "stethoscope doctor 청진기"),
("🏆", "트로피", "trophy award 트로피 우승"),
("🥇", "금메달", "first gold medal 금메달"),
("🥈", "은메달", "second silver 은메달"),
("🥉", "동메달", "third bronze 동메달"),
("🎖️", "훈장", "medal military 훈장"),
("🎗️", "리본", "ribbon awareness 리본"),
("🎫", "티켓", "ticket admission 티켓"),
("🎟️", "입장권", "admission tickets 티켓"),
("🎪", "서커스", "circus tent 서커스"),
("🎨", "팔레트", "art palette paint 그림 예술"),
("🎭", "연극", "performing arts theater 연극"),
("🎬", "클래퍼보드", "clapper film 영화 촬영"),
("🎮", "게임 컨트롤러", "video game controller 게임"),
("🎲", "주사위", "dice game 주사위"),
("🎯", "다트", "bullseye target dart 다트 목표"),
("🎳", "볼링", "bowling 볼링"),
("⚽", "축구", "soccer football 축구"),
("🏀", "농구", "basketball 농구"),
("🏈", "미식축구", "american football 미식축구"),
("⚾", "야구", "baseball 야구"),
("🎾", "테니스", "tennis 테니스"),
("🏐", "배구", "volleyball 배구"),
("🏉", "럭비", "rugby 럭비"),
("🎱", "당구", "billiards pool 당구"),
("🏓", "탁구", "ping pong table tennis 탁구"),
("🏸", "배드민턴", "badminton 배드민턴"),
("🥊", "권투 장갑", "boxing glove 권투"),
("🎣", "낚시", "fishing 낚시"),
("🏋️", "역도", "weightlifting gym 헬스 역도"),
("🧘", "명상", "yoga meditation 명상 요가"),
// 이동수단
("🚗", "자동차", "car automobile 자동차"),
("🚕", "택시", "taxi cab 택시"),
("🚙", "SUV", "suv car 차"),
("🚌", "버스", "bus 버스"),
("🚎", "무궤도 전차", "trolleybus 버스"),
("🏎️", "레이싱카", "racing car 레이싱"),
("🚓", "경찰차", "police car 경찰"),
("🚑", "구급차", "ambulance 구급차"),
("🚒", "소방차", "fire truck 소방차"),
("🚐", "미니밴", "minibus van 밴"),
("🚚", "트럭", "truck delivery 트럭"),
("✈️", "비행기", "airplane flight plane 비행기"),
("🚀", "로켓", "rocket space launch 로켓"),
("🛸", "UFO", "flying saucer ufo 유에프오"),
("🚁", "헬리콥터", "helicopter 헬리콥터"),
("🚂", "기차", "train locomotive 기차"),
("🚆", "고속열차", "train 기차"),
("🚇", "지하철", "metro subway 지하철"),
("⛵", "돛단배", "sailboat 요트"),
("🚢", "배", "ship cruise 배"),
("🚲", "자전거", "bicycle bike 자전거"),
("🛵", "스쿠터", "scooter moped 스쿠터"),
("🏍️", "오토바이", "motorcycle 오토바이"),
// 장소
("🏠", "집", "house home 집"),
("🏡", "마당 있는 집", "house garden 집"),
("🏢", "빌딩", "office building 빌딩"),
("🏣", "우체국", "post office 우체국"),
("🏥", "병원", "hospital 병원"),
("🏦", "은행", "bank 은행"),
("🏨", "호텔", "hotel 호텔"),
("🏫", "학교", "school 학교"),
("🏪", "편의점", "convenience store shop 편의점"),
("🏬", "백화점", "department store 백화점"),
("🏰", "성", "castle 성"),
("⛪", "교회", "church 교회"),
("🕌", "모스크", "mosque 모스크"),
("🗼", "에펠탑", "eiffel tower paris 파리"),
("🗽", "자유의 여신상", "statue of liberty new york 뉴욕"),
("🏔️", "산", "mountain snow 산"),
("🌋", "화산", "volcano 화산"),
("🗻", "후지산", "mount fuji japan 후지산"),
("🏕️", "캠핑", "camping tent 캠핑"),
("🏖️", "해변", "beach summer 해변 해수욕"),
("🌏", "지구", "earth globe asia 지구"),
// 기호 / 숫자
("💯", "100점", "hundred percent perfect 완벽 100"),
("🔢", "숫자", "numbers 숫자"),
("🆗", "OK", "ok button 오케이"),
("🆙", "업", "up button 업"),
("🆒", "쿨", "cool button 쿨"),
("🆕", "새것", "new button 새"),
("🆓", "무료", "free button 무료"),
("🆘", "SOS", "sos emergency 긴급 구조"),
("⚠️", "경고", "warning caution 경고 주의"),
("🚫", "금지", "prohibited no 금지"),
("✅", "체크", "check mark done 완료 확인"),
("❌", "엑스", "x cross error 실패 오류"),
("❓", "물음표", "question mark 물음표"),
("❗", "느낌표", "exclamation mark 느낌표"),
("", "더하기", "plus add 더하기"),
("", "빼기", "minus subtract 빼기"),
("➗", "나누기", "divide 나누기"),
("✖️", "곱하기", "multiply times 곱하기"),
("♾️", "무한대", "infinity 무한"),
("🔁", "반복", "repeat loop 반복"),
("🔀", "셔플", "shuffle random 랜덤"),
("▶️", "재생", "play 재생"),
("⏸️", "일시정지", "pause 일시정지"),
("⏹️", "정지", "stop 정지"),
("⏩", "빨리 감기", "fast forward 빨리감기"),
("⏪", "되감기", "rewind 되감기"),
("🔔", "알림", "bell notification 알림 벨"),
("🔕", "알림 끔", "bell off 알림끔"),
("🔊", "볼륨 크게", "loud speaker volume up 볼륨"),
("🔇", "음소거", "muted speaker 음소거"),
("📣", "메가폰", "megaphone loud 확성기"),
("📢", "스피커", "loudspeaker 스피커"),
("💬", "말풍선", "speech bubble chat 대화"),
("💭", "생각 말풍선", "thought bubble thinking 생각"),
("📧", "이메일", "email mail 이메일 메일"),
("📨", "수신 봉투", "incoming envelope 수신"),
("📩", "발신 봉투", "envelope outbox 발신"),
("📬", "우편함", "mailbox 우편함"),
("📦", "택배 박스", "package box parcel 택배 상자"),
("🎁", "선물", "gift present 선물"),
("🎀", "리본 묶음", "ribbon bow 리본"),
("🎊", "색종이", "confetti 파티 축하"),
("🎉", "파티 폭죽", "party popper celebrate 파티 축하"),
("🎈", "풍선", "balloon party 풍선"),
("🕐", "1시", "one o'clock 1시 시간"),
("🕒", "3시", "three o'clock 3시 시간"),
("🕔", "4시", "four o'clock 4시 시간"),
("⏰", "알람 시계", "alarm clock 알람 시계"),
("⏱️", "스톱워치", "stopwatch timer 스톱워치 타이머"),
("📅", "달력", "calendar date 달력 날짜"),
("📆", "찢는 달력", "tear-off calendar 달력"),
("💰", "돈 가방", "money bag 돈 부자"),
("💳", "신용카드", "credit card payment 카드 결제"),
("💵", "달러", "dollar banknote 달러"),
("💴", "엔화", "yen banknote 엔"),
("💶", "유로", "euro banknote 유로"),
("💷", "파운드", "pound banknote 파운드"),
("📊", "막대 그래프", "bar chart graph 그래프"),
("📈", "상승 그래프", "chart increasing trend 상승 트렌드"),
("📉", "하락 그래프", "chart decreasing trend 하락"),
("🔍", "돋보기", "magnifying glass search 검색 돋보기"),
("🔎", "오른쪽 돋보기", "magnifying glass right search 검색"),
("🏳️", "흰 깃발", "white flag 항복"),
("🏴", "검은 깃발", "black flag 해적"),
("🚩", "빨간 삼각기", "triangular flag 경고 깃발"),
("🏁", "체크무늬 깃발", "chequered flag finish race 결승"),
("🌐", "지구본", "globe internet web 인터넷 웹"),
("⚓", "닻", "anchor 닻"),
("🎵", "음표", "music note 음악 음표"),
("🎶", "음표들", "musical notes 음악"),
("🎼", "악보", "musical score 악보"),
("🎹", "피아노", "piano keyboard 피아노"),
("🎸", "기타", "guitar 기타"),
("🥁", "드럼", "drum 드럼"),
("🪗", "아코디언", "accordion 아코디언"),
("🎷", "색소폰", "saxophone 색소폰"),
("🎺", "트럼펫", "trumpet 트럼펫"),
("🎻", "바이올린", "violin 바이올린"),
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
IEnumerable<(string Emoji, string Name, string Tags)> matches;
if (string.IsNullOrWhiteSpace(query))
{
// 기본 화면: 자주 쓰는 이모지 30개
matches = _emojis.Take(30);
}
else
{
var q = query.Trim().ToLowerInvariant();
matches = _emojis
.Where(e => e.Name.Contains(q, StringComparison.OrdinalIgnoreCase)
|| e.Tags.Contains(q, StringComparison.OrdinalIgnoreCase)
|| e.Emoji.Contains(q))
.Take(20);
}
var items = matches.Select(e => new LauncherItem(
$"{e.Emoji} {e.Name}",
"Enter로 클립보드에 복사",
null,
e.Emoji,
Symbol: Symbols.Emoji)).ToList();
if (!items.Any() && !string.IsNullOrWhiteSpace(query))
{
items.Add(new LauncherItem(
"검색 결과 없음",
$"'{query}'에 해당하는 이모지가 없습니다",
null, null, Symbol: Symbols.Info));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string emoji)
{
try { Clipboard.SetText(emoji); }
catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,208 @@
using System.Security.Cryptography;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 인코딩/해싱 변환 핸들러. "encode " 프리픽스로 사용합니다.
/// 예: encode base64 hello → aGVsbG8=
/// encode decode base64 aGVsbG8= → hello
/// encode url https://example.com → URL 인코딩
/// encode hex hello → 68656c6c6f
/// encode md5 hello → 5d41402abc4b2a76b9719d911017c592
/// encode sha256 hello → 2cf24dba...
/// encode sha1 hello → aaf4c61d...
/// encode (빈 쿼리) → 지원 목록
/// </summary>
public class EncodeHandler : IActionHandler
{
public string? Prefix => "encode "; // 뒤에 공백 포함
public PluginMetadata Metadata => new(
"Encoder",
"인코딩/해싱 — encode base64/url/hex/md5/sha256 텍스트",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
return Task.FromResult(HelpItems());
// "decode <타입> <값>" 파턴
if (q.StartsWith("decode ", StringComparison.OrdinalIgnoreCase))
{
var rest = q[7..].Trim();
return Task.FromResult(HandleDecode(rest));
}
// "<타입> <값>" 패턴
var spaceIdx = q.IndexOf(' ');
if (spaceIdx < 0)
return Task.FromResult(HelpItems());
var type = q[..spaceIdx].Trim().ToLowerInvariant();
var input = q[(spaceIdx + 1)..];
return Task.FromResult(HandleEncode(type, input));
}
private static IEnumerable<LauncherItem> HandleEncode(string type, string input)
{
try
{
return type switch
{
"base64" or "b64" => SingleResult("Base64 인코딩",
Convert.ToBase64String(Encoding.UTF8.GetBytes(input))),
"url" => SingleResult("URL 인코딩",
Uri.EscapeDataString(input)),
"hex" => SingleResult("HEX 인코딩",
Convert.ToHexString(Encoding.UTF8.GetBytes(input)).ToLowerInvariant()),
"md5" => SingleResult("MD5 해시",
MD5Hash(input)),
"sha1" => SingleResult("SHA-1 해시",
SHA1Hash(input)),
"sha256" => SingleResult("SHA-256 해시",
SHA256Hash(input)),
"sha512" => SingleResult("SHA-512 해시",
SHA512Hash(input)),
"html" => SingleResult("HTML 엔티티 인코딩",
System.Net.WebUtility.HtmlEncode(input)),
_ =>
[
new LauncherItem(
$"알 수 없는 타입: {type}",
"base64, url, hex, md5, sha1, sha256, sha512, html 중 선택",
null, null, Symbol: Symbols.Warning)
]
};
}
catch (Exception ex)
{
return
[
new LauncherItem("변환 오류", ex.Message, null, null, Symbol: Symbols.Error)
];
}
}
private static IEnumerable<LauncherItem> HandleDecode(string rest)
{
var spaceIdx = rest.IndexOf(' ');
if (spaceIdx < 0)
return [new LauncherItem("사용법: encode decode <타입> <값>", "타입: base64, url, hex, html", null, null, Symbol: Symbols.Info)];
var type = rest[..spaceIdx].Trim().ToLowerInvariant();
var input = rest[(spaceIdx + 1)..];
try
{
return type switch
{
"base64" or "b64" => SingleResult("Base64 디코딩",
Encoding.UTF8.GetString(Convert.FromBase64String(input))),
"url" => SingleResult("URL 디코딩",
Uri.UnescapeDataString(input)),
"hex" => SingleResult("HEX 디코딩",
Encoding.UTF8.GetString(Convert.FromHexString(input))),
"html" => SingleResult("HTML 엔티티 디코딩",
System.Net.WebUtility.HtmlDecode(input)),
_ =>
[
new LauncherItem(
$"디코딩 불가 타입: {type}",
"디코딩 지원: base64, url, hex, html",
null, null, Symbol: Symbols.Warning)
]
};
}
catch (Exception ex)
{
return
[
new LauncherItem("디코딩 오류", ex.Message, null, null, Symbol: Symbols.Error)
];
}
}
private static IEnumerable<LauncherItem> SingleResult(string title, string result)
{
var preview = result.Length > 80 ? result[..77] + "…" : result;
return
[
new LauncherItem(
title,
$"{preview} · Enter로 복사",
null,
result,
Symbol: Symbols.EncodeIcon)
];
}
private static IEnumerable<LauncherItem> HelpItems()
{
return
[
new LauncherItem("encode base64 <텍스트>", "Base64 인코딩/디코딩", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode url <텍스트>", "URL 퍼센트 인코딩/디코딩", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode hex <텍스트>", "16진수 인코딩/디코딩", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode md5 <텍스트>", "MD5 해시 (단방향)", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode sha256 <텍스트>", "SHA-256 해시 (단방향)", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode html <텍스트>", "HTML 엔티티 인코딩/디코딩", null, null, Symbol: Symbols.EncodeIcon),
new LauncherItem("encode decode base64 <값>", "Base64 디코딩 예시", null, null, Symbol: Symbols.EncodeIcon),
];
}
// ─── 해시 헬퍼 ─────────────────────────────────────────────────────────────
private static string MD5Hash(string input)
{
var bytes = MD5.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string SHA1Hash(string input)
{
var bytes = SHA1.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string SHA256Hash(string input)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
private static string SHA512Hash(string input)
{
var bytes = SHA512.HashData(Encoding.UTF8.GetBytes(input));
return Convert.ToHexString(bytes).ToLowerInvariant();
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { Clipboard.SetText(text); } catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,100 @@
using AxCopilot.SDK;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 환경변수 조회 핸들러. "env" 프리픽스로 사용합니다.
/// 예: env → 자주 쓰는 환경변수 목록
/// env PATH → PATH 값 표시 (Enter로 복사)
/// env java → 이름에 "java" 포함된 환경변수 검색
/// </summary>
public class EnvHandler : IActionHandler
{
public string? Prefix => "env";
public PluginMetadata Metadata => new(
"EnvVars",
"환경변수 조회 — env 뒤에 변수명 입력",
"1.0",
"AX");
// 자주 쓰는 환경변수 우선 표시 순서
private static readonly string[] _priorityKeys =
[
"PATH", "JAVA_HOME", "PYTHON_HOME", "NODE_HOME", "GOPATH", "GOROOT",
"USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP",
"COMPUTERNAME", "USERNAME", "USERDOMAIN", "OS", "PROCESSOR_ARCHITECTURE",
"SYSTEMROOT", "WINDIR", "PROGRAMFILES", "PROGRAMFILES(X86)",
"COMMONPROGRAMFILES", "NUMBER_OF_PROCESSORS"
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var allVars = Environment.GetEnvironmentVariables()
.Cast<System.Collections.DictionaryEntry>()
.Select(e => (Key: e.Key?.ToString() ?? "", Value: e.Value?.ToString() ?? ""))
.Where(x => !string.IsNullOrEmpty(x.Key))
.ToList();
IEnumerable<(string Key, string Value)> filtered;
if (string.IsNullOrWhiteSpace(q))
{
// 우선순위 키를 앞에, 나머지 알파벳 순
var prioritySet = new HashSet<string>(_priorityKeys, StringComparer.OrdinalIgnoreCase);
var prioritized = _priorityKeys
.Where(k => allVars.Any(v => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase)))
.Select(k => allVars.First(v => string.Equals(v.Key, k, StringComparison.OrdinalIgnoreCase)));
var rest = allVars
.Where(v => !prioritySet.Contains(v.Key))
.OrderBy(v => v.Key);
filtered = prioritized.Concat(rest).Take(20);
}
else
{
// 키 또는 값에 쿼리 포함 검색
filtered = allVars
.Where(v => v.Key.Contains(q, StringComparison.OrdinalIgnoreCase)
|| v.Value.Contains(q, StringComparison.OrdinalIgnoreCase))
.OrderBy(v => v.Key.StartsWith(q, StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(v => v.Key)
.Take(20);
}
var items = filtered.Select(v =>
{
// PATH처럼 긴 값은 첫 경로만 미리보기
var preview = v.Value.Length > 80 ? v.Value[..77] + "…" : v.Value;
// PATH 변수는 세미콜론으로 분할하여 첫 항목만 표시
if (v.Key.Equals("PATH", StringComparison.OrdinalIgnoreCase) && v.Value.Contains(';'))
preview = v.Value.Split(';')[0] + $" (외 {v.Value.Split(';').Length - 1}개)";
return new LauncherItem(
v.Key,
$"{preview} · Enter로 값 복사",
null,
v.Value,
Symbol: Symbols.EnvVar);
}).ToList<LauncherItem>();
if (!items.Any())
items.Add(new LauncherItem(
$"'{q}' — 환경변수 없음",
"해당 이름의 환경변수를 찾을 수 없습니다",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string value)
{
try { System.Windows.Clipboard.SetText(value); } catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,240 @@
using System.IO;
using System.Runtime.InteropServices;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Everything SDK를 이용한 초고속 파일 검색 핸들러.
/// "es" 프리픽스로 사용합니다.
///
/// 예: es report → "report" 파일명 검색
/// es *.xlsx → 엑셀 파일 검색
/// es 회의록 → 한글 파일명 검색
///
/// Everything이 설치되어 있지 않으면 자동으로 비활성화됩니다.
/// </summary>
public class EverythingHandler : IActionHandler
{
public string? Prefix => "es";
public PluginMetadata Metadata => new(
"EverythingSearch",
"Everything 초고속 파일 검색 — es [키워드]",
"1.0",
"AX");
// ─── Everything SDK P/Invoke ─────────────────────────────────────────────
private const int EVERYTHING_OK = 0;
private const int EVERYTHING_REQUEST_FILE_NAME = 0x00000001;
private const int EVERYTHING_REQUEST_PATH = 0x00000002;
private const int EVERYTHING_REQUEST_SIZE = 0x00000010;
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern uint Everything_SetSearchW(string lpSearchString);
[DllImport("Everything64.dll")]
private static extern void Everything_SetMax(uint dwMax);
[DllImport("Everything64.dll")]
private static extern void Everything_SetRequestFlags(uint dwRequestFlags);
[DllImport("Everything64.dll")]
private static extern bool Everything_QueryW(bool bWait);
[DllImport("Everything64.dll")]
private static extern uint Everything_GetNumResults();
[DllImport("Everything64.dll")]
private static extern uint Everything_GetLastError();
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr Everything_GetResultFullPathNameW(uint nIndex, IntPtr lpString, uint nMaxCount);
[DllImport("Everything64.dll", CharSet = CharSet.Unicode)]
private static extern void Everything_GetResultSize(uint nIndex, out long lpFileSize);
[DllImport("Everything64.dll")]
private static extern uint Everything_GetMajorVersion();
// ─── 상태 ────────────────────────────────────────────────────────────────
private bool? _isAvailable;
private bool IsAvailable
{
get
{
if (_isAvailable.HasValue) return _isAvailable.Value;
try
{
Everything_GetMajorVersion();
_isAvailable = true;
}
catch (Exception)
{
_isAvailable = false;
}
return _isAvailable.Value;
}
}
// ─── IActionHandler ──────────────────────────────────────────────────────
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (!IsAvailable)
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
"Everything이 설치되어 있지 않습니다",
"voidtools.com에서 Everything을 설치하면 초고속 파일 검색을 사용할 수 있습니다",
null, null, Symbol: Symbols.Warning)
});
}
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
"Everything 파일 검색",
"검색어를 입력하세요 — 파일명, 확장자(*.xlsx), 경로 일부 등",
null, null, Symbol: Symbols.Search)
});
}
try
{
Everything_SetSearchW(q);
Everything_SetMax(30);
Everything_SetRequestFlags(EVERYTHING_REQUEST_FILE_NAME | EVERYTHING_REQUEST_PATH | EVERYTHING_REQUEST_SIZE);
if (!Everything_QueryW(true))
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
"Everything 검색 실패",
$"오류 코드: {Everything_GetLastError()} — Everything 서비스가 실행 중인지 확인하세요",
null, null, Symbol: Symbols.Warning)
});
}
var count = Everything_GetNumResults();
if (count == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem(
$"검색 결과 없음: {q}",
"다른 키워드나 와일드카드(*.xlsx)를 시도해 보세요",
null, null, Symbol: Symbols.Info)
});
}
var items = new List<LauncherItem>();
var buffer = Marshal.AllocHGlobal(520 * 2); // MAX_PATH * sizeof(wchar_t)
try
{
for (uint i = 0; i < count && i < 30; i++)
{
Everything_GetResultFullPathNameW(i, buffer, 520);
var fullPath = Marshal.PtrToStringUni(buffer) ?? "";
Everything_GetResultSize(i, out var fileSize);
var fileName = Path.GetFileName(fullPath);
var dirPath = Path.GetDirectoryName(fullPath) ?? "";
var sizeStr = fileSize > 0 ? FormatSize(fileSize) : "";
var subtitle = string.IsNullOrEmpty(sizeStr) ? dirPath : $"{sizeStr} · {dirPath}";
var symbol = Directory.Exists(fullPath) ? Symbols.Folder : GetFileSymbol(fullPath);
items.Add(new LauncherItem(fileName, subtitle, null, fullPath, Symbol: symbol));
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
catch (Exception ex)
{
LogService.Warn($"Everything 검색 오류: {ex.Message}");
return Task.FromResult<IEnumerable<LauncherItem>>(new[]
{
new LauncherItem("Everything 검색 오류", ex.Message, null, null, Symbol: Symbols.Warning)
});
}
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not string path || string.IsNullOrEmpty(path)) return;
try
{
if (Directory.Exists(path))
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
});
}
else if (File.Exists(path))
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
});
}
}
catch (Exception ex)
{
LogService.Warn($"Everything 결과 열기 실패: {ex.Message}");
}
await Task.CompletedTask;
}
// ─── 헬퍼 ────────────────────────────────────────────────────────────────
private static string FormatSize(long bytes)
{
if (bytes < 1024) return $"{bytes} B";
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
}
private static string GetFileSymbol(string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
return ext switch
{
".xlsx" or ".xls" or ".csv" => Symbols.Excel,
".docx" or ".doc" => Symbols.Word,
".pptx" or ".ppt" => Symbols.Slides,
".pdf" => Symbols.Pdf,
".png" or ".jpg" or ".jpeg" or ".gif" or ".bmp" or ".svg" => Symbols.Image,
".mp4" or ".avi" or ".mkv" or ".mov" => Symbols.Video,
".mp3" or ".wav" or ".flac" => Symbols.Music,
".zip" or ".rar" or ".7z" or ".tar" or ".gz" => Symbols.Archive,
".exe" or ".msi" => Symbols.App,
".cs" or ".py" or ".js" or ".ts" or ".java" or ".cpp" or ".c" or ".go" => Symbols.Code,
".json" or ".xml" or ".yaml" or ".yml" => Symbols.Config,
".txt" or ".md" or ".log" => Symbols.TextFile,
".html" or ".htm" or ".css" => Symbols.Web,
_ => Symbols.File,
};
}
}

View File

@@ -0,0 +1,204 @@
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 즐겨찾기(파일/폴더/경로) 핸들러. "fav" 프리픽스로 사용합니다.
/// 자주 사용하는 로컬 파일·폴더 경로를 등록하고 빠르게 실행합니다.
/// 예: fav → 전체 즐겨찾기 목록
/// fav 보고서 → "보고서" 포함 항목 필터
/// fav add 보고서 C:\work\report.xlsx → 즐겨찾기 추가
/// fav del 보고서 → 즐겨찾기 삭제
/// Enter → 파일/폴더 열기.
/// </summary>
public class FavoriteHandler : IActionHandler
{
public string? Prefix => "fav";
public PluginMetadata Metadata => new(
"Favorite",
"즐겨찾기 관리 — fav",
"1.0",
"AX");
private static readonly string FavFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "favorites.json");
private static readonly JsonSerializerOptions JsonOpts = new()
{
WriteIndented = true,
PropertyNameCaseInsensitive = true
};
private List<FavEntry> _cache = new();
private bool _loaded;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
EnsureLoaded();
var q = query.Trim();
// add 명령
if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase))
{
var rest = q[4..].Trim();
var spaceIdx = rest.IndexOf(' ');
if (spaceIdx > 0)
{
var name = rest[..spaceIdx].Trim();
var path = rest[(spaceIdx + 1)..].Trim();
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"즐겨찾기 추가: {name}",
$"경로: {path} · Enter로 추가",
null, ValueTuple.Create("__ADD__", name, path),
Symbol: Symbols.Save)
]);
}
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"사용법: fav add [이름] []",
"예: fav add 보고서 C:\\work\\report.xlsx",
null, null, Symbol: Symbols.Info)
]);
}
// del 명령
if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase))
{
var name = q[4..].Trim();
var found = _cache.FirstOrDefault(b =>
b.Name.Contains(name, StringComparison.OrdinalIgnoreCase));
if (found != null)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"즐겨찾기 삭제: {found.Name}",
$"경로: {found.Path} · Enter로 삭제",
null, ValueTuple.Create("__DEL__", found.Name),
Symbol: Symbols.Delete)
]);
}
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem($"'{name}'에 해당하는 즐겨찾기 없음", "fav으로 전체 목록 확인",
null, null, Symbol: Symbols.Warning)
]);
}
// 목록/검색
var filtered = string.IsNullOrWhiteSpace(q)
? _cache
: _cache.Where(b =>
b.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
b.Path.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList();
if (!filtered.Any())
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
_cache.Count == 0 ? "즐겨찾기가 없습니다" : $"'{q}'에 해당하는 즐겨찾기 없음",
"fav add [이름] [] ",
null, null, Symbol: Symbols.Info)
]);
}
var items = filtered.Select(b =>
{
var isDir = Directory.Exists(b.Path);
var isFile = File.Exists(b.Path);
var symbol = isDir ? Symbols.Folder : isFile ? Symbols.File : Symbols.Warning;
var hint = isDir ? "폴더 열기" : isFile ? "파일 열기" : "경로를 찾을 수 없음";
return new LauncherItem(b.Name, $"{b.Path} · {hint}", null, b.Path, Symbol: symbol);
}).ToList<LauncherItem>();
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ValueTuple<string, string, string> add && add.Item1 == "__ADD__")
{
AddFav(add.Item2, add.Item3);
NotificationService.Notify("AX Copilot", $"즐겨찾기 추가: {add.Item2}");
return Task.CompletedTask;
}
if (item.Data is ValueTuple<string, string> del && del.Item1 == "__DEL__")
{
RemoveFav(del.Item2);
NotificationService.Notify("AX Copilot", $"즐겨찾기 삭제: {del.Item2}");
return Task.CompletedTask;
}
if (item.Data is string path)
{
if (Directory.Exists(path) || File.Exists(path))
{
try { Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); }
catch (Exception ex) { LogService.Warn($"즐겨찾기 열기 실패: {ex.Message}"); }
}
else
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(path)); }
catch (Exception) { }
}
}
return Task.CompletedTask;
}
private void EnsureLoaded()
{
if (_loaded) return;
_loaded = true;
try
{
if (!File.Exists(FavFile)) return;
_cache = JsonSerializer.Deserialize<List<FavEntry>>(File.ReadAllText(FavFile), JsonOpts) ?? new();
}
catch (Exception ex) { LogService.Warn($"즐겨찾기 로드 실패: {ex.Message}"); }
}
private void AddFav(string name, string path)
{
EnsureLoaded();
_cache.RemoveAll(b => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
_cache.Insert(0, new FavEntry { Name = name, Path = path });
Save();
}
private void RemoveFav(string name)
{
EnsureLoaded();
_cache.RemoveAll(b => b.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
Save();
}
private void Save()
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FavFile)!);
File.WriteAllText(FavFile, JsonSerializer.Serialize(_cache, JsonOpts));
}
catch (Exception ex) { LogService.Warn($"즐겨찾기 저장 실패: {ex.Message}"); }
}
private class FavEntry
{
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("path")] public string Path { get; set; } = "";
}
}

View File

@@ -0,0 +1,443 @@
using System.Windows;
using System.Windows.Media;
using AxCopilot.SDK;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
/// <summary>
/// 간편 사용 설명서 핸들러. "help" 프리픽스로 사용합니다.
/// 예: help → 전체 명령어 카드 목록
/// help 계산 → "계산" 관련 명령어 필터
/// help clip → 클립보드 관련 명령어 필터
/// Enter 시 해당 예시를 클립보드에 복사합니다.
/// </summary>
public class HelpHandler : IActionHandler
{
private readonly AxCopilot.Services.SettingsService? _settings;
public HelpHandler(AxCopilot.Services.SettingsService? settings = null)
{
_settings = settings;
}
public string? Prefix => "help";
public PluginMetadata Metadata => new(
"Help",
"AX Commander 사용 설명서",
"1.0",
"AX");
/// <summary>설정에서 변경된 프리픽스와 핫키를 실시간 반영한 항목 목록을 반환합니다.</summary>
private HelpEntry[] GetEntries()
{
var capPrefix = _settings?.Settings.ScreenCapture.Prefix ?? "cap";
if (string.IsNullOrWhiteSpace(capPrefix)) capPrefix = "cap";
var hotkey = _settings?.Settings.Hotkey ?? "Alt+Space";
var entries = (HelpEntry[])_baseEntries.Clone();
for (int i = 0; i < entries.Length; i++)
{
// cap 프리픽스 동적 반영
if (entries[i].Title == "스크린샷 캡처 (예약어 변경 가능)")
{
entries[i] = entries[i] with
{
Command = capPrefix,
Example = $"{capPrefix} screen / {capPrefix} window / {capPrefix} region / {capPrefix} scroll / (설정 → 캡처 탭)"
};
}
// 글로벌 핫키 동적 반영
if (entries[i].Title == "AX Commander 열기 / 닫기")
{
entries[i] = entries[i] with
{
Command = hotkey,
Example = "설정 → 핫키에서 변경 가능"
};
}
}
return entries;
}
// ─── 명령어 카드 데이터 ────────────────────────────────────────────────────
// (카테고리, 예약어/명령, 제목, 설명, 예시, 심볼, 색)
private static readonly HelpEntry[] _baseEntries =
[
// ── 검색 ────────────────────────────────────────────────────────────────
new("검색", "", "앱 · 파일 · 북마크 퍼지 검색",
"앱 이름, 파일명, 한국어 초성, Chrome·Edge 북마크를 통합 검색",
"chrome / 보고서 / ㅅㄷ (설정) / 북마크 제목",
Symbols.Search, "#0078D4"),
// ── 계산 & 변환 ──────────────────────────────────────────────────────────
new("계산", "=", "계산기",
"수식 계산 · 단위 변환 · 실시간 환율",
"= sqrt(144) / = 100 USD to KRW / = 15km to miles",
Symbols.Calculator, "#4B5EFC"),
// ── 웹 검색 ──────────────────────────────────────────────────────────────
new("검색", "?", "웹 검색 (10개 엔진)",
"?n 네이버 · ?g 구글 · ?y 유튜브 · ?gh 깃허브 · ?d DuckDuckGo · ?w 위키피디아 · ?nw 나무위키 · ?nm 네이버지도 · ?ni 네이버이미지 · ?gi 구글이미지",
"? 오늘 날씨 / ?n 뉴스 / ?g python / ?nw 검색어 / ?y 음악 / ?gh axios",
Symbols.Globe, "#006EAF"),
// ── 클립보드 ─────────────────────────────────────────────────────────────
new("클립보드", "#", "클립보드 히스토리",
"복사 이력 검색 & 재사용 · Shift+↑↓로 여러 항목 병합",
"# / # 회의록 / (Shift+↑↓ 선택 후 Shift+Enter 병합)",
Symbols.History, "#B7791F"),
new("클립보드", "$", "클립보드 텍스트 변환 (12종)",
"현재 클립보드 내용을 즉시 변환",
"$json / $b64e / $upper / $url / $md5 / $trim",
Symbols.Clipboard, "#8764B8"),
// ── 텍스트 스니펫 ────────────────────────────────────────────────────────
new("텍스트", ";", "텍스트 스니펫",
"저장된 템플릿 불러오기 · 변수 치환 지원",
";addr / ;sig / ;greet",
Symbols.Snippet, "#0F6CBD"),
// ── 단축키 ───────────────────────────────────────────────────────────────
new("단축키", "@", "URL 단축키",
"저장해둔 URL을 키워드로 바로 열기",
"@gh / @notion / @jira",
Symbols.Globe, "#0078D4"),
new("단축키", "cd", "폴더 단축키",
"저장해둔 폴더를 키워드로 바로 열기",
"cd dl / cd work / cd desktop",
Symbols.Folder, "#107C10"),
new("단축키", ">", "터미널 명령 실행",
"PowerShell 명령을 AX Commander에서 직접 실행",
"> git status / > ipconfig / > cls",
Symbols.Terminal, "#323130"),
// ── 워크스페이스 ──────────────────────────────────────────────────────────
new("창관리", "~", "워크스페이스 저장·복원",
"현재 창 배치를 스냅샷으로 저장하고 언제든 복원",
"~save 업무 / ~restore 업무 / ~list",
Symbols.Workspace, "#C50F1F"),
new("창관리", "snap", "창 분할 레이아웃",
"창을 화면의 특정 영역에 즉시 스냅",
"snap left / snap right / snap tl / snap full",
Symbols.SnapLayout, "#B45309"),
new("창관리", "cap", "스크린샷 캡처 (예약어 변경 가능)",
"영역 선택 · 활성 창 · 스크롤 · 전체 화면. Shift+Enter로 지연 캡처(3/5/10초 타이머). 결과는 클립보드에 복사. 글로벌 단축키(PrintScreen 등)로 바로 캡처 가능. 설정 → 캡처 탭에서 예약어·단축키·스크롤 속도 변경",
"cap region / cap window / cap scroll / cap screen / Shift+Enter → 지연 캡처",
Symbols.CaptureIcon, "#BE185D"),
// ── 시스템 명령 ───────────────────────────────────────────────────────────
new("시스템", "/", "시스템 명령",
"잠금·절전·재시작·종료·타이머·알람",
"/lock / /sleep / /shutdown / /timer 5m / /alarm 14:30",
Symbols.Power, "#4A4A4A"),
new("시스템", "info · *", "시스템 정보",
"IP · 배터리 · 볼륨 · 가동시간 · CPU · 디스크. `*` 입력으로도 동일하게 사용 가능",
"info / info ip / info battery / * / * ip",
Symbols.Computer, "#5B4E7E"),
new("알림", "", "잠금 해제 사용시간 알림",
"PC 잠금 해제 시 오늘 누적 사용 시간과 격려 문구·명언 팝업 표시. 설정 → 알림 탭에서 활성화",
"설정 → 알림 탭: 활성화 토글 / 표시 위치(4방향) / 표시 간격(30분~4시간) / 자동 닫힘(5초~3분)",
Symbols.ReminderBell, "#EA8F00"),
new("시스템", "kill", "프로세스 종료",
"프로세스 이름으로 검색 후 강제 종료",
"kill chrome / kill node / kill teams",
Symbols.Error, "#CC2222"),
new("시스템", "media", "미디어 제어",
"재생·일시정지·이전·다음·볼륨 조절",
"media play / media next / media vol+ / media mute",
Symbols.MediaPlay, "#1A6B3C"),
// ── 개발자 도구 ───────────────────────────────────────────────────────────
new("개발", "json", "JSON 포맷 · 검증",
"클립보드의 JSON을 정렬·압축·유효성 검사",
"json → format / minify / validate",
Symbols.JsonValid, "#D97706"),
new("개발", "encode", "인코딩 · 해싱",
"Base64 · URL · HTML · UTF-8 · MD5 · SHA256",
"encode base64 / encode url / encode sha256",
Symbols.EncodeIcon, "#6366F1"),
new("개발", "color", "색상 변환",
"HEX ↔ RGB ↔ HSL ↔ HSV 변환 및 색상명 지원",
"color #FF5733 / color 255,87,51 / color red",
Symbols.ColorPicker, "#EC4899"),
new("개발", "port", "포트 · 프로세스 조회",
"포트 번호로 점유 프로세스 확인",
"port 3000 / port 8080 / port 443",
Symbols.PortIcon, "#006699"),
new("개발", "env", "환경변수 조회",
"시스템 환경변수 검색 및 클립보드 복사",
"env / env PATH / env JAVA",
Symbols.EnvVar, "#0D9488"),
// ── 앱 관리 ───────────────────────────────────────────────────────────────
new("앱", "emoji", "이모지 피커",
"300개+ 이모지 검색 · Enter로 클립보드 복사",
"emoji / emoji 하트 / emoji wave / emoji 불꽃",
Symbols.Emoji, "#F59E0B"),
new("앱", "recent", "최근 파일",
"Windows 최근 파일 목록 검색 & 바로 열기",
"recent / recent 보고서 / recent xlsx",
Symbols.RecentFiles, "#059669"),
new("앱", "note", "빠른 메모",
"간단한 메모를 저장하고 불러오기",
"note 내일 회의 10시 / note",
Symbols.Note, "#7C3AED"),
new("앱", "uninstall","앱 제거",
"설치된 앱 검색 후 제거",
"uninstall / uninstall kakao / uninstall zoom",
Symbols.Uninstall, "#DC2626"),
// ── 유틸리티 ────────────────────────────────────────────────────────────
new("유틸", "pick", "스포이드 색상 추출",
"화면 아무 곳을 클릭하여 HEX 색상 코드 추출 · 돋보기로 실시간 미리보기 · 결과 반투명 창 5초 표시",
"pick → 스포이드 모드 진입 → 클릭으로 색상 추출",
Symbols.ColorPicker, "#EC4899"),
new("유틸", "date", "날짜 계산 · D-day · 타임스탬프",
"날짜 가감(+30d), D-day 계산, Unix ↔ 날짜 변환, 요일·ISO 주차 조회",
"date / date +30d / date 2026-12-25 / date 1711584000 / date unix",
Symbols.DateIcon, "#0EA5E9"),
new("시스템", "svc", "서비스 관리",
"Windows 서비스 검색·시작·중지·재시작 + AX 클립보드 서비스 강제 재시작",
"svc / svc spooler / svc restart clipboard",
Symbols.ServiceIcon, "#6366F1"),
new("유틸", "pipe", "클립보드 파이프라인",
"변환을 > 로 체이닝: 대문자→공백제거→Base64 등 19종 필터 한 번에 적용",
"pipe upper > trim > b64e / pipe sort > unique > number",
Symbols.PipeIcon, "#8B5CF6"),
new("유틸", "journal", "업무 일지 자동 생성",
"오늘 사용한 앱·명령어·활성 시간을 마크다운 요약으로 자동 생성. 스탠드업/일일 보고에 바로 사용",
"journal / journal 2026-03-25",
Symbols.JournalIcon, "#0EA5E9"),
new("유틸", "routine", "루틴 자동화",
"등록된 루틴(앱·폴더·URL 조합)을 한 번에 순서대로 실행. 출근/퇴근/회의 전 세팅",
"routine / routine morning / routine endofday",
Symbols.RoutineIcon, "#F59E0B"),
new("유틸", "batch", "텍스트 일괄 처리",
"클립보드 각 줄에 동시 적용: 접두사·접미사·줄번호·정렬·중복제거·따옴표·치환·CSV 변환",
"batch number / batch prefix >> / batch sort / batch replace A B",
Symbols.BatchIcon, "#10B981"),
new("유틸", "diff", "텍스트/파일 비교",
"클립보드 최근 2개 텍스트 또는 파일 2개를 줄 단위 비교. 추가·삭제·동일 줄 하이라이트",
"diff / diff C:\\a.txt C:\\b.txt",
Symbols.DiffIcon, "#EF4444"),
new("유틸", "win", "윈도우 포커스 스위처",
"열린 창을 타이틀·프로세스명으로 검색하여 Alt+Tab 없이 즉시 전환",
"win / win chrome / win 보고서",
Symbols.WindowIcon, "#6366F1"),
new("시스템", "^", "Windows 실행 명령",
"Win+R 실행 창과 동일하게 명령어 실행. notepad, calc, cmd, control, mstsc 등 모든 Windows 실행 명령 지원",
"^ notepad / ^ cmd / ^ calc / ^ control / ^ mspaint",
Symbols.LaunchIcon, "#E08850"),
new("유틸", "stats", "텍스트 통계 분석",
"클립보드 텍스트 글자·단어·줄 수, 키워드 빈도, 읽기 시간 추정",
"stats / stats 키워드 (클립보드 텍스트 분석)",
Symbols.TextStats, "#6366F1"),
new("유틸", "fav", "즐겨찾기 (파일·폴더)",
"자주 쓰는 경로를 등록하고 빠르게 열기",
"fav / fav add 보고서 C:\\work\\report.xlsx / fav del 보고서",
Symbols.Favorite, "#F59E0B"),
new("유틸", "rename", "파일 일괄 이름변경",
"폴더 내 파일을 패턴·변수로 일괄 이름변경. {n}순번 {date}날짜 {orig}원본명",
"rename C:\\work\\*.xlsx 보고서_{n}",
Symbols.RenameIcon, "#8B5CF6"),
new("유틸", "monitor", "시스템 리소스 모니터",
"CPU·메모리·디스크·프로세스 실시간 현황 조회",
"monitor / monitor cpu / monitor mem / monitor disk",
Symbols.MonitorIcon, "#10B981"),
new("유틸", "scaffold", "프로젝트 스캐폴딩",
"내장/사용자 템플릿으로 프로젝트 폴더 구조 일괄 생성",
"scaffold / scaffold webapi / scaffold C:\\new-project webapi",
Symbols.ScaffoldIcon,"#0EA5E9"),
// ── 단축키 ───────────────────────────────────────────────────────────────
new("키보드", "Alt+Space", "AX Commander 열기 / 닫기",
"어디서든 AX Commander를 토글",
"설정 → 핫키에서 변경 가능",
Symbols.Info, "#6B7280"),
new("키보드", "Enter", "실행",
"선택된 항목 실행",
"",
Symbols.Info, "#6B7280"),
new("키보드", "Shift+Enter", "Large Type / 병합 실행 / 지연 캡처",
"텍스트를 전체화면으로 표시 · 클립보드 항목 병합 · 캡처 모드에서는 지연 캡처(3초/5초/10초) 타이머 선택",
"(#모드) Shift+↑↓ 선택 후 Shift+Enter 병합 / (cap모드) Shift+Enter → 타이머 선택",
Symbols.Info, "#6B7280"),
new("키보드", "Tab", "자동완성",
"선택 항목으로 입력 자동완성",
"",
Symbols.Info, "#6B7280"),
new("키보드", "→ (커서 끝)", "파일 액션 메뉴",
"앱·파일 선택 후 → 키: 경로복사 · 탐색기 · 관리자 · 터미널",
"",
Symbols.Info, "#6B7280"),
new("키보드", "Ctrl+,", "설정 열기",
"AX Copilot 설정 창",
"",
Symbols.Info, "#6B7280"),
];
// ─── GetItemsAsync ─────────────────────────────────────────────────────────
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var allEntries = GetEntries();
// 쿼리 없이 "help"만 입력한 경우: 전체 기능 창 여는 항목 하나만 표시
if (string.IsNullOrEmpty(q))
{
var cmdCount = allEntries.Count(e => e.Category != "키보드");
var keyCount = allEntries.Count(e => e.Category == "키보드");
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"AX Commander — 전체 기능 목록 보기",
$"총 {cmdCount}개 명령어 · {keyCount}개 단축키 · Enter → 기능 설명 창 열기",
null, "__HELP_OVERVIEW__",
Symbol: "\uE946")
]);
}
// "help 검색어" 형태: 일치하는 항목 리스트 표시
var filtered = allEntries.Where(e =>
e.Category.Contains(q, StringComparison.OrdinalIgnoreCase) ||
e.Command.Contains(q, StringComparison.OrdinalIgnoreCase) ||
e.Title.Contains(q, StringComparison.OrdinalIgnoreCase) ||
e.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
var items = filtered
.Select(e => new LauncherItem(
FormatTitle(e),
FormatSubtitle(e),
null,
e.Example,
Symbol: e.Symbol))
.ToList<LauncherItem>();
if (items.Count == 0)
{
items.Add(new LauncherItem(
$"'{q}'에 해당하는 명령어가 없습니다",
"help만 입력하면 전체 기능 창을 열 수 있어요",
null, null,
Symbol: Symbols.Info));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── ExecuteAsync ──────────────────────────────────────────────────────────
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not string s) return Task.CompletedTask;
if (s == "__HELP_OVERVIEW__")
{
// 전체 기능 창 열기
Application.Current.Dispatcher.Invoke(() =>
{
var currentEntries = GetEntries();
var models = currentEntries.Select(e => new HelpItemModel
{
Category = e.Category,
Command = string.IsNullOrEmpty(e.Command) ? "(퍼지 검색)" : e.Command,
Title = e.Title,
Description = e.Description,
Example = e.Example,
Symbol = e.Symbol,
ColorBrush = ParseColor(e.ColorHex)
});
var globalHotkey = _settings?.Settings.Hotkey ?? "Alt+Space";
new HelpDetailWindow(models, currentEntries.Count(e => e.Category != "키보드"), globalHotkey).Show();
});
return Task.CompletedTask;
}
// 일반 항목: 예시를 클립보드에 복사
if (!string.IsNullOrWhiteSpace(s))
{
try { Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(s)); }
catch (Exception) { /* 무시 */ }
}
return Task.CompletedTask;
}
private static SolidColorBrush ParseColor(string hex)
{
try
{
var color = (System.Windows.Media.Color)
System.Windows.Media.ColorConverter.ConvertFromString(hex);
return new SolidColorBrush(color);
}
catch (Exception) { return new SolidColorBrush(System.Windows.Media.Colors.Gray); }
}
// ─── 헬퍼 ─────────────────────────────────────────────────────────────────
private static string FormatTitle(HelpEntry e)
{
var prefix = string.IsNullOrEmpty(e.Command)
? $"[{e.Category}]"
: $"[{e.Category}] {e.Command}";
return $"{prefix} — {e.Title}";
}
private static string FormatSubtitle(HelpEntry e)
{
var parts = new List<string> { e.Description };
if (!string.IsNullOrEmpty(e.Example))
parts.Add($"예) {e.Example}");
return string.Join(" · ", parts);
}
// ─── 데이터 모델 ───────────────────────────────────────────────────────────
private record HelpEntry(
string Category,
string Command,
string Title,
string Description,
string Example,
string Symbol,
string ColorHex);
}

View File

@@ -0,0 +1,132 @@
using System.IO;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 업무 일지 자동 생성 핸들러. "journal" 프리픽스로 사용합니다.
/// UsageStatisticsService 데이터를 기반으로 오늘/지정일의 업무 요약을 자동 생성합니다.
/// 예: journal → 오늘의 업무 일지 생성
/// journal 2026-03-25 → 해당 날짜 일지
/// Enter → 마크다운 형식으로 클립보드에 복사.
/// </summary>
public class JournalHandler : IActionHandler
{
public string? Prefix => "journal";
public PluginMetadata Metadata => new(
"Journal",
"업무 일지 자동 생성 — journal",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
DateTime targetDate;
if (string.IsNullOrWhiteSpace(q))
targetDate = DateTime.Today;
else if (DateTime.TryParse(q, out var parsed))
targetDate = parsed.Date;
else
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("날짜 형식 오류", "예: journal 2026-03-25 또는 journal (오늘)",
null, null, Symbol: Symbols.Warning)
]);
}
// 날짜 범위 계산 (오늘~targetDate 간 차이)
var daysAgo = (DateTime.Today - targetDate).Days;
var allStats = UsageStatisticsService.GetStats(Math.Max(daysAgo + 1, 1));
var stats = allStats.FirstOrDefault(s => s.Date == targetDate.ToString("yyyy-MM-dd"));
var items = new List<LauncherItem>();
if (stats == null)
{
items.Add(new LauncherItem(
$"{targetDate:yyyy-MM-dd} — 기록 없음",
"해당 날짜의 사용 기록이 없습니다",
null, null, Symbol: Symbols.Info));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 요약 생성
var activeHours = stats.ActiveSeconds / 3600.0;
var sb = new StringBuilder();
sb.AppendLine($"## 업무 일지 — {targetDate:yyyy-MM-dd} ({targetDate:dddd})");
sb.AppendLine();
sb.AppendLine($"- **PC 활성 시간**: {activeHours:F1}시간");
sb.AppendLine($"- **런처 호출**: {stats.LauncherOpens}회");
if (stats.CommandUsage.Count > 0)
{
sb.AppendLine();
sb.AppendLine("### 사용한 명령어");
foreach (var kv in stats.CommandUsage.OrderByDescending(x => x.Value).Take(10))
sb.AppendLine($"- `{kv.Key}` — {kv.Value}회");
}
sb.AppendLine();
sb.AppendLine("---");
sb.AppendLine($"*AX Copilot 자동 생성 · {DateTime.Now:HH:mm}*");
var journal = sb.ToString();
// 요약 카드
var topCmds = stats.CommandUsage.OrderByDescending(x => x.Value).Take(3)
.Select(x => x.Key);
var cmdPreview = stats.CommandUsage.Count > 0
? $"주요 명령: {string.Join(", ", topCmds)}"
: "명령어 사용 기록 없음";
items.Add(new LauncherItem(
$"{targetDate:yyyy-MM-dd} 업무 일지 — 클립보드로 복사",
$"활성 {activeHours:F1}h · 런처 {stats.LauncherOpens}회 · {cmdPreview}",
null, journal,
Symbol: Symbols.Note));
items.Add(new LauncherItem(
$"PC 활성 시간: {activeHours:F1}시간",
"잠금 해제 시간 기준 누적",
null, $"PC 활성 시간: {activeHours:F1}시간",
Symbol: Symbols.Clock));
items.Add(new LauncherItem(
$"런처 호출: {stats.LauncherOpens}회",
"Alt+Space 또는 트레이 클릭",
null, $"런처 호출: {stats.LauncherOpens}회",
Symbol: Symbols.Search));
if (stats.CommandUsage.Count > 0)
{
foreach (var kv in stats.CommandUsage.OrderByDescending(x => x.Value).Take(5))
{
items.Add(new LauncherItem(
$"{kv.Key} — {kv.Value}회",
"Enter로 복사",
null, $"{kv.Key}: {kv.Value}회",
Symbol: Symbols.Terminal));
}
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
NotificationService.Notify("업무 일지", "클립보드에 복사되었습니다");
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,132 @@
using System.Text.Json;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// JSON 검증/포맷 핸들러. "json" 프리픽스로 사용합니다.
/// 예: json → 클립보드의 JSON을 파싱하여 미리보기 표시
/// json {"a":1} → 인라인 JSON 파싱 + 미리보기
/// json minify → 클립보드 JSON 미니파이 → 클립보드 복사
/// json format → 클립보드 JSON 예쁘게 포맷 → 클립보드 복사
/// </summary>
public class JsonHandler : IActionHandler
{
public string? Prefix => "json";
public PluginMetadata Metadata => new(
"JsonFormatter",
"JSON 검증/포맷 — json 뒤에 내용 또는 명령 입력",
"1.0",
"AX");
private static readonly JsonSerializerOptions _prettyOpts = new() { WriteIndented = true };
private static readonly JsonSerializerOptions _compactOpts = new() { WriteIndented = false };
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
// ─── 빈 쿼리 or format/minify: 클립보드에서 JSON 읽기 ─────────────────
if (string.IsNullOrWhiteSpace(q) || q.Equals("format", StringComparison.OrdinalIgnoreCase)
|| q.Equals("minify", StringComparison.OrdinalIgnoreCase)
|| q.Equals("min", StringComparison.OrdinalIgnoreCase))
{
string clipText = "";
try { clipText = Clipboard.GetText(); } catch (Exception) { }
if (string.IsNullOrWhiteSpace(clipText))
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"클립보드가 비어 있습니다",
"클립보드에 JSON 텍스트를 복사한 뒤 실행하세요",
null, null, Symbol: Symbols.Clipboard)
]);
return Task.FromResult(BuildItems(clipText,
isMinify: q.StartsWith("min", StringComparison.OrdinalIgnoreCase)));
}
// ─── 인라인 JSON ──────────────────────────────────────────────────────
return Task.FromResult(BuildItems(q, isMinify: false));
}
private static IEnumerable<LauncherItem> BuildItems(string input, bool isMinify)
{
try
{
using var doc = JsonDocument.Parse(input, new JsonDocumentOptions
{
AllowTrailingCommas = true,
CommentHandling = JsonCommentHandling.Skip
});
// 포맷된 버전
var pretty = JsonSerializer.Serialize(doc.RootElement, _prettyOpts);
var compact = JsonSerializer.Serialize(doc.RootElement, _compactOpts);
// 루트 타입 정보
var rootType = doc.RootElement.ValueKind switch
{
JsonValueKind.Object => $"Object ({doc.RootElement.EnumerateObject().Count()}개 키)",
JsonValueKind.Array => $"Array ({doc.RootElement.GetArrayLength()}개 항목)",
_ => doc.RootElement.ValueKind.ToString()
};
var targetText = isMinify ? compact : pretty;
var actionLabel = isMinify ? "미니파이" : "포맷";
// 미리보기 (처음 100자)
var preview = compact.Length > 100 ? compact[..97] + "…" : compact;
return
[
new LauncherItem(
$"✅ 유효한 JSON — {rootType}",
$"{preview} · Enter로 {actionLabel} 결과 클립보드 복사",
null,
targetText,
Symbol: Symbols.JsonValid),
new LauncherItem(
"포맷 (Pretty Print) 복사",
$"{pretty.Length}자 · 들여쓰기 2스페이스",
null,
pretty,
Symbol: Symbols.JsonFormat),
new LauncherItem(
"미니파이 (Minify) 복사",
$"{compact.Length}자 · 공백 제거",
null,
compact,
Symbol: Symbols.JsonMinify),
];
}
catch (JsonException ex)
{
var msg = ex.Message.Length > 100 ? ex.Message[..97] + "…" : ex.Message;
return
[
new LauncherItem(
"❌ JSON 오류",
msg,
null,
null,
Symbol: Symbols.Error)
];
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { Clipboard.SetText(text); } catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,350 @@
using System.IO;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// .skill.json 파일을 읽어 IActionHandler로 변환하는 로더.
/// JSON 스킬은 외부 API를 호출하는 동적 핸들러를 코드 없이 정의합니다.
/// </summary>
public static class JsonSkillLoader
{
public static IActionHandler? Load(string filePath)
{
var json = File.ReadAllText(filePath);
var def = JsonSerializer.Deserialize<JsonSkillDefinition>(json,
new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
if (def == null) return null;
return new JsonSkillHandler(def);
}
}
public class JsonSkillDefinition
{
public string Id { get; set; } = "";
public string Name { get; set; } = "";
public string Version { get; set; } = "1.0";
public string Prefix { get; set; } = "";
public JsonSkillCredential? Credential { get; set; }
public JsonSkillRequest? Request { get; set; }
public JsonSkillResponse? Response { get; set; }
public JsonSkillCache? Cache { get; set; }
}
public class JsonSkillCredential
{
public string Type { get; set; } = "bearer_token"; // bearer_token | basic_auth
public string CredentialKey { get; set; } = "";
}
public class JsonSkillRequest
{
public string Method { get; set; } = "GET";
public string Url { get; set; } = "";
public Dictionary<string, string>? Headers { get; set; }
public object? Body { get; set; }
}
public class JsonSkillResponse
{
public string ResultsPath { get; set; } = "results";
public string TitleField { get; set; } = "title";
public string? SubtitleField { get; set; }
public string? ActionUrl { get; set; }
}
public class JsonSkillCache
{
public int Ttl { get; set; } = 0; // 초 단위
}
/// <summary>
/// JSON 스킬 정의를 기반으로 실제 HTTP 호출을 수행하는 핸들러
/// </summary>
public class JsonSkillHandler : IActionHandler
{
private readonly JsonSkillDefinition _def;
private readonly HttpClient _http = new();
private List<LauncherItem>? _cache;
private DateTime _cacheExpiry = DateTime.MinValue;
public string? Prefix => _def.Prefix;
public PluginMetadata Metadata => new(_def.Id, _def.Name, _def.Version, "JSON Skill");
public JsonSkillHandler(JsonSkillDefinition def)
{
_def = def;
_http.Timeout = TimeSpan.FromSeconds(3);
// 인증 헤더 설정
if (def.Credential?.Type == "bearer_token")
{
var token = CredentialManager.GetToken(def.Credential.CredentialKey);
if (!string.IsNullOrEmpty(token))
_http.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
}
}
public async Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
// 캐시 확인
if (_cache != null && DateTime.Now < _cacheExpiry)
return _cache;
if (_def.Request == null || _def.Response == null)
return Enumerable.Empty<LauncherItem>();
try
{
var url = _def.Request.Url.Replace("{{INPUT}}", Uri.EscapeDataString(query));
// URL 유효성 검증: http/https 스킴만 허용
if (!Uri.TryCreate(url, UriKind.Absolute, out var parsedUrl) ||
(parsedUrl.Scheme != Uri.UriSchemeHttp && parsedUrl.Scheme != Uri.UriSchemeHttps))
{
LogService.Error($"[{_def.Name}] 유효하지 않은 URL: {url}");
return [new LauncherItem("설정 오류", "스킬 URL이 유효하지 않습니다 (http/https만 허용)", null, null)];
}
var response = _def.Request.Method.ToUpper() switch
{
"POST" => await _http.PostAsync(url, BuildBody(query), ct),
_ => await _http.GetAsync(url, ct)
};
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync(ct);
var items = ParseResults(json);
// 캐시 저장
if (_def.Cache?.Ttl > 0)
{
_cache = items;
_cacheExpiry = DateTime.Now.AddSeconds(_def.Cache.Ttl);
}
return items;
}
catch (TaskCanceledException)
{
// 타임아웃 → 캐시 반환
if (_cache != null)
{
LogService.Warn($"[{_def.Name}] API 타임아웃, 캐시 반환");
return _cache;
}
return [new LauncherItem("네트워크 오류", "연결을 확인하세요", null, null)];
}
catch (Exception ex)
{
LogService.Error($"[{_def.Name}] API 호출 실패: {ex.Message}");
return [new LauncherItem($"오류: {ex.Message}", _def.Name, null, null)];
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.ActionUrl != null &&
Uri.TryCreate(item.ActionUrl, UriKind.Absolute, out var uri) &&
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps))
{
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(item.ActionUrl) { UseShellExecute = true });
}
return Task.CompletedTask;
}
private HttpContent BuildBody(string query)
{
var bodyJson = JsonSerializer.Serialize(_def.Request?.Body)
.Replace("\"{{INPUT}}\"", $"\"{query}\"");
return new StringContent(bodyJson, Encoding.UTF8, "application/json");
}
private List<LauncherItem> ParseResults(string json)
{
var items = new List<LauncherItem>();
try
{
var root = JsonNode.Parse(json);
if (root == null) return items;
// resultsPath로 배열 탐색 (dot notation)
var node = NavigatePath(root, _def.Response!.ResultsPath);
if (node is not JsonArray arr) return items;
foreach (var element in arr.Take(10))
{
if (element == null) continue;
var title = NavigatePath(element, _def.Response.TitleField)?.ToString() ?? "(제목 없음)";
var subtitle = _def.Response.SubtitleField != null
? NavigatePath(element, _def.Response.SubtitleField)?.ToString() ?? ""
: "";
var actionUrl = _def.Response.ActionUrl != null
? NavigatePath(element, _def.Response.ActionUrl)?.ToString()
: null;
items.Add(new LauncherItem(title, subtitle, null, element, actionUrl, Symbols.Cloud));
}
}
catch (Exception ex)
{
LogService.Error($"[{_def.Name}] 응답 파싱 실패: {ex.Message}");
}
return items;
}
private static JsonNode? NavigatePath(JsonNode root, string path)
{
var parts = path.Split('.');
JsonNode? current = root;
foreach (var part in parts)
{
if (current == null) return null;
// 배열 인덱스 처리: field[0]
var bracketIdx = part.IndexOf('[');
if (bracketIdx >= 0)
{
var closingIdx = part.IndexOf(']');
if (closingIdx < 0) return null; // 잘못된 경로 형식 (예: field[0 )
var fieldName = part[..bracketIdx];
var index = int.Parse(part[(bracketIdx + 1)..closingIdx]);
current = current[fieldName]?[index];
}
else
{
current = current[part];
}
}
return current;
}
}
/// <summary>
/// Windows Credential Manager (advapi32.dll)를 사용해 자격증명을 안전하게 저장/조회합니다.
/// DPAPI 기반 암호화로 현재 사용자 계정에 귀속되어 저장됩니다.
/// </summary>
public static class CredentialManager
{
private const uint CRED_TYPE_GENERIC = 1;
private const uint CRED_PERSIST_LOCAL_MACHINE = 2;
[StructLayout(LayoutKind.Sequential)]
private struct FILETIME { public uint dwLowDateTime; public uint dwHighDateTime; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct CREDENTIAL
{
public uint Flags;
public uint Type;
public IntPtr TargetName;
public IntPtr Comment;
public FILETIME LastWritten;
public uint CredentialBlobSize;
public IntPtr CredentialBlob;
public uint Persist;
public uint AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredRead(string target, uint type, uint flags, out IntPtr credential);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredWrite([In] ref CREDENTIAL userCredential, uint flags);
[DllImport("advapi32.dll", SetLastError = true)]
private static extern void CredFree(IntPtr cred);
[DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool CredDelete(string target, uint type, uint flags);
/// <summary>
/// Windows Credential Manager에서 토큰을 읽습니다.
/// 저장된 자격증명이 없으면 환경변수에서 폴백합니다.
/// </summary>
public static string? GetToken(string key)
{
if (string.IsNullOrEmpty(key)) return null;
try
{
if (CredRead(key, CRED_TYPE_GENERIC, 0, out IntPtr ptr))
{
try
{
var cred = Marshal.PtrToStructure<CREDENTIAL>(ptr);
if (cred.CredentialBlobSize > 0 && cred.CredentialBlob != IntPtr.Zero)
return Marshal.PtrToStringUni(cred.CredentialBlob,
(int)cred.CredentialBlobSize / 2);
}
finally
{
CredFree(ptr);
}
}
}
catch (Exception ex)
{
LogService.Warn($"Windows Credential Manager 읽기 실패 ({key}): {ex.Message}");
}
// 환경변수 폴백 (개발 환경용)
return Environment.GetEnvironmentVariable(key.ToUpperInvariant());
}
/// <summary>
/// Windows Credential Manager에 토큰을 DPAPI로 암호화하여 저장합니다.
/// </summary>
public static void SetToken(string key, string token)
{
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(token)) return;
var blob = Encoding.Unicode.GetBytes(token);
var blobPtr = Marshal.AllocHGlobal(blob.Length);
var targetPtr = Marshal.StringToCoTaskMemUni(key);
var userPtr = Marshal.StringToCoTaskMemUni(Environment.UserName);
try
{
Marshal.Copy(blob, 0, blobPtr, blob.Length);
var cred = new CREDENTIAL
{
Type = CRED_TYPE_GENERIC,
TargetName = targetPtr,
UserName = userPtr,
CredentialBlobSize = (uint)blob.Length,
CredentialBlob = blobPtr,
Persist = CRED_PERSIST_LOCAL_MACHINE,
};
if (!CredWrite(ref cred, 0))
LogService.Error($"토큰 저장 실패: {key}, 오류 코드: {Marshal.GetLastWin32Error()}");
else
LogService.Info($"토큰 저장 완료: {key}");
}
finally
{
Marshal.FreeHGlobal(blobPtr);
Marshal.FreeCoTaskMem(targetPtr);
Marshal.FreeCoTaskMem(userPtr);
}
}
/// <summary>
/// Windows Credential Manager에서 자격증명을 삭제합니다.
/// </summary>
public static bool DeleteToken(string key) =>
!string.IsNullOrEmpty(key) && CredDelete(key, CRED_TYPE_GENERIC, 0);
}

View File

@@ -0,0 +1,107 @@
using System.Runtime.InteropServices;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 미디어 컨트롤 핸들러. "media" 프리픽스로 사용합니다.
/// 예: media play → 재생/일시정지
/// media next → 다음 트랙
/// media prev → 이전 트랙
/// media vol+ → 볼륨 올리기
/// media vol- → 볼륨 낮추기
/// media mute → 음소거 토글
/// </summary>
public class MediaHandler : IActionHandler
{
public string? Prefix => "media";
public PluginMetadata Metadata => new(
"MediaControl",
"미디어 컨트롤 — media 뒤에 명령어 입력",
"1.0",
"AX");
// Windows 미디어/볼륨 가상 키 코드
private const byte VK_MEDIA_PLAY_PAUSE = 0xB3;
private const byte VK_MEDIA_NEXT_TRACK = 0xB0;
private const byte VK_MEDIA_PREV_TRACK = 0xB1;
private const byte VK_VOLUME_UP = 0xAF;
private const byte VK_VOLUME_DOWN = 0xAE;
private const byte VK_VOLUME_MUTE = 0xAD;
// KEYEVENTF flags
private const uint KEYEVENTF_EXTENDEDKEY = 0x0001;
private const uint KEYEVENTF_KEYUP = 0x0002;
[DllImport("user32.dll", SetLastError = false)]
private static extern void keybd_event(byte bVk, byte bScan, uint dwFlags, nuint dwExtraInfo);
// 명령어 → (제목, 설명, VK코드, 심볼)
private static readonly List<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> _commands =
[
(["play", "pause", "pp"], "재생 / 일시정지", "현재 미디어 재생 또는 일시정지", VK_MEDIA_PLAY_PAUSE, Symbols.MediaPlay),
(["next", ">>"], "다음 트랙", "다음 곡으로 이동", VK_MEDIA_NEXT_TRACK, Symbols.MediaNext),
(["prev", "previous", "<<"], "이전 트랙", "이전 곡으로 이동", VK_MEDIA_PREV_TRACK, Symbols.MediaPrev),
(["vol+", "volup", "up"], "볼륨 올리기", "시스템 볼륨 증가", VK_VOLUME_UP, Symbols.VolumeUp),
(["vol-", "voldown", "down"], "볼륨 낮추기", "시스템 볼륨 감소", VK_VOLUME_DOWN, Symbols.VolumeDown),
(["mute", "음소거"], "음소거 토글", "볼륨 음소거 / 해제", VK_VOLUME_MUTE, Symbols.VolumeMute),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
IEnumerable<(string[] Keys, string Title, string Subtitle, byte Vk, string Symbol)> matches;
if (string.IsNullOrEmpty(q))
{
// 쿼리 없으면 전체 목록 표시
matches = _commands;
}
else
{
// 입력어와 일치하는 명령 필터
matches = _commands.Where(c =>
c.Keys.Any(k => k.StartsWith(q, StringComparison.OrdinalIgnoreCase)) ||
c.Title.Contains(q, StringComparison.OrdinalIgnoreCase));
}
var items = matches
.Select(c => new LauncherItem(c.Title, c.Subtitle, null, new MediaKeyData(c.Vk), Symbol: c.Symbol))
.ToList();
if (items.Count == 0)
{
items.Add(new LauncherItem(
"알 수 없는 명령어",
"play · next · prev · vol+ · vol- · mute 중 하나를 입력하세요",
null, null, Symbol: Symbols.Warning));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not MediaKeyData data) return Task.CompletedTask;
try
{
// 키 누름 → 키 뗌
keybd_event(data.Vk, 0, KEYEVENTF_EXTENDEDKEY, 0);
keybd_event(data.Vk, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
LogService.Info($"미디어 키 전송: VK=0x{data.Vk:X2}");
}
catch (Exception ex)
{
LogService.Warn($"미디어 키 전송 실패: {ex.Message}");
}
return Task.CompletedTask;
}
private record MediaKeyData(byte Vk);
}

View File

@@ -0,0 +1,163 @@
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 시스템 리소스 모니터 핸들러. "monitor" 프리픽스로 사용합니다.
/// CPU, 메모리, 디스크, 프로세스 수 등 실시간 시스템 리소스 정보를 표시합니다.
/// 예: monitor → 전체 리소스 현황
/// monitor cpu → CPU 관련 정보만
/// monitor mem → 메모리 관련 정보만
/// monitor disk → 디스크 사용량
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class MonitorHandler : IActionHandler
{
public string? Prefix => "monitor";
public PluginMetadata Metadata => new(
"Monitor",
"시스템 리소스 모니터 — monitor",
"1.0",
"AX");
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
[StructLayout(LayoutKind.Sequential)]
private struct MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
var showAll = string.IsNullOrWhiteSpace(q);
// CPU
if (showAll || q.Contains("cpu") || q.Contains("프로세서"))
{
var cpuCount = Environment.ProcessorCount;
var processes = Process.GetProcesses().Length;
var threads = 0;
try { threads = Process.GetProcesses().Sum(p => { try { return p.Threads.Count; } catch (Exception) { return 0; } }); }
catch (Exception) { }
items.Add(new LauncherItem(
$"CPU: {cpuCount}코어 · 프로세스 {processes}개 · 스레드 {threads:N0}개",
"Enter로 클립보드 복사",
null, $"CPU: {cpuCount}코어, 프로세스 {processes}개, 스레드 {threads:N0}개",
Symbol: Symbols.Processor));
}
// Memory
if (showAll || q.Contains("mem") || q.Contains("ram") || q.Contains("메모리"))
{
var mem = new MEMORYSTATUSEX { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
GlobalMemoryStatusEx(ref mem);
var totalGB = mem.ullTotalPhys / (1024.0 * 1024 * 1024);
var usedGB = (mem.ullTotalPhys - mem.ullAvailPhys) / (1024.0 * 1024 * 1024);
var pct = mem.dwMemoryLoad;
items.Add(new LauncherItem(
$"메모리: {usedGB:F1}GB / {totalGB:F1}GB ({pct}% 사용)",
$"사용 가능: {mem.ullAvailPhys / (1024.0 * 1024 * 1024):F1}GB · Enter로 복사",
null, $"메모리: {usedGB:F1}GB / {totalGB:F1}GB ({pct}% 사용)",
Symbol: Symbols.Memory));
}
// Disk
if (showAll || q.Contains("disk") || q.Contains("디스크") || q.Contains("저장"))
{
foreach (var drive in System.IO.DriveInfo.GetDrives())
{
if (!drive.IsReady || drive.DriveType != System.IO.DriveType.Fixed) continue;
var totalGB = drive.TotalSize / (1024.0 * 1024 * 1024);
var freeGB = drive.AvailableFreeSpace / (1024.0 * 1024 * 1024);
var usedGB = totalGB - freeGB;
var pct = (int)(usedGB / totalGB * 100);
items.Add(new LauncherItem(
$"디스크 {drive.Name.TrimEnd('\\')} {usedGB:F0}GB / {totalGB:F0}GB ({pct}%)",
$"여유: {freeGB:F1}GB · {drive.DriveFormat} · Enter로 복사",
null, $"디스크 {drive.Name}: {usedGB:F0}GB / {totalGB:F0}GB ({pct}%), 여유 {freeGB:F1}GB",
Symbol: Symbols.Storage));
}
}
// Uptime
if (showAll || q.Contains("uptime") || q.Contains("가동"))
{
var uptime = TimeSpan.FromMilliseconds(Environment.TickCount64);
var uptimeStr = uptime.Days > 0
? $"{uptime.Days}일 {uptime.Hours}시간 {uptime.Minutes}분"
: $"{uptime.Hours}시간 {uptime.Minutes}분";
items.Add(new LauncherItem(
$"가동 시간: {uptimeStr}",
"마지막 재시작 이후 경과 · Enter로 복사",
null, $"가동 시간: {uptimeStr}",
Symbol: Symbols.Clock));
}
// Top processes by memory
if (showAll || q.Contains("top") || q.Contains("프로세스"))
{
try
{
var topProcs = Process.GetProcesses()
.Where(p => { try { return p.WorkingSet64 > 0; } catch (Exception) { return false; } })
.OrderByDescending(p => { try { return p.WorkingSet64; } catch (Exception) { return 0L; } })
.Take(5)
.Select(p =>
{
try { return $"{p.ProcessName} ({p.WorkingSet64 / (1024 * 1024)}MB)"; }
catch (Exception) { return p.ProcessName; }
});
items.Add(new LauncherItem(
"메모리 상위 프로세스",
string.Join(", ", topProcs),
null, $"메모리 상위: {string.Join(", ", topProcs)}",
Symbol: Symbols.Computer));
}
catch (Exception) { }
}
if (items.Count == 0)
{
items.Add(new LauncherItem(
$"'{q}'에 해당하는 리소스 항목 없음",
"cpu / mem / disk / uptime / top",
null, null, Symbol: Symbols.Warning));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text && !string.IsNullOrWhiteSpace(text))
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(text)); }
catch (Exception) { }
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,215 @@
using System.IO;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 빠른 메모 핸들러. "note" 프리픽스로 사용합니다.
/// 예: note → 최근 메모 10개 목록 (Enter로 클립보드 복사)
/// note 내일 회의 9시 → 메모 저장 (타임스탬프 자동 추가)
/// note clear → 전체 메모 삭제
/// 저장 위치: %APPDATA%\AxCopilot\notes.txt
/// </summary>
public class NoteHandler : IActionHandler
{
public string? Prefix => "note";
public PluginMetadata Metadata => new(
"Note",
"빠른 메모 — note 뒤에 내용 입력",
"1.0",
"AX");
private static readonly string NotesFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "notes.txt");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
// ─── 비어 있으면 최근 메모 목록 ──────────────────────────────────────
if (string.IsNullOrWhiteSpace(q))
{
var notes = ReadNotes();
if (!notes.Any())
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"메모가 없습니다",
"note 뒤에 내용을 입력하면 저장됩니다",
null, null, Symbol: Symbols.Note)
]);
}
var items = notes.Take(10).Select(n => new LauncherItem(
n.Content.Length > 60 ? n.Content[..57] + "…" : n.Content,
$"{n.SavedAt:yyyy-MM-dd HH:mm} · Enter 복사 · Delete 삭제",
null,
n.Content,
Symbol: Symbols.Note)).ToList();
// 전체 삭제 항목
items.Add(new LauncherItem(
"전체 메모 삭제",
$"총 {notes.Count}개 메모 모두 삭제 · Enter로 실행",
null,
"__CLEAR__",
Symbol: Symbols.Delete));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── "clear" 명령 ─────────────────────────────────────────────────
if (q.Equals("clear", StringComparison.OrdinalIgnoreCase))
{
var count = ReadNotes().Count;
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"전체 메모 삭제 ({count}개)",
"모든 메모를 삭제합니다 · Enter로 실행",
null,
"__CLEAR__",
Symbol: Symbols.Delete)
]);
}
// ─── 새 메모 저장 미리보기 ────────────────────────────────────────────
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"메모 저장: {(q.Length > 60 ? q[..57] + "…" : q)}",
$"{DateTime.Now:yyyy-MM-dd HH:mm} · Enter로 저장",
null,
new ValueTuple<string, string>("__SAVE__", q),
Symbol: Symbols.Note)
]);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
switch (item.Data)
{
// ValueTuple<string,string> 명시적 타입 매칭 (object? 패턴 안전하게)
case ValueTuple<string, string> t when t.Item1 == "__SAVE__":
SaveNote(t.Item2);
NotificationService.Notify("AX Copilot",
$"메모 저장됨: {(t.Item2.Length > 30 ? t.Item2[..27] + "" : t.Item2)}");
break;
case string text when text == "__CLEAR__":
ClearNotes();
break;
case string text:
try { Clipboard.SetText(text); } catch (Exception) { }
break;
}
return Task.CompletedTask;
}
// ─── 파일 I/O ──────────────────────────────────────────────────────────────
private static void SaveNote(string content)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(NotesFile)!);
var line = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {content}{Environment.NewLine}";
File.AppendAllText(NotesFile, line, System.Text.Encoding.UTF8);
}
catch (Exception ex)
{
LogService.Warn($"메모 저장 실패: {ex.Message}");
}
}
private static List<NoteEntry> ReadNotes()
{
var result = new List<NoteEntry>();
if (!File.Exists(NotesFile)) return result;
try
{
var lines = File.ReadAllLines(NotesFile, System.Text.Encoding.UTF8);
foreach (var line in lines.Reverse())
{
if (string.IsNullOrWhiteSpace(line)) continue;
// 형식: [yyyy-MM-dd HH:mm:ss] 내용
if (line.StartsWith('[') && line.Length > 21 && line[20] == ']')
{
if (DateTime.TryParse(line[1..20], out var dt))
{
result.Add(new NoteEntry(dt, line[22..].Trim()));
continue;
}
}
result.Add(new NoteEntry(DateTime.MinValue, line.Trim()));
}
}
catch (Exception ex)
{
LogService.Warn($"메모 읽기 실패: {ex.Message}");
}
return result;
}
private static void ClearNotes()
{
try
{
if (File.Exists(NotesFile))
File.Delete(NotesFile);
}
catch (Exception ex)
{
LogService.Warn($"메모 삭제 실패: {ex.Message}");
}
}
/// <summary>
/// 특정 메모 1건 삭제. content가 일치하는 가장 최근 항목을 제거합니다.
/// </summary>
public static bool DeleteNote(string content)
{
try
{
if (!File.Exists(NotesFile)) return false;
var lines = File.ReadAllLines(NotesFile, System.Text.Encoding.UTF8).ToList();
// 뒤에서부터 찾아 가장 최근 일치 항목 제거
for (int i = lines.Count - 1; i >= 0; i--)
{
var line = lines[i];
if (string.IsNullOrWhiteSpace(line)) continue;
string extracted;
if (line.StartsWith('[') && line.Length > 21 && line[20] == ']')
extracted = line[22..].Trim();
else
extracted = line.Trim();
if (extracted == content)
{
lines.RemoveAt(i);
File.WriteAllLines(NotesFile, lines, System.Text.Encoding.UTF8);
return true;
}
}
}
catch (Exception ex)
{
LogService.Warn($"메모 개별 삭제 실패: {ex.Message}");
}
return false;
}
}
internal record NoteEntry(DateTime SavedAt, string Content);

View File

@@ -0,0 +1,229 @@
using System.Diagnostics;
using System.Net.NetworkInformation;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 포트/프로세스 점검 핸들러. "port " 프리픽스로 사용합니다.
/// 예: port → 활성 TCP 연결 목록
/// port 8080 → 8080 포트를 점유 중인 프로세스 상세
/// port chrome → chrome이 사용하는 포트 목록
/// </summary>
public class PortHandler : IActionHandler
{
public string? Prefix => "port";
public PluginMetadata Metadata => new(
"PortChecker",
"포트 & 프로세스 점검 — port 뒤에 포트번호 또는 프로세스명",
"1.0",
"AX");
// 프로세스 이름 캐시 (PID → 이름), 5초 유효
private static readonly Dictionary<int, string> _procCache = new();
// netstat 결과 캐시 (포트 → PID), 5초 유효 — netstat 단일 실행으로 N+1 해결
private static readonly Dictionary<int, int> _pidMap = new();
private static DateTime _cacheExpiry = DateTime.MinValue;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
RefreshProcessCache();
TcpConnectionInformation[] tcpConns;
try
{
var props = IPGlobalProperties.GetIPGlobalProperties();
tcpConns = props.GetActiveTcpConnections();
}
catch (Exception ex)
{
LogService.Warn($"포트 조회 실패: {ex.Message}");
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("네트워크 정보를 가져올 수 없습니다", ex.Message, null, null, Symbol: Symbols.Warning)
]);
}
var q = query.Trim();
// ─── 빈 쿼리: 활성 연결 상위 목록 ────────────────────────────────────
if (string.IsNullOrWhiteSpace(q))
{
var items = tcpConns
.Where(c => c.State == TcpState.Established || c.State == TcpState.Listen)
.OrderBy(c => c.LocalEndPoint.Port)
.Take(20)
.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
var state = c.State == TcpState.Listen ? "LISTEN" : "ESTABLISHED";
return new LauncherItem(
$":{c.LocalEndPoint.Port} → {c.RemoteEndPoint}",
$"{state} · {procName} · Enter로 포트번호 복사",
null,
c.LocalEndPoint.Port.ToString(),
Symbol: Symbols.Network);
})
.ToList<LauncherItem>();
if (!items.Any())
items.Add(new LauncherItem("활성 연결 없음", "TCP 연결이 감지되지 않았습니다", null, null, Symbol: Symbols.Network));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// ─── 숫자: 포트 번호 검색 ─────────────────────────────────────────────
if (int.TryParse(q, out var portNum))
{
var matches = tcpConns
.Where(c => c.LocalEndPoint.Port == portNum || c.RemoteEndPoint.Port == portNum)
.ToList();
if (!matches.Any())
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"포트 {portNum} — 사용 중 아님",
"해당 포트를 사용하는 TCP 연결이 없습니다",
null,
portNum.ToString(),
Symbol: Symbols.Info)
]);
}
var result = matches.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
var pid = GetPidForPort(c.LocalEndPoint.Port);
return new LauncherItem(
$":{c.LocalEndPoint.Port} ←→ {c.RemoteEndPoint}",
$"{c.State} · {procName} (PID {pid}) · Enter로 PID 복사",
null,
pid > 0 ? pid.ToString() : portNum.ToString(),
Symbol: Symbols.Network);
}).ToList<LauncherItem>();
return Task.FromResult<IEnumerable<LauncherItem>>(result);
}
// ─── 문자열: 프로세스명 검색 ──────────────────────────────────────────
var procLower = q.ToLowerInvariant();
var procPorts = tcpConns
.Where(c =>
{
var name = GetProcessNameForPort(c.LocalEndPoint.Port).ToLowerInvariant();
return name.Contains(procLower);
})
.Take(15)
.Select(c =>
{
var procName = GetProcessNameForPort(c.LocalEndPoint.Port);
return new LauncherItem(
$"{procName} : {c.LocalEndPoint.Port} → {c.RemoteEndPoint}",
$"{c.State} · Enter로 포트번호 복사",
null,
c.LocalEndPoint.Port.ToString(),
Symbol: Symbols.Network);
})
.ToList<LauncherItem>();
if (!procPorts.Any())
procPorts.Add(new LauncherItem(
$"'{q}' — 연결 없음",
"해당 프로세스의 TCP 연결이 없습니다",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(procPorts);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string text)
{
try { System.Windows.Clipboard.SetText(text); } catch (Exception) { }
}
return Task.CompletedTask;
}
// ─── 프로세스/PID 캐시 헬퍼 ──────────────────────────────────────────────────
/// <summary>
/// 프로세스 목록 + netstat PID 맵을 한 번에 갱신 (5초 캐시).
/// GetItemsAsync 진입 시 한 번만 호출하여 N+1 netstat 실행 방지.
/// </summary>
private static void RefreshProcessCache()
{
if (DateTime.Now < _cacheExpiry) return;
_procCache.Clear();
_pidMap.Clear();
// ① 프로세스 목록 (PID → 이름)
try
{
foreach (var p in Process.GetProcesses())
{
try { _procCache[p.Id] = p.ProcessName; }
catch (Exception) { }
}
}
catch (Exception ex)
{
LogService.Warn($"프로세스 목록 갱신 실패: {ex.Message}");
}
// ② netstat -ano 단 1회 실행 → 포트→PID 전체 맵 구축
try
{
var psi = new ProcessStartInfo("netstat", "-ano")
{
RedirectStandardOutput = true,
UseShellExecute = false,
CreateNoWindow = true
};
using var proc = Process.Start(psi);
if (proc != null)
{
var output = proc.StandardOutput.ReadToEnd();
proc.WaitForExit(2000);
foreach (var line in output.Split('\n'))
{
var parts = line.Trim().Split([' ', '\t'], StringSplitOptions.RemoveEmptyEntries);
// 형식: Proto Local Address Foreign Address State PID
if (parts.Length < 5) continue;
if (!int.TryParse(parts[^1], out var pid)) continue;
// Local Address에서 포트 추출 (예: 0.0.0.0:8080 또는 [::]:8080)
var localAddr = parts[1];
var colonIdx = localAddr.LastIndexOf(':');
if (colonIdx >= 0 && int.TryParse(localAddr[(colonIdx + 1)..], out var port))
_pidMap.TryAdd(port, pid);
}
}
}
catch (Exception ex)
{
LogService.Warn($"netstat 실행 실패: {ex.Message}");
}
_cacheExpiry = DateTime.Now.AddSeconds(5);
}
private static string GetProcessNameForPort(int port)
{
var pid = GetPidForPort(port);
return pid > 0 && _procCache.TryGetValue(pid, out var name) ? name : "알 수 없음";
}
/// <summary>
/// 캐시된 pidMap에서 즉시 반환 — netstat를 추가로 실행하지 않음.
/// </summary>
private static int GetPidForPort(int port)
=> _pidMap.TryGetValue(port, out var pid) ? pid : -1;
}

View File

@@ -0,0 +1,127 @@
using System.Diagnostics;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 실행 중인 프로세스를 검색하고 종료합니다. "kill " 프리픽스로 사용합니다.
/// 예: kill chrome → Chrome 프로세스 목록 표시 후 선택하면 종료
/// kill → 현재 실행 중인 모든 사용자 프로세스 표시
/// </summary>
public class ProcessHandler : IActionHandler
{
public string? Prefix => "kill "; // 뒤에 공백 포함 — 오탐 방지
public PluginMetadata Metadata => new(
"ProcessKiller",
"프로세스 종료 — kill 뒤에 프로세스명 입력",
"1.0",
"AX");
// 시스템 핵심 프로세스 보호 목록 (종료 방지)
private static readonly HashSet<string> ProtectedProcesses = new(StringComparer.OrdinalIgnoreCase)
{
"system", "smss", "csrss", "wininit", "winlogon", "services", "lsass",
"svchost", "explorer", "dwm", "fontdrvhost", "spoolsv", "registry",
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(query))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"종료할 프로세스명을 입력하세요",
"예: kill chrome · kill notepad · kill explorer",
null, null, Symbol: Symbols.Power)
]);
}
var q = query.Trim().ToLowerInvariant();
try
{
var processes = Process.GetProcesses()
.Where(p =>
!ProtectedProcesses.Contains(p.ProcessName) &&
p.ProcessName.ToLowerInvariant().Contains(q))
.OrderBy(p => p.ProcessName)
.Take(12)
.ToList();
if (processes.Count == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"'{query}' 프로세스를 찾을 수 없습니다",
"실행 중인 프로세스가 없거나 이름이 다릅니다",
null, null, Symbol: Symbols.Warning)
]);
}
// 같은 이름의 프로세스 묶기
var grouped = processes
.GroupBy(p => p.ProcessName, StringComparer.OrdinalIgnoreCase)
.Select(g =>
{
var pids = g.Select(p => p.Id).ToList();
var memMb = g.Sum(p =>
{
try { return p.WorkingSet64 / 1024 / 1024; }
catch (Exception) { return 0L; }
});
var title = g.Count() > 1
? $"{g.Key} ({g.Count()}개 인스턴스)"
: g.Key;
var subtitle = $"PID: {string.Join(", ", pids)} · 메모리: {memMb} MB · Enter로 종료";
return new LauncherItem(
title,
subtitle,
null,
new ProcessKillData(g.Key, pids),
Symbol: Symbols.Power);
})
.ToList();
return Task.FromResult<IEnumerable<LauncherItem>>(grouped);
}
catch (Exception ex)
{
LogService.Warn($"프로세스 목록 조회 실패: {ex.Message}");
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem("프로세스 목록 조회 실패", ex.Message, null, null, Symbol: Symbols.Error)
]);
}
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not ProcessKillData data) return Task.CompletedTask;
int killed = 0, failed = 0;
foreach (var pid in data.Pids)
{
try
{
var proc = Process.GetProcessById(pid);
proc.Kill(entireProcessTree: false);
killed++;
}
catch (Exception)
{
failed++;
}
}
LogService.Info($"프로세스 종료: {data.Name} — {killed}개 성공, {failed}개 실패");
return Task.CompletedTask;
}
private record ProcessKillData(string Name, List<int> Pids);
}

View File

@@ -0,0 +1,127 @@
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Phase L3-4: 파라미터 퀵링크 핸들러. "ql" 예약어로 사용합니다.
/// 예: ql maps 강남역 → "maps" 키워드 URL에 "강남역" 치환 후 열기
/// ql jira PROJ-1234 → "jira" 키워드 URL에 티켓 번호 치환
/// ql (목록) → 등록된 퀵링크 목록 표시
///
/// 퀵링크는 설정 → 일반 → 퀵링크 탭에서 등록합니다.
/// </summary>
public class QuickLinkHandler : IActionHandler
{
private readonly SettingsService _settings;
public string? Prefix => "ql";
public PluginMetadata Metadata => new(
"QuickLink",
"파라미터 퀵링크 — ql [키워드] [인자]",
"1.0",
"AX");
public QuickLinkHandler(SettingsService settings) => _settings = settings;
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var links = _settings.Settings.QuickLinks;
// 등록된 퀵링크 없음
if (links.Count == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"등록된 퀵링크 없음",
"설정 → 일반 → 퀵링크에서 추가하세요. 예: keyword=maps, url=https://map.naver.com/p/search/{0}",
null, null, Symbol: Symbols.Globe)
]);
}
var items = new List<LauncherItem>();
var parts = query.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0)
{
// 쿼리 없음 — 전체 목록 표시
foreach (var link in links)
{
items.Add(new LauncherItem(
link.Name.Length > 0 ? link.Name : link.Keyword,
$"ql {link.Keyword} [인자] · {link.Description} · {link.UrlTemplate}",
null, null, Symbol: Symbols.Globe));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
var keyword = parts[0].ToLowerInvariant();
var argQuery = parts.Length > 1 ? parts[1] : "";
// 키워드로 정확 일치 검색
var matched = links.Where(l => l.Keyword.ToLowerInvariant() == keyword).ToList();
if (matched.Count > 0 && !string.IsNullOrWhiteSpace(argQuery))
{
// 인자가 있으면 URL 치환 후 실행 항목 생성
foreach (var link in matched)
{
var url = UrlTemplateEngine.ExpandFromQuery(link.UrlTemplate, argQuery);
items.Add(new LauncherItem(
$"{(link.Name.Length > 0 ? link.Name : link.Keyword)}: {argQuery}",
url,
null, url, Symbol: Symbols.Globe));
}
}
else
{
// 키워드 퍼지 검색 (부분 일치)
var fuzzy = links
.Where(l => l.Keyword.Contains(keyword, StringComparison.OrdinalIgnoreCase) ||
l.Name.Contains(keyword, StringComparison.OrdinalIgnoreCase))
.ToList();
if (fuzzy.Count == 0)
{
items.Add(new LauncherItem(
$"'{keyword}'에 해당하는 퀵링크 없음",
"설정에서 새 퀵링크를 추가하세요",
null, null, Symbol: Symbols.Globe));
}
else
{
foreach (var link in fuzzy)
{
var hint = UrlTemplateEngine.GetPlaceholders(link.UrlTemplate);
var ph = hint.Count > 0 ? $" · 인자: {string.Join(", ", hint)}" : "";
items.Add(new LauncherItem(
$"ql {link.Keyword}{ph}",
link.Description.Length > 0 ? link.Description : link.UrlTemplate,
null, null, Symbol: Symbols.Globe));
}
}
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string url && !string.IsNullOrWhiteSpace(url))
{
try
{
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"퀵링크 열기 실패: {ex.Message}");
}
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,141 @@
using System.IO;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 최근 파일 핸들러. "recent" 프리픽스로 사용합니다.
/// Windows Recent 폴더(%APPDATA%\Microsoft\Windows\Recent)의 .lnk 파일을
/// 최근 수정 순으로 나열합니다.
/// 예: recent → 최근 20개 파일 목록
/// recent 보고서 → 이름에 "보고서" 포함 파일 필터
/// </summary>
public class RecentFilesHandler : IActionHandler
{
public string? Prefix => "recent";
public PluginMetadata Metadata => new(
"RecentFiles",
"최근 파일 — recent 뒤에 검색어 입력",
"1.0",
"AX");
private static readonly string RecentFolder = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
@"Microsoft\Windows\Recent");
// 간단한 캐시: 10초간 유효
private static (DateTime At, List<(string Name, string LinkPath, DateTime Modified)> Files)? _cache;
private static readonly TimeSpan CacheTtl = TimeSpan.FromSeconds(10);
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
// 힌트만 표시
}
var files = GetRecentFiles();
IEnumerable<(string Name, string LinkPath, DateTime Modified)> filtered = files;
if (!string.IsNullOrWhiteSpace(q))
{
filtered = files.Where(f =>
f.Name.Contains(q, StringComparison.OrdinalIgnoreCase));
}
var items = filtered.Take(20).Select(f => new LauncherItem(
f.Name,
$"{f.Modified:yyyy-MM-dd HH:mm} · Enter로 열기",
null,
f.LinkPath,
Symbol: GetSymbol(f.Name))).ToList();
if (!items.Any())
{
items.Add(new LauncherItem(
string.IsNullOrWhiteSpace(q) ? "최근 파일 없음" : "검색 결과 없음",
string.IsNullOrWhiteSpace(q)
? "Windows Recent 폴더가 비어 있습니다"
: $"'{q}' 파일을 최근 목록에서 찾을 수 없습니다",
null, null, Symbol: Symbols.Info));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is string linkPath && File.Exists(linkPath))
{
try
{
System.Diagnostics.Process.Start(
new System.Diagnostics.ProcessStartInfo(linkPath)
{ UseShellExecute = true });
}
catch (Exception ex)
{
LogService.Warn($"최근 파일 열기 실패: {ex.Message}");
}
}
return Task.CompletedTask;
}
// ─── 내부 ─────────────────────────────────────────────────────────────────
private static List<(string Name, string LinkPath, DateTime Modified)> GetRecentFiles()
{
// 캐시 유효 확인
if (_cache.HasValue && (DateTime.Now - _cache.Value.At) < CacheTtl)
return _cache.Value.Files;
var result = new List<(string, string, DateTime)>();
try
{
if (!Directory.Exists(RecentFolder))
return result;
var lnkFiles = Directory
.GetFiles(RecentFolder, "*.lnk")
.Select(p => (Path: p, Info: new FileInfo(p)))
.OrderByDescending(f => f.Info.LastWriteTime)
.Take(100)
.ToList();
foreach (var (path, info) in lnkFiles)
{
var name = Path.GetFileNameWithoutExtension(info.Name);
result.Add((name, path, info.LastWriteTime));
}
}
catch (Exception ex)
{
LogService.Warn($"최근 파일 목록 읽기 실패: {ex.Message}");
}
_cache = (DateTime.Now, result);
return result;
}
private static string GetSymbol(string name)
{
var ext = Path.GetExtension(name).ToLowerInvariant();
return ext switch
{
".exe" or ".msi" => Symbols.App,
".xlsx" or ".xls" or ".csv" => Symbols.File,
".docx" or ".doc" => Symbols.File,
".pptx" or ".ppt" => Symbols.File,
".pdf" => Symbols.File,
".txt" or ".md" or ".log" => Symbols.Text,
".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".bmp" => Symbols.Picture,
_ => Symbols.File
};
}
}

View File

@@ -0,0 +1,189 @@
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 파일 일괄 이름변경 핸들러. "rename" 프리픽스로 사용합니다.
/// 지정된 폴더 내 파일을 패턴으로 일괄 이름변경합니다.
/// 예: rename → 사용법 안내
/// rename C:\work\*.xlsx → 해당 폴더의 xlsx 파일 목록
/// rename C:\work\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ...
/// {n}=순번, {date}=오늘 날짜, {orig}=원본명
/// Enter → 실행 전 미리보기, Shift+Enter → 실행.
/// </summary>
public class RenameHandler : IActionHandler
{
public string? Prefix => "rename";
public PluginMetadata Metadata => new(
"Rename",
"파일 일괄 이름변경 — rename",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrWhiteSpace(q))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"파일 일괄 이름변경",
"rename [폴더\\패턴] [ 릿]",
null, null, Symbol: Symbols.Rename),
new LauncherItem(
"사용 예시",
"rename C:\\work\\*.xlsx 보고서_{n} → 보고서_1.xlsx, 보고서_2.xlsx ...",
null, null, Symbol: Symbols.Info),
new LauncherItem(
"변수: {n} 순번, {date} 날짜, {orig} 원본명",
"rename D:\\photos\\*.jpg {date}_{n} → 2026-03-27_1.jpg ...",
null, null, Symbol: Symbols.Info),
]);
}
// 파싱: [경로\패턴] [템플릿]
var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries);
var pattern = parts[0];
var template = parts.Length > 1 ? parts[1] : null;
// 경로 분리
var dir = Path.GetDirectoryName(pattern);
var glob = Path.GetFileName(pattern);
if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"폴더를 찾을 수 없습니다",
$"경로: {dir ?? "(비어 있음)"}",
null, null, Symbol: Symbols.Warning)
]);
}
string[] files;
try { files = Directory.GetFiles(dir, glob); }
catch (Exception) { files = Array.Empty<string>(); }
if (files.Length == 0)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"일치하는 파일이 없습니다",
$"패턴: {glob} · 폴더: {dir}",
null, null, Symbol: Symbols.Warning)
]);
}
Array.Sort(files);
// 템플릿이 없으면 파일 목록만 표시
if (string.IsNullOrWhiteSpace(template))
{
var items = files.Take(10).Select((f, i) => new LauncherItem(
Path.GetFileName(f),
dir,
null, null,
Symbol: Symbols.File)).ToList<LauncherItem>();
items.Insert(0, new LauncherItem(
$"총 {files.Length}개 파일 발견",
"뒤에 새 이름 템플릿을 추가하세요 (예: 보고서_{n})",
null, null, Symbol: Symbols.Info));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 미리보기 생성
var today = DateTime.Today.ToString("yyyy-MM-dd");
var previews = new List<(string From, string To)>();
for (int i = 0; i < files.Length; i++)
{
var origName = Path.GetFileNameWithoutExtension(files[i]);
var ext = Path.GetExtension(files[i]);
var newName = template
.Replace("{n}", (i + 1).ToString())
.Replace("{date}", today)
.Replace("{orig}", origName);
// 확장자 자동 유지 (템플릿에 확장자가 없으면)
if (!Path.HasExtension(newName))
newName += ext;
previews.Add((Path.GetFileName(files[i]), newName));
}
var result = new List<LauncherItem>();
// 실행 항목
result.Add(new LauncherItem(
$"총 {files.Length}개 파일 이름변경 실행",
$"Enter로 실행 · {previews[0].From} → {previews[0].To} ...",
null, ValueTuple.Create(dir, files, previews.Select(p => p.To).ToArray()),
Symbol: Symbols.Rename));
// 미리보기 (최대 8개)
foreach (var (from, to) in previews.Take(8))
{
result.Add(new LauncherItem(
$"{from} → {to}",
"미리보기",
null, null,
Symbol: Symbols.File));
}
if (files.Length > 8)
{
result.Add(new LauncherItem(
$"... 외 {files.Length - 8}개",
"",
null, null,
Symbol: Symbols.Info));
}
return Task.FromResult<IEnumerable<LauncherItem>>(result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not ValueTuple<string, string[], string[]> data)
return Task.CompletedTask;
var (dir, files, newNames) = data;
int renamed = 0;
int failed = 0;
for (int i = 0; i < files.Length && i < newNames.Length; i++)
{
try
{
var dest = Path.Combine(dir, newNames[i]);
if (File.Exists(dest))
{
failed++;
continue;
}
File.Move(files[i], dest);
renamed++;
}
catch (Exception)
{
failed++;
}
}
var msg = failed > 0
? $"{renamed}개 이름변경 완료, {failed}개 실패 (이미 존재하거나 접근 불가)"
: $"{renamed}개 파일 이름변경 완료";
NotificationService.Notify("AX Copilot", msg);
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,186 @@
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 루틴 자동화 핸들러. "routine" 프리픽스로 사용합니다.
/// 등록된 루틴을 실행하면 앱·폴더·URL을 순서대로 일괄 실행합니다.
/// 예: routine → 등록된 루틴 목록
/// routine morning → "morning" 루틴 실행
/// routine add morning → 루틴 추가 안내
/// 루틴 정의: %APPDATA%\AxCopilot\routines.json
/// </summary>
public class RoutineHandler : IActionHandler
{
public string? Prefix => "routine";
public PluginMetadata Metadata => new(
"Routine",
"루틴 자동화 — routine",
"1.0",
"AX");
private static readonly string RoutineFile = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "routines.json");
private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true, PropertyNameCaseInsensitive = true };
// 내장 기본 루틴
private static readonly RoutineDefinition[] BuiltInRoutines =
[
new("morning", "출근 루틴", [
new("app", "explorer.exe", "파일 탐색기"),
new("info", "info", "시스템 정보 표시"),
]),
new("endofday", "퇴근 루틴", [
new("cmd", "journal", "오늘 업무 일지 생성"),
]),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var all = LoadRoutines();
if (string.IsNullOrWhiteSpace(q))
{
var items = new List<LauncherItem>
{
new("루틴 자동화",
$"총 {all.Count}개 루틴 · 이름 입력 시 실행 · routines.json에서 편집",
null, null, Symbol: Symbols.Info)
};
foreach (var r in all)
{
var steps = string.Join(" → ", r.Steps.Select(s => s.Label));
items.Add(new LauncherItem(
$"[{r.Name}] {r.Description}",
$"{r.Steps.Length}단계: {steps} · Enter로 실행",
null, r,
Symbol: Symbols.Lightbulb));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 루틴 검색/실행
var match = all.FirstOrDefault(r =>
r.Name.Equals(q, StringComparison.OrdinalIgnoreCase));
if (match != null)
{
var steps = string.Join(" → ", match.Steps.Select(s => s.Label));
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"[{match.Name}] ",
$"{match.Description} · {steps}",
null, match,
Symbol: Symbols.Lightbulb)
]);
}
// 부분 매칭
var filtered = all.Where(r =>
r.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
r.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
var result = filtered.Select(r => new LauncherItem(
$"[{r.Name}] {r.Description}",
$"{r.Steps.Length}단계 · Enter로 실행",
null, r,
Symbol: Symbols.Lightbulb)).ToList<LauncherItem>();
if (!result.Any())
{
result.Add(new LauncherItem(
$"'{q}' 루틴 없음",
$"routines.json에서 직접 추가하거나 routine으로 목록 확인",
null, null, Symbol: Symbols.Warning));
}
return Task.FromResult<IEnumerable<LauncherItem>>(result);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not RoutineDefinition routine) return;
int executed = 0;
foreach (var step in routine.Steps)
{
try
{
switch (step.Type.ToLowerInvariant())
{
case "app":
case "url":
case "folder":
Process.Start(new ProcessStartInfo(step.Target) { UseShellExecute = true });
break;
case "cmd":
// PowerShell 명령 실행
Process.Start(new ProcessStartInfo("powershell.exe", $"-Command \"{step.Target}\"")
{ UseShellExecute = false, CreateNoWindow = true });
break;
case "info":
// 알림으로 대체
NotificationService.Notify("루틴", step.Label);
break;
}
executed++;
await Task.Delay(300, ct); // 앱 간 간격
}
catch (Exception ex)
{
LogService.Warn($"루틴 단계 실행 실패: {step.Label} — {ex.Message}");
}
}
NotificationService.Notify("루틴 완료", $"[{routine.Name}] {executed}/{routine.Steps.Length}단계 실행 완료");
}
private List<RoutineDefinition> LoadRoutines()
{
var list = new List<RoutineDefinition>(BuiltInRoutines);
try
{
if (File.Exists(RoutineFile))
{
var json = File.ReadAllText(RoutineFile);
var user = JsonSerializer.Deserialize<List<RoutineDefinition>>(json, JsonOpts);
if (user != null)
{
// 사용자 루틴이 내장 루틴을 오버라이드
foreach (var r in user)
{
list.RemoveAll(x => x.Name.Equals(r.Name, StringComparison.OrdinalIgnoreCase));
list.Add(r);
}
}
}
}
catch (Exception ex) { LogService.Warn($"루틴 로드 실패: {ex.Message}"); }
return list;
}
internal record RoutineDefinition(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("steps")] RoutineStep[] Steps);
internal record RoutineStep(
[property: JsonPropertyName("type")] string Type,
[property: JsonPropertyName("target")] string Target,
[property: JsonPropertyName("label")] string Label);
}

View File

@@ -0,0 +1,96 @@
using System.Diagnostics;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Themes;
using AxCopilot.Views;
namespace AxCopilot.Handlers;
/// <summary>
/// Windows 실행(Run) 핸들러. "^" 프리픽스로 사용합니다.
/// Windows 실행 창(Win+R)에서 입력하는 것과 동일하게 작동합니다.
/// 예: ^ notepad → 메모장 실행
/// ^ cmd → 명령 프롬프트
/// ^ calc → 계산기
/// ^ mspaint → 그림판
/// ^ control → 제어판
/// </summary>
public class RunHandler : IActionHandler
{
public string? Prefix => "^";
public PluginMetadata Metadata => new(
"Run",
"Windows 실행 명령",
"1.0",
"AX Copilot");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
if (string.IsNullOrEmpty(q))
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
"Windows 실행 명령",
"Win+R 실행 창과 동일 · 명령어 입력 후 Enter",
null, null,
Symbol: Symbols.LaunchIcon),
new LauncherItem(
"^ notepad",
"메모장 실행",
null, null, Symbol: Symbols.Info),
new LauncherItem(
"^ cmd",
"명령 프롬프트",
null, null, Symbol: Symbols.Info),
new LauncherItem(
"^ control",
"제어판",
null, null, Symbol: Symbols.Info),
new LauncherItem(
"^ calc",
"계산기",
null, null, Symbol: Symbols.Info),
]);
}
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"실행: {q}",
"Enter → Windows 실행 명령으로 실행",
null, $"__RUN__{q}",
Symbol: Symbols.LaunchIcon)
]);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
var data = item.Data as string;
if (data == null || !data.StartsWith("__RUN__")) return Task.CompletedTask;
var cmd = data["__RUN__".Length..].Trim();
if (string.IsNullOrEmpty(cmd)) return Task.CompletedTask;
try
{
Process.Start(new ProcessStartInfo(cmd)
{
UseShellExecute = true
})?.Dispose();
}
catch (Exception ex)
{
CustomMessageBox.Show(
$"실행 실패: {ex.Message}",
"AX Copilot",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
return Task.CompletedTask;
}
}

View File

@@ -0,0 +1,237 @@
using System.IO;
using System.Text.Json;
using System.Text.Json.Serialization;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 프로젝트 스캐폴딩 핸들러. "scaffold" 프리픽스로 사용합니다.
/// 로컬 템플릿으로 프로젝트 폴더 구조를 일괄 생성합니다.
/// 예: scaffold → 등록된 템플릿 목록
/// scaffold webapi → "webapi" 템플릿 적용
/// scaffold add [이름] → 현재 폴더 구조를 템플릿으로 저장 (별도 도구에서)
/// 템플릿 저장 위치: %APPDATA%\AxCopilot\templates\[이름].json
/// Enter → 대상 경로를 물어본 후 생성.
/// </summary>
public class ScaffoldHandler : IActionHandler
{
public string? Prefix => "scaffold";
public PluginMetadata Metadata => new(
"Scaffold",
"프로젝트 스캐폴딩 — scaffold",
"1.0",
"AX");
private static readonly string TemplateDir = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "templates");
// 내장 기본 템플릿
private static readonly ScaffoldTemplate[] BuiltInTemplates =
[
new("webapi", "Web API 프로젝트", new[]
{
"src/Controllers/",
"src/Models/",
"src/Services/",
"src/Middleware/",
"tests/",
"docs/",
"README.md",
".gitignore",
}),
new("console", "콘솔 애플리케이션", new[]
{
"src/",
"src/Core/",
"src/Services/",
"tests/",
"README.md",
}),
new("wpf", "WPF 데스크톱 앱", new[]
{
"src/Views/",
"src/ViewModels/",
"src/Models/",
"src/Services/",
"src/Themes/",
"src/Assets/",
"tests/",
"docs/",
}),
new("data", "데이터 파이프라인", new[]
{
"src/Extractors/",
"src/Transformers/",
"src/Loaders/",
"config/",
"scripts/",
"tests/",
"data/input/",
"data/output/",
"README.md",
}),
new("docs", "문서 프로젝트", new[]
{
"docs/",
"images/",
"templates/",
"README.md",
"CHANGELOG.md",
}),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
// 사용자 정의 템플릿 로드
var userTemplates = LoadUserTemplates();
var allTemplates = BuiltInTemplates.Concat(userTemplates).ToList();
if (string.IsNullOrWhiteSpace(q))
{
var items = allTemplates.Select(t => new LauncherItem(
$"[{t.Name}] {t.Description}",
$"{t.Paths.Length}개 폴더/파일 · Enter → 대상 경로 입력 후 생성",
null, t,
Symbol: Symbols.Folder)).ToList<LauncherItem>();
items.Insert(0, new LauncherItem(
"프로젝트 스캐폴딩",
$"총 {allTemplates.Count}개 템플릿 · 이름을 입력해 필터링",
null, null, Symbol: Symbols.Info));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// "경로 템플릿" 형태 감지 (경로에 \ 또는 / 포함)
if (q.Contains('\\') || q.Contains('/'))
{
var spaceIdx = q.LastIndexOf(' ');
if (spaceIdx > 0)
{
var targetPath = q[..spaceIdx].Trim();
var templateName = q[(spaceIdx + 1)..].Trim();
var tmpl = allTemplates.FirstOrDefault(t =>
t.Name.Equals(templateName, StringComparison.OrdinalIgnoreCase));
if (tmpl != null)
{
return Task.FromResult<IEnumerable<LauncherItem>>(
[
new LauncherItem(
$"[{tmpl.Name}] {targetPath}",
$"{tmpl.Paths.Length}개 폴더/파일 생성 · Enter로 실행",
null, ValueTuple.Create(targetPath, tmpl),
Symbol: Symbols.Save)
]);
}
}
}
// 필터링
var filtered = allTemplates.Where(t =>
t.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
t.Description.Contains(q, StringComparison.OrdinalIgnoreCase));
var result = filtered.Select(t =>
{
var preview = string.Join(", ", t.Paths.Take(4));
if (t.Paths.Length > 4) preview += $" ... (+{t.Paths.Length - 4})";
return new LauncherItem(
$"[{t.Name}] {t.Description}",
$"{preview} · 사용법: scaffold [대상경로] {t.Name}",
null, t,
Symbol: Symbols.Folder);
}).ToList<LauncherItem>();
if (!result.Any())
{
result.Add(new LauncherItem(
$"'{q}'에 해당하는 템플릿 없음",
"scaffold 으로 전체 목록 확인",
null, null, Symbol: Symbols.Warning));
}
return Task.FromResult<IEnumerable<LauncherItem>>(result);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ValueTuple<string, ScaffoldTemplate> data)
{
var (targetPath, template) = data;
return CreateStructure(targetPath, template);
}
if (item.Data is ScaffoldTemplate tmpl)
{
// 클립보드에 사용법 복사
var usage = $"scaffold [대상경로] {tmpl.Name}";
try { System.Windows.Application.Current?.Dispatcher.Invoke(
() => System.Windows.Clipboard.SetText(usage)); }
catch (Exception) { }
}
return Task.CompletedTask;
}
private static Task CreateStructure(string basePath, ScaffoldTemplate template)
{
try
{
int created = 0;
foreach (var path in template.Paths)
{
var fullPath = Path.Combine(basePath, path.Replace('/', Path.DirectorySeparatorChar));
if (path.EndsWith('/') || path.EndsWith('\\') || !Path.HasExtension(path))
{
Directory.CreateDirectory(fullPath);
}
else
{
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
if (!File.Exists(fullPath))
File.WriteAllText(fullPath, "");
}
created++;
}
NotificationService.Notify("스캐폴딩 완료",
$"[{template.Name}] {created}개 항목 생성 → {basePath}");
}
catch (Exception ex)
{
LogService.Error($"스캐폴딩 실패: {ex.Message}");
NotificationService.Notify("AX Copilot", $"스캐폴딩 실패: {ex.Message}");
}
return Task.CompletedTask;
}
private static IEnumerable<ScaffoldTemplate> LoadUserTemplates()
{
if (!Directory.Exists(TemplateDir)) yield break;
foreach (var file in Directory.GetFiles(TemplateDir, "*.json"))
{
ScaffoldTemplate? tmpl = null;
try
{
var json = File.ReadAllText(file);
tmpl = JsonSerializer.Deserialize<ScaffoldTemplate>(json);
}
catch (Exception) { }
if (tmpl != null) yield return tmpl;
}
}
internal record ScaffoldTemplate(
[property: JsonPropertyName("name")] string Name,
[property: JsonPropertyName("description")] string Description,
[property: JsonPropertyName("paths")] string[] Paths);
}

View File

@@ -0,0 +1,637 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Windows.Media.Imaging;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 화면 캡처 핸들러. "cap" 프리픽스로 사용합니다.
///
/// 예: cap → 전체 화면 캡처
/// cap screen → 전체 화면 캡처
/// cap window → 런처 호출 전 활성 창 캡처
/// cap scroll → 활성 창 스크롤 캡처 (페이지 전체)
/// cap region → 마우스로 영역 선택 후 캡처
/// 파일 저장 여부 / 경로는 설정 → 캡처 탭에서 변경 가능.
/// 기본값: 저장 안 함, 클립보드에만 복사.
/// </summary>
public class ScreenCaptureHandler : IActionHandler
{
private readonly AxCopilot.Services.SettingsService _settings;
public ScreenCaptureHandler(AxCopilot.Services.SettingsService settings)
{
_settings = settings;
}
public string? Prefix => string.IsNullOrWhiteSpace(_settings.Settings.ScreenCapture.Prefix)
? "cap"
: _settings.Settings.ScreenCapture.Prefix.Trim();
public PluginMetadata Metadata => new(
"ScreenCapture",
"화면 캡처 — cap screen/window/scroll/region",
"1.0",
"AX");
// ─── P/Invoke ──────────────────────────────────────────────────────────────
[DllImport("user32.dll")] private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
[DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] private static extern bool PrintWindow(IntPtr hwnd, IntPtr hdcBlt, uint nFlags);
[DllImport("user32.dll")] private static extern bool IsWindowVisible(IntPtr hWnd);
// 스크롤/키 메시지 전송
[DllImport("user32.dll")] private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")] private static extern IntPtr FindWindowEx(IntPtr parent, IntPtr child, string? className, string? windowText);
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left, top, right, bottom; }
private const int SW_RESTORE = 9;
private const byte VK_NEXT = 0x22; // Page Down
private const uint WM_VSCROLL = 0x0115;
private const uint WM_KEYDOWN = 0x0100;
private const uint WM_KEYUP = 0x0101;
private const int SB_PAGEDOWN = 3;
// 보안 정책: 캡처 결과는 클립보드에만 복사. 파일 저장 기능 없음.
private static readonly (string Key, string Label, string Desc)[] _options =
[
("region", "영역 선택 캡처", "마우스로 드래그하여 원하는 영역만 캡처 · Shift+Enter: 타이머 캡처"),
("window", "활성 창 캡처", "런처 호출 전 활성 창만 캡처 · Shift+Enter: 타이머 캡처"),
("scroll", "스크롤 캡처", "활성 창을 끝까지 스크롤하며 페이지 전체 캡처 · Shift+Enter: 타이머 캡처"),
("screen", "전체 화면 캡처", "모든 모니터를 포함한 전체 화면 · Shift+Enter: 타이머 캡처"),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var saveHint = "클립보드에 복사";
IEnumerable<(string Key, string Label, string Desc)> options = string.IsNullOrWhiteSpace(q)
? _options
: _options.Where(o => o.Key.StartsWith(q) || o.Label.Contains(q));
var items = options.Select(o => new LauncherItem(
o.Label,
$"{o.Desc} · {saveHint}",
null,
o.Key,
Symbol: Symbols.CaptureIcon)).ToList<LauncherItem>();
if (!items.Any())
items.Add(new LauncherItem(
$"알 수 없는 캡처 모드: {q}",
"screen / window / scroll / region",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
/// <summary>
/// 지연 캡처 타이머 선택 항목을 반환합니다.
/// Data 형식: "delay:모드:초" (예: "delay:region:3")
/// </summary>
public IEnumerable<LauncherItem> GetDelayItems(string mode)
{
var label = _options.FirstOrDefault(o => o.Key == mode).Label ?? mode;
return new[]
{
new LauncherItem($"3초 후 {label}", "Shift+Enter로 지연 캡처", null, $"delay:{mode}:3", Symbol: Symbols.Timer),
new LauncherItem($"5초 후 {label}", "Shift+Enter로 지연 캡처", null, $"delay:{mode}:5", Symbol: Symbols.Timer),
new LauncherItem($"10초 후 {label}", "Shift+Enter로 지연 캡처", null, $"delay:{mode}:10", Symbol: Symbols.Timer),
};
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not string data) return;
// 지연 캡처: "delay:모드:초"
if (data.StartsWith("delay:"))
{
var parts = data.Split(':');
if (parts.Length == 3 && int.TryParse(parts[2], out var delaySec))
{
await ExecuteDelayedCaptureAsync(parts[1], delaySec, ct);
return;
}
}
// 런처가 닫히는 동안 잠시 대기
await Task.Delay(150, ct);
await CaptureDirectAsync(data, ct);
}
/// <summary>
/// 지정된 초만큼 대기 후 캡처를 실행합니다. 완료 시 알림이 표시됩니다.
/// </summary>
private async Task ExecuteDelayedCaptureAsync(string mode, int delaySec, CancellationToken ct)
{
// 런처가 닫히는 동안 잠시 대기
await Task.Delay(200, ct);
// 알림 없이 조용히 대기 후 캡처
for (int i = delaySec; i > 0; i--)
{
ct.ThrowIfCancellationRequested();
await Task.Delay(1000, ct);
}
await CaptureDirectAsync(mode, ct);
}
/// <summary>
/// 런처 없이 직접 캡처를 실행합니다. 글로벌 단축키용.
/// </summary>
public async Task CaptureDirectAsync(string mode, CancellationToken ct = default)
{
try
{
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
// 캡처 결과는 클립보드에만 복사 (보안 정책)
switch (mode)
{
case "screen":
await CaptureScreenAsync(timestamp);
break;
case "window":
await CaptureWindowAsync(timestamp);
break;
case "scroll":
await CaptureScrollAsync(timestamp, ct);
break;
case "region":
await CaptureRegionAsync(timestamp, ct);
break;
}
}
catch (Exception ex)
{
LogService.Error($"캡처 실패: {ex.Message}");
NotificationService.Notify("AX Copilot", $"캡처 실패: {ex.Message}");
}
}
/// <summary>
/// 스크롤 캡처에 사용되는 프레임 간 대기 시간(ms). 설정에서 읽기.
/// </summary>
internal int ScrollDelayMs => Math.Max(50, _settings.Settings.ScreenCapture.ScrollDelayMs);
// ─── 전체 화면 캡처 ────────────────────────────────────────────────────────
private async Task CaptureScreenAsync(string timestamp)
{
var bounds = GetAllScreenBounds();
using var bmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
using var g = Graphics.FromImage(bmp);
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
CopyToClipboard(bmp);
await Task.Delay(10);
NotificationService.Notify("화면 캡처 완료", "클립보드에 복사되었습니다");
}
// ─── 창 캡처 ───────────────────────────────────────────────────────────────
private async Task CaptureWindowAsync(string timestamp)
{
var hwnd = WindowTracker.PreviousWindow;
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
{
NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다. 런처 호출 전 창을 확인하세요.");
return;
}
// 런처가 완전히 사라질 때까지 대기 (런처 자체가 캡처되는 버그 방지)
var launcherHwnd = GetLauncherHwnd();
if (launcherHwnd != IntPtr.Zero)
{
for (int i = 0; i < 10 && IsWindowVisible(launcherHwnd); i++)
await Task.Delay(50);
}
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
await Task.Delay(150);
if (!GetWindowRect(hwnd, out var rect)) return;
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0) return;
using var bmp = CaptureWindow(hwnd, w, h, rect);
CopyToClipboard(bmp);
NotificationService.Notify("창 캡처 완료", "클립보드에 복사되었습니다");
}
// ─── 스크롤 캡처 ───────────────────────────────────────────────────────────
private async Task CaptureScrollAsync(string timestamp, CancellationToken ct)
{
var hwnd = WindowTracker.PreviousWindow;
if (hwnd == IntPtr.Zero || !IsWindow(hwnd))
{
NotificationService.Notify("AX Copilot", "캡처할 창이 없습니다.");
return;
}
// 런처가 완전히 사라질 때까지 대기
var launcherHwnd = GetLauncherHwnd();
if (launcherHwnd != IntPtr.Zero)
{
for (int i = 0; i < 10 && IsWindowVisible(launcherHwnd); i++)
await Task.Delay(50, ct);
}
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
await Task.Delay(200, ct);
if (!GetWindowRect(hwnd, out var rect)) return;
int w = rect.right - rect.left;
int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0) return;
// 스크롤 가능한 자식 창 찾기 (웹브라우저, 텍스트뷰어 등)
var scrollTarget = FindScrollableChild(hwnd);
const int maxPages = 15;
var frames = new List<Bitmap>();
// 첫 프레임
frames.Add(CaptureWindow(hwnd, w, h, rect));
for (int i = 0; i < maxPages - 1; i++)
{
ct.ThrowIfCancellationRequested();
// Page Down 전송
if (scrollTarget != IntPtr.Zero)
SendMessage(scrollTarget, WM_VSCROLL, new IntPtr(SB_PAGEDOWN), IntPtr.Zero);
else
SendPageDown(hwnd);
await Task.Delay(ScrollDelayMs, ct);
// 현재 프레임 캡처
if (!GetWindowRect(hwnd, out var newRect)) break;
var frame = CaptureWindow(hwnd, w, h, newRect);
// 이전 프레임과 동일하면 끝 (스크롤 종료 감지)
if (frames.Count > 0 && AreSimilar(frames[^1], frame))
{
frame.Dispose();
break;
}
frames.Add(frame);
}
// 프레임들을 수직으로 이어 붙이기 (오버랩 제거)
using var stitched = StitchFrames(frames, h);
// 각 프레임 해제
foreach (var f in frames) f.Dispose();
CopyToClipboard(stitched);
NotificationService.Notify("스크롤 캡처 완료", $"{stitched.Height}px · 클립보드에 복사되었습니다");
}
// ─── 헬퍼: 창 캡처 (PrintWindow 우선, 실패 시 BitBlt 폴백) ──────────────
private static Bitmap CaptureWindow(IntPtr hwnd, int w, int h, RECT rect)
{
var bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb);
using var g = Graphics.FromImage(bmp);
// PrintWindow로 창 내용 캡처 (최소화된 창도 동작)
var hdc = g.GetHdc();
bool ok = PrintWindow(hwnd, hdc, 2); // PW_RENDERFULLCONTENT = 2
g.ReleaseHdc(hdc);
if (!ok)
{
// 폴백: 화면에서 BitBlt
g.CopyFromScreen(rect.left, rect.top, 0, 0,
new System.Drawing.Size(w, h), CopyPixelOperation.SourceCopy);
}
return bmp;
}
// ─── 헬퍼: 전체 화면 범위 ─────────────────────────────────────────────────
private static System.Drawing.Rectangle GetAllScreenBounds()
{
var screens = System.Windows.Forms.Screen.AllScreens;
int minX = screens.Min(s => s.Bounds.X);
int minY = screens.Min(s => s.Bounds.Y);
int maxX = screens.Max(s => s.Bounds.Right);
int maxY = screens.Max(s => s.Bounds.Bottom);
return new System.Drawing.Rectangle(minX, minY, maxX - minX, maxY - minY);
}
// ─── 헬퍼: 스크롤 가능 자식 창 찾기 ──────────────────────────────────────
private static IntPtr FindScrollableChild(IntPtr hwnd)
{
// 공통 스크롤 가능 클래스 탐색
foreach (var cls in new[] { "Internet Explorer_Server", "Chrome_RenderWidgetHostHWND",
"MozillaWindowClass", "RichEdit20W", "RICHEDIT50W",
"TextBox", "EDIT" })
{
var child = FindWindowEx(hwnd, IntPtr.Zero, cls, null);
if (child != IntPtr.Zero) return child;
}
return IntPtr.Zero;
}
// ─── 헬퍼: Page Down 키 전송 ─────────────────────────────────────────────
private static void SendPageDown(IntPtr hwnd)
{
SendMessage(hwnd, WM_KEYDOWN, new IntPtr(VK_NEXT), IntPtr.Zero);
SendMessage(hwnd, WM_KEYUP, new IntPtr(VK_NEXT), IntPtr.Zero);
}
// ─── 헬퍼: 두 비트맵이 유사한지 비교 (스크롤 종료 감지) ─────────────────
// LockBits를 사용하여 GetPixel 대비 ~50× 빠르게 처리.
private static bool AreSimilar(Bitmap a, Bitmap b)
{
if (a.Width != b.Width || a.Height != b.Height) return false;
int startY = (int)(a.Height * 0.8);
int w = a.Width;
int h = a.Height;
var rectA = new System.Drawing.Rectangle(0, startY, w, h - startY);
var rectB = new System.Drawing.Rectangle(0, startY, w, h - startY);
var dataA = a.LockBits(rectA, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var dataB = b.LockBits(rectB, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int sameCount = 0;
int totalSamples = 0;
int stride = dataA.Stride;
int sampleW = w / 16 + 1;
int sampleH = (h - startY) / 8 + 1;
unsafe
{
byte* ptrA = (byte*)dataA.Scan0.ToPointer();
byte* ptrB = (byte*)dataB.Scan0.ToPointer();
for (int sy = 0; sy < sampleH; sy++)
{
int row = sy * 8;
if (row >= h - startY) break;
for (int sx = 0; sx < sampleW; sx++)
{
int col = sx * 16;
if (col >= w) break;
int idx = row * stride + col * 4;
if (Math.Abs(ptrA[idx] - ptrB[idx]) < 5 &&
Math.Abs(ptrA[idx + 1] - ptrB[idx + 1]) < 5 &&
Math.Abs(ptrA[idx + 2] - ptrB[idx + 2]) < 5)
sameCount++;
totalSamples++;
}
}
}
return totalSamples > 0 && (double)sameCount / totalSamples > 0.97;
}
finally
{
a.UnlockBits(dataA);
b.UnlockBits(dataB);
}
}
// ─── 헬퍼: 프레임 이어 붙이기 (증분만 추가) ──────────────────────────────
private static Bitmap StitchFrames(List<Bitmap> frames, int windowHeight)
{
if (frames.Count == 0)
return new Bitmap(1, 1);
if (frames.Count == 1)
return new Bitmap(frames[0]);
int w = frames[0].Width;
// 각 프레임에서 새로운 부분의 시작 Y (오버랩 제외)
var newPartStarts = new List<int>(); // 인덱스 1부터: frames[i]에서 오버랩 이후 시작 행
var newPartHeights = new List<int>(); // 새로운 부분 높이
int totalHeight = windowHeight; // 첫 프레임은 전체 사용
for (int i = 1; i < frames.Count; i++)
{
int overlap = FindOverlap(frames[i - 1], frames[i]);
int newStart = overlap > 0 ? overlap : windowHeight / 5; // 오버랩 감지 실패 시 상단 20% 제거
int newH = windowHeight - newStart;
if (newH <= 0) { newH = windowHeight / 4; newStart = windowHeight - newH; }
newPartStarts.Add(newStart);
newPartHeights.Add(newH);
totalHeight += newH;
}
var result = new Bitmap(w, totalHeight, PixelFormat.Format32bppArgb);
using var g = Graphics.FromImage(result);
// 첫 프레임: 전체 그리기
g.DrawImage(frames[0], 0, 0, w, windowHeight);
// 이후 프레임: 새로운 부분(증분)만 잘라서 붙이기
int yPos = windowHeight;
for (int i = 1; i < frames.Count; i++)
{
int srcY = newPartStarts[i - 1];
int srcH = newPartHeights[i - 1];
var srcRect = new System.Drawing.Rectangle(0, srcY, w, srcH);
var dstRect = new System.Drawing.Rectangle(0, yPos, w, srcH);
g.DrawImage(frames[i], dstRect, srcRect, System.Drawing.GraphicsUnit.Pixel);
yPos += srcH;
}
return result;
}
// ─── 헬퍼: 두 프레임 사이 오버랩 픽셀 수 계산 (다중 행 비교) ────────────
private static int FindOverlap(Bitmap prev, Bitmap next)
{
int w = Math.Min(prev.Width, next.Width);
int h = prev.Height;
if (h < 16 || w < 16) return 0;
int searchRange = (int)(h * 0.7); // 최대 70% 오버랩 탐색
const int checkRows = 8; // 오버랩 후보당 검증할 행 수
var dataPrev = prev.LockBits(
new System.Drawing.Rectangle(0, 0, prev.Width, prev.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var dataNext = next.LockBits(
new System.Drawing.Rectangle(0, 0, next.Width, next.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
try
{
int stridePrev = dataPrev.Stride;
int strideNext = dataNext.Stride;
int bestOverlap = 0;
unsafe
{
byte* ptrPrev = (byte*)dataPrev.Scan0.ToPointer();
byte* ptrNext = (byte*)dataNext.Scan0.ToPointer();
// 큰 오버랩부터 탐색 (스크롤은 보통 1페이지 미만)
for (int overlap = searchRange; overlap > 8; overlap -= 2)
{
int prevStartY = h - overlap;
if (prevStartY < 0) continue;
int totalMatch = 0;
int totalCheck = 0;
// 오버랩 영역 내 여러 행을 검증
for (int r = 0; r < checkRows; r++)
{
int rowInOverlap = r * (overlap / checkRows);
int prevRow = prevStartY + rowInOverlap;
int nextRow = rowInOverlap;
if (prevRow >= h || nextRow >= next.Height) continue;
// 행 내 샘플 픽셀 비교
for (int x = 4; x < w - 4; x += 12)
{
int idxP = prevRow * stridePrev + x * 4;
int idxN = nextRow * strideNext + x * 4;
if (idxP + 2 >= dataPrev.Height * stridePrev) continue;
if (idxN + 2 >= dataNext.Height * strideNext) continue;
if (Math.Abs(ptrPrev[idxP] - ptrNext[idxN]) < 10 &&
Math.Abs(ptrPrev[idxP + 1] - ptrNext[idxN + 1]) < 10 &&
Math.Abs(ptrPrev[idxP + 2] - ptrNext[idxN + 2]) < 10)
totalMatch++;
totalCheck++;
}
}
if (totalCheck > 0 && (double)totalMatch / totalCheck > 0.80)
{
bestOverlap = overlap;
break;
}
}
}
return bestOverlap;
}
finally
{
prev.UnlockBits(dataPrev);
next.UnlockBits(dataNext);
}
}
// ─── 영역 선택 캡처 ──────────────────────────────────────────────────────
private async Task CaptureRegionAsync(string timestamp, CancellationToken ct)
{
// 전체 화면을 먼저 캡처 (배경으로 사용)
var bounds = GetAllScreenBounds();
using var fullBmp = new Bitmap(bounds.Width, bounds.Height, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(fullBmp))
g.CopyFromScreen(bounds.X, bounds.Y, 0, 0, bounds.Size, CopyPixelOperation.SourceCopy);
// WPF 오버레이 창으로 영역 선택
System.Drawing.Rectangle? selected = null;
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
var overlay = new AxCopilot.Views.RegionSelectWindow(fullBmp, bounds);
overlay.ShowDialog();
selected = overlay.SelectedRect;
});
if (selected == null || selected.Value.Width < 4 || selected.Value.Height < 4)
{
NotificationService.Notify("AX Copilot", "영역 선택이 취소되었습니다.");
return;
}
var r = selected.Value;
using var crop = new Bitmap(r.Width, r.Height, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(crop))
g.DrawImage(fullBmp, new System.Drawing.Rectangle(0, 0, r.Width, r.Height), r, System.Drawing.GraphicsUnit.Pixel);
CopyToClipboard(crop);
NotificationService.Notify("영역 캡처 완료", $"{r.Width}×{r.Height} · 클립보드에 복사되었습니다");
await Task.CompletedTask;
}
// ─── 헬퍼: 클립보드에 이미지 복사 ───────────────────────────────────────
private static void CopyToClipboard(Bitmap bmp)
{
try
{
// WPF Clipboard에 BitmapSource로 복사
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
using var ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Bmp);
ms.Position = 0;
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = ms;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
System.Windows.Clipboard.SetImage(bitmapImage);
});
}
catch (Exception ex)
{
LogService.Warn($"클립보드 이미지 복사 실패: {ex.Message}");
}
}
// ─── 헬퍼: 런처 창 핸들 조회 (캡처 시 런처 숨김 대기용) ───────────────
private static IntPtr GetLauncherHwnd()
{
try
{
IntPtr hwnd = IntPtr.Zero;
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
var launcher = System.Windows.Application.Current.Windows
.OfType<System.Windows.Window>()
.FirstOrDefault(w => w.GetType().Name == "LauncherWindow");
if (launcher != null)
hwnd = new System.Windows.Interop.WindowInteropHelper(launcher).Handle;
});
return hwnd;
}
catch (Exception) { return IntPtr.Zero; }
}
}

View File

@@ -0,0 +1,271 @@
using System.Diagnostics;
using System.ServiceProcess;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// Windows 서비스 관리 핸들러. "svc" 프리픽스로 사용합니다.
/// 서비스 검색, 상태 조회, 시작/중지/재시작.
/// 특수 명령: svc restart clipboard → 클립보드 히스토리 서비스 강제 재시작
/// 예: svc → 실행 중 서비스 목록 (상위 20개)
/// svc spooler → "spooler" 검색
/// svc start [이름] → 서비스 시작
/// svc stop [이름] → 서비스 중지
/// svc restart clipboard → AX 클립보드 히스토리 서비스 재시작
/// </summary>
public class ServiceHandler : IActionHandler
{
private readonly ClipboardHistoryService? _clipboardService;
public ServiceHandler(ClipboardHistoryService? clipboardService = null)
{
_clipboardService = clipboardService;
}
public string? Prefix => "svc";
public PluginMetadata Metadata => new(
"Service",
"Windows 서비스 관리 — svc",
"1.0",
"AX");
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
// 특수: 클립보드 서비스 재시작
if (q.Equals("restart clipboard", StringComparison.OrdinalIgnoreCase) ||
q.Equals("클립보드 재시작", StringComparison.OrdinalIgnoreCase))
{
items.Add(new LauncherItem(
"AX 클립보드 히스토리 서비스 재시작",
"클립보드 감지가 작동하지 않을 때 사용 · Enter로 실행",
null, "__RESTART_CLIPBOARD__",
Symbol: Symbols.Restart));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// start/stop/restart 명령
if (q.StartsWith("start ", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("stop ", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("restart ", StringComparison.OrdinalIgnoreCase))
{
var parts = q.Split(' ', 2, StringSplitOptions.TrimEntries);
var action = parts[0].ToLowerInvariant();
var svcName = parts.Length > 1 ? parts[1] : "";
if (!string.IsNullOrWhiteSpace(svcName))
{
try
{
var svc = ServiceController.GetServices()
.FirstOrDefault(s =>
s.ServiceName.Contains(svcName, StringComparison.OrdinalIgnoreCase) ||
s.DisplayName.Contains(svcName, StringComparison.OrdinalIgnoreCase));
if (svc != null)
{
var actionLabel = action switch
{
"start" => "시작",
"stop" => "중지",
"restart" => "재시작",
_ => action
};
items.Add(new LauncherItem(
$"[{actionLabel}] {svc.DisplayName}",
$"서비스명: {svc.ServiceName} · 현재 상태: {StatusText(svc.Status)} · Enter로 실행",
null, ($"__{action.ToUpperInvariant()}__", svc.ServiceName),
Symbol: action == "stop" ? Symbols.Error : Symbols.Restart));
}
else
{
items.Add(new LauncherItem(
$"'{svcName}' 서비스를 찾을 수 없습니다",
"서비스 이름 또는 표시 이름으로 검색",
null, null, Symbol: Symbols.Warning));
}
}
catch (Exception ex)
{
items.Add(new LauncherItem("서비스 조회 실패", ex.Message, null, null, Symbol: Symbols.Error));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
}
// 서비스 검색/목록
try
{
var services = ServiceController.GetServices();
var filtered = string.IsNullOrWhiteSpace(q)
? services.Where(s => s.Status == ServiceControllerStatus.Running)
.OrderBy(s => s.DisplayName).Take(20)
: services.Where(s =>
s.ServiceName.Contains(q, StringComparison.OrdinalIgnoreCase) ||
s.DisplayName.Contains(q, StringComparison.OrdinalIgnoreCase))
.OrderBy(s => s.DisplayName).Take(20);
// 클립보드 재시작 항목 항상 상단에 표시
items.Add(new LauncherItem(
"AX 클립보드 히스토리 서비스 재시작",
"svc restart clipboard · 클립보드 감지 문제 시 사용",
null, "__RESTART_CLIPBOARD__",
Symbol: Symbols.Restart));
foreach (var svc in filtered)
{
var status = StatusText(svc.Status);
var symbol = svc.Status == ServiceControllerStatus.Running
? "\uE73E" // 체크마크
: "\uE711"; // X
items.Add(new LauncherItem(
$"[{status}] {svc.DisplayName}",
$"{svc.ServiceName} · svc start/stop/restart {svc.ServiceName}",
null, svc.ServiceName,
Symbol: symbol));
}
}
catch (Exception ex)
{
items.Add(new LauncherItem("서비스 조회 실패", ex.Message, null, null, Symbol: Symbols.Error));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
// 클립보드 서비스 재시작
if (item.Data is string s && s == "__RESTART_CLIPBOARD__")
{
RestartClipboardService();
return Task.CompletedTask;
}
// Windows 서비스 제어
if (item.Data is ValueTuple<string, string> cmd)
{
var (action, svcName) = cmd;
return ExecuteServiceAction(action, svcName);
}
// 서비스 이름 클립보드 복사
if (item.Data is string name)
{
try { Application.Current?.Dispatcher.Invoke(() => Clipboard.SetText(name)); }
catch (Exception) { }
}
return Task.CompletedTask;
}
private void RestartClipboardService()
{
try
{
if (_clipboardService == null)
{
NotificationService.Notify("AX Copilot", "클립보드 서비스 참조를 찾을 수 없습니다.");
return;
}
// Dispose 후 재초기화
_clipboardService.Dispose();
// 약간의 딜레이 후 재초기화
Application.Current?.Dispatcher.BeginInvoke(() =>
{
try
{
_clipboardService.Reinitialize();
NotificationService.Notify("AX Copilot", "클립보드 히스토리 서비스가 재시작되었습니다.");
LogService.Info("클립보드 히스토리 서비스 강제 재시작 완료");
}
catch (Exception ex)
{
NotificationService.Notify("AX Copilot", $"클립보드 재시작 실패: {ex.Message}");
LogService.Error($"클립보드 재시작 실패: {ex.Message}");
}
}, System.Windows.Threading.DispatcherPriority.Background);
}
catch (Exception ex)
{
NotificationService.Notify("AX Copilot", $"클립보드 재시작 실패: {ex.Message}");
}
}
private static async Task ExecuteServiceAction(string action, string svcName)
{
try
{
// sc.exe를 사용하여 관리자 권한으로 실행
var verb = action switch
{
"__START__" => "start",
"__STOP__" => "stop",
"__RESTART__" => "stop", // restart는 stop + start
_ => null
};
if (verb == null) return;
var psi = new ProcessStartInfo("sc.exe", $"{verb} \"{svcName}\"")
{
Verb = "runas",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
var proc = Process.Start(psi);
proc?.WaitForExit(5000);
if (action == "__RESTART__")
{
await Task.Delay(1000);
var startPsi = new ProcessStartInfo("sc.exe", $"start \"{svcName}\"")
{
Verb = "runas",
UseShellExecute = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
Process.Start(startPsi)?.WaitForExit(5000);
}
var label = action switch
{
"__START__" => "시작",
"__STOP__" => "중지",
"__RESTART__" => "재시작",
_ => action
};
NotificationService.Notify("서비스 관리", $"{svcName} {label} 요청 완료");
}
catch (Exception ex)
{
NotificationService.Notify("AX Copilot", $"서비스 제어 실패: {ex.Message}");
}
}
private static string StatusText(ServiceControllerStatus status) => status switch
{
ServiceControllerStatus.Running => "실행 중",
ServiceControllerStatus.Stopped => "중지됨",
ServiceControllerStatus.StartPending => "시작 중",
ServiceControllerStatus.StopPending => "중지 중",
ServiceControllerStatus.Paused => "일시 중지",
ServiceControllerStatus.ContinuePending => "재개 중",
ServiceControllerStatus.PausePending => "일시 중지 중",
_ => status.ToString()
};
}

View File

@@ -0,0 +1,201 @@
using System.Runtime.InteropServices;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 창 스냅/레이아웃 핸들러. "snap" 프리픽스로 사용합니다.
/// 런처 호출 직전의 활성 창을 대상으로 배치합니다.
///
/// 예: snap left → 화면 왼쪽 절반
/// snap right → 화면 오른쪽 절반
/// snap top → 화면 위쪽 절반
/// snap bottom → 화면 아래쪽 절반
/// snap full → 전체 화면 (최대화)
/// snap tl → 좌상단 1/4
/// snap tr → 우상단 1/4
/// snap bl → 좌하단 1/4
/// snap br → 우하단 1/4
/// snap center → 화면 중앙 (80% 크기)
/// snap restore → 이전 크기/위치로 복원
/// </summary>
public class SnapHandler : IActionHandler
{
public string? Prefix => "snap";
public PluginMetadata Metadata => new(
"WindowSnap",
"창 배치 — 2/3/4분할, 1/3·2/3, 전체화면, 중앙, 복원",
"1.1",
"AX");
// ─── P/Invoke ──────────────────────────────────────────────────────────────
[DllImport("user32.dll")] private static extern bool SetWindowPos(
IntPtr hWnd, IntPtr hWndInsertAfter,
int x, int y, int cx, int cy, uint uFlags);
[DllImport("user32.dll")] private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll")] private static extern IntPtr MonitorFromWindow(IntPtr hwnd, uint dwFlags);
[DllImport("user32.dll")] private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFO lpmi);
[DllImport("user32.dll")] private static extern bool IsWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool IsIconic(IntPtr hWnd);
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left, top, right, bottom; }
[StructLayout(LayoutKind.Sequential)]
private struct MONITORINFO
{
public int cbSize;
public RECT rcMonitor;
public RECT rcWork; // 작업표시줄 제외 영역
public uint dwFlags;
}
private const uint SWP_SHOWWINDOW = 0x0040;
private const uint SWP_NOZORDER = 0x0004;
private const uint MONITOR_DEFAULTTONEAREST = 0x00000002;
private const int SW_RESTORE = 9;
private const int SW_MAXIMIZE = 3;
private static readonly (string Key, string Label, string Desc)[] _snapOptions =
[
// ── 2분할 ──
("left", "왼쪽 절반", "화면 왼쪽 50% 영역에 배치"),
("right", "오른쪽 절반", "화면 오른쪽 50% 영역에 배치"),
("top", "위쪽 절반", "화면 위쪽 50% 영역에 배치"),
("bottom", "아래쪽 절반", "화면 아래쪽 50% 영역에 배치"),
// ── 4분할 ──
("tl", "좌상단 1/4", "화면 좌상단 25% 영역에 배치"),
("tr", "우상단 1/4", "화면 우상단 25% 영역에 배치"),
("bl", "좌하단 1/4", "화면 좌하단 25% 영역에 배치"),
("br", "우하단 1/4", "화면 우하단 25% 영역에 배치"),
// ── 3분할 (좌 50% + 우 상하) ──
("l-rt", "좌반 + 우상", "왼쪽 50% + 오른쪽 상단 25% (2창용)"),
("l-rb", "좌반 + 우하", "왼쪽 50% + 오른쪽 하단 25% (2창용)"),
("r-lt", "우반 + 좌상", "오른쪽 50% + 왼쪽 상단 25% (2창용)"),
("r-lb", "우반 + 좌하", "오른쪽 50% + 왼쪽 하단 25% (2창용)"),
// ── 3등분 (가로) ──
("third-l", "좌측 1/3", "화면 왼쪽 33% 영역에 배치"),
("third-c", "중앙 1/3", "화면 가운데 33% 영역에 배치"),
("third-r", "우측 1/3", "화면 오른쪽 33% 영역에 배치"),
// ── 2/3 + 1/3 ──
("two3-l", "좌측 2/3", "화면 왼쪽 66% 영역에 배치"),
("two3-r", "우측 2/3", "화면 오른쪽 66% 영역에 배치"),
// ── 기타 ──
("full", "전체 화면", "최대화"),
("center", "화면 중앙", "화면 중앙 80% 크기로 배치"),
("restore", "원래 크기 복원", "창을 이전 크기로 복원"),
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var hwnd = WindowTracker.PreviousWindow;
var windowValid = hwnd != IntPtr.Zero && IsWindow(hwnd);
var hint = windowValid ? "Enter로 현재 활성 창에 적용" : "대상 창 없음 — 런처를 열기 전 창에 적용됩니다";
IEnumerable<(string Key, string Label, string Desc)> options = string.IsNullOrWhiteSpace(q)
? _snapOptions
: _snapOptions.Where(o => o.Key.StartsWith(q) || o.Label.Contains(q));
var items = options.Select(o => new LauncherItem(
o.Label,
$"{o.Desc} · {hint}",
null,
o.Key,
Symbol: Symbols.SnapLayout)).ToList<LauncherItem>();
if (!items.Any())
items.Add(new LauncherItem(
$"알 수 없는 스냅 방향: {q}",
"left / right / tl / tr / bl / br / third-l/c/r / two3-l/r / full / center / restore",
null, null, Symbol: Symbols.Warning));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is not string snapKey) return;
var hwnd = WindowTracker.PreviousWindow;
if (hwnd == IntPtr.Zero || !IsWindow(hwnd)) return;
// 최대화/아이콘화 상태면 먼저 복원
if (IsIconic(hwnd))
ShowWindow(hwnd, SW_RESTORE);
// 잠시 대기 (런처 닫힘 애니메이션 후 적용)
await Task.Delay(80, ct);
ApplySnap(hwnd, snapKey);
}
private static void ApplySnap(IntPtr hwnd, string key)
{
// 창이 속한 모니터의 작업 영역 가져오기
var hMonitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST);
var mi = new MONITORINFO { cbSize = Marshal.SizeOf<MONITORINFO>() };
if (!GetMonitorInfo(hMonitor, ref mi)) return;
var w = mi.rcWork;
int mw = w.right - w.left;
int mh = w.bottom - w.top;
int mx = w.left;
int my = w.top;
if (key == "full")
{
ShowWindow(hwnd, SW_MAXIMIZE);
return;
}
if (key == "restore")
{
ShowWindow(hwnd, SW_RESTORE);
return;
}
var (x, y, cw, ch) = key switch
{
// 2분할
"left" => (mx, my, mw / 2, mh),
"right" => (mx + mw / 2, my, mw / 2, mh),
"top" => (mx, my, mw, mh / 2),
"bottom" => (mx, my + mh / 2, mw, mh / 2),
// 4분할
"tl" => (mx, my, mw / 2, mh / 2),
"tr" => (mx + mw / 2, my, mw / 2, mh / 2),
"bl" => (mx, my + mh / 2, mw / 2, mh / 2),
"br" => (mx + mw / 2, my + mh / 2, mw / 2, mh / 2),
// 3분할 (좌반 + 우상/우하)
"l-rt" => (mx + mw / 2, my, mw / 2, mh / 2),
"l-rb" => (mx + mw / 2, my + mh / 2, mw / 2, mh / 2),
"r-lt" => (mx, my, mw / 2, mh / 2),
"r-lb" => (mx, my + mh / 2, mw / 2, mh / 2),
// 3등분
"third-l" => (mx, my, mw / 3, mh),
"third-c" => (mx + mw / 3, my, mw / 3, mh),
"third-r" => (mx + mw * 2 / 3, my, mw / 3, mh),
// 2/3 + 1/3
"two3-l" => (mx, my, mw * 2 / 3, mh),
"two3-r" => (mx + mw / 3, my, mw * 2 / 3, mh),
// 기타
"center" => (mx + mw / 10, my + mh / 10, mw * 8 / 10, mh * 8 / 10),
_ => (mx, my, mw, mh)
};
// SW_RESTORE 후 SetWindowPos — 최대화 플래그 해제 필요
ShowWindow(hwnd, SW_RESTORE);
SetWindowPos(hwnd, IntPtr.Zero, x, y, cw, ch, SWP_SHOWWINDOW | SWP_NOZORDER);
}
}

Some files were not shown because too many files have changed in this diff Show More