using System.IO;
using System.Text.Encodings.Web;
using System.Text.Json;
using System.Text.Json.Serialization;
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 = ["되/돼", "안/않", "혼동어", "맞춤법", "띄어쓰기", "외래어", "사용자"];
// ─── 사용자 정의 항목 (L29-3) ────────────────────────────────────────────
private sealed record CustomSpell(
[property: JsonPropertyName("wrong")] string Wrong,
[property: JsonPropertyName("correct")] string Correct,
[property: JsonPropertyName("desc")] string Desc);
private static readonly string CustomPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "spell_custom.json");
private static readonly JsonSerializerOptions JsonOpt = new()
{
WriteIndented = true,
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
private static List LoadCustom()
{
try
{
if (!File.Exists(CustomPath)) return [];
return JsonSerializer.Deserialize>(File.ReadAllText(CustomPath)) ?? [];
}
catch { return []; }
}
private static void SaveCustom(List list)
{
try
{
var dir = Path.GetDirectoryName(CustomPath)!;
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
File.WriteAllText(CustomPath, JsonSerializer.Serialize(list, JsonOpt));
}
catch { }
}
private IEnumerable AllEntries()
{
foreach (var e in Entries) yield return e;
foreach (var c in LoadCustom())
yield return new SpellEntry(c.Wrong, c.Correct, c.Desc, "사용자");
}
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
// ── add 명령 (L29-3) ─────────────────────────────────────────────────
if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase))
{
var parts = q[4..].Trim().Split(' ', 3, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2)
{
items.Add(new LauncherItem("사용법: spell add {틀린표현} {올바른표현} [설명]",
"예: spell add 어의없다 어이없다 어이(기가 막혀)가 올바른 표현",
null, null, Symbol: "\uE710"));
}
else
{
var wrong = parts[0];
var correct = parts[1];
var desc = parts.Length >= 3 ? parts[2] : "사용자 추가 항목";
items.Add(new LauncherItem(
$"맞춤법 추가: ❌ {wrong} → ✅ {correct}",
desc,
null, ("add", $"{wrong}\t{correct}\t{desc}"), Symbol: "\uE710"));
}
return Task.FromResult>(items);
}
// ── del 명령 (L29-3) ─────────────────────────────────────────────────
if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase))
{
var wrong = q[4..].Trim();
var custom = LoadCustom();
var found = custom.FirstOrDefault(c => c.Wrong.Equals(wrong, StringComparison.OrdinalIgnoreCase));
if (found != null)
{
items.Add(new LauncherItem($"사용자 항목 삭제: {found.Wrong} → {found.Correct}",
found.Desc, null, ("del", found.Wrong), Symbol: "\uE74D"));
}
else
{
items.Add(new LauncherItem($"'{wrong}' 사용자 항목을 찾을 수 없습니다",
"기본 항목은 삭제할 수 없습니다", null, null, Symbol: "\uE783"));
}
return Task.FromResult>(items);
}
// ── custom 명령 (사용자 항목만 보기) ──────────────────────────────────
if (q.Equals("custom", StringComparison.OrdinalIgnoreCase) ||
q.Equals("사용자", StringComparison.OrdinalIgnoreCase))
{
var custom = LoadCustom();
if (custom.Count == 0)
{
items.Add(new LauncherItem("사용자 항목이 없습니다",
"spell add {틀린표현} {올바른표현} [설명] 으로 추가하세요",
null, null, Symbol: "\uE7BA"));
}
else
{
items.Add(new LauncherItem($"사용자 맞춤법 항목 {custom.Count}개", "",
null, null, Symbol: "\uE7BA"));
foreach (var c in custom)
items.Add(MakeSpellItem(new SpellEntry(c.Wrong, c.Correct, c.Desc, "사용자")));
}
return Task.FromResult>(items);
}
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 = AllEntries().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")
{
var all = AllEntries().ToList();
items.Add(new LauncherItem($"맞춤법 오류 목록 총 {all.Count}개", "", null, null, Symbol: "\uE7BA"));
foreach (var cat in CategoryOrder)
{
var cnt = all.Count(e => e.Category == cat);
if (cnt == 0) continue;
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 = AllEntries().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 { }
}
else if (item.Data is ("add", string addData))
{
var parts = addData.Split('\t');
if (parts.Length >= 3)
{
var custom = LoadCustom();
custom.RemoveAll(c => c.Wrong.Equals(parts[0], StringComparison.OrdinalIgnoreCase));
custom.Add(new CustomSpell(parts[0], parts[1], parts[2]));
SaveCustom(custom);
NotificationService.Notify("맞춤법", $"'{parts[0]} → {parts[1]}' 항목이 추가되었습니다.");
}
}
else if (item.Data is ("del", string delWrong))
{
var custom = LoadCustom();
custom.RemoveAll(c => c.Wrong.Equals(delWrong, StringComparison.OrdinalIgnoreCase));
SaveCustom(custom);
NotificationService.Notify("맞춤법", $"'{delWrong}' 항목이 삭제되었습니다.");
}
return Task.CompletedTask;
}
private static LauncherItem MakeSpellItem(SpellEntry e) =>
new($"❌ {e.Wrong} → ✅ {e.Correct}",
e.Explanation,
null, ("copy", e.Correct), Symbol: "\uE7BA");
}