Files
AX-Copilot-Codex/src/AxCopilot/Handlers/BaseConvertHandler.cs
lacvet 0336904258 AX Commander 비교본 런처 기능 대량 이식
변경 목적: Agent Compare 아래 비교본의 개발 문서와 런처 소스를 기준으로 현재 AX Commander에 빠져 있던 신규 런처 기능을 동일한 흐름으로 옮겨, 비교본 수준의 기능 폭을 현재 제품에 반영했습니다.

핵심 수정사항: 비교본의 신규 런처 핸들러 다수를 src/AxCopilot/Handlers로 이식하고 App.xaml.cs 등록 흐름에 연결했습니다. 빠른 링크, 파일 태그, 알림 센터, 포모도로, 파일 브라우저, 핫키 관리, OCR, 세션/스케줄/매크로, Git/정규식/네트워크/압축/해시/UUID/JWT/QR 등 AX Commander 기능을 추가했습니다.

핵심 수정사항: 신규 기능이 실제 동작하도록 AppSettings 확장, SchedulerService/FileTagService/NotificationCenterService/IconCacheService/UrlTemplateEngine/PomodoroService 추가, 배치 이름변경/세션/스케줄/매크로 편집 창 추가, NotificationService와 Symbols 보강, QR/OCR용 csproj 의존성과 Windows 타겟 프레임워크를 반영했습니다.

문서 반영: README.md와 docs/DEVELOPMENT.md에 비교본 기반 런처 기능 이식 이력과 검증 결과를 업데이트했습니다.

검증 결과: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify\\ -p:IntermediateOutputPath=obj\\verify\\ 실행 기준 경고 0개, 오류 0개를 확인했습니다.
2026-04-05 00:59:45 +09:00

236 lines
9.4 KiB
C#

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";
}
}