[Phase L24] 양식 6종·단축키 4앱·맞춤법 핸들러·약어 커스텀 추가

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
This commit is contained in:
2026-04-04 18:37:58 +09:00
parent ad3335a63e
commit afedd826c8
5 changed files with 847 additions and 15 deletions

View File

@@ -359,6 +359,8 @@ public partial class App : System.Windows.Application
commandResolver.RegisterHandler(new WorkTimeHandler()); commandResolver.RegisterHandler(new WorkTimeHandler());
// L23-4: 한/영 타이핑 오류 교정 (prefix=fix) // L23-4: 한/영 타이핑 오류 교정 (prefix=fix)
commandResolver.RegisterHandler(new FixHandler()); commandResolver.RegisterHandler(new FixHandler());
// L24-3: 자주 틀리는 맞춤법 (prefix=spell)
commandResolver.RegisterHandler(new SpellHandler());
// ─── 플러그인 로드 ──────────────────────────────────────────────────── // ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver); var pluginHost = new PluginHost(settings, commandResolver);

View File

@@ -1,3 +1,5 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows; using System.Windows;
using AxCopilot.SDK; using AxCopilot.SDK;
using AxCopilot.Services; 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[] 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<CustomAbbr> LoadCustom()
{
try {
if (System.IO.File.Exists(CustomPath))
return JsonSerializer.Deserialize<List<CustomAbbr>>(
System.IO.File.ReadAllText(CustomPath)) ?? [];
} catch { }
return [];
}
private static void SaveCustom(List<CustomAbbr> 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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct) public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{ {
var q = query.Trim(); var q = query.Trim();
var items = new List<LauncherItem>(); var items = new List<LauncherItem>();
// ── 서브커맨드 처리 ───────────────────────────────────────────────────
// 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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
items.Add(new LauncherItem($"약어 삭제: {abbr}",
"Enter: 삭제 확인",
null, ("del", abbr), Symbol: "\uE82D"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
if (string.IsNullOrWhiteSpace(q)) if (string.IsNullOrWhiteSpace(q))
{ {
items.Add(new LauncherItem($"IT·개발 약어 사전 {Entries.Length}개", items.Add(new LauncherItem($"IT·개발 약어 사전 {Entries.Length}개",
"abbr <약어> 예: abbr api / abbr crud / abbr jwt", "abbr <약어> 예: abbr api / abbr crud / abbr jwt",
null, null, Symbol: "\uE82D")); 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")); items.Add(new LauncherItem("── 카테고리 ──", "", null, null, Symbol: "\uE82D"));
foreach (var cat in Categories) foreach (var cat in Categories)
{ {
@@ -240,11 +338,15 @@ public class AbbrHandler : IActionHandler
return Task.FromResult<IEnumerable<LauncherItem>>(items); return Task.FromResult<IEnumerable<LauncherItem>>(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(); e.Short.Equals(q, StringComparison.OrdinalIgnoreCase)).ToList();
var partial = Entries.Where(e => var partial = allEntries.Where(e =>
!exact.Contains(e) && !exact.Contains(e) &&
(e.Short.Contains(q, StringComparison.OrdinalIgnoreCase) || (e.Short.Contains(q, StringComparison.OrdinalIgnoreCase) ||
e.Full.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) public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{ {
if (item.Data is ("copy", string text)) switch (item.Data)
{ {
try case ("copy", string text):
{ try
System.Windows.Application.Current.Dispatcher.Invoke( {
() => Clipboard.SetText(text)); System.Windows.Application.Current.Dispatcher.Invoke(
NotificationService.Notify("Abbr", "클립보드에 복사했습니다."); () => Clipboard.SetText(text));
} NotificationService.Notify("Abbr", "클립보드에 복사했습니다.");
catch { } }
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; return Task.CompletedTask;
} }

View File

@@ -107,6 +107,38 @@ public class FormHandler : IActionHandler
"첫 1·2·4주 온보딩 단계별 체크리스트", "첫 1·2·4주 온보딩 단계별 체크리스트",
"onboard", ["onboard", "온보딩", "신입", "입사"], "onboard", ["onboard", "온보딩", "신입", "입사"],
() => BuildOnboard()), () => 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 = private static readonly (string Key, string[] Aliases, string Label)[] Categories =
@@ -117,6 +149,8 @@ public class FormHandler : IActionHandler
("project", ["project", "프로젝트"], "프로젝트 문서"), ("project", ["project", "프로젝트"], "프로젝트 문서"),
("review", ["review", "리뷰", "평가"], "리뷰·평가"), ("review", ["review", "리뷰", "평가"], "리뷰·평가"),
("onboard", ["onboard", "온보딩", "입사"], "온보딩"), ("onboard", ["onboard", "온보딩", "입사"], "온보딩"),
("handover", ["handover", "인수인계", "지시", "품의"], "인수인계·지시·품의"),
("daily", ["daily", "일지", "일일", "계획"], "업무 일지·계획"),
]; ];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct) public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
@@ -127,7 +161,7 @@ public class FormHandler : IActionHandler
if (string.IsNullOrWhiteSpace(q)) if (string.IsNullOrWhiteSpace(q))
{ {
items.Add(new LauncherItem("업무 양식 템플릿", items.Add(new LauncherItem("업무 양식 템플릿",
"카테고리: meeting · report · email · project · review · onboard", "카테고리: meeting · report · email · project · review · onboard · handover · daily",
null, null, Symbol: "\uE8A5")); null, null, Symbol: "\uE8A5"));
foreach (var (key, _, label) in Categories) foreach (var (key, _, label) in Categories)
@@ -163,7 +197,7 @@ public class FormHandler : IActionHandler
if (searched.Count == 0) if (searched.Count == 0)
{ {
items.Add(new LauncherItem($"'{q}' 양식을 찾을 수 없습니다", items.Add(new LauncherItem($"'{q}' 양식을 찾을 수 없습니다",
"카테고리: meeting · report · email · project · review · onboard", "카테고리: meeting · report · email · project · review · onboard · handover · daily",
null, null, Symbol: "\uE783")); null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items); return Task.FromResult<IEnumerable<LauncherItem>>(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 | | | | | | %
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 리스크 및 이슈
-
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
■ 협조 필요 사항
- -
"""; """;
} }

View File

@@ -177,6 +177,96 @@ public class KeyHandler : IActionHandler
new("Ctrl + F", "터미널 내 텍스트 검색", "terminal"), new("Ctrl + F", "터미널 내 텍스트 검색", "terminal"),
new("Ctrl + Shift + P", "명령 팔레트", "terminal"), new("Ctrl + Shift + P", "명령 팔레트", "terminal"),
new("Ctrl + Shift + F", "전체 화면 토글", "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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct) public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
@@ -187,7 +277,7 @@ public class KeyHandler : IActionHandler
if (string.IsNullOrWhiteSpace(q)) if (string.IsNullOrWhiteSpace(q))
{ {
items.Add(new LauncherItem("키보드 단축키 참조 사전", 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")); null, null, Symbol: "\uE92E"));
AddAppOverview(items); AddAppOverview(items);
return Task.FromResult<IEnumerable<LauncherItem>>(items); return Task.FromResult<IEnumerable<LauncherItem>>(items);
@@ -205,6 +295,10 @@ public class KeyHandler : IActionHandler
["vim"] = ["vim", "vi", "neovim"], ["vim"] = ["vim", "vi", "neovim"],
["excel"] = ["excel", "spreadsheet"], ["excel"] = ["excel", "spreadsheet"],
["terminal"] = ["terminal", "wt"], ["terminal"] = ["terminal", "wt"],
["word"] = ["word", "워드"],
["ppt"] = ["ppt", "powerpoint", "파워포인트", "프레젠테이션"],
["teams"] = ["teams", "팀즈"],
["outlook"] = ["outlook", "아웃룩", "mail"],
}; };
foreach (var (cat, aliases) in catKeys) foreach (var (cat, aliases) in catKeys)
@@ -220,6 +314,10 @@ public class KeyHandler : IActionHandler
"vim" => "Vim", "vim" => "Vim",
"excel" => "Excel", "excel" => "Excel",
"terminal" => "Windows Terminal", "terminal" => "Windows Terminal",
"word" => "Word",
"ppt" => "PowerPoint",
"teams" => "Teams",
"outlook" => "Outlook",
_ => cat _ => cat
}; };
items.Add(new LauncherItem($"{catName} 단축키 {catItems.Count}개", "", null, null, Symbol: "\uE92E")); items.Add(new LauncherItem($"{catName} 단축키 {catItems.Count}개", "", null, null, Symbol: "\uE92E"));
@@ -248,7 +346,7 @@ public class KeyHandler : IActionHandler
else else
{ {
items.Add(new LauncherItem($"'{keyword}' 결과 없음", 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")); null, null, Symbol: "\uE783"));
AddAppOverview(items); AddAppOverview(items);
} }
@@ -281,6 +379,10 @@ public class KeyHandler : IActionHandler
("vim", "Vim 명령", All.Count(s => s.App == "vim")), ("vim", "Vim 명령", All.Count(s => s.App == "vim")),
("excel", "Excel 단축키", All.Count(s => s.App == "excel")), ("excel", "Excel 단축키", All.Count(s => s.App == "excel")),
("terminal", "Windows Terminal 단축키", All.Count(s => s.App == "terminal")), ("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) foreach (var (key, label, count) in apps)
items.Add(new LauncherItem($"key {key}", $"{label} ({count}개)", items.Add(new LauncherItem($"key {key}", $"{label} ({count}개)",

View File

@@ -0,0 +1,276 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
/// <summary>
/// L24-3: 자주 틀리는 한국어 맞춤법 핸들러. "spell" 프리픽스로 사용합니다.
///
/// 예: spell → 클립보드 텍스트 맞춤법 검사
/// spell 되 → "되/돼" 관련 오류 목록
/// spell <단어> → 해당 단어 맞춤법 확인
/// spell list → 전체 오류 목록 카테고리
/// Enter → 올바른 표현 클립보드 복사
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
// 카테고리 키워드 매핑
var catMap = new Dictionary<string, string>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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");
}