[Phase L19] Calc·Timer·IP·NPM 핸들러 4종 추가
CalcHandler (prefix=calc, 170줄): - 삼각함수: sin·cos·tan·asin·acos·atan (도/라디안 변환 포함) - 지수·로그: sqrt·cbrt·log·log2·ln·exp·pow - 정수 연산: factorial(0~20)·GCD·LCM·mod - 기타: abs·ceil·floor·round·sign·deg·rad·상수(π·e·φ) - 결과에 과학적 표기법·log₁₀ 부가정보 동시 표시 TimerHandler (prefix=timer, 215줄): - 파싱 형식: 30(초)·5m(분)·1h30m(복합) 지원 - 정적 레지스트리로 복수 타이머 동시 운영 (ID 기반 취소) - Task.Delay 백그라운드 + NotificationService.Notify 완료 알림 - timer stop / timer stop <id> 취소 명령 - Lock→object 수정 (.NET 8 호환) IpInfoHandler (prefix=ip, 260줄): - ip my: NetworkInterface 전 어댑터 IPv4·마스크·게이트웨이 - ip <addr>: 분류(사설/공인/루프백/APIPA/멀티캐스트)·클래스 A~E·이진·16진·정수 - ip <addr>/prefix: CIDR 네트워크·브로드캐스트·와일드카드·호스트 범위·수 - ip range <s> <e>: IP 범위 계산 - ip bin/hex/int/from 변환 서브커맨드 NpmHandler (prefix=npm, 285줄): - init·install·uninstall·run·build·test·update·list·audit·publish - scripts·global·clean·ci·lock 서브커맨드 - npm/yarn/pnpm 3종 동등 명령 동시 표시 - devDependencies·전역 설치 옵션 분리 표시 - 빌드: 경고 0, 오류 0
This commit is contained in:
@@ -373,3 +373,16 @@ public record HotkeyAssignment(string HotkeyStr, string TargetPath, string Label
|
||||
| L18-2 | **텍스트 케이스 변환기** ✅ | `text` 프리픽스. 클립보드 자동 읽기. 13가지 케이스 일괄 표시: camelCase·PascalCase·snake_case·SCREAMING_SNAKE·kebab-case·URL slug·dot.case·UPPER·lower·Title·Sentence·뒤집기·trim. 인라인 입력(`text camel hello world`) 지원. Tokenize(): camelCase 경계 분리 + 구분자 정규화. ToSlug(): 유니코드 정규화(NFC→ASCII). [GeneratedRegex] 소스 생성기 | 높음 |
|
||||
| L18-3 | **화면 비율·해상도 계산기** ✅ | `aspect` 프리픽스. 9개 비율 프리셋 내장(16:9·4:3·21:9·1:1·9:16·3:2·2:1·5:4·2.35:1). `aspect 1920 1080` → GCD 약분 비율 계산·MP 표시. `aspect 16:9 1280` 너비 기준 높이 계산. `aspect 16:9 h 720` 높이 기준 너비 계산. `aspect crop 1920 1080 4:3` 크롭 영역+FFmpeg crop 파라미터. 소수 비율(2.35:1) 지원 | 높음 |
|
||||
| L18-4 | **IT·개발 약어 사전** ✅ | `abbr` 프리픽스. 150개+ 내장 약어(웹/네트워크·개발·DB·보안·클라우드·AI·데이터형식·협업 8개 카테고리). 정확 일치 → 약어/원문/설명/카테고리 상세 표시. 부분 일치 → 목록 표시. `abbr 클라우드` 카테고리별 필터. `abbr jwt` → JWT 상세. API/CRUD/REST/JWT/MCP/SOLID/CAP/ACID/OWASP 등 포함 | 높음 |
|
||||
|
||||
---
|
||||
|
||||
## Phase L19 — Calc·Timer·IP·NPM 도구 (v2.1.0) ✅ 완료
|
||||
|
||||
> **방향**: 개발자·네트워크 실용 계산 도구 강화 — 공학 계산기, 백그라운드 타이머, IP 분석, 패키지 매니저 명령어 생성.
|
||||
|
||||
| # | 기능 | 설명 | 우선순위 |
|
||||
|---|------|------|----------|
|
||||
| L19-1 | **공학 계산기** ✅ | `calc` 프리픽스. 삼각함수(sin·cos·tan·asin·acos·atan), 역삼각, 제곱근(sqrt·cbrt), 로그(log·log2·ln·exp), 거듭제곱(pow), 팩토리얼(0~20), GCD·LCM, 절댓값·올림·내림·반올림, 라디안↔도 변환, 상수(π·e·φ). 결과에 과학적 표기법·log₁₀ 부가정보 동시 표시. `calc sin 45` → 0.7071 형식 | 높음 |
|
||||
| L19-2 | **타이머·알람** ✅ | `timer` 프리픽스. `timer 30`(초)·`timer 5m`(분)·`timer 1h30m`(시간+분) 형식 파싱. `timer stop`·`timer stop <id>` 특정 타이머 취소. 정적 타이머 레지스트리로 복수 타이머 동시 운영. `Task.Delay` 백그라운드 실행 + `NotificationService.Notify`로 완료 알림. 실행 중 목록 및 남은 시간 실시간 표시 | 높음 |
|
||||
| L19-3 | **IP 주소 유틸리티** ✅ | `ip` 프리픽스. `ip my` → NetworkInterface 전 어댑터 IPv4·마스크·게이트웨이. `ip 192.168.1.1` → 분류(사설/공인/루프백/APIPA/멀티캐스트)·클래스(A~E)·이진·16진·정수 변환. `ip 10.0.0.0/8` CIDR → 네트워크·브로드캐스트·와일드카드·호스트 범위·수. `ip range <start> <end>` IP 범위 계산. `ip bin/hex/int` 표현 변환. `ip from <uint>` 정수→IP | 높음 |
|
||||
| L19-4 | **npm/yarn/pnpm 명령어 생성기** ✅ | `npm` 프리픽스. `npm install <pkg>` → npm·yarn·pnpm 3종 설치 명령 동시 표시(일반/devDependencies/전역). init·uninstall·run·build·test·update·list·audit·publish·scripts·global·clean·ci·lock 서브커맨드 지원. `npm run dev` → yarn dev / pnpm run dev 동등 명령 비교. Enter로 클립보드 복사 | 높음 |
|
||||
|
||||
@@ -316,6 +316,14 @@ public partial class App : System.Windows.Application
|
||||
commandResolver.RegisterHandler(new AspectHandler());
|
||||
// L18-4: IT·개발 약어 사전 (prefix=abbr)
|
||||
commandResolver.RegisterHandler(new AbbrHandler());
|
||||
// L19-1: 공학 계산기 (prefix=calc)
|
||||
commandResolver.RegisterHandler(new CalcHandler());
|
||||
// L19-2: 타이머·알람 (prefix=timer)
|
||||
commandResolver.RegisterHandler(new TimerHandler());
|
||||
// L19-3: IP 주소 유틸리티 (prefix=ip)
|
||||
commandResolver.RegisterHandler(new IpInfoHandler());
|
||||
// L19-4: npm/yarn/pnpm 명령어 생성기 (prefix=npm)
|
||||
commandResolver.RegisterHandler(new NpmHandler());
|
||||
|
||||
// ─── 플러그인 로드 ────────────────────────────────────────────────────
|
||||
var pluginHost = new PluginHost(settings, commandResolver);
|
||||
|
||||
288
src/AxCopilot/Handlers/CalcHandler.cs
Normal file
288
src/AxCopilot/Handlers/CalcHandler.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L19-1: 공학 계산기 핸들러. "calc" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: calc → 사용법 목록
|
||||
/// calc sin 45 → sin(45°) = 0.7071
|
||||
/// calc cos 60 → cos(60°) = 0.5
|
||||
/// calc tan 45 → tan(45°) = 1.0
|
||||
/// calc sqrt 144 → √144 = 12
|
||||
/// calc log 1000 → log₁₀(1000) = 3
|
||||
/// calc ln 2.718 → ln(2.718) ≈ 1.0
|
||||
/// calc pow 2 10 → 2¹⁰ = 1024
|
||||
/// calc factorial 10 → 10! = 3628800
|
||||
/// calc gcd 12 18 → GCD(12,18) = 6
|
||||
/// calc lcm 4 6 → LCM(4,6) = 12
|
||||
/// calc pi → π = 3.14159265358979
|
||||
/// calc e → e = 2.71828182845905
|
||||
/// calc deg 1.5707 → 라디안 → 도 변환
|
||||
/// calc rad 90 → 도 → 라디안 변환
|
||||
/// calc abs -42 → 절댓값
|
||||
/// calc ceil 3.2 → 올림
|
||||
/// calc floor 3.8 → 내림
|
||||
/// calc round 3.567 2 → 반올림 (소수점 자리)
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public class CalcHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "calc";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Calc",
|
||||
"공학 계산기 — sin·cos·log·sqrt·factorial·GCD·LCM 등",
|
||||
"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("공학 계산기",
|
||||
"calc sin/cos/tan/sqrt/log/ln/pow/factorial/gcd/lcm/pi/e/deg/rad/abs/ceil/floor/round",
|
||||
null, null, Symbol: "\uE8EF"));
|
||||
items.Add(BuildUsage("삼각함수", "calc sin 45 / calc cos 60 / calc tan 30"));
|
||||
items.Add(BuildUsage("제곱근·거듭제곱", "calc sqrt 144 / calc pow 2 10"));
|
||||
items.Add(BuildUsage("로그", "calc log 1000 / calc ln 2.718"));
|
||||
items.Add(BuildUsage("팩토리얼", "calc factorial 12"));
|
||||
items.Add(BuildUsage("GCD · LCM", "calc gcd 12 18 / calc lcm 4 6"));
|
||||
items.Add(BuildUsage("상수", "calc pi / calc e"));
|
||||
items.Add(BuildUsage("단위 변환", "calc deg 1.5707 / calc rad 90"));
|
||||
items.Add(BuildUsage("기타", "calc abs -42 / calc ceil 3.2 / calc floor 3.8 / calc round 3.567 2"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var fn = parts[0].ToLowerInvariant();
|
||||
|
||||
// 단순 상수
|
||||
if (fn is "pi") { var v = Math.PI; return Result(items, "π (Pi)", v, "π"); }
|
||||
if (fn is "e") { var v = Math.E; return Result(items, "e (자연상수)", v, "e"); }
|
||||
if (fn is "phi") { var v = 1.6180339887; return Result(items, "φ (황금비)", v, "φ"); }
|
||||
|
||||
// 단일 인수 함수
|
||||
if (parts.Length >= 2 && double.TryParse(parts[1],
|
||||
System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var a))
|
||||
{
|
||||
switch (fn)
|
||||
{
|
||||
case "sin":
|
||||
return Result(items, $"sin({a}°)", Math.Sin(DegToRad(a)), "sin");
|
||||
|
||||
case "cos":
|
||||
return Result(items, $"cos({a}°)", Math.Cos(DegToRad(a)), "cos");
|
||||
|
||||
case "tan":
|
||||
if (Math.Abs(a % 180 - 90) < 1e-9)
|
||||
{
|
||||
items.Add(ErrorItem("tan(90°±n·180°)는 정의되지 않습니다 (무한대)"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
return Result(items, $"tan({a}°)", Math.Tan(DegToRad(a)), "tan");
|
||||
|
||||
case "asin":
|
||||
if (a < -1 || a > 1) { items.Add(ErrorItem("asin 입력값은 -1 ~ 1 범위여야 합니다")); break; }
|
||||
return Result(items, $"asin({a}) °", RadToDeg(Math.Asin(a)), "asin", isAngle: true);
|
||||
|
||||
case "acos":
|
||||
if (a < -1 || a > 1) { items.Add(ErrorItem("acos 입력값은 -1 ~ 1 범위여야 합니다")); break; }
|
||||
return Result(items, $"acos({a}) °", RadToDeg(Math.Acos(a)), "acos", isAngle: true);
|
||||
|
||||
case "atan":
|
||||
return Result(items, $"atan({a}) °", RadToDeg(Math.Atan(a)), "atan", isAngle: true);
|
||||
|
||||
case "sqrt":
|
||||
if (a < 0) { items.Add(ErrorItem("음수의 실수 제곱근은 정의되지 않습니다")); break; }
|
||||
return Result(items, $"√{a}", Math.Sqrt(a), "sqrt");
|
||||
|
||||
case "cbrt":
|
||||
return Result(items, $"∛{a}", Math.Cbrt(a), "cbrt");
|
||||
|
||||
case "log":
|
||||
if (a <= 0) { items.Add(ErrorItem("log 입력값은 양수여야 합니다")); break; }
|
||||
return Result(items, $"log₁₀({a})", Math.Log10(a), "log10");
|
||||
|
||||
case "log2":
|
||||
if (a <= 0) { items.Add(ErrorItem("log2 입력값은 양수여야 합니다")); break; }
|
||||
return Result(items, $"log₂({a})", Math.Log2(a), "log2");
|
||||
|
||||
case "ln":
|
||||
if (a <= 0) { items.Add(ErrorItem("ln 입력값은 양수여야 합니다")); break; }
|
||||
return Result(items, $"ln({a})", Math.Log(a), "ln");
|
||||
|
||||
case "exp":
|
||||
return Result(items, $"e^{a}", Math.Exp(a), "exp");
|
||||
|
||||
case "factorial":
|
||||
{
|
||||
var n = (long)Math.Round(a);
|
||||
if (n < 0 || n > 20) { items.Add(ErrorItem("팩토리얼은 0~20 범위만 지원합니다")); break; }
|
||||
var fac = Factorial(n);
|
||||
items.Add(new LauncherItem($"{n}! = {fac:N0}", $"팩토리얼 · Enter 복사",
|
||||
null, ("copy", $"{fac}"), Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("결과", $"{fac}", null, ("copy", $"{fac}"), Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
case "abs":
|
||||
return Result(items, $"|{a}|", Math.Abs(a), "abs");
|
||||
|
||||
case "ceil":
|
||||
return Result(items, $"⌈{a}⌉", Math.Ceiling(a), "ceil");
|
||||
|
||||
case "floor":
|
||||
return Result(items, $"⌊{a}⌋", Math.Floor(a), "floor");
|
||||
|
||||
case "round":
|
||||
{
|
||||
int decimals = 0;
|
||||
if (parts.Length >= 3 && int.TryParse(parts[2], out var d)) decimals = Math.Clamp(d, 0, 15);
|
||||
var rounded = Math.Round(a, decimals, MidpointRounding.AwayFromZero);
|
||||
return Result(items, $"round({a}, {decimals})", rounded, "round");
|
||||
}
|
||||
|
||||
case "deg":
|
||||
return Result(items, $"{a} rad → °", RadToDeg(a), "deg", isAngle: true);
|
||||
|
||||
case "rad":
|
||||
return Result(items, $"{a}° → rad", DegToRad(a), "rad");
|
||||
|
||||
case "sign":
|
||||
items.Add(new LauncherItem($"sign({a}) = {Math.Sign(a)}",
|
||||
a > 0 ? "양수" : a < 0 ? "음수" : "0",
|
||||
null, ("copy", $"{Math.Sign(a)}"), Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
}
|
||||
|
||||
// 이인수 함수
|
||||
if (parts.Length >= 3 &&
|
||||
double.TryParse(parts[1], System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var x) &&
|
||||
double.TryParse(parts[2], System.Globalization.NumberStyles.Float,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var y))
|
||||
{
|
||||
switch (fn)
|
||||
{
|
||||
case "pow":
|
||||
return Result(items, $"{x}^{y}", Math.Pow(x, y), "pow");
|
||||
|
||||
case "log":
|
||||
if (x <= 0 || y <= 0 || y == 1) { items.Add(ErrorItem("밑과 진수는 양수, 밑 ≠ 1 이어야 합니다")); break; }
|
||||
return Result(items, $"log_({x}) {y}", Math.Log(y, x), "log");
|
||||
|
||||
case "atan2":
|
||||
return Result(items, $"atan2({x},{y}) °", RadToDeg(Math.Atan2(x, y)), "atan2", isAngle: true);
|
||||
|
||||
case "hypot":
|
||||
return Result(items, $"hypot({x},{y})", Math.Sqrt(x * x + y * y), "hypot");
|
||||
|
||||
case "gcd":
|
||||
{
|
||||
var ga = (long)Math.Abs(Math.Round(x));
|
||||
var gb = (long)Math.Abs(Math.Round(y));
|
||||
var gv = GcdLong(ga, gb);
|
||||
items.Add(new LauncherItem($"GCD({ga}, {gb}) = {gv}", "최대공약수 · Enter 복사",
|
||||
null, ("copy", $"{gv}"), Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("결과", $"{gv}", null, ("copy", $"{gv}"), Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
case "lcm":
|
||||
{
|
||||
var la = (long)Math.Abs(Math.Round(x));
|
||||
var lb = (long)Math.Abs(Math.Round(y));
|
||||
var g = GcdLong(la, lb);
|
||||
var lv = g == 0 ? 0 : la / g * lb;
|
||||
items.Add(new LauncherItem($"LCM({la}, {lb}) = {lv}", "최소공배수 · Enter 복사",
|
||||
null, ("copy", $"{lv}"), Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("결과", $"{lv}", null, ("copy", $"{lv}"), Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
case "mod":
|
||||
if (y == 0) { items.Add(ErrorItem("0으로 나눌 수 없습니다")); break; }
|
||||
return Result(items, $"{x} mod {y}", x % y, "mod");
|
||||
}
|
||||
}
|
||||
|
||||
// 파싱 실패 안내
|
||||
items.Add(new LauncherItem($"알 수 없는 함수: '{fn}'",
|
||||
"calc sin/cos/tan/sqrt/log/ln/pow/factorial/gcd/lcm/abs/ceil/floor/round/deg/rad",
|
||||
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("Calc", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
private static Task<IEnumerable<LauncherItem>> Result(
|
||||
List<LauncherItem> items, string label, double value, string fn, bool isAngle = false)
|
||||
{
|
||||
var formatted = FormatDouble(value);
|
||||
var extra = isAngle ? " °" : "";
|
||||
items.Add(new LauncherItem($"{label} = {formatted}{extra}",
|
||||
$"· Enter 복사", null, ("copy", formatted), Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("결과", $"{formatted}{extra}", null, ("copy", formatted), Symbol: "\uE8EF"));
|
||||
// 추가 정보
|
||||
if (!isAngle && value != 0)
|
||||
{
|
||||
items.Add(new LauncherItem("과학적 표기법", $"{value:E6}", null, ("copy", $"{value:E6}"), Symbol: "\uE8EF"));
|
||||
if (value > 0)
|
||||
items.Add(new LauncherItem("log₁₀ 값", $"{Math.Log10(value):F6}", null, null, Symbol: "\uE8EF"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
private static string FormatDouble(double v)
|
||||
{
|
||||
if (double.IsNaN(v)) return "NaN";
|
||||
if (double.IsPositiveInfinity(v)) return "+∞";
|
||||
if (double.IsNegativeInfinity(v)) return "-∞";
|
||||
// 정수라면 소수점 없이
|
||||
if (v == Math.Floor(v) && Math.Abs(v) < 1e15)
|
||||
return $"{(long)v}";
|
||||
return $"{v:G15}";
|
||||
}
|
||||
|
||||
private static LauncherItem BuildUsage(string title, string example) =>
|
||||
new(title, example, null, null, Symbol: "\uE8EF");
|
||||
|
||||
private static LauncherItem ErrorItem(string msg) =>
|
||||
new(msg, "올바른 입력 형식을 확인하세요", null, null, Symbol: "\uE783");
|
||||
|
||||
private static double DegToRad(double deg) => deg * Math.PI / 180.0;
|
||||
private static double RadToDeg(double rad) => rad * 180.0 / Math.PI;
|
||||
|
||||
private static long Factorial(long n)
|
||||
{
|
||||
long r = 1;
|
||||
for (long i = 2; i <= n; i++) r *= i;
|
||||
return r;
|
||||
}
|
||||
|
||||
private static long GcdLong(long a, long b) => b == 0 ? a : GcdLong(b, a % b);
|
||||
}
|
||||
367
src/AxCopilot/Handlers/IpInfoHandler.cs
Normal file
367
src/AxCopilot/Handlers/IpInfoHandler.cs
Normal file
@@ -0,0 +1,367 @@
|
||||
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");
|
||||
}
|
||||
342
src/AxCopilot/Handlers/NpmHandler.cs
Normal file
342
src/AxCopilot/Handlers/NpmHandler.cs
Normal file
@@ -0,0 +1,342 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L19-4: npm/yarn/pnpm 패키지 매니저 명령어 생성·실행 핸들러. "npm" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: npm → 자주 쓰는 명령어 목록
|
||||
/// npm init → npm/yarn/pnpm init 명령 비교
|
||||
/// npm install lodash → npm/yarn/pnpm install lodash 명령 비교
|
||||
/// npm i lodash → install 단축키
|
||||
/// npm run dev → npm/yarn/pnpm run dev
|
||||
/// npm uninstall lodash → npm/yarn/pnpm uninstall
|
||||
/// npm update → 패키지 업데이트 명령
|
||||
/// npm list → 패키지 목록 명령
|
||||
/// npm audit → 보안 감사 명령
|
||||
/// npm publish → 배포 명령
|
||||
/// npm scripts → package.json scripts 확인 방법
|
||||
/// npm global → 전역 패키지 목록 명령
|
||||
/// npm clean → 캐시·node_modules 정리 명령
|
||||
/// Enter → 클립보드 복사 (또는 터미널에서 실행).
|
||||
/// </summary>
|
||||
public class NpmHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "npm";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"NPM",
|
||||
"npm/yarn/pnpm 명령어 생성기 — install·run·audit·publish·clean",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
private record PmCmd(string Manager, string Cmd, string Description);
|
||||
|
||||
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("npm/yarn/pnpm 명령어 생성기",
|
||||
"npm install / run / init / build / test / audit / publish / clean …",
|
||||
null, null, Symbol: "\uE756"));
|
||||
AddCommandGroups(items);
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
var arg = parts.Length > 1 ? parts[1].Trim() : "";
|
||||
|
||||
// 단축키 정규화
|
||||
sub = sub switch
|
||||
{
|
||||
"i" => "install",
|
||||
"un" => "uninstall",
|
||||
"rm" => "uninstall",
|
||||
"up" => "update",
|
||||
"ls" => "list",
|
||||
_ => sub
|
||||
};
|
||||
|
||||
switch (sub)
|
||||
{
|
||||
case "init":
|
||||
AddSection(items, "프로젝트 초기화", [
|
||||
new("npm", "npm init -y", "기본값으로 package.json 생성"),
|
||||
new("yarn", "yarn init -y", "기본값으로 package.json 생성"),
|
||||
new("pnpm", "pnpm init", "기본값으로 package.json 생성"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "install" when !string.IsNullOrWhiteSpace(arg):
|
||||
AddSection(items, $"패키지 설치: {arg}", [
|
||||
new("npm", $"npm install {arg}", $"npm으로 {arg} 설치"),
|
||||
new("yarn", $"yarn add {arg}", $"yarn으로 {arg} 설치"),
|
||||
new("pnpm", $"pnpm add {arg}", $"pnpm으로 {arg} 설치"),
|
||||
]);
|
||||
AddSection(items, $"개발 의존성으로 설치: {arg}", [
|
||||
new("npm", $"npm install {arg} --save-dev", "devDependencies에 추가"),
|
||||
new("yarn", $"yarn add {arg} --dev", "devDependencies에 추가"),
|
||||
new("pnpm", $"pnpm add {arg} --save-dev", "devDependencies에 추가"),
|
||||
]);
|
||||
AddSection(items, $"전역 설치: {arg}", [
|
||||
new("npm", $"npm install -g {arg}", "전역 설치"),
|
||||
new("yarn", $"yarn global add {arg}", "전역 설치"),
|
||||
new("pnpm", $"pnpm add -g {arg}", "전역 설치"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "install":
|
||||
AddSection(items, "의존성 설치 (node_modules)", [
|
||||
new("npm", "npm install", "package.json 의존성 모두 설치"),
|
||||
new("yarn", "yarn install", "package.json 의존성 모두 설치"),
|
||||
new("pnpm", "pnpm install", "package.json 의존성 모두 설치"),
|
||||
]);
|
||||
AddSection(items, "프로덕션 의존성만", [
|
||||
new("npm", "npm install --production", "--production 플래그"),
|
||||
new("yarn", "yarn install --production", "--production 플래그"),
|
||||
new("pnpm", "pnpm install --prod", "--prod 플래그"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "uninstall" when !string.IsNullOrWhiteSpace(arg):
|
||||
AddSection(items, $"패키지 제거: {arg}", [
|
||||
new("npm", $"npm uninstall {arg}", $"npm으로 {arg} 제거"),
|
||||
new("yarn", $"yarn remove {arg}", $"yarn으로 {arg} 제거"),
|
||||
new("pnpm", $"pnpm remove {arg}", $"pnpm으로 {arg} 제거"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "run" when !string.IsNullOrWhiteSpace(arg):
|
||||
AddSection(items, $"스크립트 실행: {arg}", [
|
||||
new("npm", $"npm run {arg}", $"npm run {arg}"),
|
||||
new("yarn", $"yarn {arg}", $"yarn {arg}"),
|
||||
new("pnpm", $"pnpm run {arg}", $"pnpm run {arg}"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "start":
|
||||
case "run":
|
||||
AddSection(items, "주요 스크립트", [
|
||||
new("npm", "npm start", "start 스크립트 실행"),
|
||||
new("npm", "npm run dev", "dev 스크립트 실행"),
|
||||
new("npm", "npm run build", "build 스크립트 실행"),
|
||||
new("npm", "npm test", "test 스크립트 실행"),
|
||||
]);
|
||||
AddSection(items, "yarn 동등 명령", [
|
||||
new("yarn", "yarn start", "start"),
|
||||
new("yarn", "yarn dev", "dev"),
|
||||
new("yarn", "yarn build", "build"),
|
||||
new("yarn", "yarn test", "test"),
|
||||
]);
|
||||
AddSection(items, "pnpm 동등 명령", [
|
||||
new("pnpm", "pnpm start", "start"),
|
||||
new("pnpm", "pnpm run dev", "dev"),
|
||||
new("pnpm", "pnpm run build", "build"),
|
||||
new("pnpm", "pnpm test", "test"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "build":
|
||||
AddSection(items, "빌드", [
|
||||
new("npm", "npm run build", "build 스크립트"),
|
||||
new("yarn", "yarn build", "build 스크립트"),
|
||||
new("pnpm", "pnpm run build", "build 스크립트"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "test":
|
||||
AddSection(items, "테스트", [
|
||||
new("npm", "npm test", "test 스크립트"),
|
||||
new("npm", "npm run test:watch", "테스트 와처"),
|
||||
new("yarn", "yarn test", "test 스크립트"),
|
||||
new("pnpm", "pnpm test", "test 스크립트"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "update" or "upgrade":
|
||||
AddSection(items, "패키지 업데이트", [
|
||||
new("npm", "npm update", "모든 패키지 업데이트"),
|
||||
new("npm", "npm outdated", "오래된 패키지 확인"),
|
||||
new("npm", "npx npm-check-updates", "버전 업그레이드 체크"),
|
||||
new("yarn", "yarn upgrade", "모든 패키지 업그레이드"),
|
||||
new("pnpm", "pnpm update", "모든 패키지 업데이트"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "list" or "ls":
|
||||
AddSection(items, "패키지 목록", [
|
||||
new("npm", "npm list", "설치된 패키지 목록"),
|
||||
new("npm", "npm list --depth=0", "최상위 패키지만"),
|
||||
new("npm", "npm list -g --depth=0", "전역 패키지 목록"),
|
||||
new("yarn", "yarn list", "설치된 패키지 목록"),
|
||||
new("pnpm", "pnpm list", "설치된 패키지 목록"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "audit":
|
||||
AddSection(items, "보안 감사", [
|
||||
new("npm", "npm audit", "보안 취약점 스캔"),
|
||||
new("npm", "npm audit fix", "자동 수정"),
|
||||
new("yarn", "yarn audit", "보안 취약점 스캔"),
|
||||
new("pnpm", "pnpm audit", "보안 취약점 스캔"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "publish":
|
||||
AddSection(items, "패키지 배포", [
|
||||
new("npm", "npm publish", "npm 레지스트리에 배포"),
|
||||
new("npm", "npm publish --access public","공개 패키지로 배포"),
|
||||
new("npm", "npm version patch", "패치 버전 올리기"),
|
||||
new("npm", "npm version minor", "마이너 버전 올리기"),
|
||||
new("npm", "npm version major", "메이저 버전 올리기"),
|
||||
new("yarn", "yarn publish", "yarn으로 배포"),
|
||||
new("pnpm", "pnpm publish", "pnpm으로 배포"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "scripts":
|
||||
AddSection(items, "package.json 스크립트 확인", [
|
||||
new("npm", "npm run", "스크립트 목록 보기"),
|
||||
new("npm", "cat package.json", "package.json 확인"),
|
||||
new("yarn", "yarn run", "스크립트 목록 보기"),
|
||||
new("pnpm", "pnpm run", "스크립트 목록 보기"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "global":
|
||||
AddSection(items, "전역 패키지", [
|
||||
new("npm", "npm list -g --depth=0", "전역 패키지 목록"),
|
||||
new("npm", "npm root -g", "전역 node_modules 경로"),
|
||||
new("yarn", "yarn global list", "전역 패키지 목록"),
|
||||
new("pnpm", "pnpm list -g", "전역 패키지 목록"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "clean":
|
||||
AddSection(items, "캐시 및 모듈 정리", [
|
||||
new("npm", "npm cache clean --force", "npm 캐시 삭제"),
|
||||
new("npm", "Remove-Item -Recurse -Force node_modules", "node_modules 삭제 (PowerShell)"),
|
||||
new("npm", "rm -rf node_modules && npm install", "재설치 (bash)"),
|
||||
new("yarn", "yarn cache clean", "yarn 캐시 삭제"),
|
||||
new("pnpm", "pnpm store prune", "pnpm 스토어 정리"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "ci":
|
||||
AddSection(items, "CI/CD용 클린 설치", [
|
||||
new("npm", "npm ci", "package-lock.json 기반 클린 설치"),
|
||||
new("yarn", "yarn install --frozen-lockfile", "lockfile 기반 클린 설치"),
|
||||
new("pnpm", "pnpm install --frozen-lockfile", "lockfile 기반 클린 설치"),
|
||||
]);
|
||||
break;
|
||||
|
||||
case "lock":
|
||||
AddSection(items, "lockfile 관리", [
|
||||
new("npm", "npm install --package-lock-only", "lockfile만 갱신"),
|
||||
new("yarn", "yarn import", "package-lock → yarn.lock 변환"),
|
||||
new("pnpm", "pnpm import", "다른 lockfile → pnpm-lock.yaml 변환"),
|
||||
]);
|
||||
break;
|
||||
|
||||
default:
|
||||
items.Add(new LauncherItem($"알 수 없는 명령: '{sub}'",
|
||||
"init · install · uninstall · run · build · test · update · list · audit · publish · clean · global",
|
||||
null, null, Symbol: "\uE783"));
|
||||
AddCommandGroups(items);
|
||||
break;
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
switch (item.Data)
|
||||
{
|
||||
case ("copy", string text):
|
||||
try
|
||||
{
|
||||
System.Windows.Application.Current.Dispatcher.Invoke(
|
||||
() => Clipboard.SetText(text));
|
||||
NotificationService.Notify("NPM", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { }
|
||||
break;
|
||||
|
||||
case ("run", string cmd):
|
||||
RunInTerminal(cmd);
|
||||
break;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 헬퍼 ────────────────────────────────────────────────────────────────
|
||||
|
||||
private static void AddCommandGroups(List<LauncherItem> items)
|
||||
{
|
||||
var groups = new (string Title, string[] Keys)[]
|
||||
{
|
||||
("초기화·설치", ["npm init", "npm install", "npm install <패키지>"]),
|
||||
("개발 워크플로우", ["npm start", "npm run dev", "npm run build", "npm test"]),
|
||||
("패키지 관리", ["npm update", "npm uninstall <패키지>", "npm list"]),
|
||||
("보안·배포", ["npm audit", "npm audit fix", "npm publish"]),
|
||||
("캐시·정리", ["npm cache clean --force", "npm ci"]),
|
||||
};
|
||||
foreach (var (title, keys) in groups)
|
||||
items.Add(new LauncherItem(title, string.Join(" / ", keys), null, null, Symbol: "\uE756"));
|
||||
}
|
||||
|
||||
private static void AddSection(List<LauncherItem> items, string title, PmCmd[] cmds)
|
||||
{
|
||||
items.Add(new LauncherItem($"── {title} ──", "", null, null, Symbol: "\uE756"));
|
||||
foreach (var c in cmds)
|
||||
{
|
||||
var icon = c.Manager switch
|
||||
{
|
||||
"yarn" => "🧶",
|
||||
"pnpm" => "📦",
|
||||
_ => "📦"
|
||||
};
|
||||
items.Add(new LauncherItem(c.Cmd, $"{c.Manager} · {c.Description} · Enter 복사",
|
||||
null, ("copy", c.Cmd), Symbol: "\uE756"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void RunInTerminal(string cmd)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Windows Terminal 우선
|
||||
var wtPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
@"Microsoft\WindowsApps\wt.exe");
|
||||
if (File.Exists(wtPath))
|
||||
{
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = wtPath,
|
||||
Arguments = $"cmd /k \"{cmd}\"",
|
||||
UseShellExecute = true
|
||||
});
|
||||
return;
|
||||
}
|
||||
// cmd 폴백
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/k {cmd}",
|
||||
UseShellExecute = true
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
270
src/AxCopilot/Handlers/TimerHandler.cs
Normal file
270
src/AxCopilot/Handlers/TimerHandler.cs
Normal file
@@ -0,0 +1,270 @@
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L19-2: 타이머·알람 핸들러. "timer" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: timer → 사용법 + 실행 중인 타이머 목록
|
||||
/// timer 30 → 30초 타이머 (Enter로 시작)
|
||||
/// timer 5m → 5분 타이머
|
||||
/// timer 1h30m → 1시간 30분 타이머
|
||||
/// timer 2h → 2시간 타이머
|
||||
/// timer 10m30s → 10분 30초 타이머
|
||||
/// timer stop → 모든 타이머 취소
|
||||
/// timer stop <id> → 특정 타이머 취소
|
||||
/// Enter → 타이머 시작 (또는 중단).
|
||||
/// </summary>
|
||||
public class TimerHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "timer";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Timer",
|
||||
"타이머·알람 — 초/분/시간 단위 백그라운드 타이머",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// 타이머 레코드
|
||||
private record TimerEntry(int Id, string Label, TimeSpan Duration, DateTime StartAt, CancellationTokenSource Cts);
|
||||
|
||||
// 정적 타이머 레지스트리
|
||||
private static readonly List<TimerEntry> _timers = [];
|
||||
private static readonly object _lock = new();
|
||||
private static int _nextId = 1;
|
||||
|
||||
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("타이머·알람",
|
||||
"timer 30 / timer 5m / timer 1h30m / timer stop",
|
||||
null, null, Symbol: "\uE916"));
|
||||
items.Add(new LauncherItem("사용법", "timer <시간> · 예: 30(초) / 5m / 1h30m / 2h", null, null, Symbol: "\uE916"));
|
||||
AddRunningTimers(items);
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
var sub = parts[0].ToLowerInvariant();
|
||||
|
||||
// stop 명령
|
||||
if (sub is "stop" or "cancel" or "취소")
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_timers.Count == 0)
|
||||
{
|
||||
items.Add(new LauncherItem("실행 중인 타이머 없음", "취소할 타이머가 없습니다",
|
||||
null, null, Symbol: "\uE916"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 특정 ID 취소
|
||||
if (parts.Length >= 2 && int.TryParse(parts[1], out var stopId))
|
||||
{
|
||||
var t = _timers.FirstOrDefault(x => x.Id == stopId);
|
||||
if (t != null)
|
||||
items.Add(new LauncherItem($"타이머 #{t.Id} '{t.Label}' 취소",
|
||||
$"Enter로 취소합니다", null, ("stop", stopId.ToString()), Symbol: "\uE916"));
|
||||
else
|
||||
items.Add(new LauncherItem($"타이머 #{stopId}를 찾을 수 없습니다",
|
||||
"timer 명령으로 목록을 확인하세요", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 전체 취소
|
||||
items.Add(new LauncherItem($"모든 타이머 취소 ({_timers.Count}개)",
|
||||
"Enter로 모두 취소합니다", null, ("stop_all", ""), Symbol: "\uE916"));
|
||||
foreach (var t in _timers)
|
||||
items.Add(new LauncherItem($" #{t.Id} {t.Label}",
|
||||
$"시작: {t.StartAt:HH:mm:ss} 남은: {Remaining(t)}",
|
||||
null, ("stop", t.Id.ToString()), Symbol: "\uE916"));
|
||||
}
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 시간 파싱 시도
|
||||
if (TryParseTime(sub, out var duration) && duration > TimeSpan.Zero)
|
||||
{
|
||||
var label = FormatDuration(duration);
|
||||
var endTime = DateTime.Now.Add(duration);
|
||||
items.Add(new LauncherItem($"⏱ {label} 타이머 시작",
|
||||
$"완료: {endTime:HH:mm:ss} · Enter로 시작",
|
||||
null, ("start", DurationToSeconds(duration).ToString()), Symbol: "\uE916"));
|
||||
items.Add(new LauncherItem($"완료 예정", $"{endTime:HH:mm:ss}", null, null, Symbol: "\uE916"));
|
||||
items.Add(new LauncherItem($"경과 시간", label, null, null, Symbol: "\uE916"));
|
||||
|
||||
// 실행 중 타이머 표시
|
||||
AddRunningTimers(items);
|
||||
}
|
||||
else
|
||||
{
|
||||
items.Add(new LauncherItem($"형식 오류: '{q}'",
|
||||
"예: timer 30 / timer 5m / timer 1h30m / timer stop",
|
||||
null, null, Symbol: "\uE783"));
|
||||
}
|
||||
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
||||
{
|
||||
switch (item.Data)
|
||||
{
|
||||
case ("start", string secStr) when long.TryParse(secStr, out var sec):
|
||||
{
|
||||
var duration = TimeSpan.FromSeconds(sec);
|
||||
var label = FormatDuration(duration);
|
||||
var cts = new CancellationTokenSource();
|
||||
int id;
|
||||
lock (_lock)
|
||||
{
|
||||
id = _nextId++;
|
||||
_timers.Add(new TimerEntry(id, label, duration, DateTime.Now, cts));
|
||||
}
|
||||
NotificationService.Notify("Timer", $"⏱ #{id} {label} 타이머 시작됩니다.");
|
||||
_ = RunTimerAsync(id, label, duration, cts.Token);
|
||||
break;
|
||||
}
|
||||
|
||||
case ("stop", string idStr) when int.TryParse(idStr, out var stopId):
|
||||
{
|
||||
TimerEntry? entry = null;
|
||||
lock (_lock)
|
||||
{
|
||||
entry = _timers.FirstOrDefault(t => t.Id == stopId);
|
||||
if (entry != null) _timers.Remove(entry);
|
||||
}
|
||||
if (entry != null)
|
||||
{
|
||||
entry.Cts.Cancel();
|
||||
NotificationService.Notify("Timer", $"⏹ #{entry.Id} '{entry.Label}' 타이머가 취소되었습니다.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ("stop_all", _):
|
||||
{
|
||||
List<TimerEntry> all;
|
||||
lock (_lock)
|
||||
{
|
||||
all = [.._timers];
|
||||
_timers.Clear();
|
||||
}
|
||||
foreach (var t in all) t.Cts.Cancel();
|
||||
NotificationService.Notify("Timer", $"⏹ 타이머 {all.Count}개 모두 취소됐습니다.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 타이머 실행 ──────────────────────────────────────────────────────────
|
||||
|
||||
private static async Task RunTimerAsync(int id, string label, TimeSpan duration, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(duration, token);
|
||||
// 완료 — 레지스트리에서 제거
|
||||
lock (_lock) { _timers.RemoveAll(t => t.Id == id); }
|
||||
NotificationService.Notify("⏰ 타이머 완료", $"#{id} {label} 타이머가 종료되었습니다!");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// 취소됨 — 이미 처리됨
|
||||
}
|
||||
}
|
||||
|
||||
// ── 파싱 헬퍼 ────────────────────────────────────────────────────────────
|
||||
|
||||
private static bool TryParseTime(string s, out TimeSpan ts)
|
||||
{
|
||||
ts = TimeSpan.Zero;
|
||||
s = s.ToLowerInvariant().Trim();
|
||||
|
||||
// 숫자만 → 초
|
||||
if (long.TryParse(s, out var sec))
|
||||
{
|
||||
ts = TimeSpan.FromSeconds(sec);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 복합 형식: 1h30m20s
|
||||
long hours = 0, minutes = 0, seconds = 0;
|
||||
var rem = s;
|
||||
bool found = false;
|
||||
|
||||
if (TryExtractUnit(ref rem, 'h', out var h)) { hours = h; found = true; }
|
||||
if (TryExtractUnit(ref rem, 'm', out var m)) { minutes = m; found = true; }
|
||||
if (TryExtractUnit(ref rem, 's', out var sv)){ seconds = sv; found = true; }
|
||||
|
||||
if (found && rem.Length == 0)
|
||||
{
|
||||
ts = TimeSpan.FromSeconds(hours * 3600 + minutes * 60 + seconds);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryExtractUnit(ref string s, char unit, out long value)
|
||||
{
|
||||
value = 0;
|
||||
var idx = s.IndexOf(unit);
|
||||
if (idx <= 0) return false;
|
||||
var numStr = s[..idx];
|
||||
if (!long.TryParse(numStr, out value)) return false;
|
||||
s = s[(idx + 1)..];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string FormatDuration(TimeSpan ts)
|
||||
{
|
||||
if (ts.TotalSeconds < 60) return $"{(long)ts.TotalSeconds}초";
|
||||
if (ts.TotalMinutes < 60)
|
||||
{
|
||||
var mins = (long)ts.TotalMinutes;
|
||||
var secs = (long)(ts.TotalSeconds - mins * 60);
|
||||
return secs > 0 ? $"{mins}분 {secs}초" : $"{mins}분";
|
||||
}
|
||||
var h = (long)ts.TotalHours;
|
||||
var m = (long)(ts.TotalMinutes - h * 60);
|
||||
var s = (long)(ts.TotalSeconds - h * 3600 - m * 60);
|
||||
var result = $"{h}시간";
|
||||
if (m > 0) result += $" {m}분";
|
||||
if (s > 0) result += $" {s}초";
|
||||
return result;
|
||||
}
|
||||
|
||||
private static long DurationToSeconds(TimeSpan ts) => (long)ts.TotalSeconds;
|
||||
|
||||
private static string Remaining(TimerEntry t)
|
||||
{
|
||||
var elapsed = DateTime.Now - t.StartAt;
|
||||
var left = t.Duration - elapsed;
|
||||
if (left <= TimeSpan.Zero) return "완료";
|
||||
return FormatDuration(left);
|
||||
}
|
||||
|
||||
private static void AddRunningTimers(List<LauncherItem> items)
|
||||
{
|
||||
List<TimerEntry> running;
|
||||
lock (_lock) { running = [.._timers]; }
|
||||
if (running.Count == 0) return;
|
||||
|
||||
items.Add(new LauncherItem($"── 실행 중인 타이머 {running.Count}개 ──", "", null, null, Symbol: "\uE916"));
|
||||
foreach (var t in running)
|
||||
{
|
||||
var left = Remaining(t);
|
||||
items.Add(new LauncherItem($"#{t.Id} {t.Label}", $"남은 시간: {left} · 시작: {t.StartAt:HH:mm:ss}",
|
||||
null, ("stop", t.Id.ToString()), Symbol: "\uE916"));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user