[Phase L18] SQL포맷·TextCase·Aspect·Abbr 핸들러 4종 추가

SqlHandler.cs (신규, ~280줄, prefix=sql, partial class):
- Keywords[]: 50+ SQL 키워드 + NewlineKeywords HashSet
- Format(): Tokenize() 기반 키워드·괄호·쉼표 들여쓰기 포맷
- Minify(): 공백·괄호 주변 최소화
- TransformKeywords(): 키워드 대소문자 일괄 변환
- BuildStatsItems(): DML유형·테이블·JOIN·WHERE·서브쿼리 분석
- ExtractTables(): FROM/JOIN 테이블 추출 (TableRegex)
- sql select <table>: SELECT 쿼리 템플릿 생성
- [GeneratedRegex]: Whitespace/SpaceAroundParens/Table/SelectCols

TextCaseHandler.cs (신규, ~220줄, prefix=text, partial class):
- CaseItem record: Name/Key/Func<string,string> 13가지 케이스
- Tokenize(): CamelBoundaryRegex + SeparatorRegex 단어 분리
- ToCamel/ToPascal/ToSnake/ToConst/ToKebab/ToDot 구현
- ToSlug(): NormalizationForm.FormD → ASCII 변환 + NonSlugRegex
- 인라인 입력: text camel hello world → helloWorld
- 특정 케이스 선택 시 다른 케이스도 함께 표시

AspectHandler.cs (신규, ~260줄, prefix=aspect):
- AspectPreset record: Ratio/Name/(W,H)[] 9개 프리셋
- BuildFromResolution(): GCD 약분 비율 + 배율별 해상도
- BuildFromRatio(): 프리셋 매칭 또는 기준 너비 5개 계산
- BuildFromRatioAndDim(): 너비/높이 단방향 계산
- BuildCropItems(): 크롭 방향 판별 + FFmpeg crop 파라미터
- TryParseRatio(): :·/ 구분자 + 소수 비율 지원
- Gcd(): 재귀 최대공약수

AbbrHandler.cs (신규, ~260줄, prefix=abbr):
- AbbrEntry record: Short/Full/Description/Category
- 150+개 내장 약어 (웹/개발/DB/보안/클라우드/AI/데이터형식/협업)
- 정확 일치 1개: 약어·원문·설명·카테고리 상세 표시
- 부분 일치: 목록 표시 (Short·Full·Description 검색)
- 카테고리명 직접 입력 → 해당 카테고리 전체 목록

App.xaml.cs (수정): Phase L18 핸들러 4종 RegisterHandler 추가
docs/LAUNCHER_ROADMAP.md (수정): Phase L18 섹션 추가 ( 완료)
빌드: 경고 0, 오류 0
This commit is contained in:
2026-04-04 16:15:13 +09:00
parent 7f897d6851
commit e0548c52a9
6 changed files with 1349 additions and 0 deletions

View File

@@ -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 <table>` 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 등 포함 | 높음 |

View File

@@ -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();

View File

@@ -0,0 +1,303 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L18-4: IT·개발 약어 사전 핸들러. "abbr" 프리픽스로 사용합니다.
///
/// 예: abbr → 주요 약어 목록
/// abbr api → API 뜻 + 설명
/// abbr crud → CRUD 약어 풀이
/// abbr rest → REST 설명
/// abbr http → HTTP 약어 목록
/// Enter → 약어 또는 뜻을 클립보드에 복사.
/// </summary>
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", "RivestShamirAdleman", "공개키 비대칭 암호화 알고리즘", "보안"),
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("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");
}

View File

@@ -0,0 +1,297 @@
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// 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 → 해상도 복사.
/// </summary>
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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
// aspect <W> <H> → 비율 계산
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
}
// aspect 16:9 → 주요 해상도 목록
items.AddRange(BuildFromRatio(rw, rh));
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
}
items.Add(new LauncherItem("형식 오류",
"예: aspect 1920 1080 / aspect 16:9 1280 / aspect 16:9 h 720",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("Aspect", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
// ── 빌더 ─────────────────────────────────────────────────────────────────
private static List<LauncherItem> BuildFromResolution(int w, int h)
{
var items = new List<LauncherItem>();
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<LauncherItem> BuildFromRatio(int rw, int rh)
{
var items = new List<LauncherItem>();
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<LauncherItem> BuildFromRatioAndDim(int rw, int rh, int w, int h)
{
var items = new List<LauncherItem>();
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<LauncherItem> BuildCropItems(int srcW, int srcH, int cRw, int cRh)
{
var items = new List<LauncherItem>();
// 크롭 방향 결정
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)";
}
}

View File

@@ -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;
/// <summary>
/// L18-1: SQL 포맷터·분석기 핸들러. "sql" 프리픽스로 사용합니다.
///
/// 예: sql → 클립보드 SQL 포맷 (들여쓰기 정렬)
/// sql mini → SQL 미니파이 (공백·줄바꿈 제거)
/// sql upper → 키워드 대문자로 변환
/// sql lower → 키워드 소문자로 변환
/// sql stats → 테이블·컬럼·조건 수 분석
/// sql tables → FROM/JOIN 테이블 목록 추출
/// sql select <table> → SELECT * FROM <table> 생성
/// Enter → 결과 복사.
/// 외부 라이브러리 없이 순수 구현.
/// </summary>
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<string> 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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// sql select <table> — 클립보드 없이도 동작
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<IEnumerable<LauncherItem>>(items);
}
if (string.IsNullOrWhiteSpace(clipboard))
{
items.Add(new LauncherItem("클립보드가 비어 있습니다",
"SQL 쿼리를 복사한 뒤 사용하세요", null, null, Symbol: "\uE946"));
return Task.FromResult<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("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<LauncherItem> BuildStatsItems(string sql)
{
var items = new List<LauncherItem>();
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<string> ExtractTables(string sql)
{
var result = new HashSet<string>(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<string> ExtractSelectColumns(string sql)
{
var m = SelectColsRegex().Match(sql);
if (!m.Success) return new List<string>();
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<string> Tokenize(string sql)
{
var tokens = new List<string>();
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<LauncherItem> 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();
}

View File

@@ -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;
/// <summary>
/// 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 → 결과 복사.
/// </summary>
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<string, string> 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<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(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<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string text))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(text));
NotificationService.Notify("Text", "클립보드에 복사했습니다.");
}
catch { }
}
return Task.CompletedTask;
}
// ── 변환 함수 ─────────────────────────────────────────────────────────────
private static string TrySafeConvert(Func<string, string> fn, string input)
{
try { return fn(input); }
catch { return input; }
}
/// <summary>입력을 단어 토큰 배열로 분리 (공백, 언더스코어, 하이픈, 대문자 경계)</summary>
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();
}