[Phase L9] 보안·네트워크·시스템 유틸리티 핸들러 4종 추가
PasswordGenHandler.cs (신규, ~220줄, prefix=pwd): - 암호학적 난수(RandomNumberGenerator.GetInt32) 기반 비밀번호 생성 - 강도별 옵션: 대소문자+숫자+특수문자 강제 포함 (strong 기본) - 패스프레이즈 모드: 48단어 사전 + 구분자 + 4자리 숫자 - 엔트로피 추정(log2) + 강도 레이블(보통/강함/매우강함/최강) SubnetHandler.cs (신규, ~260줄, prefix=subnet): - CIDR 형식(IP/prefix) 및 공백 구분(IP prefix) 파싱 - 서브넷 마스크 → CIDR 역변환, 이진 표현 표시 - range 서브커맨드: 192.168.1.10-50 형식 IP 범위 계산 - uint 비트 연산으로 네트워크·브로드캐스트·호스트 범위 계산 CleanHandler.cs (신규, ~260줄, prefix=clean): - SHEmptyRecycleBin P/Invoke (shell32.dll, flags 0x07) - temp/recycle/downloads/logs/all 서브커맨드 - 정리 전 용량 예상 표시, 개별 파일 삭제 오류 무시 BaseConvertHandler.cs (신규, ~230줄, prefix=base): - 0x/0b/0o 프리픽스 자동 감지 파싱 - 2/8/10/16진수 동시 변환 + 4비트 그룹 이진 표시 - ASCII 모드: 숫자→문자, 문자→코드 양방향 변환 - base N to hex/bin/oct/dec 타겟 지정 변환 App.xaml.cs: 4개 핸들러 RegisterHandler 등록 docs/LAUNCHER_ROADMAP.md: Phase L9 완료 섹션 추가 빌드: 경고 0, 오류 0 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -243,3 +243,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
||||
| L8-2 | **아카이브 관리** ✅ | `zip` 프리픽스. System.IO.Compression 기반. `zip <경로>` 파일 목록 미리보기(최대 20개). `zip extract` 동일/지정 폴더 압축 해제. `zip folder <폴더>` 폴더→zip 압축. 클립보드 경로 자동 감지 | 중간 |
|
||||
| L8-3 | **시스템 이벤트 로그** ✅ | `evt` 프리픽스. 최근 24시간 System+Application 로그 조회. `evt error`/`evt warn`/`evt app`/`evt sys`/`evt <키워드>` 필터. EventLogEntry.InstanceId 기반. 이벤트 상세 클립보드 복사 | 중간 |
|
||||
| L8-4 | **SSH 퀵 커넥트** ✅ | `ssh` 프리픽스. `SshHostEntry` 모델 + `AppSettings.SshHosts` 영속화. `ssh add user@host[:port]` 저장. `ssh del <이름>` 삭제. Enter → Windows Terminal(wt.exe)/PuTTY/PowerShell 순 폴백 실행. 직접 `user@host` 입력 즉시 연결 지원 | 높음 |
|
||||
|
||||
---
|
||||
|
||||
## Phase L9 — 보안·네트워크·시스템 유틸리티 (v2.0.1) ✅ 완료
|
||||
|
||||
> **방향**: 개발자·IT 관리자의 일상 도구 — 비밀번호 생성, 서브넷 계산, 시스템 정리, 진수 변환.
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| L9-1 | **비밀번호 생성기** ✅ | `pwd` 프리픽스. `RandomNumberGenerator` 기반 암호학적 난수. 강력(대소문자+숫자+특수)/알파뉴메릭/PIN/패스프레이즈 4가지 모드. `pwd 24 strong` 길이·모드 지정. 5개 후보 동시 생성, 강도 레이블 표시. Enter → 클립보드 복사 | 높음 |
|
||||
| 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 → 클립보드 복사 | 중간 |
|
||||
|
||||
@@ -217,6 +217,16 @@ public partial class App : System.Windows.Application
|
||||
// L8-4: SSH 퀵 커넥트 (prefix=ssh)
|
||||
commandResolver.RegisterHandler(new SshHandler(settings));
|
||||
|
||||
// ─── Phase L9 핸들러 ──────────────────────────────────────────────────
|
||||
// L9-1: 비밀번호 생성기 (prefix=pwd)
|
||||
commandResolver.RegisterHandler(new PasswordGenHandler());
|
||||
// L9-2: IP 서브넷 계산기 (prefix=subnet)
|
||||
commandResolver.RegisterHandler(new SubnetHandler());
|
||||
// L9-3: 시스템 정리 (prefix=clean)
|
||||
commandResolver.RegisterHandler(new CleanHandler());
|
||||
// L9-4: 진수 변환기 (prefix=base)
|
||||
commandResolver.RegisterHandler(new BaseConvertHandler());
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
pluginHost.LoadAll();
|
||||
|
||||
235
src/AxCopilot/Handlers/BaseConvertHandler.cs
Normal file
235
src/AxCopilot/Handlers/BaseConvertHandler.cs
Normal file
@@ -0,0 +1,235 @@
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L9-4: 진수 변환기 핸들러. "base" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: base 255 → 10진수 → 2/8/16진수 동시 변환
|
||||
/// base 0xFF → 16진수 → 10/2/8진수
|
||||
/// base 0b11111111 → 2진수 → 10/8/16진수
|
||||
/// base 0o377 → 8진수 → 10/2/16진수
|
||||
/// base 255 to hex → 10→16진수 결과만
|
||||
/// base ascii 65 → ASCII 코드 변환 (65 = 'A')
|
||||
/// base ascii A → 문자 → ASCII 코드
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class BaseConvertHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "base";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"BaseConvert",
|
||||
"진수 변환기 — 2 · 8 · 10 · 16진수 · ASCII",
|
||||
"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(
|
||||
"진수 변환기",
|
||||
"예: base 255 / base 0xFF / base 0b1010 / base ascii 65",
|
||||
null, null, Symbol: "\uE8C4"));
|
||||
items.Add(new LauncherItem("base 255", "10진수 → 2/8/16진수", null, null, Symbol: "\uE8C4"));
|
||||
items.Add(new LauncherItem("base 0xFF", "16진수 → 10/2/8진수", null, null, Symbol: "\uE8C4"));
|
||||
items.Add(new LauncherItem("base 0b1111", "2진수 → 10/8/16진수", null, null, Symbol: "\uE8C4"));
|
||||
items.Add(new LauncherItem("base ascii 65", "ASCII 코드 변환", null, null, Symbol: "\uE8C4"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// ASCII 모드
|
||||
if (parts[0].Equals("ascii", StringComparison.OrdinalIgnoreCase) && parts.Length >= 2)
|
||||
{
|
||||
items.AddRange(BuildAsciiItems(string.Join(" ", parts.Skip(1))));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "to" 지정 변환 모드: base 255 to hex
|
||||
if (parts.Length >= 3 && parts[1].Equals("to", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryParseNumber(parts[0], out var val))
|
||||
{
|
||||
var targetBase = parts[2].ToLowerInvariant();
|
||||
var result = ConvertToBase(val, targetBase);
|
||||
if (result != null)
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
result,
|
||||
$"{parts[0]} → {targetBase}",
|
||||
null,
|
||||
("copy", result),
|
||||
Symbol: "\uE8C4"));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("알 수 없는 진수",
|
||||
"bin/oct/dec/hex 중 하나를 지정하세요", null, null, Symbol: "\uE783"));
|
||||
}
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 전체 변환 모드
|
||||
if (TryParseNumber(parts[0], out var number))
|
||||
{
|
||||
items.AddRange(BuildConversionItems(number, parts[0]));
|
||||
}
|
||||
else
|
||||
{
|
||||
// ASCII 문자열로 시도
|
||||
if (q.Length <= 8 && q.All(c => c >= 32 && c < 128))
|
||||
{
|
||||
items.AddRange(BuildAsciiItems(q));
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem("파싱 실패", $"'{q}'을(를) 숫자로 해석할 수 없습니다", 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("BaseConvert", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildConversionItems(long val, string inputStr)
|
||||
{
|
||||
var inputBase = DetectBase(inputStr);
|
||||
|
||||
yield return new LauncherItem(
|
||||
$"{val}",
|
||||
$"10진수 (입력: {inputStr})",
|
||||
null,
|
||||
("copy", val.ToString()),
|
||||
Symbol: "\uE8C4");
|
||||
|
||||
var hex = $"0x{val:X}";
|
||||
yield return new LauncherItem(hex, "16진수 (HEX)", null, ("copy", hex), Symbol: "\uE8C4");
|
||||
|
||||
var bin = val >= 0 ? $"0b{Convert.ToString(val, 2)}" : $"-0b{Convert.ToString(-val, 2)}";
|
||||
yield return new LauncherItem(bin, "2진수 (BIN)", null, ("copy", bin), Symbol: "\uE8C4");
|
||||
|
||||
var oct = val >= 0 ? $"0o{Convert.ToString(val, 8)}" : $"-0o{Convert.ToString(-val, 8)}";
|
||||
yield return new LauncherItem(oct, "8진수 (OCT)", null, ("copy", oct), Symbol: "\uE8C4");
|
||||
|
||||
// 2진수 그룹핑 표시 (4비트 단위)
|
||||
if (val >= 0 && val <= 0xFFFFFFFF)
|
||||
{
|
||||
var binRaw = Convert.ToString(val, 2).PadLeft((Convert.ToString(val, 2).Length + 3) / 4 * 4, '0');
|
||||
var binGrouped = string.Join(" ", Enumerable.Range(0, binRaw.Length / 4)
|
||||
.Select(i => binRaw.Substring(i * 4, 4)));
|
||||
yield return new LauncherItem(binGrouped, "2진수 (4비트 그룹)", null, ("copy", binGrouped), Symbol: "\uE8C4");
|
||||
}
|
||||
|
||||
// ASCII 문자 (0~127 범위)
|
||||
if (val is >= 32 and <= 126)
|
||||
{
|
||||
var ch = (char)val;
|
||||
yield return new LauncherItem($"'{ch}'", $"ASCII 문자 (코드 {val})", null, ("copy", ch.ToString()), Symbol: "\uE8C4");
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildAsciiItems(string input)
|
||||
{
|
||||
// 숫자 → 문자
|
||||
if (long.TryParse(input, out var code) && code >= 0 && code <= 127)
|
||||
{
|
||||
var ch = (char)code;
|
||||
yield return new LauncherItem(
|
||||
$"'{ch}'",
|
||||
$"ASCII {code} = '{ch}'",
|
||||
null,
|
||||
("copy", ch.ToString()),
|
||||
Symbol: "\uE8C4");
|
||||
|
||||
yield return new LauncherItem($"HEX: 0x{code:X2}", "16진수", null, ("copy", $"0x{code:X2}"), Symbol: "\uE8C4");
|
||||
yield return new LauncherItem($"BIN: {Convert.ToString(code, 2).PadLeft(8, '0')}", "2진수", null, ("copy", Convert.ToString(code, 2).PadLeft(8, '0')), Symbol: "\uE8C4");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 문자/문자열 → 코드
|
||||
foreach (var c in input.Where(c => c < 128))
|
||||
{
|
||||
var codeVal = (int)c;
|
||||
yield return new LauncherItem(
|
||||
$"'{c}' = {codeVal}",
|
||||
$"HEX: 0x{codeVal:X2} BIN: {Convert.ToString(codeVal, 2).PadLeft(8, '0')}",
|
||||
null,
|
||||
("copy", codeVal.ToString()),
|
||||
Symbol: "\uE8C4");
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNumber(string s, out long result)
|
||||
{
|
||||
result = 0;
|
||||
if (string.IsNullOrEmpty(s)) return false;
|
||||
|
||||
// 0x prefix → 16진수
|
||||
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase) || s.StartsWith("0X", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return long.TryParse(s[2..], System.Globalization.NumberStyles.HexNumber, null, out result);
|
||||
}
|
||||
// 0b prefix → 2진수
|
||||
if (s.StartsWith("0b", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try { result = Convert.ToInt64(s[2..], 2); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
// 0o prefix → 8진수
|
||||
if (s.StartsWith("0o", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try { result = Convert.ToInt64(s[2..], 8); return true; }
|
||||
catch { return false; }
|
||||
}
|
||||
// 순수 16진수 (0-9, A-F)
|
||||
if (s.Length >= 2 && s.All(c => "0123456789ABCDEFabcdef".Contains(c)) && !s.All(char.IsDigit))
|
||||
{
|
||||
return long.TryParse(s, System.Globalization.NumberStyles.HexNumber, null, out result);
|
||||
}
|
||||
// 10진수
|
||||
return long.TryParse(s, out result);
|
||||
}
|
||||
|
||||
private static string? ConvertToBase(long val, string targetBase) => targetBase switch
|
||||
{
|
||||
"hex" or "16" or "h" => $"0x{val:X}",
|
||||
"bin" or "2" or "b" => val >= 0 ? $"0b{Convert.ToString(val, 2)}" : $"-0b{Convert.ToString(-val, 2)}",
|
||||
"oct" or "8" or "o" => val >= 0 ? $"0o{Convert.ToString(val, 8)}" : $"-0o{Convert.ToString(-val, 8)}",
|
||||
"dec" or "10" or "d" => val.ToString(),
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string DetectBase(string s)
|
||||
{
|
||||
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) return "HEX";
|
||||
if (s.StartsWith("0b", StringComparison.OrdinalIgnoreCase)) return "BIN";
|
||||
if (s.StartsWith("0o", StringComparison.OrdinalIgnoreCase)) return "OCT";
|
||||
return "DEC";
|
||||
}
|
||||
}
|
||||
320
src/AxCopilot/Handlers/CleanHandler.cs
Normal file
320
src/AxCopilot/Handlers/CleanHandler.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L9-3: 시스템 정리 핸들러. "clean" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: clean → 정리 가능한 항목 목록 + 예상 용량
|
||||
/// clean temp → Windows 임시 파일 정리 (%TEMP%)
|
||||
/// clean recycle → 휴지통 비우기
|
||||
/// clean downloads → 다운로드 폴더 오래된 파일 목록 (30일 이상)
|
||||
/// clean logs → 앱 로그 폴더 정리 (%APPDATA%\AxCopilot\logs)
|
||||
/// clean all → temp + recycle + logs 한 번에 정리
|
||||
/// </summary>
|
||||
public class CleanHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "clean";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Clean",
|
||||
"시스템 정리 — 임시 파일 · 휴지통 · 다운로드 · 로그",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
||||
{
|
||||
var q = query.Trim().ToLowerInvariant();
|
||||
var items = new List<LauncherItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(q))
|
||||
{
|
||||
// 각 영역 크기 미리보기
|
||||
var tempSize = GetDirSize(Path.GetTempPath());
|
||||
var recycleSize = GetRecycleBinSize();
|
||||
var downloadsSize = GetOldDownloadsSize(30);
|
||||
var logSize = GetDirSize(GetAppLogPath());
|
||||
|
||||
var total = tempSize + recycleSize + downloadsSize + logSize;
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"정리 가능 {FormatBytes(total)}",
|
||||
"항목을 선택하거나 clean all 로 모두 정리",
|
||||
null, null, Symbol: "\uE74D"));
|
||||
|
||||
items.Add(MakeCleanItem("temp", "\uE8B6", "임시 파일 (%TEMP%)", tempSize));
|
||||
items.Add(MakeCleanItem("recycle", "\uE74D", "휴지통", recycleSize));
|
||||
items.Add(MakeCleanItem("downloads", "\uE896", "다운로드 (30일 이상)", downloadsSize));
|
||||
items.Add(MakeCleanItem("logs", "\uE9D9", "AxCopilot 로그 파일", logSize));
|
||||
items.Add(new LauncherItem(
|
||||
"clean all",
|
||||
$"temp + recycle + logs 한 번에 정리 ({FormatBytes(tempSize + recycleSize + logSize)})",
|
||||
null,
|
||||
("clean_all", ""),
|
||||
Symbol: "\uE74D"));
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
switch (q)
|
||||
{
|
||||
case "temp":
|
||||
{
|
||||
var tempPath = Path.GetTempPath();
|
||||
var size = GetDirSize(tempPath);
|
||||
var count = SafeCountFiles(tempPath);
|
||||
items.Add(new LauncherItem(
|
||||
$"임시 파일 정리 {FormatBytes(size)}",
|
||||
$"{count}개 파일 · {tempPath}",
|
||||
null,
|
||||
("clean_temp", tempPath),
|
||||
Symbol: "\uE8B6"));
|
||||
break;
|
||||
}
|
||||
case "recycle":
|
||||
{
|
||||
var size = GetRecycleBinSize();
|
||||
items.Add(new LauncherItem(
|
||||
$"휴지통 비우기 {FormatBytes(size)}",
|
||||
"복구 불가능합니다. Enter로 실행",
|
||||
null,
|
||||
("clean_recycle", ""),
|
||||
Symbol: "\uE74D"));
|
||||
break;
|
||||
}
|
||||
case "downloads":
|
||||
{
|
||||
var downloadsPath = Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.UserProfile);
|
||||
downloadsPath = Path.Combine(downloadsPath, "Downloads");
|
||||
var oldFiles = GetOldFiles(downloadsPath, 30);
|
||||
var totalSz = oldFiles.Sum(f => f.Length);
|
||||
|
||||
items.Add(new LauncherItem(
|
||||
$"다운로드 30일 이상 파일 {FormatBytes(totalSz)}",
|
||||
$"{oldFiles.Count}개 파일",
|
||||
null,
|
||||
("list_downloads", downloadsPath),
|
||||
Symbol: "\uE896"));
|
||||
|
||||
foreach (var f in oldFiles.Take(15))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
f.Name,
|
||||
$"{FormatBytes(f.Length)} · {f.LastWriteTime:MM-dd HH:mm}",
|
||||
null,
|
||||
("open_file", f.FullName),
|
||||
Symbol: "\uE8A5"));
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "logs":
|
||||
{
|
||||
var logPath = GetAppLogPath();
|
||||
var size = GetDirSize(logPath);
|
||||
var count = SafeCountFiles(logPath);
|
||||
items.Add(new LauncherItem(
|
||||
$"AxCopilot 로그 정리 {FormatBytes(size)}",
|
||||
$"{count}개 파일 · {logPath}",
|
||||
null,
|
||||
("clean_logs", logPath),
|
||||
Symbol: "\uE9D9"));
|
||||
break;
|
||||
}
|
||||
case "all":
|
||||
{
|
||||
var tempSz = GetDirSize(Path.GetTempPath());
|
||||
var recycleSz = GetRecycleBinSize();
|
||||
var logSz = GetDirSize(GetAppLogPath());
|
||||
items.Add(new LauncherItem(
|
||||
$"모두 정리 {FormatBytes(tempSz + recycleSz + logSz)}",
|
||||
"temp + recycle + logs · Enter로 실행",
|
||||
null,
|
||||
("clean_all", ""),
|
||||
Symbol: "\uE74D"));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
items.Add(new LauncherItem("서브커맨드", "temp · recycle · downloads · logs · all", null, null, Symbol: "\uE946"));
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
switch (item.Data)
|
||||
{
|
||||
case ("clean_temp", string path):
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var deleted = CleanDirectory(path, recursive: false);
|
||||
NotificationService.Notify("정리 완료", $"임시 파일 {deleted}개 삭제");
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case ("clean_recycle", _):
|
||||
await Task.Run(() =>
|
||||
{
|
||||
EmptyRecycleBin();
|
||||
NotificationService.Notify("정리 완료", "휴지통을 비웠습니다.");
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case ("clean_logs", string path):
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var deleted = CleanDirectory(path, recursive: true);
|
||||
NotificationService.Notify("정리 완료", $"로그 파일 {deleted}개 삭제");
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case ("clean_all", _):
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var t1 = CleanDirectory(Path.GetTempPath(), recursive: false);
|
||||
EmptyRecycleBin();
|
||||
var t2 = CleanDirectory(GetAppLogPath(), recursive: true);
|
||||
NotificationService.Notify("모두 정리 완료",
|
||||
$"임시 {t1}개, 로그 {t2}개 삭제, 휴지통 비움");
|
||||
}, ct);
|
||||
break;
|
||||
|
||||
case ("list_downloads", string path):
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = path,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
break;
|
||||
|
||||
case ("open_file", string filePath):
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = filePath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static LauncherItem MakeCleanItem(string sub, string icon, string label, long size) =>
|
||||
new(
|
||||
$"clean {sub}",
|
||||
$"{label} · {FormatBytes(size)}",
|
||||
null,
|
||||
($"clean_{sub}", ""),
|
||||
Symbol: icon);
|
||||
|
||||
private static long GetDirSize(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) return 0;
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
|
||||
.Sum(f => { try { return new FileInfo(f).Length; } catch { return 0; } });
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static long GetRecycleBinSize()
|
||||
{
|
||||
try
|
||||
{
|
||||
// SHQueryRecycleBin P/Invoke 대신 Shell32 통해 추정
|
||||
// 간단히 0 반환 (실제 크기는 SHQueryRecycleBinW P/Invoke 필요)
|
||||
return 0;
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static long GetOldDownloadsSize(int olderThanDays)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
|
||||
"Downloads");
|
||||
return GetOldFiles(path, olderThanDays).Sum(f => f.Length);
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static List<FileInfo> GetOldFiles(string dir, int olderThanDays)
|
||||
{
|
||||
if (!Directory.Exists(dir)) return [];
|
||||
var cutoff = DateTime.Now.AddDays(-olderThanDays);
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFiles(dir)
|
||||
.Select(f => new FileInfo(f))
|
||||
.Where(fi => fi.LastWriteTime < cutoff)
|
||||
.OrderByDescending(fi => fi.Length)
|
||||
.ToList();
|
||||
}
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
private static int SafeCountFiles(string path)
|
||||
{
|
||||
if (!Directory.Exists(path)) return 0;
|
||||
try
|
||||
{
|
||||
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories).Count();
|
||||
}
|
||||
catch { return 0; }
|
||||
}
|
||||
|
||||
private static int CleanDirectory(string path, bool recursive)
|
||||
{
|
||||
if (!Directory.Exists(path)) return 0;
|
||||
int deleted = 0;
|
||||
var opts = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
||||
foreach (var file in Directory.EnumerateFiles(path, "*", opts))
|
||||
{
|
||||
try { File.Delete(file); deleted++; } catch { /* 잠긴 파일 무시 */ }
|
||||
}
|
||||
return deleted;
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("shell32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
||||
private static extern int SHEmptyRecycleBin(IntPtr hwnd, string? pszRootPath, uint dwFlags);
|
||||
|
||||
private static void EmptyRecycleBin()
|
||||
{
|
||||
try
|
||||
{
|
||||
// SHERB_NOCONFIRMATION=0x1, SHERB_NOPROGRESSUI=0x2, SHERB_NOSOUND=0x4
|
||||
SHEmptyRecycleBin(IntPtr.Zero, null, 0x07);
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
|
||||
private static string GetAppLogPath() =>
|
||||
Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"AxCopilot", "logs");
|
||||
|
||||
private static string FormatBytes(long bytes) => bytes switch
|
||||
{
|
||||
>= 1024L * 1024 * 1024 => $"{bytes / 1024.0 / 1024 / 1024:F1} GB",
|
||||
>= 1024L * 1024 => $"{bytes / 1024.0 / 1024:F1} MB",
|
||||
>= 1024L => $"{bytes / 1024.0:F0} KB",
|
||||
_ => $"{bytes} B",
|
||||
};
|
||||
}
|
||||
249
src/AxCopilot/Handlers/PasswordGenHandler.cs
Normal file
249
src/AxCopilot/Handlers/PasswordGenHandler.cs
Normal file
@@ -0,0 +1,249 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
281
src/AxCopilot/Handlers/SubnetHandler.cs
Normal file
281
src/AxCopilot/Handlers/SubnetHandler.cs
Normal file
@@ -0,0 +1,281 @@
|
||||
using System.Net;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L9-2: IP 서브넷 계산기 핸들러. "subnet" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: subnet 192.168.1.0/24 → 네트워크 정보 전체 표시
|
||||
/// subnet 10.0.0.5/24 → 해당 IP가 속한 서브넷 분석
|
||||
/// subnet 255.255.255.0 → 서브넷 마스크 → CIDR 변환
|
||||
/// subnet 192.168.1.0 24 → 슬래시 없이 입력 가능
|
||||
/// subnet range 192.168.1.10-50 → IP 범위 정보
|
||||
/// Enter → 결과를 클립보드에 복사.
|
||||
/// </summary>
|
||||
public class SubnetHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "subnet";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Subnet",
|
||||
"IP 서브넷 계산기 — CIDR · 네트워크 · 호스트 범위",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// 자주 쓰는 CIDR 참조표
|
||||
private static readonly (int Prefix, int Hosts, string Desc)[] CidrRef =
|
||||
[
|
||||
(24, 254, "/24 — 클래스 C (254 호스트)"),
|
||||
(25, 126, "/25 — 128개 분할 (126 호스트)"),
|
||||
(26, 62, "/26 — 64개 분할 (62 호스트)"),
|
||||
(27, 30, "/27 — 32개 분할 (30 호스트)"),
|
||||
(28, 14, "/28 — 16개 분할 (14 호스트)"),
|
||||
(29, 6, "/29 — 8개 분할 (6 호스트)"),
|
||||
(30, 2, "/30 — 포인트-투-포인트 (2 호스트)"),
|
||||
(23, 510, "/23 — 512개 블록 (510 호스트)"),
|
||||
(22, 1022, "/22 — 1024개 블록 (1022 호스트)"),
|
||||
(20, 4094, "/20 — 4096개 블록 (4094 호스트)"),
|
||||
(16, 65534, "/16 — 클래스 B (65534 호스트)"),
|
||||
(8, 16777214, "/8 — 클래스 A (16M 호스트)"),
|
||||
];
|
||||
|
||||
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(
|
||||
"서브넷 계산기",
|
||||
"예: subnet 192.168.1.0/24 또는 subnet 10.0.0.5 24",
|
||||
null, null, Symbol: "\uE968"));
|
||||
|
||||
// CIDR 참조표 일부
|
||||
foreach (var (cidrLen, hosts, desc) in CidrRef.Take(6))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
desc,
|
||||
$"호스트: {hosts:N0}개",
|
||||
null,
|
||||
("copy", desc),
|
||||
Symbol: "\uE968"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// "range" 서브커맨드
|
||||
if (q.StartsWith("range ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var rangeStr = q[6..].Trim();
|
||||
items.AddRange(BuildRangeItems(rangeStr));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 서브넷 마스크 입력 (예: 255.255.255.0)
|
||||
if (TryParseMask(q, out var cidrFromMask))
|
||||
{
|
||||
items.Add(new LauncherItem(
|
||||
$"CIDR: /{cidrFromMask}",
|
||||
$"서브넷 마스크 {q} = /{cidrFromMask}",
|
||||
null,
|
||||
("copy", $"/{cidrFromMask}"),
|
||||
Symbol: "\uE968"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// CIDR 파싱: "IP/prefix" 또는 "IP prefix"
|
||||
if (!TryParseCidr(q, out var ip, out var prefix))
|
||||
{
|
||||
items.Add(new LauncherItem("형식 오류", "예: 192.168.1.0/24", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
items.AddRange(BuildSubnetItems(ip, prefix));
|
||||
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("Subnet", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 서브넷 계산 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildSubnetItems(uint ip, int prefix)
|
||||
{
|
||||
var mask = prefix == 0 ? 0u : ~0u << (32 - prefix);
|
||||
var network = ip & mask;
|
||||
var broadcast = network | ~mask;
|
||||
var firstHost = prefix < 31 ? network + 1 : network;
|
||||
var lastHost = prefix < 31 ? broadcast - 1 : broadcast;
|
||||
var hostCount = prefix >= 31 ? (1u << (32 - prefix))
|
||||
: (broadcast - network - 1);
|
||||
|
||||
var summaryText = $"""
|
||||
네트워크: {ToIp(network)}/{prefix}
|
||||
서브넷마스크: {ToIp(mask)}
|
||||
첫 호스트: {ToIp(firstHost)}
|
||||
마지막 호스트: {ToIp(lastHost)}
|
||||
브로드캐스트: {ToIp(broadcast)}
|
||||
사용 가능 호스트: {hostCount:N0}개
|
||||
""";
|
||||
|
||||
yield return new LauncherItem(
|
||||
$"{ToIp(network)}/{prefix}",
|
||||
$"호스트 {hostCount:N0}개 · 전체 복사: Enter",
|
||||
null,
|
||||
("copy", summaryText),
|
||||
Symbol: "\uE968");
|
||||
|
||||
yield return new LauncherItem("서브넷 마스크", ToIp(mask), null, ("copy", ToIp(mask)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("네트워크 주소", ToIp(network), null, ("copy", ToIp(network)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("브로드캐스트", ToIp(broadcast), null, ("copy", ToIp(broadcast)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("첫 호스트", ToIp(firstHost), null, ("copy", ToIp(firstHost)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("마지막 호스트", ToIp(lastHost), null, ("copy", ToIp(lastHost)), Symbol: "\uE968");
|
||||
yield return new LauncherItem(
|
||||
"사용 가능 호스트",
|
||||
$"{hostCount:N0}개",
|
||||
null,
|
||||
("copy", hostCount.ToString()),
|
||||
Symbol: "\uE968");
|
||||
|
||||
// 입력 IP가 이 서브넷에 속하는지 확인
|
||||
if ((ip & mask) == network && ip != network && ip != broadcast)
|
||||
{
|
||||
yield return new LauncherItem(
|
||||
$"입력 IP {ToIp(ip)}",
|
||||
"이 서브넷에 속함 ✓",
|
||||
null, null, Symbol: "\uE73E");
|
||||
}
|
||||
|
||||
// 이진 표현
|
||||
yield return new LauncherItem(
|
||||
"이진 마스크",
|
||||
ToBinary(mask),
|
||||
null,
|
||||
("copy", ToBinary(mask)),
|
||||
Symbol: "\uE8C4");
|
||||
}
|
||||
|
||||
private static IEnumerable<LauncherItem> BuildRangeItems(string rangeStr)
|
||||
{
|
||||
// "192.168.1.10-50" 또는 "192.168.1.10-192.168.1.50"
|
||||
var parts = rangeStr.Split('-', 2);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
yield return new LauncherItem("형식 오류", "예: 192.168.1.10-50", null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!TryParseIp(parts[0].Trim(), out var startIp))
|
||||
{
|
||||
yield return new LauncherItem("IP 형식 오류", parts[0], null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
uint endIp;
|
||||
if (uint.TryParse(parts[1].Trim(), out var lastOctet))
|
||||
{
|
||||
endIp = (startIp & 0xFFFFFF00) | lastOctet;
|
||||
}
|
||||
else if (!TryParseIp(parts[1].Trim(), out endIp))
|
||||
{
|
||||
yield return new LauncherItem("IP 형식 오류", parts[1], null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (endIp < startIp)
|
||||
{
|
||||
yield return new LauncherItem("오류", "끝 IP가 시작 IP보다 작습니다", null, null, Symbol: "\uE783");
|
||||
yield break;
|
||||
}
|
||||
|
||||
var count = endIp - startIp + 1;
|
||||
yield return new LauncherItem(
|
||||
$"{ToIp(startIp)} — {ToIp(endIp)}",
|
||||
$"{count}개 IP",
|
||||
null,
|
||||
("copy", $"{ToIp(startIp)} - {ToIp(endIp)} ({count}개)"),
|
||||
Symbol: "\uE968");
|
||||
|
||||
yield return new LauncherItem("시작 IP", ToIp(startIp), null, ("copy", ToIp(startIp)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("끝 IP", ToIp(endIp), null, ("copy", ToIp(endIp)), Symbol: "\uE968");
|
||||
yield return new LauncherItem("IP 수", $"{count:N0}개", null, ("copy", count.ToString()), Symbol: "\uE968");
|
||||
}
|
||||
|
||||
// ── 파싱 헬퍼 ────────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryParseCidr(string s, out uint ip, out int prefix)
|
||||
{
|
||||
ip = 0; prefix = 24;
|
||||
|
||||
// "IP/prefix" 형식
|
||||
var slashIdx = s.IndexOf('/');
|
||||
if (slashIdx >= 0)
|
||||
{
|
||||
if (!TryParseIp(s[..slashIdx].Trim(), out ip)) return false;
|
||||
if (!int.TryParse(s[(slashIdx + 1)..].Trim(), out prefix)) return false;
|
||||
prefix = Math.Clamp(prefix, 0, 32);
|
||||
return true;
|
||||
}
|
||||
|
||||
// "IP prefix" 형식 (공백 구분)
|
||||
var spaceIdx = s.LastIndexOf(' ');
|
||||
if (spaceIdx >= 0 && int.TryParse(s[(spaceIdx + 1)..].Trim(), out var p))
|
||||
{
|
||||
prefix = Math.Clamp(p, 0, 32);
|
||||
return TryParseIp(s[..spaceIdx].Trim(), out ip);
|
||||
}
|
||||
|
||||
// IP만 입력 → /24 기본
|
||||
return TryParseIp(s.Trim(), out ip);
|
||||
}
|
||||
|
||||
private static bool TryParseIp(string s, out uint ip)
|
||||
{
|
||||
ip = 0;
|
||||
if (!IPAddress.TryParse(s, out var addr)) return false;
|
||||
if (addr.AddressFamily != System.Net.Sockets.AddressFamily.InterNetwork) return false;
|
||||
var bytes = addr.GetAddressBytes();
|
||||
ip = ((uint)bytes[0] << 24) | ((uint)bytes[1] << 16)
|
||||
| ((uint)bytes[2] << 8) | (uint)bytes[3];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseMask(string s, out int prefix)
|
||||
{
|
||||
prefix = 0;
|
||||
if (!TryParseIp(s, out var mask)) return false;
|
||||
if (mask == 0) { prefix = 0; return true; }
|
||||
|
||||
// 유효한 서브넷 마스크인지 확인 (연속된 1 뒤에 0)
|
||||
var inverted = ~mask;
|
||||
if ((inverted & (inverted + 1)) != 0) return false;
|
||||
|
||||
prefix = 0;
|
||||
var tmp = mask;
|
||||
while ((tmp & 0x80000000) != 0) { prefix++; tmp <<= 1; }
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string ToIp(uint ip) =>
|
||||
$"{ip >> 24}.{(ip >> 16) & 0xFF}.{(ip >> 8) & 0xFF}.{ip & 0xFF}";
|
||||
|
||||
private static string ToBinary(uint val) =>
|
||||
$"{Convert.ToString((int)(val >> 16), 2).PadLeft(16, '0')} {Convert.ToString((int)(val & 0xFFFF), 2).PadLeft(16, '0')}";
|
||||
}
|
||||
Reference in New Issue
Block a user