[Phase L10] 텍스트·데이터·개발 유틸리티 핸들러 4종 추가
XmlHandler.cs (신규, ~290줄, prefix=xml): - 클립보드/인라인 XML 자동 포맷(들여쓰기 2칸) - compact/minify: 공백 제거 압축, validate: 줄·열 오류 표시 - xpath //path: XPathDocument 기반 최대 20건 쿼리 - attr: XmlDocument 전체 속성 추출 - yield return → 리스트 방식으로 수정 (CS1631/CS1626 해결) UuidHandler.cs (신규, ~210줄, prefix=uuid): - Guid.NewGuid() v4 기본 + N개 일괄 생성 - seq: UUIDv7 스타일(상위 48비트=Unix ms 타임스탬프, 하위=랜덤) - short: RandomNumberGenerator 4바이트 hex 짧은 ID - upper: 대문자 UUID, nil: 00000000-… Nil UUID - parse: 버전·변형·v1 타임스탬프 복원 분석 CertHandler.cs (신규, ~200줄, prefix=cert): - TcpClient + SslStream으로 TLS 인증서 직접 조회 - 만료일·D-day·발급 대상·발급 기관·SANs·지문(SHA1) 표시 - 사내 모드: 내부 호스트(192.168/10/172.16-31)만 허용 - https:// URL 형식, 포트 지정(cert domain.com 8443) 지원 LoremHandler.cs (신규, ~230줄, prefix=lorem): - 113단어 Lorem Ipsum 풀 + 82단어 한국어 더미 풀 - lorem N: N단락, words N: 단어, sentences N: 문장 모드 - ko: 한국어 문장 구조(시작어+본문+결말) 조합 - email N: 더미 이메일, name N: 한국어 성+이름 조합 App.xaml.cs: 4개 핸들러 Phase L10 블록 등록 docs/LAUNCHER_ROADMAP.md: Phase L10 완료 섹션 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -256,3 +256,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
||||
| L9-2 | **IP 서브넷 계산기** ✅ | `subnet` 프리픽스. CIDR(x.x.x.x/24) 또는 공백 구분 입력. 네트워크·마스크·브로드캐스트·첫/마지막 호스트·사용 가능 호스트 수 계산. 서브넷 마스크→CIDR 변환. `subnet range x.x.x.10-50` 범위 계산. 이진 마스크 표시 | 높음 |
|
||||
| L9-3 | **시스템 정리** ✅ | `clean` 프리픽스. 임시 파일(%TEMP%), 휴지통(SHEmptyRecycleBin), 다운로드 30일 이상, AxCopilot 로그. 예상 용량 사전 표시. `clean all`로 일괄 정리. 항목별 실시간 알림 | 중간 |
|
||||
| L9-4 | **진수 변환기** ✅ | `base` 프리픽스. 10진/16진(0x)/2진(0b)/8진(0o) 자동 감지 변환. `base 255 to hex` 단일 방향 변환. `base ascii 65` ASCII 코드↔문자 변환. 4비트 그룹 이진 표시. Enter → 클립보드 복사 | 중간 |
|
||||
|
||||
---
|
||||
|
||||
## Phase L10 — 텍스트·데이터·개발 유틸리티 (v2.0.2) ✅ 완료
|
||||
|
||||
> **방향**: 개발자 일상 도구 확충 — XML 조작, UUID 생성, SSL 인증서 점검, 더미 데이터 생성.
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| L10-1 | **XML 포맷터·검증기** ✅ | `xml` 프리픽스. 클립보드 또는 인라인 XML 자동 포맷(들여쓰기). `xml compact/minify`로 압축. `xml validate`로 유효성 검증(줄·열 오류 표시). `xml xpath //경로`로 XPath 쿼리. `xml attr`로 속성 목록 추출. Enter → 클립보드 복사 | 높음 |
|
||||
| L10-2 | **UUID/GUID 생성기** ✅ | `uuid` 프리픽스. `uuid` 기본 v4 1개 생성. `uuid 5`로 N개 일괄. `uuid upper` 대문자. `uuid seq` UUIDv7 스타일 순차 UUID(타임스탬프 상위 48비트). `uuid short` 8자리 hex 짧은 ID. `uuid nil` Nil UUID. `uuid parse <uuid>`로 버전·변형·타임스탬프 분석 | 높음 |
|
||||
| L10-3 | **SSL 인증서 체커** ✅ | `cert` 프리픽스. 도메인/IP의 TLS 인증서 조회(443 기본, 포트 지정 가능). 만료일·D-day·발급 대상·발급 기관·SANs·지문 표시. 사내 모드에서는 내부 호스트(192.168.x, 10.x, 172.16-31.x)만 허용. Enter → 결과 클립보드 복사 | 중간 |
|
||||
| L10-4 | **Lorem Ipsum 생성기** ✅ | `lorem` 프리픽스. `lorem 3`으로 3단락 생성. `lorem words 20` 단어 N개. `lorem sentences 5` 문장 N개. `lorem ko` 한국어 더미 텍스트. `lorem email 5` 더미 이메일 주소. `lorem name 5` 한국어 더미 이름. Enter → 클립보드 복사 | 중간 |
|
||||
|
||||
@@ -227,6 +227,16 @@ public partial class App : System.Windows.Application
|
||||
// L9-4: 진수 변환기 (prefix=base)
|
||||
commandResolver.RegisterHandler(new BaseConvertHandler());
|
||||
|
||||
// ─── Phase L10 핸들러 ─────────────────────────────────────────────────
|
||||
// L10-1: XML 포맷터·검증기·XPath (prefix=xml)
|
||||
commandResolver.RegisterHandler(new XmlHandler());
|
||||
// L10-2: UUID/GUID 생성기 (prefix=uuid)
|
||||
commandResolver.RegisterHandler(new UuidHandler());
|
||||
// L10-3: SSL 인증서 체커 (prefix=cert)
|
||||
commandResolver.RegisterHandler(new CertHandler());
|
||||
// L10-4: Lorem Ipsum 더미 텍스트 (prefix=lorem)
|
||||
commandResolver.RegisterHandler(new LoremHandler());
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
pluginHost.LoadAll();
|
||||
|
||||
253
src/AxCopilot/Handlers/CertHandler.cs
Normal file
253
src/AxCopilot/Handlers/CertHandler.cs
Normal file
@@ -0,0 +1,253 @@
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L10-3: SSL/TLS 인증서 체커 핸들러. "cert" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: cert google.com → google.com의 인증서 정보 조회
|
||||
/// cert github.com 443 → 포트 지정
|
||||
/// cert https://example.com → URL 형식도 지원
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
///
|
||||
/// ⚠ 외부 인터넷 접속 필요. 사내 모드에서는 내부 호스트만 조회 가능.
|
||||
/// </summary>
|
||||
public class CertHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "cert";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Cert",
|
||||
"SSL/TLS 인증서 체커 — 만료일 · 발급자 · SANs",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
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(
|
||||
"SSL 인증서 체커",
|
||||
"예: cert google.com / cert 192.168.1.1 / cert example.com 8443",
|
||||
null, null, Symbol: "\uE72E"));
|
||||
items.Add(new LauncherItem("cert google.com", "google.com 인증서 조회", null, null, Symbol: "\uE72E"));
|
||||
items.Add(new LauncherItem("cert github.com", "github.com 인증서 조회", null, null, Symbol: "\uE72E"));
|
||||
items.Add(new LauncherItem("cert 192.168.1.1", "내부 서버 인증서 조회", null, null, Symbol: "\uE72E"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 비동기 조회 시작 — 빠른 반환 후 결과를 기다리지 않음
|
||||
// 실제 조회는 ExecuteAsync에서 처리하며, 여기서는 "조회 중" 항목만 반환
|
||||
var (host, port) = ParseHostPort(q);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(host))
|
||||
{
|
||||
items.Add(new LauncherItem("형식 오류", "예: cert domain.com 또는 cert domain.com 443", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 사내 모드 확인
|
||||
var settings = (System.Windows.Application.Current as App)?.SettingsService?.Settings;
|
||||
var isInternal = settings?.InternalModeEnabled ?? true;
|
||||
if (isInternal && !IsInternalHost(host))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
"사내 모드 제한",
|
||||
$"'{host}'은 외부 호스트입니다. 설정에서 사외 모드를 활성화하세요.",
|
||||
null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"{host}:{port} 인증서 조회",
|
||||
"Enter를 눌러 조회하세요",
|
||||
null,
|
||||
("check", $"{host}:{port}"),
|
||||
Symbol: "\uE72E"));
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public async 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("Cert", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.Data is not ("check", string target)) return;
|
||||
|
||||
var parts = target.Split(':');
|
||||
var host = parts[0];
|
||||
var port = parts.Length > 1 && int.TryParse(parts[1], out var p) ? p : 443;
|
||||
|
||||
NotificationService.Notify("Cert", $"{host}:{port} 인증서 조회 중…");
|
||||
|
||||
try
|
||||
{
|
||||
var certInfo = await FetchCertInfoAsync(host, port, ct);
|
||||
var summary = BuildSummary(certInfo);
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||
() => Clipboard.SetText(summary));
|
||||
NotificationService.Notify("Cert", certInfo.StatusLine);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
NotificationService.Notify("Cert", "조회가 취소되었습니다.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NotificationService.Notify("Cert", $"오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── 인증서 조회 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static async Task<CertInfo> FetchCertInfoAsync(string host, int port, CancellationToken ct)
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
await client.ConnectAsync(host, port, ct);
|
||||
|
||||
using var sslStream = new SslStream(
|
||||
client.GetStream(),
|
||||
leaveInnerStreamOpen: false,
|
||||
userCertificateValidationCallback: (_, cert, _, _) => true); // 만료 인증서도 정보 확인
|
||||
|
||||
await sslStream.AuthenticateAsClientAsync(
|
||||
new SslClientAuthenticationOptions
|
||||
{
|
||||
TargetHost = host,
|
||||
RemoteCertificateValidationCallback = (_, _, _, _) => true,
|
||||
}, ct);
|
||||
|
||||
var cert = sslStream.RemoteCertificate as X509Certificate2
|
||||
?? new X509Certificate2(sslStream.RemoteCertificate!);
|
||||
|
||||
return BuildCertInfo(host, port, cert);
|
||||
}
|
||||
|
||||
private static CertInfo BuildCertInfo(string host, int port, X509Certificate2 cert)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var notAfter = cert.NotAfter.ToUniversalTime();
|
||||
var daysLeft = (int)(notAfter - now).TotalDays;
|
||||
var subject = cert.Subject;
|
||||
var issuer = cert.Issuer;
|
||||
|
||||
// SANs (Subject Alternative Names)
|
||||
var sans = new List<string>();
|
||||
foreach (var ext in cert.Extensions)
|
||||
{
|
||||
if (ext.Oid?.Value == "2.5.29.17") // SAN OID
|
||||
{
|
||||
var raw = ext.Format(false);
|
||||
sans.AddRange(raw.Split(new[] { ", ", ",\r\n" }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(s => s.Trim())
|
||||
.Where(s => s.StartsWith("DNS Name=", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(s => s[9..]));
|
||||
}
|
||||
}
|
||||
|
||||
var status = daysLeft > 30 ? "유효"
|
||||
: daysLeft > 0 ? "만료 임박"
|
||||
: "만료됨";
|
||||
|
||||
return new CertInfo
|
||||
{
|
||||
Host = host,
|
||||
Port = port,
|
||||
Subject = subject,
|
||||
Issuer = issuer,
|
||||
NotBefore = cert.NotBefore,
|
||||
NotAfter = cert.NotAfter,
|
||||
DaysLeft = daysLeft,
|
||||
Sans = sans,
|
||||
Thumbprint = cert.Thumbprint,
|
||||
Status = status,
|
||||
StatusLine = $"{status} · D-{(daysLeft > 0 ? daysLeft.ToString() : "만료")} · {host}:{port}",
|
||||
};
|
||||
}
|
||||
|
||||
private static string BuildSummary(CertInfo c)
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
sb.AppendLine($"호스트: {c.Host}:{c.Port}");
|
||||
sb.AppendLine($"상태: {c.Status} (만료까지 {c.DaysLeft}일)");
|
||||
sb.AppendLine($"발급 대상: {c.Subject}");
|
||||
sb.AppendLine($"발급 기관: {c.Issuer}");
|
||||
sb.AppendLine($"유효 시작: {c.NotBefore:yyyy-MM-dd HH:mm:ss}");
|
||||
sb.AppendLine($"만료 일자: {c.NotAfter:yyyy-MM-dd HH:mm:ss}");
|
||||
if (c.Sans.Count > 0)
|
||||
sb.AppendLine($"SANs: {string.Join(", ", c.Sans.Take(10))}");
|
||||
sb.AppendLine($"지문(SHA1): {c.Thumbprint}");
|
||||
return sb.ToString().TrimEnd();
|
||||
}
|
||||
|
||||
// ── 파싱 헬퍼 ────────────────────────────────────────────────────────────
|
||||
|
||||
private static (string Host, int Port) ParseHostPort(string q)
|
||||
{
|
||||
// https:// 또는 http:// 제거
|
||||
if (q.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
q = q[8..];
|
||||
else if (q.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
|
||||
q = q[7..];
|
||||
|
||||
// 경로 제거
|
||||
var slashIdx = q.IndexOf('/');
|
||||
if (slashIdx >= 0) q = q[..slashIdx];
|
||||
|
||||
var colonIdx = q.LastIndexOf(':');
|
||||
if (colonIdx >= 0 && int.TryParse(q[(colonIdx + 1)..], out var port))
|
||||
return (q[..colonIdx], port);
|
||||
|
||||
return (q, 443);
|
||||
}
|
||||
|
||||
private static bool IsInternalHost(string host)
|
||||
{
|
||||
if (host is "localhost" or "127.0.0.1") return true;
|
||||
if (host.StartsWith("192.168.")) return true;
|
||||
if (host.StartsWith("10.")) return true;
|
||||
if (host.StartsWith("172.16.") || host.StartsWith("172.17.") ||
|
||||
host.StartsWith("172.18.") || host.StartsWith("172.19.") ||
|
||||
host.StartsWith("172.20.") || host.StartsWith("172.21.") ||
|
||||
host.StartsWith("172.22.") || host.StartsWith("172.23.") ||
|
||||
host.StartsWith("172.24.") || host.StartsWith("172.25.") ||
|
||||
host.StartsWith("172.26.") || host.StartsWith("172.27.") ||
|
||||
host.StartsWith("172.28.") || host.StartsWith("172.29.") ||
|
||||
host.StartsWith("172.30.") || host.StartsWith("172.31.")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private record CertInfo
|
||||
{
|
||||
public string Host { get; init; } = "";
|
||||
public int Port { get; init; }
|
||||
public string Subject { get; init; } = "";
|
||||
public string Issuer { get; init; } = "";
|
||||
public DateTime NotBefore { get; init; }
|
||||
public DateTime NotAfter { get; init; }
|
||||
public int DaysLeft { get; init; }
|
||||
public List<string> Sans { get; init; } = new();
|
||||
public string Thumbprint { get; init; } = "";
|
||||
public string Status { get; init; } = "";
|
||||
public string StatusLine { get; init; } = "";
|
||||
}
|
||||
}
|
||||
284
src/AxCopilot/Handlers/LoremHandler.cs
Normal file
284
src/AxCopilot/Handlers/LoremHandler.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L10-4: Lorem Ipsum / 더미 텍스트 생성기 핸들러. "lorem" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: lorem → 1단락 생성 (기본)
|
||||
/// lorem 3 → 3단락 생성
|
||||
/// lorem words 20 → 단어 20개
|
||||
/// lorem sentences 5 → 문장 5개
|
||||
/// lorem ko → 한국어 더미 텍스트 1단락
|
||||
/// lorem ko 3 → 한국어 더미 텍스트 3단락
|
||||
/// lorem email 5 → 더미 이메일 주소 5개
|
||||
/// lorem name 5 → 더미 이름 5개 (한국어)
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class LoremHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "lorem";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Lorem",
|
||||
"더미 텍스트 생성기 — Lorem Ipsum · 한국어 · 이메일 · 이름",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// ── Lorem Ipsum 단어 풀 ─────────────────────────────────────────────────
|
||||
private static readonly string[] LoremWords =
|
||||
[
|
||||
"lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipiscing", "elit",
|
||||
"sed", "do", "eiusmod", "tempor", "incididunt", "ut", "labore", "et", "dolore",
|
||||
"magna", "aliqua", "enim", "ad", "minim", "veniam", "quis", "nostrud",
|
||||
"exercitation", "ullamco", "laboris", "nisi", "aliquip", "ex", "ea", "commodo",
|
||||
"consequat", "duis", "aute", "irure", "in", "reprehenderit", "voluptate",
|
||||
"velit", "esse", "cillum", "eu", "fugiat", "nulla", "pariatur", "excepteur",
|
||||
"sint", "occaecat", "cupidatat", "non", "proident", "sunt", "culpa", "qui",
|
||||
"officia", "deserunt", "mollit", "anim", "id", "est", "laborum", "perspiciatis",
|
||||
"unde", "omnis", "iste", "natus", "error", "voluptatem", "accusantium",
|
||||
"doloremque", "laudantium", "totam", "rem", "aperiam", "eaque", "ipsa", "quae",
|
||||
"ab", "illo", "inventore", "veritatis", "quasi", "architecto", "beatae", "vitae",
|
||||
"dicta", "explicabo", "nemo", "ipsam", "quia", "voluptas", "aspernatur", "aut",
|
||||
"odit", "fugit", "consequuntur", "magni", "dolores", "ratione", "sequi",
|
||||
"nesciunt", "neque", "porro", "quisquam", "dolorem", "numquam", "eius", "modi",
|
||||
"temporibus", "incidunt", "magnam", "aliquam", "quaerat", "minima", "nostrum",
|
||||
"exercitationem", "ullam", "corporis", "suscipit", "laboriosam", "nisi",
|
||||
"aliquid", "commodi", "consequatur", "quidem", "rerum", "facilis",
|
||||
];
|
||||
|
||||
// ── 한국어 더미 단어 풀 ──────────────────────────────────────────────────
|
||||
private static readonly string[] KorWords =
|
||||
[
|
||||
"가나다라", "마바사아", "자차카타", "파하", "데이터", "처리", "시스템", "네트워크",
|
||||
"소프트웨어", "알고리즘", "데이터베이스", "인터페이스", "프레임워크", "모듈", "클래스",
|
||||
"메서드", "함수", "변수", "구조체", "배열", "목록", "사전", "집합", "스택", "큐",
|
||||
"트리", "그래프", "정렬", "탐색", "분석", "설계", "구현", "테스트", "배포", "운영",
|
||||
"서비스", "플랫폼", "클라우드", "컨테이너", "가상화", "보안", "암호화", "인증", "권한",
|
||||
"로그", "모니터링", "알림", "이벤트", "트랜잭션", "세션", "쿠키", "토큰", "키",
|
||||
"값", "객체", "인스턴스", "프로세스", "스레드", "비동기", "병렬", "동기화", "잠금",
|
||||
"버퍼", "캐시", "인덱스", "쿼리", "뷰", "프로시저", "스키마", "테이블", "컬럼",
|
||||
"행", "열", "기본키", "외래키", "조인", "집계", "필터", "정렬", "그룹화", "분류",
|
||||
];
|
||||
|
||||
private static readonly string[] KorSentenceStarters =
|
||||
[
|
||||
"이 시스템은", "해당 모듈은", "기능을 구현하면", "데이터를 처리하는",
|
||||
"네트워크 연결이", "서비스가 시작되면", "사용자 인터페이스는", "알고리즘이",
|
||||
"처리 과정에서", "설계 단계에서", "구현 방식은", "테스트 결과",
|
||||
];
|
||||
|
||||
private static readonly string[] KorSentenceEnders =
|
||||
[
|
||||
"처리됩니다.", "구현되어 있습니다.", "필요합니다.", "중요한 역할을 합니다.",
|
||||
"확인할 수 있습니다.", "설계되어 있습니다.", "활용됩니다.", "반환됩니다.",
|
||||
"저장됩니다.", "업데이트됩니다.", "삭제됩니다.", "초기화됩니다.",
|
||||
];
|
||||
|
||||
// ── 더미 이름/이메일 데이터 ───────────────────────────────────────────────
|
||||
private static readonly string[] KorLastNames = ["김", "이", "박", "최", "정", "강", "조", "윤", "장", "임", "한", "오", "서", "신", "권", "황", "안", "송", "류", "전"];
|
||||
private static readonly string[] KorFirstNames = ["민준", "서연", "예준", "서현", "도윤", "지우", "시우", "수아", "지호", "하은", "준서", "하린", "건우", "소연", "현우", "지민", "우진", "지유", "연우", "채원"];
|
||||
private static readonly string[] EmailDomains = ["example.com", "test.co.kr", "dummy.net", "sample.org", "mock.io", "placeholder.dev"];
|
||||
|
||||
private static readonly Random Rng = new();
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
var para = GenerateParagraph(false);
|
||||
items.Add(new LauncherItem(
|
||||
"Lorem Ipsum 1단락",
|
||||
para.Length > 80 ? para[..80] + "…" : para,
|
||||
null,
|
||||
("copy", para),
|
||||
Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem 3", "3단락 생성", null, null, Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem words 20", "단어 20개", null, null, Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem sentences 3", "문장 3개", null, null, Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem ko", "한국어 더미 텍스트", null, null, Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem email 5", "더미 이메일 5개", null, null, Symbol: "\uE8BD"));
|
||||
items.Add(new LauncherItem("lorem name 5", "더미 이름 5개", null, null, Symbol: "\uE8BD"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case "words":
|
||||
case "word":
|
||||
case "w":
|
||||
{
|
||||
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 500) : 20;
|
||||
var text = GenerateWords(cnt, false);
|
||||
items.Add(new LauncherItem(
|
||||
$"단어 {cnt}개",
|
||||
text.Length > 80 ? text[..80] + "…" : text,
|
||||
null, ("copy", text), Symbol: "\uE8BD"));
|
||||
break;
|
||||
}
|
||||
|
||||
case "sentences":
|
||||
case "sentence":
|
||||
case "s":
|
||||
{
|
||||
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 50) : 5;
|
||||
var text = GenerateSentences(cnt, false);
|
||||
items.Add(new LauncherItem(
|
||||
$"문장 {cnt}개",
|
||||
text.Length > 80 ? text[..80] + "…" : text,
|
||||
null, ("copy", text), Symbol: "\uE8BD"));
|
||||
break;
|
||||
}
|
||||
|
||||
case "ko":
|
||||
case "kor":
|
||||
case "korean":
|
||||
{
|
||||
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 10) : 1;
|
||||
var text = GenerateParagraphs(cnt, true);
|
||||
items.Add(new LauncherItem(
|
||||
$"한국어 더미 텍스트 {cnt}단락",
|
||||
text.Length > 80 ? text[..80] + "…" : text,
|
||||
null, ("copy", text), Symbol: "\uE8BD"));
|
||||
break;
|
||||
}
|
||||
|
||||
case "email":
|
||||
{
|
||||
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 20) : 5;
|
||||
var emails = Enumerable.Range(0, cnt).Select(_ => GenerateEmail()).ToList();
|
||||
var all = string.Join("\n", emails);
|
||||
items.Add(new LauncherItem(
|
||||
$"더미 이메일 {cnt}개",
|
||||
"전체 복사: Enter",
|
||||
null, ("copy", all), Symbol: "\uE8BD"));
|
||||
foreach (var email in emails)
|
||||
items.Add(new LauncherItem(email, "Enter 복사", null, ("copy", email), Symbol: "\uE8BD"));
|
||||
break;
|
||||
}
|
||||
|
||||
case "name":
|
||||
case "names":
|
||||
{
|
||||
var cnt = parts.Length > 1 && int.TryParse(parts[1], out var n) ? Math.Clamp(n, 1, 20) : 5;
|
||||
var names = Enumerable.Range(0, cnt).Select(_ => GenerateKorName()).ToList();
|
||||
var all = string.Join("\n", names);
|
||||
items.Add(new LauncherItem(
|
||||
$"더미 이름 {cnt}개",
|
||||
"전체 복사: Enter",
|
||||
null, ("copy", all), Symbol: "\uE8BD"));
|
||||
foreach (var name in names)
|
||||
items.Add(new LauncherItem(name, "한국어 이름 · Enter 복사", null, ("copy", name), Symbol: "\uE8BD"));
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
// 숫자 단독 → 단락 수
|
||||
var cnt = int.TryParse(sub, out var n) ? Math.Clamp(n, 1, 10) : 1;
|
||||
var text = GenerateParagraphs(cnt, false);
|
||||
items.Add(new LauncherItem(
|
||||
$"Lorem Ipsum {cnt}단락",
|
||||
text.Length > 80 ? text[..80] + "…" : text,
|
||||
null, ("copy", text), Symbol: "\uE8BD"));
|
||||
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("Lorem", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 생성 헬퍼 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static string GenerateParagraphs(int count, bool korean)
|
||||
{
|
||||
var paras = Enumerable.Range(0, count).Select(_ => GenerateParagraph(korean));
|
||||
return string.Join("\n\n", paras);
|
||||
}
|
||||
|
||||
private static string GenerateParagraph(bool korean)
|
||||
{
|
||||
var sentenceCount = Rng.Next(4, 8);
|
||||
return GenerateSentences(sentenceCount, korean);
|
||||
}
|
||||
|
||||
private static string GenerateSentences(int count, bool korean)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(' ');
|
||||
sb.Append(GenerateSentence(korean));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string GenerateSentence(bool korean)
|
||||
{
|
||||
if (korean)
|
||||
{
|
||||
var starter = KorSentenceStarters[Rng.Next(KorSentenceStarters.Length)];
|
||||
var wordCnt = Rng.Next(3, 8);
|
||||
var words = Enumerable.Range(0, wordCnt).Select(_ => KorWords[Rng.Next(KorWords.Length)]);
|
||||
var ender = KorSentenceEnders[Rng.Next(KorSentenceEnders.Length)];
|
||||
return $"{starter} {string.Join(" ", words)} {ender}";
|
||||
}
|
||||
else
|
||||
{
|
||||
var wordCnt = Rng.Next(6, 15);
|
||||
var words = Enumerable.Range(0, wordCnt).Select((_, idx) =>
|
||||
{
|
||||
var w = LoremWords[Rng.Next(LoremWords.Length)];
|
||||
return idx == 0 ? char.ToUpper(w[0]) + w[1..] : w;
|
||||
});
|
||||
return string.Join(" ", words) + ".";
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateWords(int count, bool korean)
|
||||
{
|
||||
var pool = korean ? KorWords : LoremWords;
|
||||
return string.Join(" ", Enumerable.Range(0, count).Select(_ => pool[Rng.Next(pool.Length)]));
|
||||
}
|
||||
|
||||
private static string GenerateEmail()
|
||||
{
|
||||
var first = LoremWords[Rng.Next(LoremWords.Length)];
|
||||
var second = LoremWords[Rng.Next(LoremWords.Length)];
|
||||
var num = Rng.Next(10, 999);
|
||||
var domain = EmailDomains[Rng.Next(EmailDomains.Length)];
|
||||
return $"{first}.{second}{num}@{domain}";
|
||||
}
|
||||
|
||||
private static string GenerateKorName()
|
||||
{
|
||||
var last = KorLastNames[Rng.Next(KorLastNames.Length)];
|
||||
var first = KorFirstNames[Rng.Next(KorFirstNames.Length)];
|
||||
return last + first;
|
||||
}
|
||||
}
|
||||
300
src/AxCopilot/Handlers/UuidHandler.cs
Normal file
300
src/AxCopilot/Handlers/UuidHandler.cs
Normal file
@@ -0,0 +1,300 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L10-2: UUID/GUID 생성기 핸들러. "uuid" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: uuid → UUID v4 1개 생성
|
||||
/// uuid 5 → UUID v4 5개 생성
|
||||
/// uuid upper → 대문자 UUID 생성
|
||||
/// uuid v4 → UUID v4 (랜덤)
|
||||
/// uuid seq → 순차 UUID (시간 기반, 정렬 가능)
|
||||
/// uuid short → 짧은 고유 ID (8자리 hex)
|
||||
/// uuid nil → Nil UUID (00000000-…)
|
||||
/// uuid parse <uuid> → UUID 분석 (버전, 타임스탬프 등)
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class UuidHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "uuid";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"UUID",
|
||||
"UUID/GUID 생성기 — v4 · 순차 · 짧은 ID · 분석",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
// 기본: v4 1개 생성
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
items.Add(new LauncherItem(
|
||||
uuid,
|
||||
"UUID v4 (랜덤) · Enter 복사",
|
||||
null,
|
||||
("copy", uuid),
|
||||
Symbol: "\uF0E2"));
|
||||
|
||||
items.Add(new LauncherItem("uuid 5", "5개 생성", null, null, Symbol: "\uF0E2"));
|
||||
items.Add(new LauncherItem("uuid upper", "대문자 UUID", null, null, Symbol: "\uF0E2"));
|
||||
items.Add(new LauncherItem("uuid seq", "순차 UUID (정렬가능)", null, null, Symbol: "\uF0E2"));
|
||||
items.Add(new LauncherItem("uuid short", "짧은 ID (8자리)", null, null, Symbol: "\uF0E2"));
|
||||
items.Add(new LauncherItem("uuid parse …", "UUID 분석", null, null, Symbol: "\uF0E2"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
// "uuid parse <value>"
|
||||
if (sub == "parse" && parts.Length >= 2)
|
||||
{
|
||||
items.AddRange(ParseUuid(string.Join(" ", parts.Skip(1))));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid nil"
|
||||
if (sub == "nil")
|
||||
{
|
||||
var nil = "00000000-0000-0000-0000-000000000000";
|
||||
items.Add(new LauncherItem(nil, "Nil UUID", null, ("copy", nil), Symbol: "\uF0E2"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid seq"
|
||||
if (sub == "seq")
|
||||
{
|
||||
items.AddRange(GenerateSequential(5));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid short"
|
||||
if (sub == "short")
|
||||
{
|
||||
items.AddRange(GenerateShort(5));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid upper"
|
||||
if (sub == "upper")
|
||||
{
|
||||
var upper = Guid.NewGuid().ToString().ToUpperInvariant();
|
||||
items.Add(new LauncherItem(upper, "대문자 UUID v4 · Enter 복사", null, ("copy", upper), Symbol: "\uF0E2"));
|
||||
for (var i = 0; i < 4; i++)
|
||||
{
|
||||
var u = Guid.NewGuid().ToString().ToUpperInvariant();
|
||||
items.Add(new LauncherItem(u, "대문자 UUID v4", null, ("copy", u), Symbol: "\uF0E2"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid v4"
|
||||
if (sub == "v4")
|
||||
{
|
||||
items.AddRange(GenerateV4(5));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "uuid <숫자>" — N개 생성
|
||||
if (int.TryParse(sub, out var count))
|
||||
{
|
||||
count = Math.Clamp(count, 1, 20);
|
||||
items.AddRange(GenerateV4(count));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// UUID 자체를 입력한 경우 → parse
|
||||
if (Guid.TryParse(q, out _))
|
||||
{
|
||||
items.AddRange(ParseUuid(q));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.Add(new LauncherItem("알 수 없는 명령",
|
||||
"예: uuid / uuid 5 / uuid upper / uuid seq / uuid short / uuid parse",
|
||||
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("UUID", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 생성 헬퍼 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> GenerateV4(int count)
|
||||
{
|
||||
var all = new List<string>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var uuid = Guid.NewGuid().ToString();
|
||||
all.Add(uuid);
|
||||
}
|
||||
|
||||
if (count > 1)
|
||||
{
|
||||
yield return new LauncherItem(
|
||||
$"UUID v4 {count}개",
|
||||
"전체 복사: Enter",
|
||||
null,
|
||||
("copy", string.Join("\n", all)),
|
||||
Symbol: "\uF0E2");
|
||||
}
|
||||
|
||||
foreach (var uuid in all)
|
||||
yield return new LauncherItem(uuid, "UUID v4 · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 시간 기반 순차 UUID (UUIDv7 스타일: 밀리초 타임스탬프 + 랜덤 비트).
|
||||
/// 정렬 가능하고 시간 정보 포함.
|
||||
/// </summary>
|
||||
private static IEnumerable<LauncherItem> GenerateSequential(int count)
|
||||
{
|
||||
var all = new List<string>();
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (i > 0) System.Threading.Thread.Sleep(1); // 밀리초 차이 보장
|
||||
var uuid = NewSequentialGuid();
|
||||
all.Add(uuid);
|
||||
}
|
||||
|
||||
yield return new LauncherItem(
|
||||
$"순차 UUID {count}개",
|
||||
"시간 기반 정렬 가능 · 전체 복사: Enter",
|
||||
null,
|
||||
("copy", string.Join("\n", all)),
|
||||
Symbol: "\uF0E2");
|
||||
|
||||
foreach (var uuid in all)
|
||||
yield return new LauncherItem(uuid, "순차 UUID · Enter 복사", null, ("copy", uuid), Symbol: "\uF0E2");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> GenerateShort(int count)
|
||||
{
|
||||
var all = new List<string>();
|
||||
for (var i = 0; i < count; i++)
|
||||
all.Add(NewShortId());
|
||||
|
||||
yield return new LauncherItem(
|
||||
$"짧은 ID {count}개",
|
||||
"8자리 hex · 전체 복사: Enter",
|
||||
null,
|
||||
("copy", string.Join("\n", all)),
|
||||
Symbol: "\uF0E2");
|
||||
|
||||
foreach (var id in all)
|
||||
yield return new LauncherItem(id, "짧은 ID (8자리) · Enter 복사", null, ("copy", id), Symbol: "\uF0E2");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> ParseUuid(string raw)
|
||||
{
|
||||
raw = raw.Trim();
|
||||
if (!Guid.TryParse(raw, out var guid))
|
||||
{
|
||||
yield return new LauncherItem("UUID 파싱 실패", $"'{raw}'은 유효한 UUID가 아닙니다", null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var bytes = guid.ToByteArray();
|
||||
var version = (bytes[7] >> 4) & 0x0F;
|
||||
var variant = (bytes[8] >> 6) & 0x03;
|
||||
|
||||
yield return new LauncherItem(
|
||||
guid.ToString(),
|
||||
$"버전 {version} · 변형 {variant}",
|
||||
null,
|
||||
("copy", guid.ToString()),
|
||||
Symbol: "\uF0E2");
|
||||
|
||||
yield return new LauncherItem("소문자", guid.ToString(), null, ("copy", guid.ToString()), Symbol: "\uF0E2");
|
||||
yield return new LauncherItem("대문자", guid.ToString().ToUpper(), null, ("copy", guid.ToString().ToUpper()), Symbol: "\uF0E2");
|
||||
yield return new LauncherItem("중괄호", $"{{{guid}}}", null, ("copy", $"{{{guid}}}"), Symbol: "\uF0E2");
|
||||
yield return new LauncherItem("대시 없음", guid.ToString("N"), null, ("copy", guid.ToString("N")), Symbol: "\uF0E2");
|
||||
yield return new LauncherItem("버전", $"UUID v{version}", null, null, Symbol: "\uF0E2");
|
||||
yield return new LauncherItem("변형", variant == 2 ? "RFC 4122" : variant == 3 ? "Microsoft" : $"변형 {variant}", null, null, Symbol: "\uF0E2");
|
||||
|
||||
// 시간 기반 버전 (v1)이면 타임스탬프 복원 시도
|
||||
if (version == 1)
|
||||
{
|
||||
var ts = ExtractV1Timestamp(bytes);
|
||||
if (ts.HasValue)
|
||||
yield return new LauncherItem("타임스탬프", ts.Value.ToString("yyyy-MM-dd HH:mm:ss.fff UTC"), null, null, Symbol: "\uF0E2");
|
||||
}
|
||||
}
|
||||
|
||||
// ── UUID 생성 구현 ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>UUIDv7 스타일 순차 GUID: 상위 48비트 = Unix ms 타임스탬프, 하위 = 랜덤</summary>
|
||||
private static string NewSequentialGuid()
|
||||
{
|
||||
var ms = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
var rand = RandomNumberGenerator.GetBytes(10);
|
||||
var bytes = new byte[16];
|
||||
|
||||
// 상위 6바이트 = 타임스탬프 (big-endian ms)
|
||||
bytes[0] = (byte)(ms >> 40);
|
||||
bytes[1] = (byte)(ms >> 32);
|
||||
bytes[2] = (byte)(ms >> 24);
|
||||
bytes[3] = (byte)(ms >> 16);
|
||||
bytes[4] = (byte)(ms >> 8);
|
||||
bytes[5] = (byte)(ms);
|
||||
|
||||
// 버전 비트 (v7 = 0111)
|
||||
bytes[6] = (byte)((rand[0] & 0x0F) | 0x70);
|
||||
bytes[7] = rand[1];
|
||||
|
||||
// 변형 비트 (RFC 4122 = 10xx)
|
||||
bytes[8] = (byte)((rand[2] & 0x3F) | 0x80);
|
||||
bytes[9] = rand[3];
|
||||
|
||||
// 나머지 랜덤
|
||||
Array.Copy(rand, 4, bytes, 10, 6);
|
||||
|
||||
return new Guid(bytes).ToString();
|
||||
}
|
||||
|
||||
private static string NewShortId()
|
||||
{
|
||||
var bytes = RandomNumberGenerator.GetBytes(4);
|
||||
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static DateTime? ExtractV1Timestamp(byte[] bytes)
|
||||
{
|
||||
try
|
||||
{
|
||||
// UUID v1: time_low(4) + time_mid(2) + time_hi_version(2)
|
||||
var timeLow = (long)((uint)((bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]));
|
||||
var timeMid = (long)((ushort)((bytes[5] << 8) | bytes[4]));
|
||||
var timeHigh = (long)((ushort)((bytes[7] << 8) | bytes[6]) & 0x0FFF);
|
||||
var ticks = (timeHigh << 48) | (timeMid << 32) | timeLow;
|
||||
// UUID epoch = Oct 15, 1582
|
||||
var uuidEpoch = new DateTime(1582, 10, 15, 0, 0, 0, DateTimeKind.Utc);
|
||||
return uuidEpoch.AddTicks(ticks);
|
||||
}
|
||||
catch { return null; }
|
||||
}
|
||||
}
|
||||
345
src/AxCopilot/Handlers/XmlHandler.cs
Normal file
345
src/AxCopilot/Handlers/XmlHandler.cs
Normal file
@@ -0,0 +1,345 @@
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using System.Xml;
|
||||
using System.Xml.XPath;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L10-1: XML 포맷터·검증기·XPath 쿼리 핸들러. "xml" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: xml → 클립보드의 XML 자동 포맷
|
||||
/// xml <root><a>1</a> → 인라인 XML 포맷
|
||||
/// xml compact → 클립보드 XML 압축 (공백 제거)
|
||||
/// xml xpath //a → 클립보드 XML에 XPath 쿼리
|
||||
/// xml validate → XML 유효성 검증
|
||||
/// xml attr → XML 속성 목록 추출
|
||||
/// xml minify → compact 와 동일
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class XmlHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "xml";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"XML",
|
||||
"XML 포맷터 · 압축 · 검증 · XPath 쿼리",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
var clip = GetClipboard();
|
||||
if (!string.IsNullOrWhiteSpace(clip) && clip.TrimStart().StartsWith('<'))
|
||||
{
|
||||
// 클립보드에 XML이 있으면 즉시 포맷 미리보기
|
||||
items.AddRange(BuildXmlItems(clip, "클립보드"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("XML 도구",
|
||||
"예: xml <root>… / xml compact / xml xpath //path / xml validate",
|
||||
null, null, Symbol: "\uE943"));
|
||||
items.Add(new LauncherItem("xml compact", "XML 압축 (공백 제거)", null, null, Symbol: "\uE943"));
|
||||
items.Add(new LauncherItem("xml validate", "XML 유효성 검증", null, null, Symbol: "\uE943"));
|
||||
items.Add(new LauncherItem("xml xpath //", "XPath 쿼리", null, null, Symbol: "\uE943"));
|
||||
items.Add(new LauncherItem("xml attr", "속성 목록 추출", null, null, Symbol: "\uE943"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case "compact":
|
||||
case "minify":
|
||||
{
|
||||
var src = GetClipboard();
|
||||
if (TryMinify(src, out var mini))
|
||||
items.Add(new LauncherItem(mini.Length > 80 ? mini[..80] + "…" : mini,
|
||||
$"압축됨 {src.Length} → {mini.Length} 글자 · Enter 복사",
|
||||
null, ("copy", mini), Symbol: "\uE943"));
|
||||
else
|
||||
items.Add(new LauncherItem("XML 파싱 오류", "클립보드에 유효한 XML이 없습니다", null, null, Symbol: "\uE783"));
|
||||
break;
|
||||
}
|
||||
case "validate":
|
||||
{
|
||||
var src = GetClipboard();
|
||||
if (string.IsNullOrWhiteSpace(src))
|
||||
{
|
||||
items.Add(new LauncherItem("클립보드 비어 있음", "XML을 클립보드에 복사 후 시도하세요", null, null, Symbol: "\uE783"));
|
||||
break;
|
||||
}
|
||||
var (ok, err) = ValidateXml(src);
|
||||
items.Add(ok
|
||||
? new LauncherItem("✔ 유효한 XML", $"{src.Length:N0}자 · 잘 형식화된 XML입니다", null, null, Symbol: "\uE73E")
|
||||
: new LauncherItem("✘ XML 오류", err ?? "알 수 없는 오류", null, null, Symbol: "\uE783"));
|
||||
break;
|
||||
}
|
||||
case "xpath":
|
||||
{
|
||||
var xpathExpr = parts.Length > 1 ? parts[1].Trim() : "";
|
||||
if (string.IsNullOrWhiteSpace(xpathExpr))
|
||||
{
|
||||
items.Add(new LauncherItem("XPath 식을 입력하세요", "예: xml xpath //item/title", null, null, Symbol: "\uE783"));
|
||||
break;
|
||||
}
|
||||
var src = GetClipboard();
|
||||
items.AddRange(RunXPath(src, xpathExpr));
|
||||
break;
|
||||
}
|
||||
case "attr":
|
||||
{
|
||||
var src = GetClipboard();
|
||||
items.AddRange(ExtractAttributes(src));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
// 인라인 XML 입력 또는 전체 포맷
|
||||
var xmlSrc = q.TrimStart().StartsWith('<') ? q : GetClipboard();
|
||||
items.AddRange(BuildXmlItems(xmlSrc, q.TrimStart().StartsWith('<') ? "입력" : "클립보드"));
|
||||
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("XML", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 빌더 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildXmlItems(string xml, string source)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(xml))
|
||||
{
|
||||
yield return new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (TryFormat(xml, out var formatted))
|
||||
{
|
||||
var preview = formatted.Length > 100 ? formatted[..100].Replace("\n", " ") + "…" : formatted.Replace("\n", " ");
|
||||
yield return new LauncherItem(
|
||||
$"포맷된 XML ({source})",
|
||||
preview,
|
||||
null,
|
||||
("copy", formatted),
|
||||
Symbol: "\uE943");
|
||||
|
||||
// 루트 요소 이름
|
||||
if (TryGetRootElement(xml, out var root))
|
||||
yield return new LauncherItem("루트 요소", $"<{root}>", null, ("copy", root), Symbol: "\uE943");
|
||||
|
||||
// 요소 수
|
||||
var elemCount = CountElements(xml);
|
||||
yield return new LauncherItem("요소 수", $"{elemCount:N0}개 요소", null, null, Symbol: "\uE943");
|
||||
|
||||
// 압축 버전
|
||||
if (TryMinify(xml, out var mini))
|
||||
yield return new LauncherItem(
|
||||
"압축 XML",
|
||||
$"{xml.Length:N0} → {mini.Length:N0} 글자",
|
||||
null,
|
||||
("copy", mini),
|
||||
Symbol: "\uE943");
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return new LauncherItem("XML 파싱 오류", "유효한 XML을 입력해주세요", null, null, Symbol: "\uE783");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> RunXPath(string xml, string xpath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(xml))
|
||||
return [new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783")];
|
||||
|
||||
XPathDocument doc;
|
||||
try { doc = new XPathDocument(new System.IO.StringReader(xml)); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
return [new LauncherItem("XML 파싱 오류", ex.Message, null, null, Symbol: "\uE783")];
|
||||
}
|
||||
|
||||
XPathNodeIterator iter;
|
||||
try
|
||||
{
|
||||
var nav = doc.CreateNavigator();
|
||||
iter = nav.Select(xpath);
|
||||
}
|
||||
catch (XPathException ex)
|
||||
{
|
||||
return [new LauncherItem("XPath 오류", ex.Message, null, null, Symbol: "\uE783")];
|
||||
}
|
||||
|
||||
var results = new List<string>();
|
||||
while (iter.MoveNext() && results.Count < 20)
|
||||
results.Add(iter.Current!.OuterXml);
|
||||
|
||||
if (results.Count == 0)
|
||||
return [new LauncherItem("결과 없음", $"XPath '{xpath}' — 일치하는 노드 없음", null, null, Symbol: "\uE946")];
|
||||
|
||||
var items = new List<LauncherItem>();
|
||||
var joined = string.Join("\n", results);
|
||||
items.Add(new LauncherItem(
|
||||
$"XPath 결과 {results.Count}개",
|
||||
results[0].Length > 80 ? results[0][..80] + "…" : results[0],
|
||||
null, ("copy", joined), Symbol: "\uE943"));
|
||||
|
||||
foreach (var r in results.Take(10))
|
||||
{
|
||||
var disp = r.Length > 80 ? r[..80] + "…" : r;
|
||||
items.Add(new LauncherItem(disp, "Enter 복사", null, ("copy", r), Symbol: "\uE943"));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> ExtractAttributes(string xml)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(xml))
|
||||
return [new LauncherItem("XML 없음", "클립보드에 XML을 복사해주세요", null, null, Symbol: "\uE783")];
|
||||
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
var attrs = new List<(string Element, string Attr, string Value)>();
|
||||
|
||||
foreach (XmlNode node in doc.SelectNodes("//*")!)
|
||||
{
|
||||
if (node.Attributes == null) continue;
|
||||
foreach (XmlAttribute attr in node.Attributes)
|
||||
attrs.Add((node.Name, attr.Name, attr.Value));
|
||||
}
|
||||
|
||||
if (attrs.Count == 0)
|
||||
return [new LauncherItem("속성 없음", "XML에 속성(attribute)이 없습니다", null, null, Symbol: "\uE946")];
|
||||
|
||||
var items = new List<LauncherItem>
|
||||
{
|
||||
new($"속성 {attrs.Count}개", "전체 복사: Enter", null,
|
||||
("copy", string.Join("\n", attrs.Select(a => $"{a.Element}@{a.Attr}={a.Value}"))),
|
||||
Symbol: "\uE943"),
|
||||
};
|
||||
items.AddRange(attrs.Take(15).Select(a =>
|
||||
new LauncherItem($"{a.Element}@{a.Attr}", a.Value, null, ("copy", a.Value), Symbol: "\uE943")));
|
||||
return items;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return [new LauncherItem("XML 오류", ex.Message, null, null, Symbol: "\uE783")];
|
||||
}
|
||||
}
|
||||
|
||||
// ── XML 헬퍼 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryFormat(string xml, out string result)
|
||||
{
|
||||
result = "";
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
var sb = new StringBuilder();
|
||||
var st = new XmlWriterSettings { Indent = true, IndentChars = " ", NewLineChars = "\n" };
|
||||
using var writer = XmlWriter.Create(sb, st);
|
||||
doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
result = sb.ToString();
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static bool TryMinify(string xml, out string result)
|
||||
{
|
||||
result = "";
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
var sb = new StringBuilder();
|
||||
var st = new XmlWriterSettings { Indent = false, NewLineHandling = NewLineHandling.None };
|
||||
using var writer = XmlWriter.Create(sb, st);
|
||||
doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
result = sb.ToString();
|
||||
return true;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static (bool Ok, string? Error) ValidateXml(string xml)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
return (true, null);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
return (false, $"줄 {ex.LineNumber}, 열 {ex.LinePosition}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetRootElement(string xml, out string root)
|
||||
{
|
||||
root = "";
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
root = doc.DocumentElement?.Name ?? "";
|
||||
return !string.IsNullOrEmpty(root);
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
|
||||
private static int CountElements(string xml)
|
||||
{
|
||||
try
|
||||
{
|
||||
var doc = new XmlDocument();
|
||||
doc.LoadXml(xml);
|
||||
return doc.SelectNodes("//*")?.Count ?? 0;
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static string GetClipboard()
|
||||
{
|
||||
try
|
||||
{
|
||||
return System.Windows.Application.Current.Dispatcher.Invoke(
|
||||
() => Clipboard.ContainsText() ? Clipboard.GetText() : "");
|
||||
}
|
||||
catch { return ""; }
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user