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:
2026-04-05 00:59:45 +09:00
parent 0929778ca7
commit 0336904258
115 changed files with 30749 additions and 1 deletions

View File

@@ -0,0 +1,344 @@
using System.Globalization;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L11-4: 유니코드 문자 조회 핸들러. "unicode" 프리픽스로 사용합니다.
///
/// 예: unicode A → 문자 'A'의 코드포인트·카테고리·이름 조회
/// unicode U+1F600 → 코드포인트로 문자 조회
/// unicode 0x1F600 → 16진수 코드포인트
/// unicode 128512 → 10진수 코드포인트
/// unicode 가 → 한글 문자 분석
/// unicode smile → 문자 설명으로 검색 (이모지 이름 포함)
/// Enter → 문자를 클립보드에 복사.
/// </summary>
public class UnicodeHandler : IActionHandler
{
public string? Prefix => "unicode";
public PluginMetadata Metadata => new(
"Unicode",
"유니코드 문자 조회 — 코드포인트 · 카테고리 · 블록",
"1.0",
"AX");
// 주요 유니코드 블록 범위
private static readonly (int Start, int End, string Name)[] UnicodeBlocks =
[
(0x0000, 0x007F, "Basic Latin"),
(0x0080, 0x00FF, "Latin-1 Supplement"),
(0x0100, 0x017F, "Latin Extended-A"),
(0x0370, 0x03FF, "Greek and Coptic"),
(0x0400, 0x04FF, "Cyrillic"),
(0x0600, 0x06FF, "Arabic"),
(0x0900, 0x097F, "Devanagari"),
(0x1100, 0x11FF, "Hangul Jamo"),
(0x2000, 0x206F, "General Punctuation"),
(0x2100, 0x214F, "Letterlike Symbols"),
(0x2200, 0x22FF, "Mathematical Operators"),
(0x2300, 0x23FF, "Miscellaneous Technical"),
(0x2600, 0x26FF, "Miscellaneous Symbols"),
(0x2700, 0x27BF, "Dingbats"),
(0x3000, 0x303F, "CJK Symbols and Punctuation"),
(0x3040, 0x309F, "Hiragana"),
(0x30A0, 0x30FF, "Katakana"),
(0x4E00, 0x9FFF, "CJK Unified Ideographs"),
(0xAC00, 0xD7AF, "Hangul Syllables"),
(0xE000, 0xF8FF, "Private Use Area"),
(0xF000, 0xF0FF, "Segoe MDL2 Assets (PUA)"),
(0x1F300, 0x1F5FF, "Miscellaneous Symbols and Pictographs"),
(0x1F600, 0x1F64F, "Emoticons"),
(0x1F680, 0x1F6FF, "Transport and Map Symbols"),
(0x1F900, 0x1F9FF, "Supplemental Symbols and Pictographs"),
];
// 자주 쓰는 특수 문자 예제
private static readonly (string Char, string Desc)[] QuickChars =
[
("©", "Copyright Sign (U+00A9)"),
("®", "Registered Sign (U+00AE)"),
("™", "Trade Mark Sign (U+2122)"),
("•", "Bullet (U+2022)"),
("→", "Rightwards Arrow (U+2192)"),
("←", "Leftwards Arrow (U+2190)"),
("✓", "Check Mark (U+2713)"),
("✗", "Ballot X (U+2717)"),
("★", "Black Star (U+2605)"),
("♥", "Black Heart Suit (U+2665)"),
("😀", "Grinning Face (U+1F600)"),
("한", "Korean Syllable (AC00~D7AF)"),
];
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("유니코드 문자 조회",
"예: unicode A / unicode U+1F600 / unicode 가 / unicode 0x2665",
null, null, Symbol: "\uE8D2"));
foreach (var (ch, desc) in QuickChars.Take(8))
items.Add(new LauncherItem(ch, desc, null, ("copy", ch), Symbol: "\uE8D2"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// U+XXXX 형식
if (q.StartsWith("U+", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("u+", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(q[2..], NumberStyles.HexNumber, null, out var cp))
items.AddRange(BuildCodePointItems(cp));
else
items.Add(new LauncherItem("형식 오류", "예: U+1F600", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 0x 16진수
if (q.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ||
q.StartsWith("0X", StringComparison.OrdinalIgnoreCase))
{
if (int.TryParse(q[2..], NumberStyles.HexNumber, null, out var cp))
items.AddRange(BuildCodePointItems(cp));
else
items.Add(new LauncherItem("형식 오류", "예: 0x1F600", null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 순수 10진수 코드포인트
if (int.TryParse(q, out var decCp) && decCp >= 0)
{
items.AddRange(BuildCodePointItems(decCp));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 문자(1~2자) 직접 입력 → 분석
var codePoints = GetCodePoints(q);
if (codePoints.Count > 0 && codePoints.Count <= 6)
{
if (codePoints.Count == 1)
{
items.AddRange(BuildCodePointItems(codePoints[0]));
}
else
{
// 여러 문자 일괄 분석
items.Add(new LauncherItem($"'{q}' {codePoints.Count}개 코드포인트", "전체 분석", null, null, Symbol: "\uE8D2"));
foreach (var cp in codePoints)
items.AddRange(BuildCodePointItems(cp));
}
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 6자 초과 → 통계만
if (codePoints.Count > 0)
{
items.Add(new LauncherItem(
$"'{(q.Length > 10 ? q[..10] + "" : q)}' {codePoints.Count}개 코드포인트",
$"범위: U+{codePoints.Min():X4} ~ U+{codePoints.Max():X4}",
null,
("copy", string.Join(" ", codePoints.Select(c => $"U+{c:X4}"))),
Symbol: "\uE8D2"));
}
else
{
items.Add(new LauncherItem("조회 실패",
$"'{q}'을(를) 인식할 수 없습니다. 예: unicode A / unicode U+1F600",
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("Unicode", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 코드포인트 분석 ────────────────────────────────────────────────────────
private static IEnumerable<LauncherItem> BuildCodePointItems(int codePoint)
{
if (codePoint < 0 || codePoint > 0x10FFFF)
{
yield return new LauncherItem("범위 초과", "유효한 유니코드 범위: U+0000 ~ U+10FFFF", null, null, Symbol: "\uE783");
yield break;
}
var charStr = char.ConvertFromUtf32(codePoint);
var category = GetCategoryName(charStr);
var block = GetBlock(codePoint);
var name = GetCharName(codePoint);
var display = codePoint < 32 || (codePoint >= 127 && codePoint < 160) ? $"(제어문자 U+{codePoint:X4})" : charStr;
yield return new LauncherItem(
display,
$"U+{codePoint:X4} · {name}",
null,
("copy", charStr),
Symbol: "\uE8D2");
yield return new LauncherItem("코드포인트", $"U+{codePoint:X4}", null, ("copy", $"U+{codePoint:X4}"), Symbol: "\uE8D2");
yield return new LauncherItem("10진수", $"{codePoint}", null, ("copy", codePoint.ToString()), Symbol: "\uE8D2");
yield return new LauncherItem("HTML 엔티티", $"&#{codePoint};", null, ("copy", $"&#{codePoint};"), Symbol: "\uE8D2");
yield return new LauncherItem("HTML Hex", $"&#x{codePoint:X};", null, ("copy", $"&#x{codePoint:X};"), Symbol: "\uE8D2");
// UTF-8 바이트
var utf8 = System.Text.Encoding.UTF8.GetBytes(charStr);
var utf8Hex = string.Join(" ", utf8.Select(b => $"{b:X2}"));
yield return new LauncherItem("UTF-8", utf8Hex, null, ("copy", utf8Hex), Symbol: "\uE8D2");
// UTF-16
var utf16 = System.Text.Encoding.Unicode.GetBytes(charStr);
var utf16Hex = string.Join(" ", utf16.Select(b => $"{b:X2}"));
yield return new LauncherItem("UTF-16 LE", utf16Hex, null, ("copy", utf16Hex), Symbol: "\uE8D2");
yield return new LauncherItem("카테고리", category, null, null, Symbol: "\uE8D2");
yield return new LauncherItem("블록", block, null, null, Symbol: "\uE8D2");
// 한글 음절이면 분해
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3)
{
var (initial, vowel, final) = DecomposeHangul(codePoint);
yield return new LauncherItem("초성", initial, null, ("copy", initial), Symbol: "\uE8D2");
yield return new LauncherItem("중성", vowel, null, ("copy", vowel), Symbol: "\uE8D2");
if (!string.IsNullOrEmpty(final))
yield return new LauncherItem("종성", final, null, ("copy", final), Symbol: "\uE8D2");
}
}
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
private static List<int> GetCodePoints(string s)
{
var result = new List<int>();
for (var i = 0; i < s.Length; )
{
var cp = char.ConvertToUtf32(s, i);
result.Add(cp);
i += char.IsSurrogatePair(s, i) ? 2 : 1;
}
return result;
}
private static string GetCategoryName(string charStr)
{
if (string.IsNullOrEmpty(charStr)) return "Unknown";
var cat = char.GetUnicodeCategory(charStr, 0);
return cat switch
{
UnicodeCategory.UppercaseLetter => "대문자 (Lu)",
UnicodeCategory.LowercaseLetter => "소문자 (Ll)",
UnicodeCategory.TitlecaseLetter => "타이틀케이스 (Lt)",
UnicodeCategory.ModifierLetter => "수정 문자 (Lm)",
UnicodeCategory.OtherLetter => "기타 문자 (Lo)",
UnicodeCategory.DecimalDigitNumber => "10진수 숫자 (Nd)",
UnicodeCategory.LetterNumber => "문자 숫자 (Nl)",
UnicodeCategory.OtherNumber => "기타 숫자 (No)",
UnicodeCategory.SpaceSeparator => "공백 (Zs)",
UnicodeCategory.LineSeparator => "줄 구분자 (Zl)",
UnicodeCategory.ParagraphSeparator => "단락 구분자 (Zp)",
UnicodeCategory.Control => "제어 문자 (Cc)",
UnicodeCategory.MathSymbol => "수학 기호 (Sm)",
UnicodeCategory.CurrencySymbol => "통화 기호 (Sc)",
UnicodeCategory.ModifierSymbol => "수정 기호 (Sk)",
UnicodeCategory.OtherSymbol => "기타 기호 (So)",
UnicodeCategory.OpenPunctuation => "여는 구두점 (Ps)",
UnicodeCategory.ClosePunctuation => "닫는 구두점 (Pe)",
UnicodeCategory.DashPunctuation => "대시 구두점 (Pd)",
UnicodeCategory.ConnectorPunctuation => "연결 구두점 (Pc)",
UnicodeCategory.OtherPunctuation => "기타 구두점 (Po)",
_ => cat.ToString(),
};
}
private static string GetBlock(int cp)
{
foreach (var (start, end, name) in UnicodeBlocks)
if (cp >= start && cp <= end) return $"{name} (U+{start:X4}~U+{end:X4})";
if (cp >= 0x10000) return $"Supplementary Planes (U+{cp:X4})";
return $"U+{cp:X4} 범위 불명";
}
private static string GetCharName(int cp) => cp switch
{
0x0020 => "Space",
0x0021 => "Exclamation Mark",
0x0022 => "Quotation Mark",
0x0023 => "Number Sign",
0x0024 => "Dollar Sign",
0x0025 => "Percent Sign",
0x0026 => "Ampersand",
0x0027 => "Apostrophe",
0x0028 => "Left Parenthesis",
0x0029 => "Right Parenthesis",
0x002A => "Asterisk",
0x002B => "Plus Sign",
0x002C => "Comma",
0x002D => "Hyphen-Minus",
0x002E => "Full Stop",
0x002F => "Solidus",
>= 0x0030 and <= 0x0039 => $"Digit {(char)cp}",
>= 0x0041 and <= 0x005A => $"Latin Capital Letter {(char)cp}",
>= 0x0061 and <= 0x007A => $"Latin Small Letter {(char)cp}",
0x00A9 => "Copyright Sign",
0x00AE => "Registered Sign",
0x2122 => "Trade Mark Sign",
0x2022 => "Bullet",
0x2192 => "Rightwards Arrow",
0x2190 => "Leftwards Arrow",
0x2191 => "Upwards Arrow",
0x2193 => "Downwards Arrow",
0x2713 => "Check Mark",
0x2717 => "Ballot X",
0x2605 => "Black Star",
0x2606 => "White Star",
0x2665 => "Black Heart Suit",
0x2764 => "Heavy Black Heart",
0x1F600 => "Grinning Face",
0x1F601 => "Grinning Face With Smiling Eyes",
0x1F602 => "Face With Tears of Joy",
0x1F603 => "Smiling Face With Open Mouth",
0x1F609 => "Winking Face",
0x1F60D => "Smiling Face With Heart-Eyes",
0x1F621 => "Pouting Face",
0x1F625 => "Disappointed but Relieved Face",
>= 0xAC00 and <= 0xD7A3 => "Hangul Syllable",
>= 0x1100 and <= 0x11FF => "Hangul Jamo",
>= 0x3131 and <= 0x318E => "Hangul Compatibility Jamo",
>= 0x4E00 and <= 0x9FFF => "CJK Unified Ideograph",
>= 0x3040 and <= 0x309F => "Hiragana",
>= 0x30A0 and <= 0x30FF => "Katakana",
_ => $"U+{cp:X4}",
};
private static (string Initial, string Vowel, string Final) DecomposeHangul(int cp)
{
string[] initials = ["ㄱ","ㄲ","ㄴ","ㄷ","ㄸ","ㄹ","ㅁ","ㅂ","ㅃ","ㅅ","ㅆ","ㅇ","ㅈ","ㅉ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"];
string[] vowels = ["ㅏ","ㅐ","ㅑ","ㅒ","ㅓ","ㅔ","ㅕ","ㅖ","ㅗ","ㅘ","ㅙ","ㅚ","ㅛ","ㅜ","ㅝ","ㅞ","ㅟ","ㅠ","ㅡ","ㅢ","ㅣ"];
string[] finals = ["", "ㄱ","ㄲ","ㄳ","ㄴ","ㄵ","ㄶ","ㄷ","ㄹ","ㄺ","ㄻ","ㄼ","ㄽ","ㄾ","ㄿ","ㅀ","ㅁ","ㅂ","ㅄ","ㅅ","ㅆ","ㅇ","ㅈ","ㅊ","ㅋ","ㅌ","ㅍ","ㅎ"];
var offset = cp - 0xAC00;
var finIdx = offset % 28;
var vowIdx = (offset / 28) % 21;
var iniIdx = offset / 28 / 21;
return (initials[iniIdx], vowels[vowIdx], finals[finIdx]);
}
}