[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:
@@ -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 등 포함 | 높음 |
|
||||
|
||||
@@ -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();
|
||||
|
||||
303
src/AxCopilot/Handlers/AbbrHandler.cs
Normal file
303
src/AxCopilot/Handlers/AbbrHandler.cs
Normal 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", "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<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");
|
||||
}
|
||||
297
src/AxCopilot/Handlers/AspectHandler.cs
Normal file
297
src/AxCopilot/Handlers/AspectHandler.cs
Normal 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)";
|
||||
}
|
||||
}
|
||||
467
src/AxCopilot/Handlers/SqlHandler.cs
Normal file
467
src/AxCopilot/Handlers/SqlHandler.cs
Normal 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();
|
||||
}
|
||||
259
src/AxCopilot/Handlers/TextCaseHandler.cs
Normal file
259
src/AxCopilot/Handlers/TextCaseHandler.cs
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user