Files
AX-Copilot-Codex/src/AxCopilot/Handlers/PasswordGenHandler.cs
lacvet 0336904258 AX Commander 비교본 런처 기능 대량 이식
변경 목적: 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개를 확인했습니다.
2026-04-05 00:59:45 +09:00

250 lines
8.9 KiB
C#

using System.Security.Cryptography;
using System.Text;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L9-1: 비밀번호 생성기 핸들러. "pwd" 프리픽스로 사용합니다.
///
/// 예: pwd → 기본 16자 비밀번호 5개 생성
/// pwd 24 → 24자 비밀번호 5개 생성
/// pwd 32 strong → 32자 강력 옵션 (대소문자+숫자+특수)
/// pwd 16 alpha → 알파벳+숫자만 (특수문자 제외)
/// pwd 20 pin → 숫자만 (PIN 코드)
/// pwd passphrase → 단어 조합 기억하기 쉬운 패스프레이즈
/// Enter → 클립보드에 복사.
/// </summary>
public class PasswordGenHandler : IActionHandler
{
public string? Prefix => "pwd";
public PluginMetadata Metadata => new(
"PasswordGen",
"비밀번호 생성기 — 길이 · 복잡도 · 패스프레이즈",
"1.0",
"AX");
private const string LowerChars = "abcdefghijklmnopqrstuvwxyz";
private const string UpperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private const string DigitChars = "0123456789";
private const string SpecialChars = "!@#$%^&*()-_=+[]{}|;:,.<>?";
// 기억하기 쉬운 단어 목록 (간결한 영단어)
private static readonly string[] WordList =
[
"apple","bridge","cloud","dawn","eagle","flame","grape","harbor",
"ivory","jungle","kite","lemon","maple","night","ocean","pearl",
"quartz","river","storm","tiger","ultra","violet","water","xenon",
"yellow","zenith","amber","blaze","cedar","delta","ember","frost",
"glass","honey","iron","jade","knot","lunar","mango","nova",
"orbit","prism","quest","range","solar","track","umbra","valor",
];
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim().ToLowerInvariant();
var items = new List<LauncherItem>();
// 패스프레이즈 모드
if (q.StartsWith("passphrase") || q.StartsWith("phrase"))
{
var wordCount = 4;
var parts2 = q.Split(' ');
if (parts2.Length >= 2 && int.TryParse(parts2[1], out var wc))
wordCount = Math.Clamp(wc, 2, 8);
items.Add(new LauncherItem(
"패스프레이즈 생성",
$"{wordCount}단어 조합 · 기억하기 쉬운 형식",
null, null, Symbol: "\uE8D4"));
for (int i = 0; i < 5; i++)
{
var phrase = GeneratePassphrase(wordCount);
var strength = EstimateEntropy(phrase);
items.Add(new LauncherItem(
phrase,
$"엔트로피: ~{strength}bit",
null,
("copy", phrase),
Symbol: "\uE8D4"));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 파라미터 파싱: [길이] [모드]
int length = 16;
string mode = "strong";
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length >= 1 && int.TryParse(parts[0], out var l))
{
length = Math.Clamp(l, 4, 128);
if (parts.Length >= 2) mode = parts[1];
}
else if (parts.Length >= 1 && parts[0] is "strong" or "alpha" or "pin" or "simple")
{
mode = parts[0];
}
// 문자 집합 결정
var charset = BuildCharset(mode);
// 강도 표시
var modeLabel = mode switch
{
"alpha" => "알파뉴메릭 (대소문자+숫자)",
"pin" => "숫자 PIN",
"simple" => "간단 (소문자+숫자)",
_ => "강력 (대소문자+숫자+특수)",
};
items.Add(new LauncherItem(
$"비밀번호 생성 {length}자",
modeLabel,
null, null, Symbol: "\uE8D4"));
// 5개 후보 생성
for (int i = 0; i < 5; i++)
{
var pw = GeneratePassword(charset, length, mode);
var strength = GetStrengthLabel(pw);
items.Add(new LauncherItem(
pw,
strength,
null,
("copy", pw),
Symbol: "\uE8D4"));
}
// 옵션 안내
items.Add(new LauncherItem(
"pwd <길이> alpha",
"알파뉴메릭 (특수문자 제외)",
null, null, Symbol: "\uE946"));
items.Add(new LauncherItem(
"pwd <길이> pin",
"숫자만 (PIN 코드)",
null, null, Symbol: "\uE946"));
items.Add(new LauncherItem(
"pwd passphrase",
"단어 조합 패스프레이즈",
null, null, Symbol: "\uE946"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
if (item.Data is ("copy", string pw))
{
try
{
System.Windows.Application.Current.Dispatcher.Invoke(
() => Clipboard.SetText(pw));
NotificationService.Notify("비밀번호", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 헬퍼 ────────────────────────────────────────────────────────────────
private static string BuildCharset(string mode) => mode switch
{
"pin" => DigitChars,
"alpha" => LowerChars + UpperChars + DigitChars,
"simple" => LowerChars + DigitChars,
_ => LowerChars + UpperChars + DigitChars + SpecialChars,
};
private static string GeneratePassword(string charset, int length, string mode)
{
// strong 모드: 각 카테고리 최소 1개 보장
if (mode == "strong" && length >= 4)
{
var mandatory = new[]
{
RandomChar(LowerChars),
RandomChar(UpperChars),
RandomChar(DigitChars),
RandomChar(SpecialChars),
};
var remaining = length - mandatory.Length;
var bulk = Enumerable.Range(0, remaining)
.Select(_ => RandomChar(charset))
.ToList();
var all = mandatory.Concat(bulk).ToArray();
Shuffle(all);
return new string(all);
}
// alpha/pin/simple
return new string(Enumerable.Range(0, length)
.Select(_ => RandomChar(charset))
.ToArray());
}
private static string GeneratePassphrase(int wordCount)
{
var words = Enumerable.Range(0, wordCount)
.Select(_ => WordList[RandomInt(WordList.Length)]);
var num = RandomInt(9000) + 1000;
var sep = new[] { "-", "_", ".", "!" }[RandomInt(4)];
return string.Join(sep, words) + sep + num;
}
private static char RandomChar(string charset) =>
charset[RandomInt(charset.Length)];
private static int RandomInt(int max) =>
(int)(RandomNumberGenerator.GetInt32(int.MaxValue) % max);
private static void Shuffle<T>(T[] arr)
{
for (int i = arr.Length - 1; i > 0; i--)
{
var j = RandomInt(i + 1);
(arr[i], arr[j]) = (arr[j], arr[i]);
}
}
private static string GetStrengthLabel(string pw)
{
var hasLower = pw.Any(char.IsLower);
var hasUpper = pw.Any(char.IsUpper);
var hasDigit = pw.Any(char.IsDigit);
var hasSpecial = pw.Any(c => SpecialChars.Contains(c));
var types = new[] { hasLower, hasUpper, hasDigit, hasSpecial }.Count(b => b);
var strength = (pw.Length, types) switch
{
( >= 24, >= 4) => "매우 강함 🔐",
( >= 16, >= 3) => "강함 🔒",
( >= 12, >= 2) => "보통 🔑",
_ => "약함 ⚠",
};
return $"{strength} · {pw.Length}자";
}
private static int EstimateEntropy(string pw)
{
// 간략 엔트로피 추정: log2(charset^length)
var hasLower = pw.Any(char.IsLower);
var hasUpper = pw.Any(char.IsUpper);
var hasDigit = pw.Any(char.IsDigit);
var hasSpecial = pw.Any(c => !char.IsLetterOrDigit(c));
var pool = (hasLower ? 26 : 0) + (hasUpper ? 26 : 0)
+ (hasDigit ? 10 : 0) + (hasSpecial ? 32 : 0);
if (pool == 0) pool = 36;
return (int)(pw.Length * Math.Log2(pool));
}
}