변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다. 핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다. 핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다. 문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다. 검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
325 lines
15 KiB
C#
325 lines
15 KiB
C#
using System.Text;
|
|
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L20-2: 랜덤 생성기 핸들러. "rand" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: rand → 사용법 목록
|
|
/// rand → 1~100 기본 난수
|
|
/// rand 50 → 1~50 난수
|
|
/// rand 10 99 → 10~99 난수
|
|
/// rand str → 랜덤 문자열 (12자, 영숫자)
|
|
/// rand str 20 → 20자 랜덤 문자열
|
|
/// rand str 16 alpha → 16자 알파벳만
|
|
/// rand str 8 num → 8자 숫자만
|
|
/// rand str 12 hex → 12자 hex 문자열
|
|
/// rand str 16 special → 특수문자 포함
|
|
/// rand color → 랜덤 HEX 색상
|
|
/// rand pick 항목1 항목2 → 목록에서 무작위 선택
|
|
/// rand dice → 주사위 1d6
|
|
/// rand dice 2d6 → 2개 주사위
|
|
/// rand dice 1d20 → 20면체 주사위
|
|
/// rand coin → 동전 던지기
|
|
/// rand shuffle a b c d → 목록 셔플
|
|
/// rand uuid → UUID v4
|
|
/// rand token → 보안 토큰 (32자 hex)
|
|
/// rand pin → 6자리 PIN
|
|
/// rand pin 4 → 4자리 PIN
|
|
/// Enter → 결과 복사.
|
|
/// </summary>
|
|
public class RandHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "rand";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"Rand",
|
|
"랜덤 생성기 — 숫자·문자열·색상·주사위·UUID·토큰·PIN·셔플",
|
|
"1.0",
|
|
"AX");
|
|
|
|
private static readonly Random _rng = Random.Shared;
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var items = new List<LauncherItem>();
|
|
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
// 기본: 1~100 난수 + 사용법
|
|
var n = _rng.Next(1, 101);
|
|
items.Add(new LauncherItem($"{n}",
|
|
"1~100 랜덤 숫자 · Enter 복사", null, ("copy", n.ToString()), Symbol: "\uE8D0"));
|
|
items.Add(new LauncherItem("사용법",
|
|
"rand <max> / rand <min> <max> / rand str / rand color / rand dice / rand pick …",
|
|
null, null, Symbol: "\uE8D0"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var sub = parts[0].ToLowerInvariant();
|
|
|
|
switch (sub)
|
|
{
|
|
// rand str [length] [charset]
|
|
case "str" or "string" or "문자열":
|
|
{
|
|
int len = 12;
|
|
if (parts.Length >= 2 && int.TryParse(parts[1], out var l)) len = Math.Clamp(l, 1, 256);
|
|
var charset = parts.Length >= 3 ? parts[2].ToLowerInvariant() : "alnum";
|
|
if (parts.Length == 2 && !int.TryParse(parts[1], out _)) charset = parts[1].ToLowerInvariant();
|
|
var cs = GetCharset(charset);
|
|
if (string.IsNullOrEmpty(cs)) { items.Add(ErrorItem("charset: alpha/num/alnum/hex/special")); break; }
|
|
var result = RandStr(len, cs);
|
|
items.Add(new LauncherItem(result, $"{len}자 랜덤 문자열 ({charset}) · Enter 복사",
|
|
null, ("copy", result), Symbol: "\uE8D0"));
|
|
// 다른 자릿수 미리 생성
|
|
foreach (var altLen in new[] { 8, 16, 32 }.Where(x => x != len))
|
|
items.Add(new LauncherItem(RandStr(altLen, cs), $"{altLen}자 ({charset})",
|
|
null, ("copy", RandStr(altLen, cs)), Symbol: "\uE8D0"));
|
|
break;
|
|
}
|
|
|
|
// rand color
|
|
case "color" or "색상" or "colour":
|
|
{
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var r = _rng.Next(256); var g = _rng.Next(256); var b = _rng.Next(256);
|
|
var hex = $"#{r:X2}{g:X2}{b:X2}";
|
|
var rgb = $"rgb({r}, {g}, {b})";
|
|
var hsl = RgbToHsl(r, g, b);
|
|
items.Add(new LauncherItem(hex, $"{rgb} {hsl} · Enter 복사",
|
|
null, ("copy", hex), Symbol: "\uE8D0"));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// rand dice [NdS]
|
|
case "dice" or "주사위":
|
|
{
|
|
var spec = parts.Length >= 2 ? parts[1].ToLowerInvariant() : "1d6";
|
|
if (!TryParseDice(spec, out var dCount, out var dSides))
|
|
{ items.Add(ErrorItem("형식: 1d6 / 2d10 / 1d20")); break; }
|
|
var rolls = Enumerable.Range(0, dCount).Select(_ => _rng.Next(1, dSides + 1)).ToList();
|
|
var total = rolls.Sum();
|
|
var detail = string.Join(" + ", rolls);
|
|
items.Add(new LauncherItem($"{dCount}d{dSides} → {total}",
|
|
$"각 주사위: {detail} · Enter 복사", null, ("copy", total.ToString()), Symbol: "\uE8D0"));
|
|
if (dCount > 1)
|
|
{
|
|
items.Add(CopyItem("합계", total.ToString()));
|
|
items.Add(CopyItem("상세", detail));
|
|
items.Add(CopyItem("최솟값", rolls.Min().ToString()));
|
|
items.Add(CopyItem("최댓값", rolls.Max().ToString()));
|
|
}
|
|
// 다시 굴리기 미리보기
|
|
items.Add(new LauncherItem("── 다시 굴리기 (미리보기) ──", "", null, null, Symbol: "\uE8D0"));
|
|
for (int i = 0; i < 3; i++)
|
|
{
|
|
var r2 = Enumerable.Range(0, dCount).Select(_ => _rng.Next(1, dSides + 1)).ToList();
|
|
items.Add(new LauncherItem($"{r2.Sum()}", string.Join(" + ", r2),
|
|
null, ("copy", r2.Sum().ToString()), Symbol: "\uE8D0"));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// rand coin
|
|
case "coin" or "동전":
|
|
{
|
|
var result = _rng.Next(2) == 0 ? "앞면 (Head)" : "뒷면 (Tail)";
|
|
items.Add(new LauncherItem(result, "동전 던지기 · Enter 복사",
|
|
null, ("copy", result), Symbol: "\uE8D0"));
|
|
// 연속 5회
|
|
items.Add(new LauncherItem("── 연속 5회 ──", "", null, null, Symbol: "\uE8D0"));
|
|
for (int i = 0; i < 5; i++)
|
|
items.Add(new LauncherItem(_rng.Next(2) == 0 ? "앞면 ⬤" : "뒷면 ○",
|
|
$"#{i + 1}", null, null, Symbol: "\uE8D0"));
|
|
break;
|
|
}
|
|
|
|
// rand pick item1 item2 ...
|
|
case "pick" or "선택":
|
|
{
|
|
if (parts.Length < 2) { items.Add(ErrorItem("예: rand pick 치킨 피자 짜장면")); break; }
|
|
var pool = parts[1..];
|
|
var picked = pool[_rng.Next(pool.Length)];
|
|
items.Add(new LauncherItem(picked,
|
|
$"{pool.Length}개 중 선택 · Enter 복사",
|
|
null, ("copy", picked), Symbol: "\uE8D0"));
|
|
items.Add(new LauncherItem("── 다시 선택 ──", "", null, null, Symbol: "\uE8D0"));
|
|
for (int i = 0; i < Math.Min(3, pool.Length); i++)
|
|
items.Add(new LauncherItem(pool[_rng.Next(pool.Length)],
|
|
$"후보 {i + 1}", null, null, Symbol: "\uE8D0"));
|
|
break;
|
|
}
|
|
|
|
// rand shuffle item1 item2 ...
|
|
case "shuffle" or "섞기":
|
|
{
|
|
if (parts.Length < 2) { items.Add(ErrorItem("예: rand shuffle a b c d e")); break; }
|
|
var list = parts[1..].ToList();
|
|
for (int i = list.Count - 1; i > 0; i--)
|
|
{
|
|
var j = _rng.Next(i + 1);
|
|
(list[i], list[j]) = (list[j], list[i]);
|
|
}
|
|
var joined = string.Join(" ", list);
|
|
items.Add(new LauncherItem(joined,
|
|
$"{list.Count}개 셔플 · Enter 복사",
|
|
null, ("copy", joined), Symbol: "\uE8D0"));
|
|
items.Add(CopyItem("쉼표 구분", string.Join(", ", list)));
|
|
for (int r = 0; r < list.Count; r++)
|
|
items.Add(new LauncherItem($"#{r + 1} {list[r]}", "", null, null, Symbol: "\uE8D0"));
|
|
break;
|
|
}
|
|
|
|
// rand uuid
|
|
case "uuid":
|
|
{
|
|
for (int i = 0; i < 5; i++)
|
|
{
|
|
var guid = Guid.NewGuid().ToString();
|
|
items.Add(new LauncherItem(guid, $"UUID v4 · Enter 복사",
|
|
null, ("copy", guid), Symbol: "\uE8D0"));
|
|
}
|
|
break;
|
|
}
|
|
|
|
// rand token
|
|
case "token" or "secret" or "key":
|
|
{
|
|
int tLen = 32;
|
|
if (parts.Length >= 2 && int.TryParse(parts[1], out var tl)) tLen = Math.Clamp(tl, 8, 128);
|
|
var tokenBytes = new byte[tLen];
|
|
System.Security.Cryptography.RandomNumberGenerator.Fill(tokenBytes);
|
|
var tokenHex = BitConverter.ToString(tokenBytes).Replace("-", "").ToLowerInvariant();
|
|
var tokenB64 = Convert.ToBase64String(tokenBytes);
|
|
items.Add(new LauncherItem(tokenHex, $"{tLen}바이트 보안 토큰 (hex) · Enter 복사",
|
|
null, ("copy", tokenHex), Symbol: "\uE8D0"));
|
|
items.Add(CopyItem("Hex", tokenHex));
|
|
items.Add(CopyItem("Base64", tokenB64));
|
|
break;
|
|
}
|
|
|
|
// rand pin [length]
|
|
case "pin":
|
|
{
|
|
int pLen = 6;
|
|
if (parts.Length >= 2 && int.TryParse(parts[1], out var pl)) pLen = Math.Clamp(pl, 4, 12);
|
|
var pin = string.Concat(Enumerable.Range(0, pLen).Select(_ => _rng.Next(10)));
|
|
items.Add(new LauncherItem(pin, $"{pLen}자리 PIN · Enter 복사",
|
|
null, ("copy", pin), Symbol: "\uE8D0"));
|
|
items.Add(new LauncherItem("── 추가 PIN ──", "", null, null, Symbol: "\uE8D0"));
|
|
for (int i = 0; i < 4; i++)
|
|
items.Add(new LauncherItem(
|
|
string.Concat(Enumerable.Range(0, pLen).Select(_ => _rng.Next(10))),
|
|
$"{pLen}자리", null, null, Symbol: "\uE8D0"));
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
// rand <max> 또는 rand <min> <max>
|
|
if (int.TryParse(sub, out var max))
|
|
{
|
|
int min = 1;
|
|
if (parts.Length >= 2 && int.TryParse(parts[1], out var mx)) { min = max; max = mx; }
|
|
if (min > max) (min, max) = (max, min);
|
|
var n = _rng.Next(min, max + 1);
|
|
items.Add(new LauncherItem($"{n}",
|
|
$"{min}~{max} 랜덤 숫자 · Enter 복사",
|
|
null, ("copy", n.ToString()), Symbol: "\uE8D0"));
|
|
items.Add(new LauncherItem("── 추가 결과 ──", "", null, null, Symbol: "\uE8D0"));
|
|
for (int i = 0; i < 4; i++)
|
|
items.Add(new LauncherItem(_rng.Next(min, max + 1).ToString(),
|
|
$"{min}~{max}", null, null, Symbol: "\uE8D0"));
|
|
}
|
|
else
|
|
{
|
|
items.Add(new LauncherItem($"알 수 없는 서브커맨드: '{sub}'",
|
|
"rand / rand <n> / rand str / rand color / rand dice / rand pick / rand uuid / rand token / rand pin",
|
|
null, null, Symbol: "\uE783"));
|
|
}
|
|
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("Rand", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
|
|
|
private static string GetCharset(string name) => name switch
|
|
{
|
|
"alpha" or "알파" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
"num" or "숫자" => "0123456789",
|
|
"alnum" or "영숫자" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
|
|
"hex" => "0123456789abcdef",
|
|
"lower" => "abcdefghijklmnopqrstuvwxyz",
|
|
"upper" => "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
|
"special" => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*",
|
|
_ => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
};
|
|
|
|
private static string RandStr(int len, string charset)
|
|
{
|
|
var sb = new StringBuilder(len);
|
|
for (int i = 0; i < len; i++) sb.Append(charset[_rng.Next(charset.Length)]);
|
|
return sb.ToString();
|
|
}
|
|
|
|
private static bool TryParseDice(string s, out int count, out int sides)
|
|
{
|
|
count = sides = 0;
|
|
var d = s.IndexOf('d');
|
|
if (d < 0) return false;
|
|
var left = d == 0 ? "1" : s[..d];
|
|
var right = s[(d + 1)..];
|
|
return int.TryParse(left, out count) && count >= 1 && count <= 100 &&
|
|
int.TryParse(right, out sides) && sides >= 2 && sides <= 10000;
|
|
}
|
|
|
|
private static string RgbToHsl(int r, int g, int b)
|
|
{
|
|
var rf = r / 255.0; var gf = g / 255.0; var bf = b / 255.0;
|
|
var max = Math.Max(rf, Math.Max(gf, bf));
|
|
var min = Math.Min(rf, Math.Min(gf, bf));
|
|
var l = (max + min) / 2;
|
|
if (max == min) return $"hsl(0, 0%, {l:P0})";
|
|
var d = max - min;
|
|
var s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
|
var h = max == rf ? (gf - bf) / d + (gf < bf ? 6 : 0)
|
|
: max == gf ? (bf - rf) / d + 2
|
|
: (rf - gf) / d + 4;
|
|
h /= 6;
|
|
return $"hsl({h * 360:F0}, {s:P0}, {l:P0})";
|
|
}
|
|
|
|
private static LauncherItem CopyItem(string label, string value) =>
|
|
new(label, value, null, ("copy", value), Symbol: "\uE8D0");
|
|
|
|
private static LauncherItem ErrorItem(string msg) =>
|
|
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
|
}
|