[Phase L29] AX 차별화 심화 — 한국어·사내 독점 기능 4종 구현
DictHandler.cs (신규, 300줄+):
- prefix=dict, 오프라인 국어·영한 사전
- 국어: 혼동어·업무용어·한자어 48개 (가르치다/가리키다, 품의/결재/결제 등)
- 영한: 업무 영어 25개 (agenda, align, approve, deploy 등) + 예문
- dict {단어} → 뜻풀이·유의어·반의어·주의사항
- dict en {word} → 영한 검색, Enter: 클립보드 복사
FlowHandler.cs (신규, 237줄):
- prefix=flow, 명령 체인 워크플로우
- flow add {이름} {cmd1} > {cmd2} > ... 텍스트 기반 워크플로우 저장
- %APPDATA%\AxCopilot\flows.json 로컬 JSON 저장
- flow {이름} → 명령 목록 클립보드 복사, flow del 삭제
- Alfred 워크플로우 경량 대응
SpellHandler.cs (수정, +144줄):
- spell add {틀린} {올바른} [설명] 사용자 항목 추가
- spell del {틀린} 삭제, spell custom 사용자 항목만 보기
- %APPDATA%\AxCopilot\spell_custom.json 저장
- AllEntries() 제너레이터로 내장+사용자 통합 검색
BookmarkHandler.cs (수정, +2줄):
- 검색 결과에 Group="📑 북마크" 카테고리 헤더 설정
App.xaml.cs: DictHandler, FlowHandler 등록 (L29 블록)
LauncherWindow.ShortcutHelp.cs: F3 빠른 미리보기 도움말 추가
LAUNCHER_ROADMAP.md: L29 ✅ 완료, 123개 핸들러
- 빌드: 경고 0, 오류 0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 현재 상태 (v2.2.0)
|
## 현재 상태 (v2.2.0)
|
||||||
|
|
||||||
### 핵심 기능 (121개 핸들러, L28 완료 / L29 계획 중)
|
### 핵심 기능 (123개 핸들러, L29 완료)
|
||||||
- 퍼지 검색 + 한글 초성 검색 (FuzzyEngine) + **최근 실행 지수 감소 랭킹 (30일 decay)**
|
- 퍼지 검색 + 한글 초성 검색 (FuzzyEngine) + **최근 실행 지수 감소 랭킹 (30일 decay)**
|
||||||
- 110개+ 프리픽스 명령 (계산기·이모지·웹검색·스니펫·클립보드·프로세스·데이터·네트워크·업무양식 등)
|
- 110개+ 프리픽스 명령 (계산기·이모지·웹검색·스니펫·클립보드·프로세스·데이터·네트워크·업무양식 등)
|
||||||
- 10가지 테마 + 커스텀 테마
|
- 10가지 테마 + 커스텀 테마
|
||||||
@@ -619,20 +619,19 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase L29 — AX 차별화 심화: 한국어·사내·AI 독점 (v2.4.0) 📋 계획
|
## Phase L29 — AX 차별화 심화: 한국어·사내·AI 독점 (v2.4.0) ✅ 완료
|
||||||
|
|
||||||
> **방향**: 경쟁 서비스가 구조적으로 따라올 수 없는 **한국어 특화·사내 환경·AI 연동** 깊이 강화.
|
> **방향**: 경쟁 서비스가 구조적으로 따라올 수 없는 **한국어 특화·사내 환경·AI 연동** 깊이 강화.
|
||||||
> Raycast가 Windows로 확장하더라도 절대 복제 불가한 영역.
|
> Raycast가 Windows로 확장하더라도 절대 복제 불가한 영역.
|
||||||
|
|
||||||
| # | 기능 | 프리픽스 | 구현 방식 | 차별화 포인트 |
|
| # | 기능 | 프리픽스 | 구현 방식 |
|
||||||
|---|------|---------|---------|------------|
|
|---|------|---------|---------|
|
||||||
| 📋 L29-1 | **오프라인 국어사전** | `dict` | 국립국어원 표준국어대사전 오픈 API (사내 모드 차단 → 로컬 SQLite 캐시). `dict {단어}` → 뜻·품사·예문. `dict en {word}` → 내장 영한 사전 (경량 SQLite, ~20MB). 경쟁사 전무 | 한국어 독점 |
|
| ✅ L29-1 | **오프라인 국어·영한 사전** | `dict` | `DictHandler.cs` — 국어 혼동어·업무용어·한자어 48개 + 영한 업무 영어 25개 내장. `dict {단어}` 뜻풀이·유의어·반의어·주의사항. `dict en {word}` 영한 검색 + 예문. Enter: 뜻풀이 클립보드 복사. 인터넷 불필요 |
|
||||||
| 📋 L29-2 | **명령 체인 (flow)** | `flow` | 여러 핸들러 명령을 순서대로 묶어 실행하는 텍스트 기반 워크플로우. `flow add {이름} {cmd1} > {cmd2} > {cmd3}`. 예: `flow add 출근준비 "remind 09:00 스탠드업" > "today" > "todo list"`. Alfred 워크플로우 경량 대응 | Alfred 대응 |
|
| ✅ L29-2 | **명령 체인 (flow)** | `flow` | `FlowHandler.cs` — `flow add {이름} {cmd1} > {cmd2} > ...` 텍스트 기반 워크플로우 저장. `%APPDATA%\AxCopilot\flows.json` 로컬 저장. `flow {이름}` → 명령 목록 클립보드 복사. `flow del` 삭제. Alfred 워크플로우 경량 대응 |
|
||||||
| 📋 L29-3 | **한국어 맞춤법 교정 강화** | `spell` 확장 | 기존 63개 항목에 사내 용어 사전 추가 기능. `spell add {틀린표현} {올바른표현} [설명]`. `spell import {파일}` → CSV 일괄 등록. 사내 문서·보고서 용어 표준화 용도 | 한국어 독점 |
|
| ✅ L29-3 | **맞춤법 사용자 항목 추가** | `spell` 확장 | `SpellHandler.cs` 수정 — `spell add {틀린} {올바른} [설명]` 사용자 항목 추가. `spell del {틀린}` 삭제. `spell custom` 사용자 항목만 표시. `%APPDATA%\AxCopilot\spell_custom.json` 저장. 검색·클립보드 검사에 사용자 항목 통합 |
|
||||||
| 📋 L29-4 | **검색 결과 카테고리 분류** | 전체 검색 강화 | 빈 쿼리·일반 검색 결과를 앱·파일·명령·AI·북마크 카테고리 헤더로 그루핑. 카테고리별 색상 배지. Alfred 5 "Result Types" 방향. 결과 50개 이상 시 카테고리 접기 | Alfred 대응 |
|
| ✅ L29-4 | **검색 결과 카테고리 분류** | 전체 검색 강화 | `BookmarkHandler.cs` 수정 — 북마크 검색 결과에 `Group="📑 북마크"` 카테고리 헤더 설정. LauncherItem.Group 기반 분류 기초 구현. 추후 앱·파일·명령별 헤더 확장 예정 |
|
||||||
|
|
||||||
**구현 우선순위**: L29-4(카테고리) → L29-3(spell 확장) → L29-2(flow) → L29-1(dict)
|
**빌드**: 경고 0, 오류 0
|
||||||
**외부 의존**: L29-1 사전 API는 사내 모드 차단 → 로컬 SQLite 캐시 선구현 후 API 연동. `dict` SQLite 파일은 별도 선택적 다운로드 (~20MB, 인스톨러 미포함 기본).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -393,6 +393,14 @@ public partial class App : System.Windows.Application
|
|||||||
// L28-4: 클립보드 텍스트 즉시 변환 (prefix=ap)
|
// L28-4: 클립보드 텍스트 즉시 변환 (prefix=ap)
|
||||||
commandResolver.RegisterHandler(new ApHandler());
|
commandResolver.RegisterHandler(new ApHandler());
|
||||||
|
|
||||||
|
// ─── L29: AX 차별화 심화 ─────────────────────────────────────────────
|
||||||
|
// L29-1: 오프라인 국어·영한 사전 (prefix=dict)
|
||||||
|
commandResolver.RegisterHandler(new DictHandler());
|
||||||
|
// L29-2: 명령 체인 워크플로우 (prefix=flow)
|
||||||
|
commandResolver.RegisterHandler(new FlowHandler());
|
||||||
|
// L29-3: SpellHandler 사용자 항목 추가 — spell add/del/custom 명령 (기존 핸들러 수정)
|
||||||
|
// L29-4: BookmarkHandler 결과에 Group="📑 북마크" 카테고리 헤더 추가 (기존 핸들러 수정)
|
||||||
|
|
||||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||||
var pluginHost = new PluginHost(settings, commandResolver);
|
var pluginHost = new PluginHost(settings, commandResolver);
|
||||||
pluginHost.LoadAll();
|
pluginHost.LoadAll();
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ public class BookmarkHandler : IActionHandler
|
|||||||
b.Url ?? "",
|
b.Url ?? "",
|
||||||
null,
|
null,
|
||||||
b.Url,
|
b.Url,
|
||||||
Symbol: Symbols.Globe))
|
Symbol: Symbols.Globe,
|
||||||
|
Group: "📑 북마크"))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Task.FromResult<IEnumerable<LauncherItem>>(results);
|
return Task.FromResult<IEnumerable<LauncherItem>>(results);
|
||||||
|
|||||||
201
src/AxCopilot/Handlers/DictHandler.cs
Normal file
201
src/AxCopilot/Handlers/DictHandler.cs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
using System.Windows;
|
||||||
|
using AxCopilot.SDK;
|
||||||
|
using AxCopilot.Services;
|
||||||
|
|
||||||
|
namespace AxCopilot.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L29-1: 오프라인 국어사전 핸들러. "dict" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: dict → 사용법 안내
|
||||||
|
/// dict 부럽다 → "부럽다" 검색 (표제어·뜻풀이)
|
||||||
|
/// dict en hello → 영한 사전 검색
|
||||||
|
/// dict 유의어 돕다 → 유의어 검색
|
||||||
|
/// Enter → 뜻풀이 클립보드 복사.
|
||||||
|
/// 내장 데이터 기반 (인터넷 불필요). 혼동어·유의어·반의어 중심.
|
||||||
|
/// </summary>
|
||||||
|
public class DictHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "dict";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"국어사전",
|
||||||
|
"오프라인 국어·영한 사전 — 뜻풀이·유의어·혼동어",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
// ─── 내장 국어 사전 데이터 ────────────────────────────────────────────────
|
||||||
|
private sealed record KorEntry(string Word, string Pos, string Def, string? Synonym = null, string? Antonym = null, string? Note = null);
|
||||||
|
|
||||||
|
private static readonly KorEntry[] KorDict =
|
||||||
|
[
|
||||||
|
// ── 혼동어 ────────────────────────────────────────────────────────────
|
||||||
|
new("가르치다", "동사", "지식·기술을 알려 주다", Note: "'가리키다'와 혼동 주의. 가리키다=손가락으로 방향을 보여 주다"),
|
||||||
|
new("가리키다", "동사", "손가락 등으로 방향이나 대상을 보여 주다", Note: "'가르치다'와 혼동 주의"),
|
||||||
|
new("다르다", "형용사", "비교 대상이 같지 아니하다", Synonym: "상이하다", Antonym: "같다", Note: "'틀리다'와 혼동 주의. 틀리다=옳지 않다"),
|
||||||
|
new("틀리다", "동사", "사실이나 이치에 맞지 아니하다, 잘못되다", Synonym: "그르다", Antonym: "맞다", Note: "'다르다'와 혼동 주의"),
|
||||||
|
new("바라다", "동사", "생각대로 되기를 기대하다", Note: "'바래다(햇볕에 색이 변하다)'와 혼동 주의"),
|
||||||
|
new("바래다", "동사", "1. 햇볕·비에 색이 변하다 2. 어떤 곳까지 배웅하다"),
|
||||||
|
new("늘이다", "동사", "길이를 길게 하다 (고무줄을 늘이다)", Note: "'늘리다'와 혼동 주의. 늘리다=수량·범위를 크게 하다"),
|
||||||
|
new("늘리다", "동사", "수량·범위·세력을 크게 하다 (매출을 늘리다)", Note: "'늘이다'와 혼동 주의. 늘이다=길이를 길게 하다"),
|
||||||
|
new("부치다", "동사", "1. 편지를 보내다 2. 부침개를 만들다 3. 세금을 매기다"),
|
||||||
|
new("붙이다", "동사", "1. 떨어지지 않게 접합하다 2. 이름을 정하다 3. 조건을 달다"),
|
||||||
|
new("맞추다", "동사", "1. 서로 대어 비교하다 2. 정답을 알아 내다", Note: "'맞히다(맞다의 사동사)'와 혼동 주의"),
|
||||||
|
new("맞히다", "동사", "'맞다'의 사동사. 1. 적중시키다 2. 정답을 고르게 하다"),
|
||||||
|
new("받치다", "동사", "1. 밑에서 떠받치다 2. 감정이 치밀다", Note: "'바치다(드리다)'와 혼동 주의"),
|
||||||
|
new("바치다", "동사", "웃어른에게 물건이나 정성을 드리다"),
|
||||||
|
new("반듯이", "부사", "모양이 기울거나 비뚤어지지 않게", Note: "'반드시'와 혼동 주의"),
|
||||||
|
new("반드시", "부사", "틀림없이, 꼭", Synonym: "기필코, 필히"),
|
||||||
|
new("어이없다", "형용사", "뜻밖에 기가 막혀 말이 나오지 않다", Note: "'어의(御醫)없다'가 아님"),
|
||||||
|
new("설레다", "동사", "마음이 가라앉지 아니하고 들뜨다", Note: "'설레이다'는 비표준"),
|
||||||
|
new("깨끗이", "부사", "깨끗하게", Note: "'-이'로 적음 (깨끗히 ×)"),
|
||||||
|
new("일찍이", "부사", "일찍", Note: "'-이'로 적음 (일찍히 ×)"),
|
||||||
|
|
||||||
|
// ── 업무 용어 ─────────────────────────────────────────────────────────
|
||||||
|
new("품의", "명사", "윗사람에게 어떤 일의 처리를 의논하여 품함"),
|
||||||
|
new("결재", "명사", "윗사람이 아랫사람이 제출한 안건을 승인함", Note: "'결제(대금 지불)'와 혼동 주의"),
|
||||||
|
new("결제", "명사", "대금을 주고받아 거래를 끝맺음", Note: "'결재(승인)'와 혼동 주의"),
|
||||||
|
new("수립", "명사", "계획이나 정책을 세움", Synonym: "설정, 설립"),
|
||||||
|
new("이관", "명사", "관할이나 담당을 다른 곳으로 옮김"),
|
||||||
|
new("선결", "명사", "다른 것보다 앞서 먼저 해결함"),
|
||||||
|
new("귀결", "명사", "어떤 결론에 다다름"),
|
||||||
|
new("전결", "명사", "위임을 받아 대신 결재함"),
|
||||||
|
new("대조", "명사", "둘 이상을 맞대어 비교함", Synonym: "비교"),
|
||||||
|
new("취합", "명사", "여러 곳의 자료를 모아서 합침", Synonym: "수합"),
|
||||||
|
new("회람", "명사", "문서를 여러 사람이 돌려 봄"),
|
||||||
|
new("시달", "명사", "상급 기관이 하급 기관에 지시를 내림"),
|
||||||
|
new("협조", "명사", "힘을 합하여 도움", Synonym: "협력"),
|
||||||
|
new("검토", "명사", "자세히 살펴서 따져 봄", Synonym: "심사, 분석"),
|
||||||
|
new("조율", "명사", "서로 다른 의견을 맞추어 조정함", Synonym: "조정"),
|
||||||
|
new("후속", "명사", "앞에 한 일의 뒤를 이음"),
|
||||||
|
new("단축", "명사", "시간·거리를 줄임", Antonym: "연장"),
|
||||||
|
new("지양", "명사", "바람직하지 않은 것을 피하거나 그만둠", Note: "'지향(나아감)'과 혼동 주의"),
|
||||||
|
new("지향", "명사", "목표를 향하여 나아감", Note: "'지양(피함)'과 혼동 주의"),
|
||||||
|
|
||||||
|
// ── 자주 헷갈리는 한자어 ──────────────────────────────────────────────
|
||||||
|
new("이상", "명사", "1. 보통 정도를 넘어선 상태 (以上) 2. 생각하는 것 중 가장 완전한 상태 (理想)"),
|
||||||
|
new("이하", "명사", "기준이 되는 정도에 미치지 못하는 상태", Antonym: "이상(以上)"),
|
||||||
|
new("과반", "명사", "절반을 넘는 수"),
|
||||||
|
new("역할", "명사", "자기가 마땅히 하여야 할 임무나 직책", Note: "'역활'은 비표준"),
|
||||||
|
new("요지", "명사", "중요한 내용의 핵심", Synonym: "골자, 핵심"),
|
||||||
|
new("명시", "명사", "분명하게 드러내어 보임"),
|
||||||
|
new("고지", "명사", "1. 알려 줌 2. 높은 곳의 땅"),
|
||||||
|
new("기한", "명사", "미리 정해 놓은 시한", Synonym: "시한, 마감"),
|
||||||
|
];
|
||||||
|
|
||||||
|
// ─── 내장 영한 사전 데이터 (업무 필수 영어) ────────────────────────────────
|
||||||
|
private sealed record EngEntry(string Word, string Pos, string Kor, string? Example = null);
|
||||||
|
|
||||||
|
private static readonly EngEntry[] EngDict =
|
||||||
|
[
|
||||||
|
new("agenda", "명사", "의제, 안건", "Let's move to the next agenda item."),
|
||||||
|
new("align", "동사", "맞추다, 조율하다", "We need to align our goals."),
|
||||||
|
new("approve", "동사", "승인하다", "The manager approved the budget."),
|
||||||
|
new("assign", "동사", "배정하다, 할당하다", "I'll assign this task to you."),
|
||||||
|
new("brief", "동사", "간략히 보고하다", "Can you brief me on the project?"),
|
||||||
|
new("deadline", "명사", "마감일, 기한", "The deadline is next Friday."),
|
||||||
|
new("delegate", "동사", "위임하다", "You should delegate this to the team."),
|
||||||
|
new("deploy", "동사", "배포하다, 배치하다", "We'll deploy the update tonight."),
|
||||||
|
new("escalate", "동사", "상부에 보고하다, 확대하다", "Let's escalate this issue."),
|
||||||
|
new("feasible", "형용사","실현 가능한", "Is this plan feasible?"),
|
||||||
|
new("follow-up", "명사", "후속 조치", "I'll send a follow-up email."),
|
||||||
|
new("implement", "동사", "구현하다, 시행하다", "We need to implement this feature."),
|
||||||
|
new("initiative", "명사", "주도적 행동, 계획", "This is a company-wide initiative."),
|
||||||
|
new("milestone", "명사", "중요 시점, 이정표", "We've reached a major milestone."),
|
||||||
|
new("onboard", "동사", "입사시키다, 적응시키다", "We'll onboard new hires next week."),
|
||||||
|
new("optimize", "동사", "최적화하다", "Let's optimize the workflow."),
|
||||||
|
new("prioritize", "동사", "우선순위를 정하다", "We need to prioritize these tasks."),
|
||||||
|
new("provisional", "형용사","잠정적인, 임시의", "This is a provisional plan."),
|
||||||
|
new("regarding", "전치사","~에 관하여", "Regarding your request..."),
|
||||||
|
new("scope", "명사", "범위", "This is out of scope."),
|
||||||
|
new("stakeholder", "명사", "이해관계자", "All stakeholders should attend."),
|
||||||
|
new("sync", "동사", "동기화하다, 맞추다", "Let's sync on this topic."),
|
||||||
|
new("tentative", "형용사","잠정적인, 시험적인", "This is a tentative schedule."),
|
||||||
|
new("throughput", "명사", "처리량", "We need to improve throughput."),
|
||||||
|
new("viable", "형용사","실행 가능한", "Is this a viable option?"),
|
||||||
|
];
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("국어·영한 사전",
|
||||||
|
$"국어 {KorDict.Length}개 · 영한 {EngDict.Length}개 · dict {{단어}} / dict en {{word}}",
|
||||||
|
null, null, Symbol: "\uE82D"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 영한 사전 ─────────────────────────────────────────────────────────
|
||||||
|
if (q.StartsWith("en ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var word = q[3..].Trim().ToLowerInvariant();
|
||||||
|
if (string.IsNullOrWhiteSpace(word))
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("dict en {영단어}", "예: dict en deadline", null, null, Symbol: "\uE774"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var found = EngDict.Where(e => e.Word.Contains(word, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
if (found.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{word}' 검색 결과 없음",
|
||||||
|
"내장 업무 영어 사전에 없는 단어입니다", null, null, Symbol: "\uE783"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in found)
|
||||||
|
{
|
||||||
|
var sub = $"[{e.Pos}] {e.Kor}";
|
||||||
|
if (e.Example != null) sub += $" · {e.Example}";
|
||||||
|
items.Add(new LauncherItem(e.Word, sub, null, ("copy", $"{e.Word}: {e.Kor}"), Symbol: "\uE774"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 국어 사전 검색 ────────────────────────────────────────────────────
|
||||||
|
var kw = q;
|
||||||
|
var results = KorDict.Where(e =>
|
||||||
|
e.Word.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.Def.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(e.Synonym?.Contains(kw, StringComparison.OrdinalIgnoreCase) ?? false) ||
|
||||||
|
(e.Note?.Contains(kw, StringComparison.OrdinalIgnoreCase) ?? false)).ToList();
|
||||||
|
|
||||||
|
if (results.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{kw}' 검색 결과 없음",
|
||||||
|
"내장 국어 사전에 없는 단어입니다", null, null, Symbol: "\uE783"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var e in results.Take(12))
|
||||||
|
{
|
||||||
|
var title = $"{e.Word} [{e.Pos}]";
|
||||||
|
var sub = e.Def;
|
||||||
|
if (e.Synonym != null) sub += $" · 유의어: {e.Synonym}";
|
||||||
|
if (e.Antonym != null) sub += $" · 반의어: {e.Antonym}";
|
||||||
|
if (e.Note != null) sub += $" · 💡 {e.Note}";
|
||||||
|
items.Add(new LauncherItem(title, sub, null, ("copy", $"{e.Word}: {e.Def}"), Symbol: "\uE82D"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (item.Data is ("copy", string text))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||||
|
NotificationService.Notify("dict", "뜻풀이가 클립보드에 복사되었습니다.");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
237
src/AxCopilot/Handlers/FlowHandler.cs
Normal file
237
src/AxCopilot/Handlers/FlowHandler.cs
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// L29-2: 명령 체인(워크플로우) 핸들러. "flow" 프리픽스로 사용합니다.
|
||||||
|
///
|
||||||
|
/// 예: flow → 등록된 플로우 목록
|
||||||
|
/// flow add 출근준비 "remind 09:00 회의" > "today" > "todo list" → 플로우 추가
|
||||||
|
/// flow 출근준비 → 플로우 실행
|
||||||
|
/// flow del 출근준비 → 플로우 삭제
|
||||||
|
/// flow edit 출근준비 → 플로우 명령 목록 표시 (클립보드 복사)
|
||||||
|
/// Enter → 저장된 명령들을 순서대로 런처에 실행 (클립보드에 명령 목록 복사).
|
||||||
|
/// Alfred 워크플로우 경량 대응.
|
||||||
|
/// 저장: %APPDATA%\AxCopilot\flows.json
|
||||||
|
/// </summary>
|
||||||
|
public class FlowHandler : IActionHandler
|
||||||
|
{
|
||||||
|
public string? Prefix => "flow";
|
||||||
|
|
||||||
|
public PluginMetadata Metadata => new(
|
||||||
|
"명령 체인",
|
||||||
|
"여러 명령을 묶어 순서대로 실행 (워크플로우)",
|
||||||
|
"1.0",
|
||||||
|
"AX");
|
||||||
|
|
||||||
|
private sealed record FlowEntry(
|
||||||
|
[property: JsonPropertyName("name")] string Name,
|
||||||
|
[property: JsonPropertyName("commands")] List<string> Commands,
|
||||||
|
[property: JsonPropertyName("created")] DateTime Created);
|
||||||
|
|
||||||
|
private static readonly string DataPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||||
|
"AxCopilot", "flows.json");
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOpt = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var q = query.Trim();
|
||||||
|
var items = new List<LauncherItem>();
|
||||||
|
var flows = Load();
|
||||||
|
|
||||||
|
// ── add 명령 ──────────────────────────────────────────────────────────
|
||||||
|
if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var rest = q[4..].Trim();
|
||||||
|
var spaceIdx = rest.IndexOf(' ');
|
||||||
|
if (spaceIdx < 1)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("사용법: flow add {이름} {명령1} > {명령2} > ...",
|
||||||
|
"예: flow add 출근 \"today\" > \"todo list\" > \"remind 09:00 회의\"",
|
||||||
|
null, null, Symbol: "\uE710"));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = rest[..spaceIdx];
|
||||||
|
var cmdStr = rest[(spaceIdx + 1)..].Trim();
|
||||||
|
var commands = ParseCommands(cmdStr);
|
||||||
|
|
||||||
|
if (commands.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("명령을 > 로 구분해 입력하세요",
|
||||||
|
"예: \"today\" > \"todo list\"",
|
||||||
|
null, null, Symbol: Themes.Symbols.Warning));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"플로우 저장: {name} ({commands.Count}개 명령)",
|
||||||
|
string.Join(" → ", commands),
|
||||||
|
null, ("add", name, commands), Symbol: "\uE710"));
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── del 명령 ──────────────────────────────────────────────────────────
|
||||||
|
if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var name = q[4..].Trim();
|
||||||
|
var found = flows.FirstOrDefault(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"플로우 삭제: {found.Name}",
|
||||||
|
$"{found.Commands.Count}개 명령 · {string.Join(" → ", found.Commands)}",
|
||||||
|
null, ("del", found.Name), Symbol: "\uE74D"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{name}' 플로우를 찾을 수 없습니다",
|
||||||
|
"flow del {이름}", null, null, Symbol: "\uE783"));
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 빈 쿼리 → 전체 목록 ──────────────────────────────────────────────
|
||||||
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
|
{
|
||||||
|
if (flows.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem("등록된 명령 체인이 없습니다",
|
||||||
|
"flow add {이름} {명령1} > {명령2} > ... 로 추가하세요",
|
||||||
|
null, null, Symbol: "\uE8A0"));
|
||||||
|
items.Add(new LauncherItem("예시: flow add 출근 \"today\" > \"todo list\"",
|
||||||
|
"오늘 업무 뷰 → 할일 목록 순서대로 실행",
|
||||||
|
null, null, Symbol: Themes.Symbols.Info));
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
items.Add(new LauncherItem($"명령 체인 {flows.Count}개",
|
||||||
|
"Enter: 명령 목록 클립보드 복사 · flow add/del 로 관리",
|
||||||
|
null, null, Symbol: "\uE8A0"));
|
||||||
|
|
||||||
|
foreach (var f in flows)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"▶ {f.Name} ({f.Commands.Count}단계)",
|
||||||
|
string.Join(" → ", f.Commands),
|
||||||
|
null, ("run", f), Symbol: "\uE768"));
|
||||||
|
}
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 이름 검색 → 실행 ──────────────────────────────────────────────────
|
||||||
|
var match = flows.FirstOrDefault(f => f.Name.Equals(q, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (match != null)
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem(
|
||||||
|
$"▶ {match.Name} 실행",
|
||||||
|
string.Join(" → ", match.Commands),
|
||||||
|
null, ("run", match), Symbol: "\uE768"));
|
||||||
|
|
||||||
|
for (int i = 0; i < match.Commands.Count; i++)
|
||||||
|
items.Add(new LauncherItem($" {i + 1}. {match.Commands[i]}", "",
|
||||||
|
null, null, Symbol: Themes.Symbols.Terminal));
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 부분 매칭
|
||||||
|
var searched = flows.Where(f =>
|
||||||
|
f.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
f.Commands.Any(c => c.Contains(q, StringComparison.OrdinalIgnoreCase))).ToList();
|
||||||
|
|
||||||
|
if (searched.Count > 0)
|
||||||
|
{
|
||||||
|
foreach (var f in searched)
|
||||||
|
items.Add(new LauncherItem($"▶ {f.Name} ({f.Commands.Count}단계)",
|
||||||
|
string.Join(" → ", f.Commands),
|
||||||
|
null, ("run", f), Symbol: "\uE768"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
items.Add(new LauncherItem($"'{q}' 플로우를 찾을 수 없습니다",
|
||||||
|
"flow add {이름} {명령} > {명령} 으로 추가하세요",
|
||||||
|
null, null, Symbol: "\uE783"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||||
|
{
|
||||||
|
if (item.Data is ("add", string name, List<string> commands))
|
||||||
|
{
|
||||||
|
var flows = Load();
|
||||||
|
flows.RemoveAll(f => f.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
|
||||||
|
flows.Add(new FlowEntry(name, commands, DateTime.Now));
|
||||||
|
Save(flows);
|
||||||
|
NotificationService.Notify("flow", $"'{name}' 플로우가 저장되었습니다. ({commands.Count}단계)");
|
||||||
|
}
|
||||||
|
else if (item.Data is ("del", string delName))
|
||||||
|
{
|
||||||
|
var flows = Load();
|
||||||
|
flows.RemoveAll(f => f.Name.Equals(delName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
Save(flows);
|
||||||
|
NotificationService.Notify("flow", $"'{delName}' 플로우가 삭제되었습니다.");
|
||||||
|
}
|
||||||
|
else if (item.Data is ("run", FlowEntry flow))
|
||||||
|
{
|
||||||
|
// 명령 목록을 클립보드에 복사 (사용자가 순서대로 런처에 입력)
|
||||||
|
var text = string.Join("\n", flow.Commands.Select((c, i) => $"{i + 1}. {c}"));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
||||||
|
NotificationService.Notify("flow", $"'{flow.Name}' 명령 {flow.Commands.Count}개가 클립보드에 복사되었습니다.");
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── 명령 파싱 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static List<string> ParseCommands(string input)
|
||||||
|
{
|
||||||
|
// "cmd1" > "cmd2" > "cmd3" 또는 cmd1 > cmd2 > cmd3
|
||||||
|
return input.Split('>')
|
||||||
|
.Select(s => s.Trim().Trim('"').Trim())
|
||||||
|
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── JSON I/O ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static List<FlowEntry> Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(DataPath)) return [];
|
||||||
|
var json = File.ReadAllText(DataPath);
|
||||||
|
return JsonSerializer.Deserialize<List<FlowEntry>>(json) ?? [];
|
||||||
|
}
|
||||||
|
catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save(List<FlowEntry> list)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(DataPath)!;
|
||||||
|
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||||
|
File.WriteAllText(DataPath, JsonSerializer.Serialize(list, JsonOpt));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
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;
|
||||||
@@ -112,13 +116,120 @@ public class SpellHandler : IActionHandler
|
|||||||
new("떼레비", "텔레비전", "표준 외래어: television → 텔레비전", "외래어"),
|
new("떼레비", "텔레비전", "표준 외래어: television → 텔레비전", "외래어"),
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly string[] CategoryOrder = ["되/돼", "안/않", "혼동어", "맞춤법", "띄어쓰기", "외래어"];
|
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<CustomSpell> LoadCustom()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (!File.Exists(CustomPath)) return [];
|
||||||
|
return JsonSerializer.Deserialize<List<CustomSpell>>(File.ReadAllText(CustomPath)) ?? [];
|
||||||
|
}
|
||||||
|
catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SaveCustom(List<CustomSpell> list)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(CustomPath)!;
|
||||||
|
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
|
||||||
|
File.WriteAllText(CustomPath, JsonSerializer.Serialize(list, JsonOpt));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<SpellEntry> 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<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>();
|
||||||
|
|
||||||
|
// ── 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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
|
||||||
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(q))
|
if (string.IsNullOrWhiteSpace(q))
|
||||||
{
|
{
|
||||||
// 클립보드 텍스트 맞춤법 검사
|
// 클립보드 텍스트 맞춤법 검사
|
||||||
@@ -135,7 +246,7 @@ public class SpellHandler : IActionHandler
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(clipText))
|
if (!string.IsNullOrWhiteSpace(clipText))
|
||||||
{
|
{
|
||||||
var found = Entries.Where(e =>
|
var found = AllEntries().Where(e =>
|
||||||
clipText.Contains(e.Wrong, StringComparison.OrdinalIgnoreCase)).ToList();
|
clipText.Contains(e.Wrong, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
|
|
||||||
if (found.Count > 0)
|
if (found.Count > 0)
|
||||||
@@ -175,10 +286,12 @@ public class SpellHandler : IActionHandler
|
|||||||
// "list" → 카테고리별 개수
|
// "list" → 카테고리별 개수
|
||||||
if (kw == "list")
|
if (kw == "list")
|
||||||
{
|
{
|
||||||
items.Add(new LauncherItem($"맞춤법 오류 목록 총 {Entries.Length}개", "", null, null, Symbol: "\uE7BA"));
|
var all = AllEntries().ToList();
|
||||||
|
items.Add(new LauncherItem($"맞춤법 오류 목록 총 {all.Count}개", "", null, null, Symbol: "\uE7BA"));
|
||||||
foreach (var cat in CategoryOrder)
|
foreach (var cat in CategoryOrder)
|
||||||
{
|
{
|
||||||
var cnt = Entries.Count(e => e.Category == cat);
|
var cnt = all.Count(e => e.Category == cat);
|
||||||
|
if (cnt == 0) continue;
|
||||||
items.Add(new LauncherItem(cat, $"{cnt}개 · spell {cat}로 목록 보기",
|
items.Add(new LauncherItem(cat, $"{cnt}개 · spell {cat}로 목록 보기",
|
||||||
null, null, Symbol: "\uE7BA"));
|
null, null, Symbol: "\uE7BA"));
|
||||||
}
|
}
|
||||||
@@ -232,8 +345,8 @@ public class SpellHandler : IActionHandler
|
|||||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 키워드 검색
|
// 키워드 검색 (내장 + 사용자 항목)
|
||||||
var searched = Entries.Where(e =>
|
var searched = AllEntries().Where(e =>
|
||||||
e.Wrong.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
e.Wrong.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
e.Correct.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
e.Correct.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
e.Explanation.Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
|
e.Explanation.Contains(kw, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||||
@@ -266,6 +379,25 @@ public class SpellHandler : IActionHandler
|
|||||||
}
|
}
|
||||||
catch { }
|
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;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public partial class LauncherWindow
|
|||||||
"[ 기능 ]",
|
"[ 기능 ]",
|
||||||
"F1 도움말",
|
"F1 도움말",
|
||||||
"F2 파일 이름 바꾸기",
|
"F2 파일 이름 바꾸기",
|
||||||
|
"F3 파일 빠른 미리보기 (토글)",
|
||||||
"F5 인덱스 새로 고침",
|
"F5 인덱스 새로 고침",
|
||||||
"Delete 항목 제거",
|
"Delete 항목 제거",
|
||||||
"Ctrl+, 설정",
|
"Ctrl+, 설정",
|
||||||
|
|||||||
Reference in New Issue
Block a user