From afedd826c8242559630a5318116c01bc7f08fe22 Mon Sep 17 00:00:00 2001 From: lacvet Date: Sat, 4 Apr 2026 18:37:58 +0900 Subject: [PATCH] =?UTF-8?q?[Phase=20L24]=20=EC=96=91=EC=8B=9D=206=EC=A2=85?= =?UTF-8?q?=C2=B7=EB=8B=A8=EC=B6=95=ED=82=A4=204=EC=95=B1=C2=B7=EB=A7=9E?= =?UTF-8?q?=EC=B6=A4=EB=B2=95=20=ED=95=B8=EB=93=A4=EB=9F=AC=C2=B7=EC=95=BD?= =?UTF-8?q?=EC=96=B4=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FormHandler.cs (양식 6종 추가, 카테고리 2개 신설): - 업무 인수인계서 (handover): 인계 목록·시스템·연락처 포함 - 업무 지시서 (handover): 우선순위·산출물·보고 방법 포함 - 품의서 (handover): 결재란·예산·기대효과 포함 - 업무 일지 (daily): 일별 업무·미완료·협의사항 포함 - 일일 업무 보고 (daily): 완료·진행·예정·이슈 포함 - 주요 업무 계획표 (daily): 월간 목표·주차별·세부 계획 포함 - Categories에 handover/daily 2개 신설, 안내 문자열 갱신 KeyHandler.cs (단축키 4개 앱 추가): - Word 단축키 25개 (서식·맞춤법·스타일·이동 포함) - PowerPoint 단축키 18개 (슬라이드 쇼·그룹화·정렬 포함) - Teams 단축키 18개 (회의·탭 전환·채널 이동 포함) - Outlook 단축키 21개 (메일·일정·플래그·정크 포함) - catKeys·catName switch·AddAppOverview·안내 문자열 모두 갱신 SpellHandler.cs (신규 생성, 63개 항목): - 되/돼·안/않·혼동어·맞춤법·띄어쓰기·외래어 6개 카테고리 - 빈 쿼리 시 클립보드 자동 맞춤법 검사 - list/카테고리/키워드 검색 지원 - Enter: 올바른 표현 클립보드 복사 AbbrHandler.cs (사용자 정의 약어 기능 추가): - abbr_custom.json 로컬 저장 (CustomAbbr 모델) - abbr add <약어> <풀이> [설명] → 커스텀 약어 등록 - abbr del <약어> → 커스텀 약어 삭제 - abbr custom → 사용자 정의 목록 보기 - 검색 시 내장+커스텀 통합 검색 App.xaml.cs: SpellHandler 등록 (L24-3) 빌드: 경고 0, 오류 0 --- src/AxCopilot/App.xaml.cs | 2 + src/AxCopilot/Handlers/AbbrHandler.cs | 146 ++++++++++- src/AxCopilot/Handlers/FormHandler.cs | 332 ++++++++++++++++++++++++- src/AxCopilot/Handlers/KeyHandler.cs | 106 +++++++- src/AxCopilot/Handlers/SpellHandler.cs | 276 ++++++++++++++++++++ 5 files changed, 847 insertions(+), 15 deletions(-) create mode 100644 src/AxCopilot/Handlers/SpellHandler.cs diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs index e68db80..267adbb 100644 --- a/src/AxCopilot/App.xaml.cs +++ b/src/AxCopilot/App.xaml.cs @@ -359,6 +359,8 @@ public partial class App : System.Windows.Application commandResolver.RegisterHandler(new WorkTimeHandler()); // L23-4: 한/영 타이핑 오류 교정 (prefix=fix) commandResolver.RegisterHandler(new FixHandler()); + // L24-3: 자주 틀리는 맞춤법 (prefix=spell) + commandResolver.RegisterHandler(new SpellHandler()); // ─── 플러그인 로드 ──────────────────────────────────────────────────── var pluginHost = new PluginHost(settings, commandResolver); diff --git a/src/AxCopilot/Handlers/AbbrHandler.cs b/src/AxCopilot/Handlers/AbbrHandler.cs index 0cadc7e..0092f67 100644 --- a/src/AxCopilot/Handlers/AbbrHandler.cs +++ b/src/AxCopilot/Handlers/AbbrHandler.cs @@ -1,3 +1,5 @@ +using System.Text.Json; +using System.Text.Json.Serialization; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; @@ -201,16 +203,112 @@ public class AbbrHandler : IActionHandler private static readonly string[] Categories = Entries.Select(e => e.Category).Distinct().ToArray(); + // ── 커스텀 약어 ─────────────────────────────────────────────────────────── + private static readonly string CustomPath = System.IO.Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "AxCopilot", "abbr_custom.json"); + + private sealed class CustomAbbr + { + [JsonPropertyName("short")] public string Short { get; set; } = ""; + [JsonPropertyName("full")] public string Full { get; set; } = ""; + [JsonPropertyName("desc")] public string Description { get; set; } = ""; + [JsonPropertyName("cat")] public string Category { get; set; } = "사용자정의"; + } + + private static List LoadCustom() + { + try { + if (System.IO.File.Exists(CustomPath)) + return JsonSerializer.Deserialize>( + System.IO.File.ReadAllText(CustomPath)) ?? []; + } catch { } + return []; + } + + private static void SaveCustom(List list) + { + System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(CustomPath)!); + System.IO.File.WriteAllText(CustomPath, JsonSerializer.Serialize(list, + new JsonSerializerOptions { WriteIndented = true, + Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping })); + } + public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); + // ── 서브커맨드 처리 ─────────────────────────────────────────────────── + // abbr custom → 사용자 정의 약어 목록 + if (q.Equals("custom", StringComparison.OrdinalIgnoreCase)) + { + var customList = LoadCustom(); + if (customList.Count == 0) + { + items.Add(new LauncherItem("사용자 정의 약어 없음", + "abbr add <약어> <풀이> [설명] 으로 추가하세요", + null, null, Symbol: "\uE82D")); + } + else + { + items.Add(new LauncherItem($"사용자 정의 약어 {customList.Count}개", "", null, null, Symbol: "\uE82D")); + foreach (var c in customList) + items.Add(new LauncherItem(c.Short, + $"{c.Full} · {c.Description}", + null, ("copy", $"{c.Short}: {c.Full}"), Symbol: "\uE82D")); + } + return Task.FromResult>(items); + } + + // abbr add <약어> <풀이> [설명] + if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase)) + { + var rest = q[4..].Trim(); + var parts = rest.Split(' ', 3, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length < 2) + { + items.Add(new LauncherItem("사용법: abbr add <약어> <풀이> [설명]", + "예: abbr add ROI 투자수익률 Return on Investment", + null, null, Symbol: "\uE82D")); + return Task.FromResult>(items); + } + var abbr = parts[0].ToUpper(); + var full = parts[1]; + var desc = parts.Length > 2 ? parts[2] : ""; + var encoded = $"{abbr}|{full}|{desc}"; + items.Add(new LauncherItem($"약어 추가: {abbr} = {full}", + desc.Length > 0 ? desc : "Enter: 저장", + null, ("add", encoded), Symbol: "\uE82D")); + return Task.FromResult>(items); + } + + // abbr del <약어> + if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase)) + { + var abbr = q[4..].Trim().ToUpper(); + if (string.IsNullOrEmpty(abbr)) + { + items.Add(new LauncherItem("사용법: abbr del <약어>", + "예: abbr del ROI", + null, null, Symbol: "\uE82D")); + return Task.FromResult>(items); + } + items.Add(new LauncherItem($"약어 삭제: {abbr}", + "Enter: 삭제 확인", + null, ("del", abbr), Symbol: "\uE82D")); + return Task.FromResult>(items); + } + if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem($"IT·개발 약어 사전 {Entries.Length}개", "abbr <약어> 예: abbr api / abbr crud / abbr jwt", null, null, Symbol: "\uE82D")); + // 커스텀 약어 추가 안내 + items.Add(new LauncherItem("abbr add <약어> <풀이> — 나만의 약어 추가", + "예: abbr add ROI 투자수익률", + null, null, Symbol: "\uE82D")); items.Add(new LauncherItem("── 카테고리 ──", "", null, null, Symbol: "\uE82D")); foreach (var cat in Categories) { @@ -240,11 +338,15 @@ public class AbbrHandler : IActionHandler return Task.FromResult>(items); } - // 약어 검색 (정확 일치 우선, 그 다음 부분 일치) - var exact = Entries.Where(e => + // 약어 검색 (정확 일치 우선, 그 다음 부분 일치) — 내장 + 커스텀 통합 + var customSearch = LoadCustom(); + var customEntries = customSearch.Select(c => new AbbrEntry(c.Short, c.Full, c.Description, c.Category)).ToList(); + var allEntries = Entries.Concat(customEntries).ToList(); + + var exact = allEntries.Where(e => e.Short.Equals(q, StringComparison.OrdinalIgnoreCase)).ToList(); - var partial = Entries.Where(e => + var partial = allEntries.Where(e => !exact.Contains(e) && (e.Short.Contains(q, StringComparison.OrdinalIgnoreCase) || e.Full.Contains(q, StringComparison.OrdinalIgnoreCase) || @@ -283,15 +385,37 @@ public class AbbrHandler : IActionHandler public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { - if (item.Data is ("copy", string text)) + switch (item.Data) { - try - { - System.Windows.Application.Current.Dispatcher.Invoke( - () => Clipboard.SetText(text)); - NotificationService.Notify("Abbr", "클립보드에 복사했습니다."); - } - catch { } + case ("copy", string text): + try + { + System.Windows.Application.Current.Dispatcher.Invoke( + () => Clipboard.SetText(text)); + NotificationService.Notify("Abbr", "클립보드에 복사했습니다."); + } + catch { } + break; + + case ("add", string encoded): + var parts = encoded.Split('|', 3); + var custom = LoadCustom(); + custom.RemoveAll(c => c.Short.Equals(parts[0], StringComparison.OrdinalIgnoreCase)); + custom.Add(new CustomAbbr { + Short = parts[0].ToUpper(), + Full = parts.Length > 1 ? parts[1] : "", + Description = parts.Length > 2 ? parts[2] : "", + }); + SaveCustom(custom); + NotificationService.Notify("약어", $"{parts[0].ToUpper()} 등록 완료"); + break; + + case ("del", string abbr): + var customDel = LoadCustom(); + customDel.RemoveAll(c => c.Short.Equals(abbr, StringComparison.OrdinalIgnoreCase)); + SaveCustom(customDel); + NotificationService.Notify("약어", $"{abbr.ToUpper()} 삭제 완료"); + break; } return Task.CompletedTask; } diff --git a/src/AxCopilot/Handlers/FormHandler.cs b/src/AxCopilot/Handlers/FormHandler.cs index a04fe46..9ea2f1f 100644 --- a/src/AxCopilot/Handlers/FormHandler.cs +++ b/src/AxCopilot/Handlers/FormHandler.cs @@ -107,6 +107,38 @@ public class FormHandler : IActionHandler "첫 1·2·4주 온보딩 단계별 체크리스트", "onboard", ["onboard", "온보딩", "신입", "입사"], () => BuildOnboard()), + + // ── 인수인계 (handover) ────────────────────────────────────────────── + new("업무 인수인계서", + "퇴직·이동 시 업무 인수인계 목록 및 주의사항", + "handover", ["handover", "인수인계", "transfer", "인계"], + () => BuildHandover()), + + new("업무 지시서", + "담당자·기한·우선순위 포함 업무 지시 양식", + "handover", ["handover", "지시", "task", "업무지시"], + () => BuildTaskOrder()), + + new("품의서", + "결재 요청을 위한 품의서 양식", + "handover", ["handover", "품의", "approval", "결재"], + () => BuildApproval()), + + // ── 일지 (daily) ──────────────────────────────────────────────────── + new("업무 일지", + "일별 업무 처리 현황 기록 양식", + "daily", ["daily", "일지", "업무일지", "log"], + () => BuildWorkLog()), + + new("일일 업무 보고", + "당일 완료·진행·내일 예정 업무 보고 양식", + "daily", ["daily", "일일보고", "daily report", "eod"], + () => BuildDailyReport()), + + new("주요 업무 계획표", + "월간·분기 주요 업무 계획 및 일정표", + "daily", ["daily", "계획표", "plan", "일정"], + () => BuildWorkPlan()), ]; private static readonly (string Key, string[] Aliases, string Label)[] Categories = @@ -117,6 +149,8 @@ public class FormHandler : IActionHandler ("project", ["project", "프로젝트"], "프로젝트 문서"), ("review", ["review", "리뷰", "평가"], "리뷰·평가"), ("onboard", ["onboard", "온보딩", "입사"], "온보딩"), + ("handover", ["handover", "인수인계", "지시", "품의"], "인수인계·지시·품의"), + ("daily", ["daily", "일지", "일일", "계획"], "업무 일지·계획"), ]; public Task> GetItemsAsync(string query, CancellationToken ct) @@ -127,7 +161,7 @@ public class FormHandler : IActionHandler if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("업무 양식 템플릿", - "카테고리: meeting · report · email · project · review · onboard", + "카테고리: meeting · report · email · project · review · onboard · handover · daily", null, null, Symbol: "\uE8A5")); foreach (var (key, _, label) in Categories) @@ -163,7 +197,7 @@ public class FormHandler : IActionHandler if (searched.Count == 0) { items.Add(new LauncherItem($"'{q}' 양식을 찾을 수 없습니다", - "카테고리: meeting · report · email · project · review · onboard", + "카테고리: meeting · report · email · project · review · onboard · handover · daily", null, null, Symbol: "\uE783")); return Task.FromResult>(items); } @@ -779,6 +813,300 @@ $""" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ▸ 메모 + - +"""; + } + + private static string BuildHandover() + { + var today = DateTime.Today; + return $""" +업무 인수인계서 + +인계자: ___________ 인수자: ___________ +인계일: {today:yyyy년 MM월 dd일} 완료 예정일: ___________ +부서: ___________ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +1. 담당 업무 목록 + + 번호 | 업무명 | 진행상태 | 담당기간 | 비고 + -----|--------|----------|----------|---- + 1 | | | | + 2 | | | | + 3 | | | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +2. 진행 중인 업무 상세 + + [업무명] + - 현재 진행 단계: + - 다음 처리 내용: + - 관련 담당자: + - 위치/경로: (파일/시스템/폴더) + - 주의사항: + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +3. 관련 시스템·계정 + + 시스템명 | ID | 비고 + ---------|----|---- + | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +4. 주요 연락처 + + 이름 | 소속 | 연락처 | 관계 + -----|------|--------|---- + | | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +5. 인수인계 특이사항 및 요청사항 + + - + +인계자 서명: ___________ 인수자 서명: ___________ +"""; + } + + private static string BuildTaskOrder() + { + var today = DateTime.Today; + return $""" +업무 지시서 + +지시일: {today:yyyy년 MM월 dd일} +지시자: ___________ 수신자: ___________ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 업무 개요 + + 업무명: + 우선순위: □ 긴급 □ 높음 □ 보통 □ 낮음 + 완료 기한: + 관련 프로젝트: + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 업무 내용 및 지시 사항 + + 1. + 2. + 3. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 세부 요구사항 + + - + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 산출물·보고 방법 + + 산출물: + 보고 방법: □ 구두 □ 이메일 □ 문서 □ 시스템 등록 + 보고 기한: + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 참고 자료 + + - + +수신 확인: ___________ 확인일: ___________ +"""; + } + + private static string BuildApproval() + { + var today = DateTime.Today; + return $""" +품 의 서 + +기안일: {today:yyyy년 MM월 dd일} +기안자: ___________ 부서: ___________ + + 결재 | 담당 | 팀장 | 부장 | 이사 + -----|------|------|------|---- + | | | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 제목: + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 목적 및 배경 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 추진 내용 + + 1. + 2. + 3. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 예산 현황 (해당 시) + + 항목 | 금액 | 비고 + -----|------|---- + | | + 합계 | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 일정 + + 착수: ___________ 완료: ___________ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 기대 효과 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 첨부 자료 + + □ 없음 □ 견적서 □ 계획서 □ 기타: ___________ +"""; + } + + private static string BuildWorkLog() + { + var today = DateTime.Today; + var dow = today.DayOfWeek switch { + DayOfWeek.Monday => "월", DayOfWeek.Tuesday => "화", + DayOfWeek.Wednesday => "수", DayOfWeek.Thursday => "목", + DayOfWeek.Friday => "금", DayOfWeek.Saturday => "토", _ => "일" + }; + return $""" +업무 일지 + +날짜: {today:yyyy년 MM월 dd일} ({dow}요일) 작성자: ___________ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 오늘 처리한 업무 + + 번호 | 업무 내용 | 소요 시간 | 완료 여부 + -----|-----------|-----------|---------- + 1 | | | □ + 2 | | | □ + 3 | | | □ + 4 | | | □ + 5 | | | □ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 미완료·이월 업무 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 협의·회의 사항 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 특이사항 / 이슈 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 내일 예정 업무 + + 1. + 2. + 3. +"""; + } + + private static string BuildDailyReport() + { + var today = DateTime.Today; + return $""" +일일 업무 보고 + +날짜: {today:yyyy년 MM월 dd일} 보고자: ___________ 팀: ___________ + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 금일 완료 업무 + + 1. + 2. + 3. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 진행 중 업무 + + 업무명 | 진행률 | 완료 예정일 | 비고 + -------|--------|-------------|---- + | % | | + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 내일 예정 업무 + + 1. + 2. + 3. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 이슈 / 협조 요청 + + □ 없음 + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 기타 공유 사항 + + - +"""; + } + + private static string BuildWorkPlan() + { + var today = DateTime.Today; + var firstDay = new DateTime(today.Year, today.Month, 1); + return $""" +주요 업무 계획표 + +기간: {today:yyyy년 MM월} 작성자: ___________ 부서: ___________ +작성일: {today:yyyy-MM-dd} + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 월간 주요 목표 + + 1. + 2. + 3. + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 주차별 업무 계획 + + ▶ 1주차 ({firstDay:MM/dd} ~) + - + + ▶ 2주차 ({firstDay.AddDays(7):MM/dd} ~) + - + + ▶ 3주차 ({firstDay.AddDays(14):MM/dd} ~) + - + + ▶ 4주차 ({firstDay.AddDays(21):MM/dd} ~) + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 프로젝트·업무별 세부 계획 + + 번호 | 업무명 | 담당자 | 시작일 | 완료 예정일 | 우선순위 | 진행률 + -----|--------|--------|--------|-------------|----------|------- + 1 | | | | | | % + 2 | | | | | | % + 3 | | | | | | % + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 리스크 및 이슈 + + - + +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +■ 협조 필요 사항 + - """; } diff --git a/src/AxCopilot/Handlers/KeyHandler.cs b/src/AxCopilot/Handlers/KeyHandler.cs index 01da781..335aec3 100644 --- a/src/AxCopilot/Handlers/KeyHandler.cs +++ b/src/AxCopilot/Handlers/KeyHandler.cs @@ -177,6 +177,96 @@ public class KeyHandler : IActionHandler new("Ctrl + F", "터미널 내 텍스트 검색", "terminal"), new("Ctrl + Shift + P", "명령 팔레트", "terminal"), new("Ctrl + Shift + F", "전체 화면 토글", "terminal"), + + // Word + new("Ctrl + B", "굵게", "word"), + new("Ctrl + I", "기울임꼴", "word"), + new("Ctrl + U", "밑줄", "word"), + new("Ctrl + Z", "실행 취소", "word"), + new("Ctrl + Y", "다시 실행", "word"), + new("Ctrl + S", "저장", "word"), + new("Ctrl + P", "인쇄", "word"), + new("Ctrl + F", "찾기", "word"), + new("Ctrl + H", "찾아 바꾸기", "word"), + new("Ctrl + A", "전체 선택", "word"), + new("Ctrl + C / X / V", "복사 / 잘라내기 / 붙여넣기", "word"), + new("Ctrl + K", "하이퍼링크 삽입", "word"), + new("Ctrl + Enter", "페이지 나누기 삽입", "word"), + new("Alt + Shift + D", "현재 날짜 삽입", "word"), + new("Ctrl + Shift + >/<","글꼴 크기 늘리기/줄이기", "word"), + new("Ctrl + ] / [", "글꼴 크기 +1 / -1", "word"), + new("Ctrl + L/E/R/J", "왼쪽/가운데/오른쪽/양쪽 정렬", "word"), + new("Ctrl + 1 / 2 / 5", "줄 간격 1 / 2 / 1.5배", "word"), + new("Ctrl + Alt + 1/2/3","제목1 / 제목2 / 제목3 스타일", "word"), + new("Ctrl + Shift + N", "기본 스타일 적용", "word"), + new("F7", "맞춤법 및 문법 검사", "word"), + new("Ctrl + Shift + C", "서식 복사", "word"), + new("Ctrl + Shift + V", "서식 붙여넣기", "word"), + new("Shift + F3", "대/소문자 전환", "word"), + new("Ctrl + Home / End","문서 처음 / 끝으로 이동", "word"), + + // PowerPoint + new("F5", "처음부터 슬라이드 쇼 시작", "ppt"), + new("Shift + F5", "현재 슬라이드부터 시작", "ppt"), + new("Esc", "슬라이드 쇼 종료", "ppt"), + new("B", "슬라이드 쇼 중 화면 검정", "ppt"), + new("W", "슬라이드 쇼 중 화면 흰색", "ppt"), + new("Ctrl + M", "새 슬라이드 삽입", "ppt"), + new("Ctrl + D", "슬라이드 복제", "ppt"), + new("Ctrl + Z / Y", "실행 취소 / 다시 실행", "ppt"), + new("Ctrl + S", "저장", "ppt"), + new("Ctrl + G", "개체 그룹화", "ppt"), + new("Ctrl + Shift + G", "그룹 해제", "ppt"), + new("Tab", "다음 개체로 포커스 이동", "ppt"), + new("Shift + Tab", "이전 개체로 포커스 이동", "ppt"), + new("Ctrl + A", "모든 개체 선택", "ppt"), + new("Alt + Shift + ←/→","슬라이드 내 개체 정렬 (왼쪽/오른쪽)","ppt"), + new("Ctrl + Shift + >/<","글꼴 크기 늘리기/줄이기", "ppt"), + new("Arrow Keys", "슬라이드 쇼 중 다음/이전", "ppt"), + new("숫자 + Enter", "슬라이드 쇼 중 특정 슬라이드로 이동", "ppt"), + + // Teams + new("Ctrl + E", "검색창 포커스", "teams"), + new("Ctrl + /", "명령 팔레트 열기", "teams"), + new("Ctrl + N", "새 채팅 시작", "teams"), + new("Ctrl + Shift + M", "오디오 뮤트 토글", "teams"), + new("Ctrl + Shift + O", "비디오 카메라 토글", "teams"), + new("Ctrl + Shift + H", "회의 종료", "teams"), + new("Ctrl + Shift + K", "손 들기/내리기", "teams"), + new("Ctrl + Shift + B", "배경 흐림 토글", "teams"), + new("Ctrl + Shift + F", "전체 화면 토글", "teams"), + new("Ctrl + Shift + A", "팀 활동 피드 열기", "teams"), + new("Ctrl + 1", "활동 탭", "teams"), + new("Ctrl + 2", "채팅 탭", "teams"), + new("Ctrl + 3", "팀 탭", "teams"), + new("Ctrl + 4", "캘린더 탭", "teams"), + new("Ctrl + 5", "통화 탭", "teams"), + new("Alt + ↑/↓", "채널 목록 위/아래 이동", "teams"), + new("Ctrl + R", "메시지에 답장", "teams"), + new("Enter", "회의 참가 (알림에서)", "teams"), + + // Outlook + new("Ctrl + N", "새 이메일 작성", "outlook"), + new("Ctrl + R", "답장", "outlook"), + new("Ctrl + Shift + R", "전체 답장", "outlook"), + new("Ctrl + F", "이메일 전달", "outlook"), + new("Ctrl + Enter", "이메일 전송", "outlook"), + new("Ctrl + S", "초안 저장", "outlook"), + new("Delete", "이메일 삭제", "outlook"), + new("Ctrl + Z", "실행 취소", "outlook"), + new("Ctrl + 1", "메일 보기", "outlook"), + new("Ctrl + 2", "일정 보기", "outlook"), + new("Ctrl + 3", "연락처 보기", "outlook"), + new("Ctrl + 4", "작업 보기", "outlook"), + new("F9", "모든 계정 보내기/받기", "outlook"), + new("Ctrl + Shift + I", "받은 편지함으로 이동", "outlook"), + new("Ctrl + Shift + V", "이메일 이동", "outlook"), + new("Ctrl + Shift + G", "플래그 설정", "outlook"), + new("Ctrl + Q", "읽음 표시", "outlook"), + new("Ctrl + U", "읽지 않음 표시", "outlook"), + new("Ctrl + Shift + J", "정크 메일 처리", "outlook"), + new("Ctrl + K", "이름 확인 (주소 자동 완성)", "outlook"), + new("Alt + S", "이메일 보내기", "outlook"), ]; public Task> GetItemsAsync(string query, CancellationToken ct) @@ -187,7 +277,7 @@ public class KeyHandler : IActionHandler if (string.IsNullOrWhiteSpace(q)) { items.Add(new LauncherItem("키보드 단축키 참조 사전", - "key win / vscode / chrome / vim / excel / terminal / key <키워드 검색>", + "key win / vscode / chrome / vim / excel / terminal / word / ppt / teams / outlook / key <키워드 검색>", null, null, Symbol: "\uE92E")); AddAppOverview(items); return Task.FromResult>(items); @@ -205,6 +295,10 @@ public class KeyHandler : IActionHandler ["vim"] = ["vim", "vi", "neovim"], ["excel"] = ["excel", "spreadsheet"], ["terminal"] = ["terminal", "wt"], + ["word"] = ["word", "워드"], + ["ppt"] = ["ppt", "powerpoint", "파워포인트", "프레젠테이션"], + ["teams"] = ["teams", "팀즈"], + ["outlook"] = ["outlook", "아웃룩", "mail"], }; foreach (var (cat, aliases) in catKeys) @@ -220,6 +314,10 @@ public class KeyHandler : IActionHandler "vim" => "Vim", "excel" => "Excel", "terminal" => "Windows Terminal", + "word" => "Word", + "ppt" => "PowerPoint", + "teams" => "Teams", + "outlook" => "Outlook", _ => cat }; items.Add(new LauncherItem($"{catName} 단축키 {catItems.Count}개", "", null, null, Symbol: "\uE92E")); @@ -248,7 +346,7 @@ public class KeyHandler : IActionHandler else { items.Add(new LauncherItem($"'{keyword}' 결과 없음", - "key win / vscode / chrome / vim / excel / terminal", + "key win / vscode / chrome / vim / excel / terminal / word / ppt / teams / outlook", null, null, Symbol: "\uE783")); AddAppOverview(items); } @@ -281,6 +379,10 @@ public class KeyHandler : IActionHandler ("vim", "Vim 명령", All.Count(s => s.App == "vim")), ("excel", "Excel 단축키", All.Count(s => s.App == "excel")), ("terminal", "Windows Terminal 단축키", All.Count(s => s.App == "terminal")), + ("word", "Word 단축키", All.Count(s => s.App == "word")), + ("ppt", "PowerPoint 단축키", All.Count(s => s.App == "ppt")), + ("teams", "Teams 단축키", All.Count(s => s.App == "teams")), + ("outlook", "Outlook 단축키", All.Count(s => s.App == "outlook")), }; foreach (var (key, label, count) in apps) items.Add(new LauncherItem($"key {key}", $"{label} ({count}개)", diff --git a/src/AxCopilot/Handlers/SpellHandler.cs b/src/AxCopilot/Handlers/SpellHandler.cs new file mode 100644 index 0000000..6ddd74f --- /dev/null +++ b/src/AxCopilot/Handlers/SpellHandler.cs @@ -0,0 +1,276 @@ +using System.Windows; +using AxCopilot.SDK; +using AxCopilot.Services; + +namespace AxCopilot.Handlers; + +/// +/// L24-3: 자주 틀리는 한국어 맞춤법 핸들러. "spell" 프리픽스로 사용합니다. +/// +/// 예: spell → 클립보드 텍스트 맞춤법 검사 +/// spell 되 → "되/돼" 관련 오류 목록 +/// spell <단어> → 해당 단어 맞춤법 확인 +/// spell list → 전체 오류 목록 카테고리 +/// Enter → 올바른 표현 클립보드 복사 +/// +public class SpellHandler : IActionHandler +{ + public string? Prefix => "spell"; + + public PluginMetadata Metadata => new( + "맞춤법", + "자주 틀리는 한국어 맞춤법 참조 — 되/돼·안/않·혼동어·외래어 등", + "1.0", + "AX"); + + private sealed record SpellEntry(string Wrong, string Correct, string Explanation, string Category); + + private static readonly SpellEntry[] Entries = + [ + // ── 되/돼 ──────────────────────────────────────────────────────────── + new("됬다", "됐다", "됐다(되었다의 준말)", "되/돼"), + new("됬어", "됐어", "됐어(되었어의 준말)", "되/돼"), + new("됬나요", "됐나요", "됐나요(되었나요의 준말)", "되/돼"), + new("됬습니다", "됐습니다", "됐습니다(되었습니다의 준말)", "되/돼"), + new("돼었다", "됐다", "됐다(되었다의 준말)", "되/돼"), + new("안됬어", "안 됐어", "됐다(되었다의 준말), 안은 띄어씀", "되/돼"), + new("어떻게됬나요", "어떻게 됐나요","됐나요(되었나요의 준말)", "되/돼"), + new("잘됬다", "잘됐다", "잘됐다(잘 되었다의 준말)", "되/돼"), + new("해됬다", "해 됐다", "됐다(되었다의 준말)", "되/돼"), + new("이렇게됬다", "이렇게 됐다", "됐다(되었다의 준말), 띄어씀 주의", "되/돼"), + + // ── 안/않 ──────────────────────────────────────────────────────────── + new("안되다", "안 되다", "'안'은 부사이므로 띄어씀", "안/않"), + new("안하다", "안 하다", "'안'은 부사이므로 띄어씀", "안/않"), + new("않되다", "안 되다", "'않다'는 '아니하다'의 준말로 용언", "안/않"), + new("하지않다", "하지 않다", "'않다'는 용언, 앞 단어와 띄어씀", "안/않"), + new("하지않고", "하지 않고", "'않고'는 용언, 앞 단어와 띄어씀", "안/않"), + new("하지않아", "하지 않아", "'않아'는 용언, 앞 단어와 띄어씀", "안/않"), + new("되지않다", "되지 않다", "'않다'는 용언, 앞 단어와 띄어씀", "안/않"), + new("않됩니다", "안 됩니다", "'않다'는 '아니하다', '안'은 부사", "안/않"), + + // ── 혼동어 ────────────────────────────────────────────────────────── + new("로서/로써", "로서(자격), 로써(수단)", "의미 구분: 학생으로서(자격), 말로써(수단)", "혼동어"), + new("낫다/낳다", "낫다(회복), 낳다(출산)", "의미 구분: 병이 낫다 / 아이를 낳다", "혼동어"), + new("맞히다/맞추다","맞히다(정답), 맞추다(조합)","의미 구분: 문제를 맞히다 / 퍼즐을 맞추다", "혼동어"), + new("반드시/반듯이","반드시(꼭), 반듯이(곧게)", "의미 구분: 반드시 와라 / 반듯이 서라", "혼동어"), + new("부치다/붙이다","부치다(편지), 붙이다(접착)","의미 구분: 편지를 부치다 / 우표를 붙이다", "혼동어"), + new("이따가/있다가","이따가(나중에), 있다가(머물다가)","의미 구분: 이따가 와라 / 집에 있다가 나와", "혼동어"), + new("웬/왠", "웬(어떤), 왠지(왜인지)", "의미 구분: 웬 일이니 / 왠지 모르게", "혼동어"), + new("어떻게/어떡해","어떻게(방법), 어떡해(어떻게 해)","의미 구분: 어떻게 해야 하나 / 이걸 어떡해", "혼동어"), + new("바라다/바래다","바라다(희망), 바래다(색이 변함)","의미 구분: 합격을 바란다 / 색이 바랬다", "혼동어"), + new("~던지/~든지", "~던지(과거경험), ~든지(선택)","의미 구분: 얼마나 좋았던지 / 뭐든지 해라", "혼동어"), + new("틀리다/다르다","틀리다(오답), 다르다(차이)", "의미 구분: 답이 틀렸다 / 나와 다르다", "혼동어"), + + // ── 맞춤법 ────────────────────────────────────────────────────────── + new("설레임", "설렘", "표준어는 '설렘'", "맞춤법"), + new("오랫만에", "오랜만에", "표준어는 '오랜만에'", "맞춤법"), + new("왠만하면", "웬만하면", "표준어는 '웬만하면'", "맞춤법"), + new("몇일", "며칠", "표준어는 '며칠'", "맞춤법"), + new("어의없다", "어이없다", "표준어는 '어이없다'", "맞춤법"), + new("내노라하는", "내로라하는", "표준어는 '내로라하는'", "맞춤법"), + new("금새", "금세", "표준어는 '금세' ('금시에'의 준말)", "맞춤법"), + new("새벽녁", "새벽녘", "표준어는 '새벽녘'", "맞춤법"), + new("무릎쓰고", "무릅쓰고", "표준어는 '무릅쓰고'", "맞춤법"), + new("짜집기", "짜깁기", "표준어는 '짜깁기'", "맞춤법"), + new("역활", "역할", "표준어는 '역할'", "맞춤법"), + new("희안하다", "희한하다", "표준어는 '희한하다'", "맞춤법"), + new("요컨데", "요컨대", "표준어는 '요컨대'", "맞춤법"), + new("알맞는", "알맞은", "형용사이므로 '알맞은'이 맞음", "맞춤법"), + new("예쁘다/이쁘다","예쁘다가 표준어","'이쁘다'는 비표준어", "맞춤법"), + new("이따위", "이따위", "표준어 (맞음)", "맞춤법"), + new("구렛나루", "구레나룻", "표준어는 '구레나룻'", "맞춤법"), + new("꼭두각시", "꼭두각시", "표준어 (맞음)", "맞춤법"), + + // ── 띄어쓰기 ──────────────────────────────────────────────────────── + new("할수있다", "할 수 있다", "의존명사 '수'는 띄어씀", "띄어쓰기"), + new("해야한다", "해야 한다", "'한다'는 독립 서술어, 띄어씀", "띄어쓰기"), + new("것같다", "것 같다", "의존명사 '것'은 띄어씀", "띄어쓰기"), + new("수밖에없다", "수밖에 없다", "'없다'는 독립 서술어, 띄어씀", "띄어쓰기"), + new("한번", "한 번(횟수), 한번(시도)","횟수=한 번 / 시도=한번", "띄어쓰기"), + new("이상한거같다", "이상한 것 같다","'것'은 의존명사로 띄어씀", "띄어쓰기"), + new("될것같다", "될 것 같다", "'것'은 의존명사로 띄어씀", "띄어쓰기"), + new("할것이다", "할 것이다", "'것'은 의존명사로 띄어씀", "띄어쓰기"), + new("있을때", "있을 때", "'때'는 의존명사로 띄어씀", "띄어쓰기"), + new("모를때", "모를 때", "'때'는 의존명사로 띄어씀", "띄어쓰기"), + + // ── 외래어 ────────────────────────────────────────────────────────── + new("리더쉽", "리더십", "표준 외래어: ~ship은 '십'으로", "외래어"), + new("멤버쉽", "멤버십", "표준 외래어: ~ship은 '십'으로", "외래어"), + new("파트너쉽", "파트너십", "표준 외래어: ~ship은 '십'으로", "외래어"), + new("인턴쉽", "인턴십", "표준 외래어: ~ship은 '십'으로", "외래어"), + new("메세지", "메시지", "표준 외래어: message → 메시지", "외래어"), + new("써비스", "서비스", "표준 외래어: service → 서비스", "외래어"), + new("케릭터", "캐릭터", "표준 외래어: character → 캐릭터", "외래어"), + new("컨텐츠", "콘텐츠", "표준 외래어: contents → 콘텐츠", "외래어"), + new("엑기스", "엑스", "표준 외래어: extract → 엑기스 (비표준)","외래어"), + new("렌트카", "렌터카", "표준 외래어: rent-a-car → 렌터카", "외래어"), + new("쥬스", "주스", "표준 외래어: juice → 주스", "외래어"), + new("후라이", "프라이", "표준 외래어: fry → 프라이", "외래어"), + new("후라이팬", "프라이팬", "표준 외래어: frying pan → 프라이팬", "외래어"), + new("비스켓", "비스킷", "표준 외래어: biscuit → 비스킷", "외래어"), + new("떼레비", "텔레비전", "표준 외래어: television → 텔레비전", "외래어"), + ]; + + private static readonly string[] CategoryOrder = ["되/돼", "안/않", "혼동어", "맞춤법", "띄어쓰기", "외래어"]; + + public Task> GetItemsAsync(string query, CancellationToken ct) + { + var q = query.Trim(); + var items = new List(); + + if (string.IsNullOrWhiteSpace(q)) + { + // 클립보드 텍스트 맞춤법 검사 + string clipText = ""; + try + { + System.Windows.Application.Current.Dispatcher.Invoke(() => + { + if (Clipboard.ContainsText()) + clipText = Clipboard.GetText(); + }); + } + catch { } + + if (!string.IsNullOrWhiteSpace(clipText)) + { + var found = Entries.Where(e => + clipText.Contains(e.Wrong, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (found.Count > 0) + { + items.Add(new LauncherItem($"클립보드에서 맞춤법 오류 {found.Count}개 발견", + "Enter: 올바른 표현 복사", null, null, Symbol: "\uE7BA")); + foreach (var e in found) + items.Add(MakeSpellItem(e)); + return Task.FromResult>(items); + } + else + { + items.Add(new LauncherItem("클립보드 텍스트에서 오류를 찾지 못했습니다", + "카테고리: 되/돼 · 안/않 · 맞춤법 · 띄어쓰기 · 혼동어 · 외래어", + null, null, Symbol: "\uE7BA")); + } + } + else + { + items.Add(new LauncherItem($"한국어 맞춤법 참조 {Entries.Length}개", + "카테고리: 되/돼 · 안/않 · 맞춤법 · 띄어쓰기 · 혼동어 · 외래어", + null, null, Symbol: "\uE7BA")); + } + + // 카테고리 목록 + foreach (var cat in CategoryOrder) + { + var cnt = Entries.Count(e => e.Category == cat); + items.Add(new LauncherItem($"spell {cat}", $"{cat} ({cnt}개)", + null, null, Symbol: "\uE7BA")); + } + return Task.FromResult>(items); + } + + var kw = q.ToLowerInvariant(); + + // "list" → 카테고리별 개수 + if (kw == "list") + { + items.Add(new LauncherItem($"맞춤법 오류 목록 총 {Entries.Length}개", "", null, null, Symbol: "\uE7BA")); + foreach (var cat in CategoryOrder) + { + var cnt = Entries.Count(e => e.Category == cat); + items.Add(new LauncherItem(cat, $"{cnt}개 · spell {cat}로 목록 보기", + null, null, Symbol: "\uE7BA")); + } + return Task.FromResult>(items); + } + + // 카테고리 키워드 매핑 + var catMap = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["되"] = "되/돼", + ["돼"] = "됩니다", + ["됐"] = "됩니다", + ["됩"] = "됩니다", + ["되/돼"] = "됩니다", + ["안"] = "안/않", + ["않"] = "안/않", + ["안/않"] = "안/않", + ["혼동"] = "혼동어", + ["혼동어"] = "혼동어", + ["맞춤법"] = "맞춤법", + ["맞춤"] = "맞춤법", + ["띄어"] = "띄어쓰기", + ["띄어쓰기"] = "띄어쓰기", + ["외래어"] = "외래어", + ["외래"] = "외래어", + }; + + // 카테고리 직접 매핑 + foreach (var cat in CategoryOrder) + { + if (cat.Equals(q, StringComparison.OrdinalIgnoreCase) || + catMap.TryGetValue(q, out var mappedCat) && mappedCat == cat) + { + var catList = Entries.Where(e => e.Category == cat).ToList(); + items.Add(new LauncherItem($"{cat} {catList.Count}개", + "Enter: 올바른 표현 복사", null, null, Symbol: "\uE7BA")); + foreach (var e in catList) + items.Add(MakeSpellItem(e)); + return Task.FromResult>(items); + } + } + + // catMap으로 카테고리 찾기 + if (catMap.TryGetValue(q, out var foundCat) && CategoryOrder.Contains(foundCat)) + { + var catList = Entries.Where(e => e.Category == foundCat).ToList(); + items.Add(new LauncherItem($"{foundCat} {catList.Count}개", + "Enter: 올바른 표현 복사", null, null, Symbol: "\uE7BA")); + foreach (var e in catList) + items.Add(MakeSpellItem(e)); + return Task.FromResult>(items); + } + + // 키워드 검색 + var searched = Entries.Where(e => + e.Wrong.Contains(kw, StringComparison.OrdinalIgnoreCase) || + e.Correct.Contains(kw, StringComparison.OrdinalIgnoreCase) || + e.Explanation.Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList(); + + if (searched.Count == 0) + { + items.Add(new LauncherItem($"'{q}' 검색 결과 없음", + "카테고리: 되/돼 · 안/않 · 맞춤법 · 띄어쓰기 · 혼동어 · 외래어", + null, null, Symbol: "\uE783")); + return Task.FromResult>(items); + } + + items.Add(new LauncherItem($"'{q}' 검색 결과 {searched.Count}개", + "Enter: 올바른 표현 복사", null, null, Symbol: "\uE7BA")); + foreach (var e in searched) + items.Add(MakeSpellItem(e)); + + return Task.FromResult>(items); + } + + public Task ExecuteAsync(LauncherItem item, CancellationToken ct) + { + if (item.Data is ("copy", string text)) + { + try + { + System.Windows.Application.Current.Dispatcher.Invoke( + () => Clipboard.SetText(text)); + NotificationService.Notify("맞춤법", "올바른 표현을 복사했습니다."); + } + catch { } + } + return Task.CompletedTask; + } + + private static LauncherItem MakeSpellItem(SpellEntry e) => + new($"❌ {e.Wrong} → ✅ {e.Correct}", + e.Explanation, + null, ("copy", e.Correct), Symbol: "\uE7BA"); +}