diff --git a/docs/LAUNCHER_ROADMAP.md b/docs/LAUNCHER_ROADMAP.md
index b4b12cd..9c5a385 100644
--- a/docs/LAUNCHER_ROADMAP.md
+++ b/docs/LAUNCHER_ROADMAP.md
@@ -360,3 +360,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
| L17-2 | **숫자 포맷·읽기** ✅ | `num` 프리픽스. `num 1234567` → 천단위·한글 단위(만·억·조)·한국어 읽기·영어 읽기·16진수·8진수·2진수·과학표기·로마 숫자 일괄 표시. `0x/0b/0o` 접두사 진수 입력. `num 42 ko` 한국어 읽기만. `num 42 en` 영어 읽기만. 1~3999 로마 숫자 변환. ToKorean(): 조·억·만 단위 재귀 분해 | 높음 |
| L17-3 | **YAML 파서·분석기** ✅ | `yaml` 프리픽스. 클립보드 자동 읽기. 외부 라이브러리 없이 순수 구현(경량 파서). `yaml validate` 유효성 검사. `yaml keys` 최상위 키 목록. `yaml get key.sub` 점 표기법 경로 조회. `yaml stats` 줄·키·깊이 통계. `yaml flat` 점 표기법 평탄화(flatten). [GeneratedRegex] 소스 생성기 | 높음 |
| L17-4 | **.gitignore 생성기** ✅ | `gitignore` 프리픽스. Node/Python/C#(.NET)/Java/Go/Rust/React(Next.js·Vite·Vue)/Flutter/Android/iOS/Unity/Windows/macOS/Linux 14개 내장 템플릿. 별칭(nodejs·npm·dotnet·net·maven·golang·cargo·nextjs·swift 등) 지원. 여러 템플릿 명 입력 시 자동 병합. 미리보기 12줄 표시. Enter → 클립보드 복사 | 높음 |
+
+---
+
+## Phase L18 — SQL·TextCase·Aspect·Abbr 도구 (v2.1.0) ✅ 완료
+
+> **방향**: 개발자 텍스트 처리·분석 도구 강화 — SQL 포맷, 텍스트 케이스, 해상도 계산, 약어 사전.
+
+| # | 기능 | 설명 | 우선순위 |
+|---|------|------|----------|
+| L18-1 | **SQL 포맷터·분석기** ✅ | `sql` 프리픽스. 클립보드 SQL 자동 읽기. 키워드 기반 들여쓰기 포맷(새 줄 시작 키워드 집합). `sql mini` 미니파이. `sql upper/lower` 키워드 대소문자 변환. `sql stats` 테이블·JOIN·WHERE 조건·서브쿼리·DML 유형 분석. `sql tables` FROM/JOIN 테이블 추출. `sql select
` SELECT 쿼리 템플릿 생성. [GeneratedRegex] 소스 생성기 | 높음 |
+| L18-2 | **텍스트 케이스 변환기** ✅ | `text` 프리픽스. 클립보드 자동 읽기. 13가지 케이스 일괄 표시: camelCase·PascalCase·snake_case·SCREAMING_SNAKE·kebab-case·URL slug·dot.case·UPPER·lower·Title·Sentence·뒤집기·trim. 인라인 입력(`text camel hello world`) 지원. Tokenize(): camelCase 경계 분리 + 구분자 정규화. ToSlug(): 유니코드 정규화(NFC→ASCII). [GeneratedRegex] 소스 생성기 | 높음 |
+| L18-3 | **화면 비율·해상도 계산기** ✅ | `aspect` 프리픽스. 9개 비율 프리셋 내장(16:9·4:3·21:9·1:1·9:16·3:2·2:1·5:4·2.35:1). `aspect 1920 1080` → GCD 약분 비율 계산·MP 표시. `aspect 16:9 1280` 너비 기준 높이 계산. `aspect 16:9 h 720` 높이 기준 너비 계산. `aspect crop 1920 1080 4:3` 크롭 영역+FFmpeg crop 파라미터. 소수 비율(2.35:1) 지원 | 높음 |
+| L18-4 | **IT·개발 약어 사전** ✅ | `abbr` 프리픽스. 150개+ 내장 약어(웹/네트워크·개발·DB·보안·클라우드·AI·데이터형식·협업 8개 카테고리). 정확 일치 → 약어/원문/설명/카테고리 상세 표시. 부분 일치 → 목록 표시. `abbr 클라우드` 카테고리별 필터. `abbr jwt` → JWT 상세. API/CRUD/REST/JWT/MCP/SOLID/CAP/ACID/OWASP 등 포함 | 높음 |
diff --git a/src/AxCopilot/App.xaml.cs b/src/AxCopilot/App.xaml.cs
index 91856c7..c98f70c 100644
--- a/src/AxCopilot/App.xaml.cs
+++ b/src/AxCopilot/App.xaml.cs
@@ -307,6 +307,16 @@ public partial class App : System.Windows.Application
// L17-4: .gitignore 생성기 (prefix=gitignore)
commandResolver.RegisterHandler(new GitignoreHandler());
+ // ─── Phase L18 핸들러 ─────────────────────────────────────────────────
+ // L18-1: SQL 포맷터·분석기 (prefix=sql)
+ commandResolver.RegisterHandler(new SqlHandler());
+ // L18-2: 텍스트 케이스 변환기 (prefix=text)
+ commandResolver.RegisterHandler(new TextCaseHandler());
+ // L18-3: 화면 비율·해상도 계산기 (prefix=aspect)
+ commandResolver.RegisterHandler(new AspectHandler());
+ // L18-4: IT·개발 약어 사전 (prefix=abbr)
+ commandResolver.RegisterHandler(new AbbrHandler());
+
// ─── 플러그인 로드 ────────────────────────────────────────────────────
var pluginHost = new PluginHost(settings, commandResolver);
pluginHost.LoadAll();
diff --git a/src/AxCopilot/Handlers/AbbrHandler.cs b/src/AxCopilot/Handlers/AbbrHandler.cs
new file mode 100644
index 0000000..0cadc7e
--- /dev/null
+++ b/src/AxCopilot/Handlers/AbbrHandler.cs
@@ -0,0 +1,303 @@
+using System.Windows;
+using AxCopilot.SDK;
+using AxCopilot.Services;
+using AxCopilot.Themes;
+
+namespace AxCopilot.Handlers;
+
+///
+/// L18-4: IT·개발 약어 사전 핸들러. "abbr" 프리픽스로 사용합니다.
+///
+/// 예: abbr → 주요 약어 목록
+/// abbr api → API 뜻 + 설명
+/// abbr crud → CRUD 약어 풀이
+/// abbr rest → REST 설명
+/// abbr http → HTTP 약어 목록
+/// Enter → 약어 또는 뜻을 클립보드에 복사.
+///
+public class AbbrHandler : IActionHandler
+{
+ public string? Prefix => "abbr";
+
+ public PluginMetadata Metadata => new(
+ "Abbr",
+ "IT·개발 약어 사전 — API · CRUD · REST · HTTP · SQL 등 내장",
+ "1.0",
+ "AX");
+
+ private record AbbrEntry(string Short, string Full, string Description, string Category);
+
+ // ── 내장 약어 사전 (~150개) ───────────────────────────────────────────────
+ private static readonly AbbrEntry[] Entries =
+ [
+ // 웹/네트워크
+ new("API", "Application Programming Interface", "소프트웨어 간 통신 규격", "웹/네트워크"),
+ new("REST", "Representational State Transfer", "HTTP 기반 아키텍처 스타일", "웹/네트워크"),
+ new("HTTP", "HyperText Transfer Protocol", "웹 통신 프로토콜", "웹/네트워크"),
+ new("HTTPS", "HTTP Secure", "TLS/SSL로 암호화된 HTTP", "웹/네트워크"),
+ new("HTML", "HyperText Markup Language", "웹 페이지 마크업 언어", "웹/네트워크"),
+ new("CSS", "Cascading Style Sheets", "웹 스타일 시트 언어", "웹/네트워크"),
+ new("URL", "Uniform Resource Locator", "인터넷 자원 주소", "웹/네트워크"),
+ new("URI", "Uniform Resource Identifier", "자원 식별자 (URL의 상위 개념)", "웹/네트워크"),
+ new("DNS", "Domain Name System", "도메인 ↔ IP 변환 시스템", "웹/네트워크"),
+ new("IP", "Internet Protocol", "인터넷 패킷 전송 프로토콜", "웹/네트워크"),
+ new("TCP", "Transmission Control Protocol", "신뢰성 연결형 전송 프로토콜", "웹/네트워크"),
+ new("UDP", "User Datagram Protocol", "비연결형 전송 프로토콜", "웹/네트워크"),
+ new("SSH", "Secure Shell", "원격 보안 접속 프로토콜", "웹/네트워크"),
+ new("FTP", "File Transfer Protocol", "파일 전송 프로토콜", "웹/네트워크"),
+ new("SMTP", "Simple Mail Transfer Protocol", "이메일 전송 프로토콜", "웹/네트워크"),
+ new("CDN", "Content Delivery Network", "콘텐츠 분산 배포 네트워크", "웹/네트워크"),
+ new("VPN", "Virtual Private Network", "가상 사설 네트워크", "웹/네트워크"),
+ new("NAT", "Network Address Translation", "사설↔공인 IP 주소 변환", "웹/네트워크"),
+ new("CORS", "Cross-Origin Resource Sharing", "브라우저 교차 출처 리소스 공유", "웹/네트워크"),
+ new("SSE", "Server-Sent Events", "서버→클라이언트 단방향 스트림", "웹/네트워크"),
+ new("WebSocket","WebSocket Protocol", "양방향 실시간 통신 프로토콜", "웹/네트워크"),
+ new("gRPC", "Google Remote Procedure Call", "프로토콜 버퍼 기반 RPC 프레임워크", "웹/네트워크"),
+ new("GraphQL", "Graph Query Language", "API 쿼리 언어 (Facebook 개발)", "웹/네트워크"),
+ new("SOAP", "Simple Object Access Protocol", "XML 기반 웹 서비스 프로토콜", "웹/네트워크"),
+ new("WSDL", "Web Services Description Language", "웹 서비스 인터페이스 설명 언어", "웹/네트워크"),
+ new("TLS", "Transport Layer Security", "암호화 통신 프로토콜 (SSL 후속)", "웹/네트워크"),
+ new("SSL", "Secure Sockets Layer", "암호화 통신 프로토콜 (TLS의 전신)", "웹/네트워크"),
+ new("OAuth", "Open Authorization", "위임 인증 표준 프로토콜", "웹/네트워크"),
+ new("MIME", "Multipurpose Internet Mail Extensions", "이메일·웹 콘텐츠 타입 표준", "웹/네트워크"),
+
+ // 개발/프로그래밍
+ new("CRUD", "Create Read Update Delete", "기본 데이터 처리 4가지 연산", "개발"),
+ new("OOP", "Object-Oriented Programming", "객체 지향 프로그래밍", "개발"),
+ new("SOLID", "Single responsibility Open/closed Liskov Interface-segregation Dependency-inversion",
+ "객체지향 5가지 설계 원칙", "개발"),
+ new("DRY", "Don't Repeat Yourself", "코드 중복 최소화 원칙", "개발"),
+ new("KISS", "Keep It Simple, Stupid", "단순하게 유지하라 원칙", "개발"),
+ new("YAGNI", "You Aren't Gonna Need It", "필요할 때만 구현하라 원칙", "개발"),
+ new("TDD", "Test-Driven Development", "테스트 주도 개발 방법론", "개발"),
+ new("BDD", "Behavior-Driven Development", "행동 주도 개발 방법론", "개발"),
+ new("DDD", "Domain-Driven Design", "도메인 주도 설계", "개발"),
+ new("MVC", "Model-View-Controller", "UI 아키텍처 패턴", "개발"),
+ new("MVP", "Model-View-Presenter", "MVC의 변형, Presenter가 View와 Model 중재", "개발"),
+ new("MVVM", "Model-View-ViewModel", "WPF·모바일 아키텍처 패턴", "개발"),
+ new("SRP", "Single Responsibility Principle", "단일 책임 원칙 (SOLID의 S)", "개발"),
+ new("OCP", "Open/Closed Principle", "개방/폐쇄 원칙 (SOLID의 O)", "개발"),
+ new("LSP", "Liskov Substitution Principle", "리스코프 치환 원칙 (SOLID의 L)", "개발"),
+ new("ISP", "Interface Segregation Principle", "인터페이스 분리 원칙 (SOLID의 I)", "개발"),
+ new("DIP", "Dependency Inversion Principle", "의존성 역전 원칙 (SOLID의 D)", "개발"),
+ new("IoC", "Inversion of Control", "제어 역전 (프레임워크 핵심 개념)", "개발"),
+ new("DI", "Dependency Injection", "의존성 주입 패턴", "개발"),
+ new("AOP", "Aspect-Oriented Programming", "관점 지향 프로그래밍", "개발"),
+ new("FP", "Functional Programming", "함수형 프로그래밍", "개발"),
+ new("DSL", "Domain-Specific Language", "특정 도메인 전용 언어", "개발"),
+ new("SDK", "Software Development Kit", "소프트웨어 개발 도구 모음", "개발"),
+ new("IDE", "Integrated Development Environment", "통합 개발 환경", "개발"),
+ new("CLI", "Command Line Interface", "명령줄 인터페이스", "개발"),
+ new("GUI", "Graphical User Interface", "그래픽 사용자 인터페이스", "개발"),
+ new("UI", "User Interface", "사용자 인터페이스", "개발"),
+ new("UX", "User Experience", "사용자 경험", "개발"),
+ new("SPA", "Single Page Application", "단일 페이지 애플리케이션", "개발"),
+ new("SSR", "Server-Side Rendering", "서버 사이드 렌더링", "개발"),
+ new("CSR", "Client-Side Rendering", "클라이언트 사이드 렌더링", "개발"),
+ new("PWA", "Progressive Web App", "프로그레시브 웹 앱", "개발"),
+ new("CDK", "Cloud Development Kit", "클라우드 인프라 코드 개발 도구", "개발"),
+ new("ORM", "Object-Relational Mapping", "객체-관계형 데이터베이스 매핑", "개발"),
+ new("REPL", "Read-Eval-Print Loop", "대화형 프로그래밍 환경", "개발"),
+ new("GC", "Garbage Collection", "자동 메모리 회수", "개발"),
+ new("JIT", "Just-In-Time Compilation", "실행 시점 컴파일", "개발"),
+ new("AOT", "Ahead-Of-Time Compilation", "사전 컴파일", "개발"),
+ new("WASM", "WebAssembly", "브라우저용 바이너리 형식", "개발"),
+ new("AST", "Abstract Syntax Tree", "추상 구문 트리", "개발"),
+ new("LSP", "Language Server Protocol", "언어 분석 서버 표준 프로토콜", "개발"),
+
+ // 데이터베이스
+ new("SQL", "Structured Query Language", "관계형 DB 쿼리 언어", "DB"),
+ new("NoSQL", "Not Only SQL", "비관계형 데이터베이스 총칭", "DB"),
+ new("ACID", "Atomicity Consistency Isolation Durability", "DB 트랜잭션 4가지 성질", "DB"),
+ new("CAP", "Consistency Availability Partition-tolerance","분산 DB 설계 이론 (셋 중 2개만 보장)", "DB"),
+ new("ETL", "Extract Transform Load", "데이터 추출·변환·적재 프로세스", "DB"),
+ new("OLTP", "Online Transaction Processing", "트랜잭션 처리 중심 DB", "DB"),
+ new("OLAP", "Online Analytical Processing", "분석 처리 중심 DB (데이터 웨어하우스)", "DB"),
+ new("DDL", "Data Definition Language", "DB 구조 정의 SQL (CREATE/ALTER/DROP)", "DB"),
+ new("DML", "Data Manipulation Language", "데이터 조작 SQL (SELECT/INSERT/UPDATE/DELETE)", "DB"),
+ new("DCL", "Data Control Language", "접근 권한 SQL (GRANT/REVOKE)", "DB"),
+
+ // 보안
+ new("JWT", "JSON Web Token", "JSON 기반 인증 토큰 표준", "보안"),
+ new("RBAC", "Role-Based Access Control", "역할 기반 접근 제어", "보안"),
+ new("ABAC", "Attribute-Based Access Control", "속성 기반 접근 제어", "보안"),
+ new("MFA", "Multi-Factor Authentication", "다중 인증", "보안"),
+ new("2FA", "Two-Factor Authentication", "이중 인증", "보안"),
+ new("XSS", "Cross-Site Scripting", "악성 스크립트 삽입 공격", "보안"),
+ new("CSRF", "Cross-Site Request Forgery", "교차 사이트 요청 위조 공격", "보안"),
+ new("SQL Injection", "SQL Injection Attack", "SQL 쿼리 삽입 공격", "보안"),
+ new("OWASP", "Open Web Application Security Project", "웹 보안 가이드라인 기관", "보안"),
+ new("CVE", "Common Vulnerabilities and Exposures", "공개 보안 취약점 DB", "보안"),
+ new("HTTPS", "HTTP Secure", "TLS로 암호화된 HTTP", "보안"),
+ new("AES", "Advanced Encryption Standard", "대칭키 암호화 표준 (128/192/256bit)", "보안"),
+ new("RSA", "Rivest–Shamir–Adleman", "공개키 비대칭 암호화 알고리즘", "보안"),
+ new("HMAC", "Hash-based Message Authentication Code", "해시 기반 메시지 인증 코드", "보안"),
+ new("PKI", "Public Key Infrastructure", "공개키 인프라", "보안"),
+
+ // 클라우드/인프라
+ new("AWS", "Amazon Web Services", "아마존 클라우드 서비스", "클라우드"),
+ new("GCP", "Google Cloud Platform", "구글 클라우드 서비스", "클라우드"),
+ new("SaaS", "Software as a Service", "구독형 소프트웨어 서비스", "클라우드"),
+ new("PaaS", "Platform as a Service", "플랫폼 서비스", "클라우드"),
+ new("IaaS", "Infrastructure as a Service", "인프라 서비스", "클라우드"),
+ new("FaaS", "Function as a Service", "서버리스 함수 서비스", "클라우드"),
+ new("K8s", "Kubernetes", "컨테이너 오케스트레이션 플랫폼", "클라우드"),
+ new("CI/CD", "Continuous Integration/Continuous Delivery","지속적 통합/배포 파이프라인", "클라우드"),
+ new("IaC", "Infrastructure as Code", "코드로 관리하는 인프라", "클라우드"),
+ new("VPC", "Virtual Private Cloud", "가상 사설 클라우드 네트워크", "클라우드"),
+ new("SLA", "Service Level Agreement", "서비스 수준 계약 (가용성 보장)", "클라우드"),
+ new("SLO", "Service Level Objective", "서비스 수준 목표", "클라우드"),
+ new("RTO", "Recovery Time Objective", "복구 목표 시간", "클라우드"),
+ new("RPO", "Recovery Point Objective", "복구 목표 지점", "클라우드"),
+ new("CDN", "Content Delivery Network", "콘텐츠 분산 배포 네트워크", "클라우드"),
+ new("ECS", "Elastic Container Service", "AWS 컨테이너 관리 서비스", "클라우드"),
+ new("ECR", "Elastic Container Registry", "AWS 컨테이너 이미지 저장소", "클라우드"),
+ new("EKS", "Elastic Kubernetes Service", "AWS 관리형 Kubernetes", "클라우드"),
+
+ // AI/ML
+ new("AI", "Artificial Intelligence", "인공지능", "AI/ML"),
+ new("ML", "Machine Learning", "머신러닝", "AI/ML"),
+ new("DL", "Deep Learning", "딥러닝", "AI/ML"),
+ new("LLM", "Large Language Model", "대규모 언어 모델", "AI/ML"),
+ new("NLP", "Natural Language Processing", "자연어 처리", "AI/ML"),
+ new("CNN", "Convolutional Neural Network", "합성곱 신경망", "AI/ML"),
+ new("RNN", "Recurrent Neural Network", "순환 신경망", "AI/ML"),
+ new("GAN", "Generative Adversarial Network", "생성적 적대 신경망", "AI/ML"),
+ new("RAG", "Retrieval-Augmented Generation", "검색 증강 생성 (LLM + 검색)", "AI/ML"),
+ new("RLHF", "Reinforcement Learning from Human Feedback","인간 피드백 강화학습", "AI/ML"),
+ new("GPT", "Generative Pre-trained Transformer", "생성형 사전학습 변환기 (OpenAI)", "AI/ML"),
+ new("BERT", "Bidirectional Encoder Representations from Transformers","양방향 트랜스포머 언어 모델", "AI/ML"),
+ new("SFT", "Supervised Fine-Tuning", "지도 학습 파인튜닝", "AI/ML"),
+ new("LoRA", "Low-Rank Adaptation", "저순위 행렬 파인튜닝 기법", "AI/ML"),
+ new("MCP", "Model Context Protocol", "LLM 외부 도구 연결 표준 프로토콜", "AI/ML"),
+ new("CoT", "Chain of Thought", "사고 단계 명시 프롬프팅", "AI/ML"),
+
+ // 데이터 형식
+ new("JSON", "JavaScript Object Notation", "경량 데이터 교환 형식", "데이터형식"),
+ new("XML", "Extensible Markup Language", "확장 가능 마크업 언어", "데이터형식"),
+ new("YAML", "YAML Ain't Markup Language", "사람이 읽기 쉬운 데이터 직렬화 형식", "데이터형식"),
+ new("TOML", "Tom's Obvious, Minimal Language", "설정 파일 전용 경량 형식", "데이터형식"),
+ new("CSV", "Comma-Separated Values", "쉼표 구분 데이터 형식", "데이터형식"),
+ new("TSV", "Tab-Separated Values", "탭 구분 데이터 형식", "데이터형식"),
+ new("Protobuf","Protocol Buffers", "구글 이진 직렬화 형식", "데이터형식"),
+ new("Avro", "Apache Avro", "Hadoop 생태계 이진 직렬화", "데이터형식"),
+ new("Parquet", "Apache Parquet", "열 지향 이진 데이터 형식", "데이터형식"),
+ new("Base64", "Base 64 encoding", "이진 데이터 텍스트 인코딩 (64진수)", "데이터형식"),
+
+ // 버전 관리/협업
+ new("VCS", "Version Control System", "버전 관리 시스템", "협업"),
+ new("SCM", "Source Code Management", "소스 코드 관리", "협업"),
+ new("PR", "Pull Request", "코드 병합 요청 (GitHub 용어)", "협업"),
+ new("MR", "Merge Request", "코드 병합 요청 (GitLab 용어)", "협업"),
+ new("LGTM", "Looks Good To Me", "코드 리뷰 승인 표현", "협업"),
+ new("WIP", "Work In Progress", "진행 중 작업", "협업"),
+ new("RFC", "Request for Comments", "의견 요청 문서 (표준화 프로세스)", "협업"),
+ new("POC", "Proof of Concept", "개념 검증", "협업"),
+ new("MVP", "Minimum Viable Product", "최소 기능 제품", "협업"),
+ new("KPI", "Key Performance Indicator", "핵심 성과 지표", "협업"),
+ new("OKR", "Objectives and Key Results", "목표 및 핵심 결과", "협업"),
+ new("SOP", "Standard Operating Procedure", "표준 운영 절차", "협업"),
+ ];
+
+ private static readonly string[] Categories = Entries.Select(e => e.Category).Distinct().ToArray();
+
+ public Task> GetItemsAsync(string query, CancellationToken ct)
+ {
+ var q = query.Trim();
+ var items = new List();
+
+ 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("── 카테고리 ──", "", null, null, Symbol: "\uE82D"));
+ foreach (var cat in Categories)
+ {
+ var cnt = Entries.Count(e => e.Category == cat);
+ items.Add(new LauncherItem(cat, $"{cnt}개 · abbr {cat}로 목록 보기",
+ null, null, Symbol: "\uE82D"));
+ }
+ // 자주 쓰는 약어 샘플
+ items.Add(new LauncherItem("── 자주 쓰는 약어 ──", "", null, null, Symbol: "\uE82D"));
+ var popular = new[] { "API", "CRUD", "REST", "JWT", "SQL", "CI/CD", "TDD", "OOP" };
+ foreach (var p in popular)
+ {
+ var e = Entries.FirstOrDefault(x => x.Short.Equals(p, StringComparison.OrdinalIgnoreCase));
+ if (e != null)
+ items.Add(MakeAbbrItem(e));
+ }
+ return Task.FromResult>(items);
+ }
+
+ // 카테고리 검색
+ if (Categories.Any(c => c.Equals(q, StringComparison.OrdinalIgnoreCase)))
+ {
+ var catEntries = Entries.Where(e => e.Category.Equals(q, StringComparison.OrdinalIgnoreCase)).ToList();
+ items.Add(new LauncherItem($"{q} {catEntries.Count}개", "", null, null, Symbol: "\uE82D"));
+ foreach (var e in catEntries)
+ items.Add(MakeAbbrItem(e));
+ return Task.FromResult>(items);
+ }
+
+ // 약어 검색 (정확 일치 우선, 그 다음 부분 일치)
+ var exact = Entries.Where(e =>
+ e.Short.Equals(q, StringComparison.OrdinalIgnoreCase)).ToList();
+
+ var partial = Entries.Where(e =>
+ !exact.Contains(e) &&
+ (e.Short.Contains(q, StringComparison.OrdinalIgnoreCase) ||
+ e.Full.Contains(q, StringComparison.OrdinalIgnoreCase) ||
+ e.Description.Contains(q, StringComparison.OrdinalIgnoreCase))).ToList();
+
+ var all = exact.Concat(partial).ToList();
+
+ if (all.Count == 0)
+ {
+ items.Add(new LauncherItem($"'{q}' 약어를 찾을 수 없습니다",
+ "카테고리: " + string.Join(", ", Categories),
+ null, null, Symbol: "\uE946"));
+ return Task.FromResult>(items);
+ }
+
+ if (exact.Count == 1)
+ {
+ // 정확 일치 1개 → 상세 표시
+ var e = exact[0];
+ items.Add(new LauncherItem($"{e.Short} = {e.Full}",
+ e.Description, null, ("copy", $"{e.Short}: {e.Full}"), Symbol: "\uE82D"));
+ items.Add(new LauncherItem("약어", e.Short, null, ("copy", e.Short), Symbol: "\uE82D"));
+ items.Add(new LauncherItem("원문", e.Full, null, ("copy", e.Full), Symbol: "\uE82D"));
+ items.Add(new LauncherItem("설명", e.Description, null, ("copy", e.Description), Symbol: "\uE82D"));
+ items.Add(new LauncherItem("카테고리",e.Category, null, null, Symbol: "\uE82D"));
+ }
+ else
+ {
+ items.Add(new LauncherItem($"'{q}' 검색 결과 {all.Count}개", "", null, null, Symbol: "\uE82D"));
+ foreach (var e in all.Take(30))
+ items.Add(MakeAbbrItem(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("Abbr", "클립보드에 복사했습니다.");
+ }
+ catch { }
+ }
+ return Task.CompletedTask;
+ }
+
+ private static LauncherItem MakeAbbrItem(AbbrEntry e) =>
+ new(e.Short,
+ $"{e.Full} · {e.Description}",
+ null, ("copy", $"{e.Short}: {e.Full}"), Symbol: "\uE82D");
+}
diff --git a/src/AxCopilot/Handlers/AspectHandler.cs b/src/AxCopilot/Handlers/AspectHandler.cs
new file mode 100644
index 0000000..f9b42a5
--- /dev/null
+++ b/src/AxCopilot/Handlers/AspectHandler.cs
@@ -0,0 +1,297 @@
+using System.Windows;
+using AxCopilot.SDK;
+using AxCopilot.Services;
+using AxCopilot.Themes;
+
+namespace AxCopilot.Handlers;
+
+///
+/// L18-3: 화면 비율·해상도 계산기 핸들러. "aspect" 프리픽스로 사용합니다.
+///
+/// 예: aspect → 주요 비율 목록
+/// aspect 1920 1080 → 1920x1080 비율 계산
+/// aspect 16:9 1280 → 16:9 비율에서 너비 1280의 높이
+/// aspect 16:9 h 720 → 16:9 비율에서 높이 720의 너비
+/// aspect 4:3 → 4:3 비율의 주요 해상도 목록
+/// aspect crop 1920 1080 4:3 → 크롭 영역 계산
+/// Enter → 해상도 복사.
+///
+public class AspectHandler : IActionHandler
+{
+ public string? Prefix => "aspect";
+
+ public PluginMetadata Metadata => new(
+ "Aspect",
+ "화면 비율·해상도 계산기 — 16:9 · 4:3 · 21:9 · 크롭 영역",
+ "1.0",
+ "AX");
+
+ private record AspectPreset(string Ratio, string Name, (int W, int H)[] Resolutions);
+
+ private static readonly AspectPreset[] Presets =
+ [
+ new("16:9", "와이드스크린 (모니터·TV·유튜브)",
+ [(3840,2160),(2560,1440),(1920,1080),(1600,900),(1366,768),(1280,720),(960,540),(854,480),(640,360)]),
+
+ new("4:3", "전통 CRT·클래식 TV",
+ [(2048,1536),(1600,1200),(1400,1050),(1280,960),(1024,768),(800,600),(640,480)]),
+
+ new("21:9", "울트라와이드 시네마",
+ [(5120,2160),(3440,1440),(2560,1080),(2560,1080),(1280,540)]),
+
+ new("1:1", "정사각형 (인스타그램·SNS)",
+ [(4096,4096),(2048,2048),(1080,1080),(720,720),(512,512)]),
+
+ new("9:16", "세로 (모바일·스토리·릴스)",
+ [(1080,1920),(720,1280),(540,960),(360,640)]),
+
+ new("3:2", "DSLR 카메라 (35mm)",
+ [(6000,4000),(4500,3000),(3000,2000),(1500,1000)]),
+
+ new("2:1", "시네마 와이드",
+ [(4096,2048),(2048,1024),(1920,960)]),
+
+ new("5:4", "구형 모니터",
+ [(1280,1024),(1024,819)]),
+
+ new("2.35:1","영화 시네마스코프",
+ [(2560,1090),(1920,817),(1280,544)]),
+ ];
+
+ public Task> GetItemsAsync(string query, CancellationToken ct)
+ {
+ var q = query.Trim();
+ var items = new List();
+
+ if (string.IsNullOrWhiteSpace(q))
+ {
+ items.Add(new LauncherItem("화면 비율·해상도 계산기",
+ "예: aspect 1920 1080 / aspect 16:9 1280 / aspect 4:3",
+ null, null, Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("── 주요 비율 ──", "", null, null, Symbol: "\uE7F4"));
+ foreach (var p in Presets)
+ items.Add(new LauncherItem($"{p.Ratio} {p.Name}",
+ $"주요 해상도: {string.Join(", ", p.Resolutions.Take(3).Select(r => $"{r.W}×{r.H}"))}…",
+ null, null, Symbol: "\uE7F4"));
+ return Task.FromResult>(items);
+ }
+
+ var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+
+ // aspect → 비율 계산
+ if (parts.Length >= 2 && int.TryParse(parts[0], out var w1) && int.TryParse(parts[1], out var h1))
+ {
+ items.AddRange(BuildFromResolution(w1, h1));
+ return Task.FromResult>(items);
+ }
+
+ // aspect <비율> 형식 (예: 16:9, 4:3, 16/9)
+ if (TryParseRatio(parts[0], out var rw, out var rh))
+ {
+ // aspect 16:9 <너비> 또는 aspect 16:9 h <높이>
+ if (parts.Length >= 2)
+ {
+ var isHeight = parts.Length >= 3 &&
+ parts[1].ToLowerInvariant() is "h" or "height" or "높이";
+ var dimStr = isHeight ? parts[2] : parts[1];
+
+ if (int.TryParse(dimStr, out var dim))
+ {
+ if (isHeight)
+ {
+ var calcW = (int)Math.Round((double)dim * rw / rh);
+ items.AddRange(BuildFromRatioAndDim(rw, rh, calcW, dim));
+ }
+ else
+ {
+ var calcH = (int)Math.Round((double)dim * rh / rw);
+ items.AddRange(BuildFromRatioAndDim(rw, rh, dim, calcH));
+ }
+ return Task.FromResult>(items);
+ }
+ }
+
+ // aspect 16:9 → 주요 해상도 목록
+ items.AddRange(BuildFromRatio(rw, rh));
+ return Task.FromResult>(items);
+ }
+
+ // crop 서브커맨드
+ if (parts[0].ToLowerInvariant() == "crop" && parts.Length >= 5)
+ {
+ if (int.TryParse(parts[1], out var srcW) &&
+ int.TryParse(parts[2], out var srcH) &&
+ TryParseRatio(parts[3], out var cRw, out var cRh))
+ {
+ items.AddRange(BuildCropItems(srcW, srcH, cRw, cRh));
+ return Task.FromResult>(items);
+ }
+ }
+
+ items.Add(new LauncherItem("형식 오류",
+ "예: aspect 1920 1080 / aspect 16:9 1280 / aspect 16:9 h 720",
+ null, null, Symbol: "\uE783"));
+ 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("Aspect", "클립보드에 복사했습니다.");
+ }
+ catch { }
+ }
+ return Task.CompletedTask;
+ }
+
+ // ── 빌더 ─────────────────────────────────────────────────────────────────
+
+ private static List BuildFromResolution(int w, int h)
+ {
+ var items = new List();
+ var gcd = Gcd(w, h);
+ var rw = w / gcd;
+ var rh = h / gcd;
+ var ratio = $"{rw}:{rh}";
+ var frac = (double)w / h;
+
+ items.Add(new LauncherItem($"{w} × {h} → 비율 {ratio}",
+ $"소수 비율: {frac:F4}", null, ("copy", ratio), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem($"비율", ratio, null, ("copy", ratio), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem($"소수 비율", $"{frac:F4}", null, ("copy", $"{frac:F4}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem($"픽셀 수", $"{(long)w * h:N0} px ({(long)w * h / 1_000_000.0:F1} MP)",
+ null, null, Symbol: "\uE7F4"));
+
+ // 비슷한 프리셋 찾기
+ var preset = Presets.FirstOrDefault(p => p.Ratio == ratio);
+ if (preset != null)
+ {
+ items.Add(new LauncherItem($"── {preset.Ratio} {preset.Name} ──", "", null, null, Symbol: "\uE7F4"));
+ foreach (var (pw, ph) in preset.Resolutions)
+ items.Add(new LauncherItem($"{pw} × {ph}", FormatPixels(pw, ph),
+ null, ("copy", $"{pw}x{ph}"), Symbol: "\uE7F4"));
+ }
+ else
+ {
+ // 같은 비율의 다른 해상도 계산
+ items.Add(new LauncherItem("── 같은 비율 기타 해상도 ──", "", null, null, Symbol: "\uE7F4"));
+ var scales = new[] { 0.25, 0.5, 0.75, 1.25, 1.5, 2.0 };
+ foreach (var s in scales)
+ {
+ var sw = (int)Math.Round(w * s / gcd) * gcd;
+ var sh = (int)Math.Round(h * s / gcd) * gcd;
+ if (sw > 0 && sh > 0)
+ items.Add(new LauncherItem($"{sw} × {sh} ({s:P0})",
+ FormatPixels(sw, sh), null, ("copy", $"{sw}x{sh}"), Symbol: "\uE7F4"));
+ }
+ }
+ return items;
+ }
+
+ private static List BuildFromRatio(int rw, int rh)
+ {
+ var items = new List();
+ var preset = Presets.FirstOrDefault(p => p.Ratio == $"{rw}:{rh}");
+
+ if (preset != null)
+ {
+ items.Add(new LauncherItem($"{preset.Ratio} {preset.Name}",
+ $"주요 해상도 {preset.Resolutions.Length}개", null, null, Symbol: "\uE7F4"));
+ foreach (var (pw, ph) in preset.Resolutions)
+ items.Add(new LauncherItem($"{pw} × {ph}", FormatPixels(pw, ph),
+ null, ("copy", $"{pw}x{ph}"), Symbol: "\uE7F4"));
+ }
+ else
+ {
+ items.Add(new LauncherItem($"{rw}:{rh} 비율",
+ "자주 쓰는 너비 기준 해상도", null, null, Symbol: "\uE7F4"));
+ var widths = new[] { 640, 1280, 1920, 2560, 3840 };
+ foreach (var bw in widths)
+ {
+ var bh = (int)Math.Round((double)bw * rh / rw);
+ items.Add(new LauncherItem($"{bw} × {bh}", FormatPixels(bw, bh),
+ null, ("copy", $"{bw}x{bh}"), Symbol: "\uE7F4"));
+ }
+ }
+ return items;
+ }
+
+ private static List BuildFromRatioAndDim(int rw, int rh, int w, int h)
+ {
+ var items = new List();
+ var label = $"{rw}:{rh} → {w} × {h}";
+ items.Add(new LauncherItem(label,
+ $"픽셀 {(long)w * h:N0} · Enter 복사", null, ("copy", $"{w}x{h}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem($"너비", $"{w} px", null, ("copy", $"{w}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem($"높이", $"{h} px", null, ("copy", $"{h}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("CSS", $"{w}px × {h}px", null, ("copy", $"{w}px × {h}px"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("w×h", $"{w}x{h}", null, ("copy", $"{w}x{h}"), Symbol: "\uE7F4"));
+ return items;
+ }
+
+ private static List BuildCropItems(int srcW, int srcH, int cRw, int cRh)
+ {
+ var items = new List();
+ // 크롭 방향 결정
+ var srcRatio = (double)srcW / srcH;
+ var cropRatio = (double)cRw / cRh;
+
+ int cropW, cropH, offsetX, offsetY;
+ if (srcRatio > cropRatio)
+ {
+ // 좌우 크롭
+ cropH = srcH;
+ cropW = (int)Math.Round(srcH * cropRatio);
+ offsetX = (srcW - cropW) / 2;
+ offsetY = 0;
+ }
+ else
+ {
+ // 상하 크롭
+ cropW = srcW;
+ cropH = (int)Math.Round(srcW / cropRatio);
+ offsetX = 0;
+ offsetY = (srcH - cropH) / 2;
+ }
+
+ items.Add(new LauncherItem($"크롭: {srcW}×{srcH} → {cRw}:{cRh}",
+ $"크롭 영역: {cropW}×{cropH} 오프셋: ({offsetX},{offsetY})",
+ null, ("copy", $"{cropW}x{cropH}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("크롭 크기", $"{cropW} × {cropH}", null, ("copy", $"{cropW}x{cropH}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("X 오프셋", $"{offsetX} px", null, ("copy", $"{offsetX}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("Y 오프셋", $"{offsetY} px", null, ("copy", $"{offsetY}"), Symbol: "\uE7F4"));
+ items.Add(new LauncherItem("FFmpeg crop", $"crop={cropW}:{cropH}:{offsetX}:{offsetY}",
+ null, ("copy", $"crop={cropW}:{cropH}:{offsetX}:{offsetY}"), Symbol: "\uE7F4"));
+ return items;
+ }
+
+ // ── 헬퍼 ─────────────────────────────────────────────────────────────────
+
+ private static bool TryParseRatio(string s, out int rw, out int rh)
+ {
+ rw = rh = 0;
+ var sep = s.Contains(':') ? ':' : s.Contains('/') ? '/' : '\0';
+ if (sep == '\0') return false;
+ var parts = s.Split(sep);
+ if (parts.Length != 2) return false;
+ return double.TryParse(parts[0], System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out var drw) &&
+ double.TryParse(parts[1], System.Globalization.NumberStyles.Any,
+ System.Globalization.CultureInfo.InvariantCulture, out var drh) &&
+ (rw = (int)Math.Round(drw * 100)) > 0 &&
+ (rh = (int)Math.Round(drh * 100)) > 0;
+ }
+
+ private static int Gcd(int a, int b) => b == 0 ? a : Gcd(b, a % b);
+
+ private static string FormatPixels(int w, int h)
+ {
+ var mp = (long)w * h / 1_000_000.0;
+ return $"{(long)w * h:N0} px ({mp:F1} MP)";
+ }
+}
diff --git a/src/AxCopilot/Handlers/SqlHandler.cs b/src/AxCopilot/Handlers/SqlHandler.cs
new file mode 100644
index 0000000..521fe21
--- /dev/null
+++ b/src/AxCopilot/Handlers/SqlHandler.cs
@@ -0,0 +1,467 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Windows;
+using AxCopilot.SDK;
+using AxCopilot.Services;
+using AxCopilot.Themes;
+
+namespace AxCopilot.Handlers;
+
+///
+/// L18-1: SQL 포맷터·분석기 핸들러. "sql" 프리픽스로 사용합니다.
+///
+/// 예: sql → 클립보드 SQL 포맷 (들여쓰기 정렬)
+/// sql mini → SQL 미니파이 (공백·줄바꿈 제거)
+/// sql upper → 키워드 대문자로 변환
+/// sql lower → 키워드 소문자로 변환
+/// sql stats → 테이블·컬럼·조건 수 분석
+/// sql tables → FROM/JOIN 테이블 목록 추출
+/// sql select → SELECT * FROM 생성
+/// Enter → 결과 복사.
+/// 외부 라이브러리 없이 순수 구현.
+///
+public partial class SqlHandler : IActionHandler
+{
+ public string? Prefix => "sql";
+
+ public PluginMetadata Metadata => new(
+ "SQL",
+ "SQL 포맷터·분석기 — 들여쓰기 · 미니파이 · 키워드 · 테이블 추출",
+ "1.0",
+ "AX");
+
+ // SQL 키워드 목록
+ private static readonly string[] Keywords =
+ [
+ "SELECT", "FROM", "WHERE", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN",
+ "OUTER JOIN", "FULL JOIN", "CROSS JOIN", "ON", "AND", "OR", "NOT",
+ "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", "DELETE",
+ "CREATE TABLE", "CREATE INDEX", "CREATE VIEW", "DROP TABLE", "DROP INDEX",
+ "ALTER TABLE", "ADD COLUMN", "DROP COLUMN", "RENAME TO",
+ "GROUP BY", "ORDER BY", "HAVING", "LIMIT", "OFFSET", "DISTINCT",
+ "UNION", "UNION ALL", "INTERSECT", "EXCEPT",
+ "CASE", "WHEN", "THEN", "ELSE", "END",
+ "IN", "NOT IN", "EXISTS", "NOT EXISTS", "BETWEEN", "LIKE", "IS NULL", "IS NOT NULL",
+ "AS", "WITH", "RECURSIVE",
+ "COUNT", "SUM", "AVG", "MIN", "MAX", "COALESCE", "NULLIF", "CAST",
+ "SUBSTRING", "TRIM", "UPPER", "LOWER", "LENGTH", "REPLACE",
+ "NOW", "CURRENT_DATE", "CURRENT_TIMESTAMP", "DATE_FORMAT",
+ "BEGIN", "COMMIT", "ROLLBACK", "TRANSACTION",
+ "PRIMARY KEY", "FOREIGN KEY", "REFERENCES", "UNIQUE", "NOT NULL", "DEFAULT",
+ "INDEX", "CONSTRAINT",
+ ];
+
+ // 새 줄 시작 키워드
+ private static readonly HashSet NewlineKeywords = new(StringComparer.OrdinalIgnoreCase)
+ {
+ "SELECT", "FROM", "WHERE", "JOIN", "LEFT JOIN", "RIGHT JOIN", "INNER JOIN",
+ "OUTER JOIN", "FULL JOIN", "CROSS JOIN", "ON",
+ "GROUP BY", "ORDER BY", "HAVING", "LIMIT", "OFFSET",
+ "UNION", "UNION ALL", "INTERSECT", "EXCEPT",
+ "INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM", "DELETE",
+ "CREATE TABLE", "CREATE INDEX", "CREATE VIEW",
+ "DROP TABLE", "ALTER TABLE",
+ "WITH", "AND", "OR",
+ };
+
+ public Task> GetItemsAsync(string query, CancellationToken ct)
+ {
+ var q = query.Trim();
+ var items = new List();
+
+ string? clipboard = null;
+ try
+ {
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ if (Clipboard.ContainsText())
+ clipboard = Clipboard.GetText();
+ });
+ }
+ catch { }
+
+ if (string.IsNullOrWhiteSpace(q))
+ {
+ items.Add(new LauncherItem("SQL 포맷터·분석기",
+ "클립보드 SQL 포맷 · sql mini / upper / lower / stats / tables",
+ null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql", "들여쓰기 포맷", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql mini", "미니파이 (한 줄)", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql upper", "키워드 대문자", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql lower", "키워드 소문자", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql stats", "테이블·컬럼·조건 분석", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql tables", "FROM/JOIN 테이블 목록", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("sql select T", "SELECT 쿼리 빠른 생성", null, null, Symbol: "\uE8F1"));
+
+ if (string.IsNullOrWhiteSpace(clipboard))
+ {
+ items.Add(new LauncherItem("클립보드가 비어 있습니다",
+ "SQL 쿼리를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
+ return Task.FromResult>(items);
+ }
+
+ // 미리보기: 기본 포맷
+ var preview = Format(clipboard);
+ var prevLine = preview.Split('\n').FirstOrDefault() ?? "";
+ items.Add(new LauncherItem("클립보드 SQL 포맷",
+ prevLine.Length > 60 ? prevLine[..60] + "…" : prevLine,
+ null, ("copy", preview), Symbol: "\uE8F1"));
+ return Task.FromResult>(items);
+ }
+
+ var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
+ var sub = parts[0].ToLowerInvariant();
+
+ // sql select — 클립보드 없이도 동작
+ if (sub == "select")
+ {
+ var table = parts.Length > 1 ? parts[1].Trim() : "your_table";
+ var generated = BuildSelectTemplate(table);
+ items.Add(new LauncherItem($"SELECT * FROM {table}",
+ "Enter → 복사", null, ("copy", generated), Symbol: "\uE8F1"));
+ foreach (var line in generated.Split('\n').Take(8))
+ items.Add(new LauncherItem(line, "", null, null, Symbol: "\uE8F1"));
+ return Task.FromResult>(items);
+ }
+
+ if (string.IsNullOrWhiteSpace(clipboard))
+ {
+ items.Add(new LauncherItem("클립보드가 비어 있습니다",
+ "SQL 쿼리를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
+ return Task.FromResult>(items);
+ }
+
+ switch (sub)
+ {
+ case "format":
+ case "fmt":
+ case "pretty":
+ {
+ var result = Format(clipboard);
+ items.Add(new LauncherItem("SQL 포맷 완료",
+ $"{result.Split('\n').Length}줄 · Enter 복사", null, ("copy", result), Symbol: "\uE8F1"));
+ AddPreview(items, result, 8);
+ break;
+ }
+
+ case "mini":
+ case "minify":
+ case "compact":
+ {
+ var result = Minify(clipboard);
+ items.Add(new LauncherItem("SQL 미니파이 완료",
+ $"{result.Length}자 · Enter 복사", null, ("copy", result), Symbol: "\uE8F1"));
+ var prev = result.Length > 80 ? result[..80] + "…" : result;
+ items.Add(new LauncherItem(prev, "", null, ("copy", result), Symbol: "\uE8F1"));
+ break;
+ }
+
+ case "upper":
+ {
+ var result = TransformKeywords(clipboard, upper: true);
+ items.Add(new LauncherItem("키워드 대문자 변환",
+ "Enter → 복사", null, ("copy", result), Symbol: "\uE8F1"));
+ AddPreview(items, result, 6);
+ break;
+ }
+
+ case "lower":
+ {
+ var result = TransformKeywords(clipboard, upper: false);
+ items.Add(new LauncherItem("키워드 소문자 변환",
+ "Enter → 복사", null, ("copy", result), Symbol: "\uE8F1"));
+ AddPreview(items, result, 6);
+ break;
+ }
+
+ case "stats":
+ case "stat":
+ case "analyze":
+ {
+ items.AddRange(BuildStatsItems(clipboard));
+ break;
+ }
+
+ case "tables":
+ case "table":
+ {
+ var tables = ExtractTables(clipboard);
+ items.Add(new LauncherItem($"테이블 {tables.Count}개",
+ "FROM / JOIN 에서 추출", null, null, Symbol: "\uE8F1"));
+ foreach (var t in tables)
+ items.Add(new LauncherItem(t, "", null, ("copy", t), Symbol: "\uE8F1"));
+ if (tables.Count == 0)
+ items.Add(new LauncherItem("테이블 없음", "FROM 절이 없습니다", null, null, Symbol: "\uE946"));
+ break;
+ }
+
+ default:
+ {
+ // 기본 동작: 포맷
+ var result = Format(clipboard);
+ items.Add(new LauncherItem("SQL 포맷 완료",
+ $"{result.Split('\n').Length}줄", null, ("copy", result), Symbol: "\uE8F1"));
+ AddPreview(items, result, 8);
+ break;
+ }
+ }
+
+ 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("SQL", "클립보드에 복사했습니다.");
+ }
+ catch { }
+ }
+ return Task.CompletedTask;
+ }
+
+ // ── SQL 포맷 ─────────────────────────────────────────────────────────────
+
+ private static string Format(string sql)
+ {
+ // 정규화: 여러 공백 → 1개, 개행 제거
+ var flat = WhitespaceRegex().Replace(sql.Replace('\n', ' ').Replace('\r', ' '), " ").Trim();
+
+ var sb = new StringBuilder();
+ var indent = 0;
+ var tokens = Tokenize(flat);
+
+ foreach (var token in tokens)
+ {
+ var upper = token.ToUpperInvariant();
+
+ // 닫기 괄호 → 들여쓰기 감소
+ if (upper == ")")
+ {
+ indent = Math.Max(0, indent - 1);
+ if (sb.Length > 0 && sb[^1] != '\n')
+ sb.AppendLine();
+ sb.Append(new string(' ', indent * 2));
+ sb.Append(token);
+ continue;
+ }
+
+ // 새 줄 시작 키워드
+ if (NewlineKeywords.Contains(upper))
+ {
+ if (sb.Length > 0)
+ {
+ sb.AppendLine();
+ sb.Append(new string(' ', indent * 2));
+ }
+ sb.Append(token.ToUpperInvariant());
+ sb.Append(' ');
+ continue;
+ }
+
+ // 열기 괄호 → 들여쓰기 증가
+ if (upper == "(")
+ {
+ sb.Append(token);
+ indent++;
+ continue;
+ }
+
+ // 쉼표 → 뒤에 공백
+ if (upper == ",")
+ {
+ sb.Append(',');
+ sb.AppendLine();
+ sb.Append(new string(' ', indent * 2 + 2));
+ continue;
+ }
+
+ sb.Append(token);
+ sb.Append(' ');
+ }
+
+ return sb.ToString().TrimEnd();
+ }
+
+ private static string Minify(string sql)
+ {
+ var flat = WhitespaceRegex().Replace(sql.Replace('\n', ' ').Replace('\r', ' '), " ").Trim();
+ // 괄호 주변 공백 제거
+ flat = SpaceAroundParensRegex().Replace(flat, "$1");
+ flat = SpaceBeforeCommaRegex().Replace(flat, ",");
+ return flat;
+ }
+
+ private static string TransformKeywords(string sql, bool upper)
+ {
+ var result = sql;
+ // 긴 키워드부터 처리 (LEFT JOIN before JOIN 등)
+ foreach (var kw in Keywords.OrderByDescending(k => k.Length))
+ {
+ var pattern = $@"\b{Regex.Escape(kw)}\b";
+ var replacement = upper ? kw.ToUpperInvariant() : kw.ToLowerInvariant();
+ result = Regex.Replace(result, pattern, replacement, RegexOptions.IgnoreCase);
+ }
+ return result;
+ }
+
+ private static List BuildStatsItems(string sql)
+ {
+ var items = new List();
+ var upper = sql.ToUpperInvariant();
+ var tables = ExtractTables(sql);
+ var selCols = ExtractSelectColumns(sql);
+ var wheres = CountConditions(sql);
+ var joins = CountMatches(upper, @"\bJOIN\b");
+ var subqs = CountMatches(upper, @"\bSELECT\b") - 1;
+ var orderBy = upper.Contains("ORDER BY");
+ var groupBy = upper.Contains("GROUP BY");
+ var hasLimit = upper.Contains("LIMIT");
+ var dml = DetectDml(upper);
+
+ items.Add(new LauncherItem($"SQL 분석 [{dml}]",
+ $"테이블 {tables.Count}개 · JOIN {joins}개 · WHERE 조건 {wheres}개",
+ null, null, Symbol: "\uE8F1"));
+
+ items.Add(new LauncherItem("DML 유형", dml, null, ("copy", dml), Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("테이블 수", $"{tables.Count}개", null, ("copy", $"{tables.Count}"), Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("JOIN 수", $"{joins}개", null, ("copy", $"{joins}"), Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("WHERE 조건 수", $"{wheres}개", null, ("copy", $"{wheres}"), Symbol: "\uE8F1"));
+ if (selCols.Count > 0)
+ items.Add(new LauncherItem("SELECT 컬럼", $"{selCols.Count}개", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("서브쿼리", $"{Math.Max(0, subqs)}개", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("GROUP BY", groupBy ? "있음" : "없음", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("ORDER BY", orderBy ? "있음" : "없음", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("LIMIT", hasLimit ? "있음" : "없음", null, null, Symbol: "\uE8F1"));
+ items.Add(new LauncherItem("전체 길이", $"{sql.Length}자", null, null, Symbol: "\uE8F1"));
+ return items;
+ }
+
+ private static List ExtractTables(string sql)
+ {
+ var result = new HashSet(StringComparer.OrdinalIgnoreCase);
+ var matches = TableRegex().Matches(sql);
+ foreach (Match m in matches)
+ {
+ var t = m.Groups[1].Value.Trim().Trim('[', ']', '`', '"');
+ if (!string.IsNullOrWhiteSpace(t) && !IsKeyword(t))
+ result.Add(t);
+ }
+ return result.ToList();
+ }
+
+ private static List ExtractSelectColumns(string sql)
+ {
+ var m = SelectColsRegex().Match(sql);
+ if (!m.Success) return new List();
+ var cols = m.Groups[1].Value;
+ return cols.Split(',').Select(c => c.Trim()).Where(c => !string.IsNullOrEmpty(c)).ToList();
+ }
+
+ private static int CountConditions(string sql)
+ {
+ var upper = sql.ToUpperInvariant();
+ var idx = upper.IndexOf("WHERE", StringComparison.Ordinal);
+ if (idx < 0) return 0;
+ var wherePart = upper[idx..];
+ // AND/OR 수 + 1
+ return Regex.Matches(wherePart, @"\b(AND|OR)\b").Count + 1;
+ }
+
+ private static int CountMatches(string text, string pattern) =>
+ Regex.Matches(text, pattern, RegexOptions.IgnoreCase).Count;
+
+ private static string DetectDml(string upper)
+ {
+ if (upper.TrimStart().StartsWith("SELECT")) return "SELECT";
+ if (upper.TrimStart().StartsWith("INSERT")) return "INSERT";
+ if (upper.TrimStart().StartsWith("UPDATE")) return "UPDATE";
+ if (upper.TrimStart().StartsWith("DELETE")) return "DELETE";
+ if (upper.TrimStart().StartsWith("CREATE")) return "CREATE";
+ if (upper.TrimStart().StartsWith("ALTER")) return "ALTER";
+ if (upper.TrimStart().StartsWith("DROP")) return "DROP";
+ if (upper.TrimStart().StartsWith("WITH")) return "CTE/WITH";
+ return "기타";
+ }
+
+ private static bool IsKeyword(string s) =>
+ Keywords.Any(k => k.Equals(s, StringComparison.OrdinalIgnoreCase));
+
+ private static List Tokenize(string sql)
+ {
+ var tokens = new List();
+ var current = new StringBuilder();
+ var inStr = false;
+ var strChar = ' ';
+
+ for (var i = 0; i < sql.Length; i++)
+ {
+ var c = sql[i];
+
+ if (inStr)
+ {
+ current.Append(c);
+ if (c == strChar) inStr = false;
+ continue;
+ }
+
+ if (c is '\'' or '"' or '`')
+ {
+ if (current.Length > 0) { tokens.Add(current.ToString()); current.Clear(); }
+ current.Append(c);
+ inStr = true; strChar = c;
+ continue;
+ }
+
+ if (c is '(' or ')' or ',')
+ {
+ if (current.Length > 0) { tokens.Add(current.ToString().Trim()); current.Clear(); }
+ tokens.Add(c.ToString());
+ continue;
+ }
+
+ if (c == ' ')
+ {
+ if (current.Length > 0) { tokens.Add(current.ToString().Trim()); current.Clear(); }
+ continue;
+ }
+
+ current.Append(c);
+ }
+
+ if (current.Length > 0) tokens.Add(current.ToString().Trim());
+ return tokens.Where(t => !string.IsNullOrEmpty(t)).ToList();
+ }
+
+ private static string BuildSelectTemplate(string table) =>
+ $"SELECT\n *\nFROM\n {table}\nWHERE\n 1 = 1\nLIMIT 100;";
+
+ private static void AddPreview(List items, string text, int maxLines)
+ {
+ foreach (var line in text.Split('\n').Take(maxLines))
+ {
+ var t = line.TrimEnd();
+ if (!string.IsNullOrWhiteSpace(t))
+ items.Add(new LauncherItem(t, "", null, null, Symbol: "\uE8F1"));
+ }
+ }
+
+ [GeneratedRegex(@"\s+")]
+ private static partial Regex WhitespaceRegex();
+
+ [GeneratedRegex(@"\s*([(),])\s*")]
+ private static partial Regex SpaceAroundParensRegex();
+
+ [GeneratedRegex(@"\s+,")]
+ private static partial Regex SpaceBeforeCommaRegex();
+
+ [GeneratedRegex(@"(?:FROM|JOIN)\s+([\w\.\[\]`""]+)", RegexOptions.IgnoreCase)]
+ private static partial Regex TableRegex();
+
+ [GeneratedRegex(@"SELECT\s+(.*?)\s+FROM", RegexOptions.IgnoreCase | RegexOptions.Singleline)]
+ private static partial Regex SelectColsRegex();
+}
diff --git a/src/AxCopilot/Handlers/TextCaseHandler.cs b/src/AxCopilot/Handlers/TextCaseHandler.cs
new file mode 100644
index 0000000..d025fa7
--- /dev/null
+++ b/src/AxCopilot/Handlers/TextCaseHandler.cs
@@ -0,0 +1,259 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Windows;
+using AxCopilot.SDK;
+using AxCopilot.Services;
+using AxCopilot.Themes;
+
+namespace AxCopilot.Handlers;
+
+///
+/// L18-2: 텍스트 케이스 변환 핸들러. "text" 프리픽스로 사용합니다.
+///
+/// 예: text → 클립보드 텍스트 모든 케이스 변환 목록
+/// text camel → camelCase
+/// text pascal → PascalCase
+/// text snake → snake_case
+/// text kebab → kebab-case
+/// text slug → url-slug (소문자 + 하이픈)
+/// text upper → UPPER CASE
+/// text lower → lower case
+/// text title → Title Case
+/// text sentence → Sentence case
+/// text const → SCREAMING_SNAKE_CASE
+/// text dot → dot.case
+/// text reverse → 문자 순서 뒤집기
+/// text trim → 앞뒤 공백·줄바꿈 제거
+/// Enter → 결과 복사.
+///
+public partial class TextCaseHandler : IActionHandler
+{
+ public string? Prefix => "text";
+
+ public PluginMetadata Metadata => new(
+ "Text",
+ "텍스트 케이스 변환 — camelCase · snake_case · PascalCase · slug 등",
+ "1.0",
+ "AX");
+
+ private record CaseItem(string Name, string Key, Func Convert);
+
+ private static readonly CaseItem[] Cases =
+ [
+ new("camelCase", "camel", ToCamel),
+ new("PascalCase", "pascal", ToPascal),
+ new("snake_case", "snake", ToSnake),
+ new("SCREAMING_SNAKE_CASE", "const", ToConst),
+ new("kebab-case", "kebab", ToKebab),
+ new("URL slug", "slug", ToSlug),
+ new("dot.case", "dot", ToDot),
+ new("UPPER CASE", "upper", s => s.ToUpperInvariant()),
+ new("lower case", "lower", s => s.ToLowerInvariant()),
+ new("Title Case", "title", ToTitle),
+ new("Sentence case", "sentence", ToSentence),
+ new("뒤집기 (reverse)", "reverse", s => new string(s.Reverse().ToArray())),
+ new("공백 정리 (trim)", "trim", s => s.Trim()),
+ ];
+
+ public Task> GetItemsAsync(string query, CancellationToken ct)
+ {
+ var q = query.Trim();
+ var items = new List();
+
+ string? clipboard = null;
+ try
+ {
+ System.Windows.Application.Current.Dispatcher.Invoke(() =>
+ {
+ if (Clipboard.ContainsText())
+ clipboard = Clipboard.GetText().Trim();
+ });
+ }
+ catch { }
+
+ if (string.IsNullOrWhiteSpace(q))
+ {
+ items.Add(new LauncherItem("텍스트 케이스 변환기",
+ "클립보드 텍스트를 다양한 케이스로 변환 · text camel / snake / pascal / kebab…",
+ null, null, Symbol: "\uE8AB"));
+
+ if (string.IsNullOrWhiteSpace(clipboard))
+ {
+ items.Add(new LauncherItem("클립보드가 비어 있습니다",
+ "텍스트를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
+ // 케이스 목록만 안내
+ foreach (var c in Cases)
+ items.Add(new LauncherItem($"text {c.Key}", c.Name, null, null, Symbol: "\uE8AB"));
+ return Task.FromResult>(items);
+ }
+
+ // 클립보드 텍스트 → 모든 케이스 변환 목록
+ var preview = clipboard.Length > 30 ? clipboard[..30] + "…" : clipboard;
+ items.Add(new LauncherItem($"입력: \"{preview}\"", $"{clipboard.Length}자 · 아래에서 선택",
+ null, null, Symbol: "\uE8AB"));
+
+ foreach (var c in Cases)
+ {
+ var result = TrySafeConvert(c.Convert, clipboard);
+ items.Add(new LauncherItem(result, c.Name,
+ null, ("copy", result), Symbol: "\uE8AB"));
+ }
+ return Task.FromResult>(items);
+ }
+
+ // 서브커맨드로 특정 케이스 변환
+ var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
+ var sub = parts[0].ToLowerInvariant();
+ // 인라인 텍스트 입력 지원: text camel hello world → helloWorld
+ var inlineText = parts.Length > 1 ? parts[1] : clipboard;
+
+ if (string.IsNullOrWhiteSpace(inlineText))
+ {
+ items.Add(new LauncherItem("텍스트가 없습니다",
+ "클립보드에 텍스트를 복사하거나 text camel <직접입력> 형식으로 사용하세요",
+ null, null, Symbol: "\uE946"));
+ return Task.FromResult>(items);
+ }
+
+ var caseItem = Cases.FirstOrDefault(c =>
+ c.Key.Equals(sub, StringComparison.OrdinalIgnoreCase));
+
+ if (caseItem != null)
+ {
+ var result = TrySafeConvert(caseItem.Convert, inlineText);
+ var sourceLabel = parts.Length > 1 ? $"입력: \"{inlineText}\"" : $"클립보드: \"{(inlineText.Length > 30 ? inlineText[..30] + "…" : inlineText)}\"";
+ items.Add(new LauncherItem(result,
+ $"{caseItem.Name} · Enter 복사", null, ("copy", result), Symbol: "\uE8AB"));
+ items.Add(new LauncherItem(sourceLabel, "원본", null, ("copy", inlineText), Symbol: "\uE8AB"));
+
+ // 다른 케이스도 함께 표시
+ items.Add(new LauncherItem("── 다른 케이스 ──", "", null, null, Symbol: "\uE8AB"));
+ foreach (var c in Cases.Where(c => c.Key != sub))
+ {
+ var r = TrySafeConvert(c.Convert, inlineText);
+ if (r != result)
+ items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB"));
+ }
+ }
+ else
+ {
+ // 알 수 없는 서브커맨드 → 모든 케이스 변환
+ items.Add(new LauncherItem($"알 수 없는 케이스: '{sub}'",
+ "camel · pascal · snake · const · kebab · slug · dot · upper · lower · title · sentence · reverse · trim",
+ null, null, Symbol: "\uE783"));
+
+ if (!string.IsNullOrWhiteSpace(clipboard))
+ {
+ foreach (var c in Cases)
+ {
+ var r = TrySafeConvert(c.Convert, clipboard);
+ items.Add(new LauncherItem(r, c.Name, null, ("copy", r), Symbol: "\uE8AB"));
+ }
+ }
+ }
+
+ 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("Text", "클립보드에 복사했습니다.");
+ }
+ catch { }
+ }
+ return Task.CompletedTask;
+ }
+
+ // ── 변환 함수 ─────────────────────────────────────────────────────────────
+
+ private static string TrySafeConvert(Func fn, string input)
+ {
+ try { return fn(input); }
+ catch { return input; }
+ }
+
+ /// 입력을 단어 토큰 배열로 분리 (공백, 언더스코어, 하이픈, 대문자 경계)
+ private static string[] Tokenize(string s)
+ {
+ // camelCase/PascalCase 분리
+ var withSpaces = CamelBoundaryRegex().Replace(s, "$1 $2");
+ // 구분자 → 공백
+ var normalized = SeparatorRegex().Replace(withSpaces, " ");
+ return normalized.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ }
+
+ private static string ToCamel(string s)
+ {
+ var words = Tokenize(s);
+ if (words.Length == 0) return s;
+ var sb = new StringBuilder(words[0].ToLowerInvariant());
+ for (var i = 1; i < words.Length; i++)
+ sb.Append(char.ToUpperInvariant(words[i][0]) + words[i][1..].ToLowerInvariant());
+ return sb.ToString();
+ }
+
+ private static string ToPascal(string s)
+ {
+ var words = Tokenize(s);
+ var sb = new StringBuilder();
+ foreach (var w in words)
+ sb.Append(char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant());
+ return sb.ToString();
+ }
+
+ private static string ToSnake(string s) =>
+ string.Join("_", Tokenize(s).Select(w => w.ToLowerInvariant()));
+
+ private static string ToConst(string s) =>
+ string.Join("_", Tokenize(s).Select(w => w.ToUpperInvariant()));
+
+ private static string ToKebab(string s) =>
+ string.Join("-", Tokenize(s).Select(w => w.ToLowerInvariant()));
+
+ private static string ToSlug(string s)
+ {
+ var normalized = s.Normalize(NormalizationForm.FormD);
+ var ascii = new StringBuilder();
+ foreach (var c in normalized)
+ if (c < 128) ascii.Append(c);
+ var slug = SeparatorRegex().Replace(ascii.ToString().ToLowerInvariant(), "-");
+ slug = NonSlugRegex().Replace(slug, "");
+ slug = MultipleDashRegex().Replace(slug, "-");
+ return slug.Trim('-');
+ }
+
+ private static string ToDot(string s) =>
+ string.Join(".", Tokenize(s).Select(w => w.ToLowerInvariant()));
+
+ private static string ToTitle(string s)
+ {
+ var words = s.Split(' ');
+ return string.Join(" ", words.Select(w =>
+ w.Length == 0 ? w : char.ToUpperInvariant(w[0]) + w[1..].ToLowerInvariant()));
+ }
+
+ private static string ToSentence(string s)
+ {
+ if (string.IsNullOrEmpty(s)) return s;
+ var lower = s.ToLowerInvariant();
+ return char.ToUpperInvariant(lower[0]) + lower[1..];
+ }
+
+ [GeneratedRegex(@"([a-z])([A-Z])")]
+ private static partial Regex CamelBoundaryRegex();
+
+ [GeneratedRegex(@"[\s\-_./\\]+")]
+ private static partial Regex SeparatorRegex();
+
+ [GeneratedRegex(@"[^a-z0-9\-]")]
+ private static partial Regex NonSlugRegex();
+
+ [GeneratedRegex(@"-{2,}")]
+ private static partial Regex MultipleDashRegex();
+}