변경 목적: 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개를 확인했습니다.
345 lines
15 KiB
C#
345 lines
15 KiB
C#
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]);
|
|
}
|
|
}
|