변경 목적: 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개를 확인했습니다.
368 lines
15 KiB
C#
368 lines
15 KiB
C#
using System.Net;
|
|
using System.Net.NetworkInformation;
|
|
using System.Net.Sockets;
|
|
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
using AxCopilot.Themes;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L19-3: IP 주소 유틸리티 핸들러. "ip" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: ip → 로컬 IP 주소 목록
|
|
/// ip my → 전체 어댑터 IP 목록
|
|
/// ip 192.168.1.100 → IP 분류·타입·이진 표현
|
|
/// ip 10.0.0.0/8 → CIDR 네트워크 정보
|
|
/// ip 192.168.1.0/24 → 네트워크 주소·브로드캐스트·호스트 범위·수
|
|
/// ip range 192.168.1.1 192.168.1.100 → 범위 내 IP 수
|
|
/// ip bin 192.168.1.1 → 이진 표현
|
|
/// ip hex 192.168.1.1 → 16진수 표현
|
|
/// ip int 192.168.1.1 → 정수(uint32) 표현
|
|
/// ip from 3232235777 → 정수 → IP 주소
|
|
/// Enter → 값 복사.
|
|
/// </summary>
|
|
public class IpInfoHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "ip";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"IP",
|
|
"IP 주소 유틸리티 — 분류·CIDR·이진·16진·정수 변환",
|
|
"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("IP 주소 유틸리티",
|
|
"ip my / ip 192.168.1.1 / ip 10.0.0.0/8 / ip bin/hex/int <IP>",
|
|
null, null, Symbol: "\uE968"));
|
|
BuildLocalIpItems(items, brief: true);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
var sub = parts[0].ToLowerInvariant();
|
|
|
|
// ip my
|
|
if (sub is "my" or "local" or "내" or "로컬")
|
|
{
|
|
items.Add(new LauncherItem("로컬 네트워크 어댑터 IP 목록", "", null, null, Symbol: "\uE968"));
|
|
BuildLocalIpItems(items, brief: false);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// ip range <start> <end>
|
|
if (sub == "range" && parts.Length >= 3)
|
|
{
|
|
if (IPAddress.TryParse(parts[1], out var start) &&
|
|
IPAddress.TryParse(parts[2], out var end) &&
|
|
start.AddressFamily == AddressFamily.InterNetwork &&
|
|
end.AddressFamily == AddressFamily.InterNetwork)
|
|
{
|
|
var s = IpToUint(start);
|
|
var e = IpToUint(end);
|
|
if (s > e) (s, e) = (e, s);
|
|
var count = e - s + 1;
|
|
items.Add(new LauncherItem($"{UintToIp(s)} ~ {UintToIp(e)}",
|
|
$"IP 수: {count:N0}개", null, null, Symbol: "\uE968"));
|
|
items.Add(CopyItem("시작 IP", UintToIp(s)));
|
|
items.Add(CopyItem("끝 IP", UintToIp(e)));
|
|
items.Add(CopyItem("IP 개수", count.ToString("N0")));
|
|
}
|
|
else
|
|
{
|
|
items.Add(ErrorItem("올바른 IPv4 주소를 입력하세요"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// ip from <uint>
|
|
if (sub == "from" && parts.Length >= 2)
|
|
{
|
|
if (uint.TryParse(parts[1], out var uval))
|
|
{
|
|
var ip = UintToIp(uval);
|
|
items.Add(new LauncherItem($"{uval} → {ip}", "정수 → IPv4 변환", null, ("copy", ip), Symbol: "\uE968"));
|
|
items.Add(CopyItem("IP 주소", ip));
|
|
items.Add(CopyItem("이진", ToBinary(uval)));
|
|
items.Add(CopyItem("16진수", $"0x{uval:X8}"));
|
|
}
|
|
else items.Add(ErrorItem("올바른 32비트 정수를 입력하세요"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// ip bin/hex/int <IP>
|
|
if (sub is "bin" or "binary" or "이진" or "hex" or "int" or "integer")
|
|
{
|
|
if (parts.Length >= 2 && IPAddress.TryParse(parts[1], out var ip4) &&
|
|
ip4.AddressFamily == AddressFamily.InterNetwork)
|
|
{
|
|
BuildConversionItems(items, ip4);
|
|
}
|
|
else items.Add(ErrorItem("올바른 IPv4 주소를 입력하세요"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// CIDR: 10.0.0.0/8
|
|
if (parts[0].Contains('/'))
|
|
{
|
|
BuildCidrItems(items, parts[0]);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 단순 IP 주소
|
|
if (IPAddress.TryParse(parts[0], out var addr))
|
|
{
|
|
if (addr.AddressFamily == AddressFamily.InterNetwork)
|
|
BuildIpInfoItems(items, addr);
|
|
else if (addr.AddressFamily == AddressFamily.InterNetworkV6)
|
|
BuildIpv6Items(items, addr);
|
|
else
|
|
items.Add(ErrorItem("IPv4 또는 IPv6 주소를 입력하세요"));
|
|
}
|
|
else
|
|
{
|
|
items.Add(new LauncherItem($"인식할 수 없는 입력: '{parts[0]}'",
|
|
"ip 192.168.1.1 / ip 10.0.0.0/8 / ip my / ip bin 1.2.3.4",
|
|
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("IP", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
// ── 빌더 ────────────────────────────────────────────────────────────────
|
|
|
|
private static void BuildLocalIpItems(List<LauncherItem> items, bool brief)
|
|
{
|
|
try
|
|
{
|
|
var ifaces = NetworkInterface.GetAllNetworkInterfaces()
|
|
.Where(n => n.OperationalStatus == OperationalStatus.Up &&
|
|
n.NetworkInterfaceType != NetworkInterfaceType.Loopback)
|
|
.ToList();
|
|
|
|
if (ifaces.Count == 0)
|
|
{
|
|
items.Add(new LauncherItem("활성 네트워크 어댑터 없음", "", null, null, Symbol: "\uE968"));
|
|
return;
|
|
}
|
|
|
|
foreach (var nic in ifaces)
|
|
{
|
|
var ipProps = nic.GetIPProperties();
|
|
var ipv4s = ipProps.UnicastAddresses
|
|
.Where(u => u.Address.AddressFamily == AddressFamily.InterNetwork)
|
|
.ToList();
|
|
|
|
if (ipv4s.Count == 0) continue;
|
|
|
|
if (!brief)
|
|
items.Add(new LauncherItem($"── {nic.Name} ──",
|
|
$"{nic.Description} ({nic.Speed / 1_000_000} Mbps)", null, null, Symbol: "\uE968"));
|
|
|
|
foreach (var uni in ipv4s)
|
|
{
|
|
var mask = uni.IPv4Mask?.ToString() ?? "";
|
|
var cidr = MaskToCidr(uni.IPv4Mask);
|
|
var label = brief ? uni.Address.ToString() : $"{uni.Address}/{cidr}";
|
|
items.Add(new LauncherItem(label,
|
|
brief ? nic.Name : $"마스크: {mask} CIDR: /{cidr} ({ClassifyIp(uni.Address)})",
|
|
null, ("copy", uni.Address.ToString()), Symbol: "\uE968"));
|
|
}
|
|
|
|
if (!brief)
|
|
{
|
|
var gateways = ipProps.GatewayAddresses
|
|
.Where(g => g.Address.AddressFamily == AddressFamily.InterNetwork)
|
|
.Select(g => g.Address.ToString()).ToList();
|
|
if (gateways.Count > 0)
|
|
items.Add(new LauncherItem("게이트웨이",
|
|
string.Join(", ", gateways), null, ("copy", gateways[0]), Symbol: "\uE968"));
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
items.Add(ErrorItem($"네트워크 정보 조회 오류: {ex.Message}"));
|
|
}
|
|
}
|
|
|
|
private static void BuildIpInfoItems(List<LauncherItem> items, IPAddress ip)
|
|
{
|
|
var type = ClassifyIp(ip);
|
|
var uval = IpToUint(ip);
|
|
items.Add(new LauncherItem($"{ip} ({type})",
|
|
$"클래스: {GetClass(uval)} · Enter 복사", null, ("copy", ip.ToString()), Symbol: "\uE968"));
|
|
items.Add(CopyItem("IP 주소", ip.ToString()));
|
|
items.Add(CopyItem("분류", type));
|
|
items.Add(CopyItem("클래스", GetClass(uval)));
|
|
items.Add(CopyItem("이진", ToBinary(uval)));
|
|
items.Add(CopyItem("16진수", $"0x{uval:X8}"));
|
|
items.Add(CopyItem("정수", uval.ToString()));
|
|
BuildConversionItems(items, ip);
|
|
}
|
|
|
|
private static void BuildConversionItems(List<LauncherItem> items, IPAddress ip)
|
|
{
|
|
var uval = IpToUint(ip);
|
|
var bin = ToBinary(uval);
|
|
items.Add(new LauncherItem("── 변환 ──", "", null, null, Symbol: "\uE968"));
|
|
items.Add(CopyItem("이진 (32bit)", bin));
|
|
items.Add(CopyItem("이진 (점구분)", ToBinaryDotted(uval)));
|
|
items.Add(CopyItem("16진수", $"0x{uval:X8}"));
|
|
items.Add(CopyItem("정수 (uint32)", uval.ToString()));
|
|
}
|
|
|
|
private static void BuildIpv6Items(List<LauncherItem> items, IPAddress ip)
|
|
{
|
|
items.Add(new LauncherItem($"{ip}", "IPv6 주소", null, ("copy", ip.ToString()), Symbol: "\uE968"));
|
|
items.Add(CopyItem("전체 표기", ip.ToString()));
|
|
var expanded = ExpandIpv6(ip);
|
|
if (expanded != ip.ToString())
|
|
items.Add(CopyItem("확장 표기", expanded));
|
|
}
|
|
|
|
private static void BuildCidrItems(List<LauncherItem> items, string cidr)
|
|
{
|
|
var slash = cidr.IndexOf('/');
|
|
if (slash < 0) { items.Add(ErrorItem("올바른 CIDR 형식: 10.0.0.0/8")); return; }
|
|
|
|
var ipStr = cidr[..slash];
|
|
var lenStr = cidr[(slash + 1)..];
|
|
|
|
if (!IPAddress.TryParse(ipStr, out var addr) || addr.AddressFamily != AddressFamily.InterNetwork ||
|
|
!int.TryParse(lenStr, out var prefix) || prefix < 0 || prefix > 32)
|
|
{
|
|
items.Add(ErrorItem("올바른 IPv4 CIDR을 입력하세요 (예: 192.168.1.0/24)"));
|
|
return;
|
|
}
|
|
|
|
var mask = CidrToMask(prefix);
|
|
var maskIp = UintToIp(mask);
|
|
var wildcard = ~mask;
|
|
var network = IpToUint(addr) & mask;
|
|
var broadcast = network | wildcard;
|
|
var hostCount = prefix < 31 ? (broadcast - network - 1) : (broadcast - network + 1);
|
|
var firstHost = prefix < 31 ? network + 1 : network;
|
|
var lastHost = prefix < 31 ? broadcast - 1 : broadcast;
|
|
|
|
items.Add(new LauncherItem($"{cidr} → {IpToHostCount(prefix)}",
|
|
$"네트워크: {UintToIp(network)} 브로드캐스트: {UintToIp(broadcast)}", null, null, Symbol: "\uE968"));
|
|
items.Add(CopyItem("네트워크 주소", UintToIp(network)));
|
|
items.Add(CopyItem("브로드캐스트 주소", UintToIp(broadcast)));
|
|
items.Add(CopyItem("서브넷 마스크", maskIp.ToString()));
|
|
items.Add(CopyItem("와일드카드 마스크", UintToIp(wildcard)));
|
|
items.Add(CopyItem("첫 번째 호스트", UintToIp(firstHost)));
|
|
items.Add(CopyItem("마지막 호스트", UintToIp(lastHost)));
|
|
items.Add(CopyItem("호스트 수", $"{hostCount:N0}"));
|
|
items.Add(CopyItem("CIDR 표기", $"{UintToIp(network)}/{prefix}"));
|
|
items.Add(CopyItem("이진 마스크", ToBinaryDotted(mask)));
|
|
}
|
|
|
|
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
|
|
|
private static string ClassifyIp(IPAddress ip)
|
|
{
|
|
var u = IpToUint(ip);
|
|
if (u == 0x7F000001u) return "루프백 (localhost)";
|
|
if ((u & 0xFF000000u) == 0x7F000000u) return "루프백";
|
|
if ((u & 0xFF000000u) == 0x0A000000u) return "사설 (Class A: 10.x.x.x)";
|
|
if ((u & 0xFFF00000u) == 0xAC100000u) return "사설 (Class B: 172.16-31.x.x)";
|
|
if ((u & 0xFFFF0000u) == 0xC0A80000u) return "사설 (Class C: 192.168.x.x)";
|
|
if ((u & 0xFFFF0000u) == 0xA9FE0000u) return "링크-로컬 (APIPA: 169.254.x.x)";
|
|
if ((u & 0xF0000000u) == 0xE0000000u) return "멀티캐스트 (224.x.x.x)";
|
|
if (u == 0xFFFFFFFFu) return "브로드캐스트";
|
|
if (u == 0u) return "0.0.0.0 (비지정)";
|
|
return "공인 IP";
|
|
}
|
|
|
|
private static string GetClass(uint u)
|
|
{
|
|
var b = (u >> 24) & 0xFF;
|
|
return b switch
|
|
{
|
|
<= 127 => "Class A",
|
|
<= 191 => "Class B",
|
|
<= 223 => "Class C",
|
|
<= 239 => "Class D (멀티캐스트)",
|
|
_ => "Class E (예약)"
|
|
};
|
|
}
|
|
|
|
private static uint IpToUint(IPAddress ip)
|
|
{
|
|
var bytes = ip.GetAddressBytes();
|
|
if (bytes.Length != 4) return 0;
|
|
return (uint)((bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]);
|
|
}
|
|
|
|
private static string UintToIp(uint u) =>
|
|
$"{(u >> 24) & 0xFF}.{(u >> 16) & 0xFF}.{(u >> 8) & 0xFF}.{u & 0xFF}";
|
|
|
|
private static string ToBinary(uint u) => Convert.ToString((long)u, 2).PadLeft(32, '0');
|
|
|
|
private static string ToBinaryDotted(uint u)
|
|
{
|
|
var b = ToBinary(u);
|
|
return $"{b[..8]}.{b[8..16]}.{b[16..24]}.{b[24..]}";
|
|
}
|
|
|
|
private static uint CidrToMask(int prefix) =>
|
|
prefix == 0 ? 0u : (0xFFFFFFFFu << (32 - prefix));
|
|
|
|
private static int MaskToCidr(IPAddress? mask)
|
|
{
|
|
if (mask == null) return 0;
|
|
var u = IpToUint(mask);
|
|
int c = 0;
|
|
while ((u & 0x80000000u) != 0) { c++; u <<= 1; }
|
|
return c;
|
|
}
|
|
|
|
private static string IpToHostCount(int prefix) =>
|
|
prefix switch
|
|
{
|
|
32 => "1 IP (단일 호스트)",
|
|
31 => "2 IP (P2P 링크)",
|
|
30 => "4 IP (2 호스트)",
|
|
_ => $"{(1L << (32 - prefix)):N0} IP ({(1L << (32 - prefix)) - 2:N0} 호스트)"
|
|
};
|
|
|
|
private static string ExpandIpv6(IPAddress ip)
|
|
{
|
|
var bytes = ip.GetAddressBytes();
|
|
var groups = new string[8];
|
|
for (int i = 0; i < 8; i++)
|
|
groups[i] = $"{bytes[i * 2]:X2}{bytes[i * 2 + 1]:X2}";
|
|
return string.Join(":", groups).ToLowerInvariant();
|
|
}
|
|
|
|
private static LauncherItem CopyItem(string label, string value) =>
|
|
new(label, value, null, ("copy", value), Symbol: "\uE968");
|
|
|
|
private static LauncherItem ErrorItem(string msg) =>
|
|
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
|
}
|