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개를 확인했습니다.
This commit is contained in:
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