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:
284
src/AxCopilot/Handlers/UnitHandler.cs
Normal file
284
src/AxCopilot/Handlers/UnitHandler.cs
Normal file
@@ -0,0 +1,284 @@
|
||||
using System.Windows;
|
||||
using AxCopilot.SDK;
|
||||
using AxCopilot.Services;
|
||||
using AxCopilot.Themes;
|
||||
|
||||
namespace AxCopilot.Handlers;
|
||||
|
||||
/// <summary>
|
||||
/// L17-1: 단위 변환기 핸들러. "unit" 프리픽스로 사용합니다.
|
||||
///
|
||||
/// 예: unit 100 km m → 100km → m
|
||||
/// unit 72 f c → 화씨 72°F → 섭씨
|
||||
/// unit 5 kg lb → 5kg → 파운드
|
||||
/// unit 1 gb mb → 1GB → MB
|
||||
/// unit 60 mph kmh → 속도 변환
|
||||
/// unit length → 길이 단위 목록
|
||||
/// unit weight / temp / area / speed / data → 카테고리 목록
|
||||
/// Enter → 결과 복사.
|
||||
/// </summary>
|
||||
public class UnitHandler : IActionHandler
|
||||
{
|
||||
public string? Prefix => "unit";
|
||||
|
||||
public PluginMetadata Metadata => new(
|
||||
"Unit",
|
||||
"단위 변환기 — 길이·무게·온도·넓이·속도·데이터",
|
||||
"1.0",
|
||||
"AX");
|
||||
|
||||
// ── 단위 정의 (기준 단위 → SI 변환 계수) ─────────────────────────────────
|
||||
// 온도는 별도 처리 (비선형)
|
||||
|
||||
private enum UnitCategory { Length, Weight, Area, Speed, Data, Temperature, Pressure, Volume }
|
||||
|
||||
private record UnitDef(string[] Names, double ToBase, UnitCategory Cat, string Display);
|
||||
|
||||
private static readonly UnitDef[] Units =
|
||||
[
|
||||
// 길이 (기준: m)
|
||||
new(["km","킬로미터"], 1000, UnitCategory.Length, "킬로미터 (km)"),
|
||||
new(["m","미터"], 1, UnitCategory.Length, "미터 (m)"),
|
||||
new(["cm","센티미터"], 0.01, UnitCategory.Length, "센티미터 (cm)"),
|
||||
new(["mm","밀리미터"], 0.001, UnitCategory.Length, "밀리미터 (mm)"),
|
||||
new(["mi","mile","마일"], 1609.344, UnitCategory.Length, "마일 (mi)"),
|
||||
new(["yd","yard","야드"], 0.9144, UnitCategory.Length, "야드 (yd)"),
|
||||
new(["ft","feet","foot","피트"], 0.3048, UnitCategory.Length, "피트 (ft)"),
|
||||
new(["in","inch","인치"], 0.0254, UnitCategory.Length, "인치 (in)"),
|
||||
new(["nm","해리"], 1852, UnitCategory.Length, "해리 (nm)"),
|
||||
|
||||
// 무게 (기준: kg)
|
||||
new(["t","ton","톤"], 1000, UnitCategory.Weight, "톤 (t)"),
|
||||
new(["kg","킬로그램"], 1, UnitCategory.Weight, "킬로그램 (kg)"),
|
||||
new(["g","그램"], 0.001, UnitCategory.Weight, "그램 (g)"),
|
||||
new(["mg","밀리그램"], 1e-6, UnitCategory.Weight, "밀리그램 (mg)"),
|
||||
new(["lb","lbs","파운드"], 0.453592, UnitCategory.Weight, "파운드 (lb)"),
|
||||
new(["oz","온스"], 0.0283495, UnitCategory.Weight, "온스 (oz)"),
|
||||
new(["근"], 0.6, UnitCategory.Weight, "근 (600g)"),
|
||||
|
||||
// 넓이 (기준: m²)
|
||||
new(["km2","km²"], 1e6, UnitCategory.Area, "제곱킬로미터 (km²)"),
|
||||
new(["m2","m²","sqm"], 1, UnitCategory.Area, "제곱미터 (m²)"),
|
||||
new(["cm2","cm²"], 0.0001, UnitCategory.Area, "제곱센티미터 (cm²)"),
|
||||
new(["ha","헥타르"], 10000, UnitCategory.Area, "헥타르 (ha)"),
|
||||
new(["a","아르"], 100, UnitCategory.Area, "아르 (a)"),
|
||||
new(["acre","에이커"], 4046.856, UnitCategory.Area, "에이커 (acre)"),
|
||||
new(["ft2","ft²","sqft"], 0.092903, UnitCategory.Area, "제곱피트 (ft²)"),
|
||||
new(["평"], 3.30579, UnitCategory.Area, "평 (3.3058m²)"),
|
||||
|
||||
// 속도 (기준: m/s)
|
||||
new(["mps","m/s"], 1, UnitCategory.Speed, "미터/초 (m/s)"),
|
||||
new(["kph","kmh","km/h","kmph"], 0.277778, UnitCategory.Speed, "킬로미터/시 (km/h)"),
|
||||
new(["mph","mi/h"], 0.44704, UnitCategory.Speed, "마일/시 (mph)"),
|
||||
new(["knot","kn","노트"], 0.514444, UnitCategory.Speed, "노트 (kn)"),
|
||||
new(["fps","ft/s"], 0.3048, UnitCategory.Speed, "피트/초 (ft/s)"),
|
||||
|
||||
// 데이터 (기준: byte)
|
||||
new(["b","bit","비트"], 0.125, UnitCategory.Data, "비트 (bit)"),
|
||||
new(["byte","바이트"], 1, UnitCategory.Data, "바이트 (byte)"),
|
||||
new(["kb","킬로바이트"], 1024, UnitCategory.Data, "킬로바이트 (KB)"),
|
||||
new(["mb","메가바이트"], 1048576, UnitCategory.Data, "메가바이트 (MB)"),
|
||||
new(["gb","기가바이트"], 1073741824, UnitCategory.Data, "기가바이트 (GB)"),
|
||||
new(["tb","테라바이트"], 1099511627776,UnitCategory.Data, "테라바이트 (TB)"),
|
||||
new(["pb","페타바이트"], 1.12589990684e15, UnitCategory.Data, "페타바이트 (PB)"),
|
||||
|
||||
// 온도 (기준: °C, 변환은 특수 처리)
|
||||
new(["c","°c","celsius","섭씨"], 1, UnitCategory.Temperature, "섭씨 (°C)"),
|
||||
new(["f","°f","fahrenheit","화씨"],1, UnitCategory.Temperature, "화씨 (°F)"),
|
||||
new(["k","kelvin","켈빈"], 1, UnitCategory.Temperature, "켈빈 (K)"),
|
||||
|
||||
// 압력 (기준: Pa)
|
||||
new(["pa","파스칼"], 1, UnitCategory.Pressure, "파스칼 (Pa)"),
|
||||
new(["kpa"], 1000, UnitCategory.Pressure, "킬로파스칼 (kPa)"),
|
||||
new(["mpa"], 1e6, UnitCategory.Pressure, "메가파스칼 (MPa)"),
|
||||
new(["atm","기압"], 101325, UnitCategory.Pressure, "기압 (atm)"),
|
||||
new(["bar","바"], 100000, UnitCategory.Pressure, "바 (bar)"),
|
||||
new(["psi"], 6894.757, UnitCategory.Pressure, "PSI (psi)"),
|
||||
|
||||
// 부피 (기준: L)
|
||||
new(["l","liter","리터"], 1, UnitCategory.Volume, "리터 (L)"),
|
||||
new(["ml","밀리리터"], 0.001, UnitCategory.Volume, "밀리리터 (mL)"),
|
||||
new(["m3","m³","cbm"], 1000, UnitCategory.Volume, "세제곱미터 (m³)"),
|
||||
new(["cm3","cm³","cc"], 0.001, UnitCategory.Volume, "세제곱센티미터 (cc)"),
|
||||
new(["gallon","gal","갤런"], 3.78541, UnitCategory.Volume, "갤런 (US, gal)"),
|
||||
new(["floz","fl.oz"], 0.0295735, UnitCategory.Volume, "액량온스 (fl.oz)"),
|
||||
new(["cup","컵"], 0.236588, UnitCategory.Volume, "컵 (cup)"),
|
||||
];
|
||||
|
||||
private static readonly Dictionary<string, UnitCategory> CategoryKeywords =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["length"] = UnitCategory.Length, ["길이"] = UnitCategory.Length,
|
||||
["weight"] = UnitCategory.Weight, ["무게"] = UnitCategory.Weight,
|
||||
["mass"] = UnitCategory.Weight,
|
||||
["area"] = UnitCategory.Area, ["넓이"] = UnitCategory.Area,
|
||||
["speed"] = UnitCategory.Speed, ["속도"] = UnitCategory.Speed,
|
||||
["data"] = UnitCategory.Data, ["데이터"]= UnitCategory.Data,
|
||||
["temp"] = UnitCategory.Temperature, ["온도"] = UnitCategory.Temperature,
|
||||
["temperature"] = UnitCategory.Temperature,
|
||||
["pressure"] = UnitCategory.Pressure, ["압력"] = UnitCategory.Pressure,
|
||||
["volume"] = UnitCategory.Volume, ["부피"] = UnitCategory.Volume,
|
||||
};
|
||||
|
||||
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("단위 변환기",
|
||||
"예: unit 100 km m / unit 72 f c / unit 5 kg lb / unit 1 gb mb",
|
||||
null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("── 카테고리 ──", "", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit length", "길이 단위 (km·m·cm·ft·in·mi)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit weight", "무게 단위 (kg·g·lb·oz·근)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit temp", "온도 단위 (°C·°F·K)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit area", "넓이 단위 (m²·ha·acre·평)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit speed", "속도 단위 (km/h·mph·m/s·knot)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit data", "데이터 단위 (bit·B·KB·MB·GB·TB)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit pressure", "압력 단위 (Pa·atm·bar·psi)", null, null, Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem("unit volume", "부피 단위 (L·mL·m³·gallon·cup)", null, null, Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// 카테고리 목록
|
||||
if (parts.Length == 1 && CategoryKeywords.TryGetValue(parts[0], out var cat))
|
||||
{
|
||||
var catUnits = Units.Where(u => u.Cat == cat).ToList();
|
||||
items.Add(new LauncherItem($"{cat} 단위 {catUnits.Count}개",
|
||||
"예: unit 100 km m", null, null, Symbol: "\uE8EF"));
|
||||
foreach (var u in catUnits)
|
||||
items.Add(new LauncherItem(u.Display, string.Join(", ", u.Names), null, null, Symbol: "\uE8EF"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 변환: unit <값> <from> <to>
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
items.Add(new LauncherItem("입력 형식",
|
||||
"unit <값> <단위> [대상단위] 예: unit 100 km m", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
if (!double.TryParse(parts[0].Replace(",", ""), System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
items.Add(new LauncherItem("숫자 형식 오류",
|
||||
"첫 번째 값이 숫자여야 합니다 예: unit 100 km m", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var fromKey = parts[1].ToLowerInvariant();
|
||||
var fromDef = FindUnit(fromKey);
|
||||
if (fromDef == null)
|
||||
{
|
||||
items.Add(new LauncherItem($"'{parts[1]}' 단위를 찾을 수 없습니다",
|
||||
"unit length / weight / temp / area / speed / data 로 단위 목록 확인",
|
||||
null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
// 대상 단위 지정
|
||||
if (parts.Length >= 3)
|
||||
{
|
||||
var toKey = parts[2].ToLowerInvariant();
|
||||
var toDef = FindUnit(toKey);
|
||||
if (toDef == null)
|
||||
{
|
||||
items.Add(new LauncherItem($"'{parts[2]}' 단위를 찾을 수 없습니다",
|
||||
"", null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
if (fromDef.Cat != toDef.Cat)
|
||||
{
|
||||
items.Add(new LauncherItem("카테고리 불일치",
|
||||
$"{fromDef.Cat} ≠ {toDef.Cat} — 같은 종류끼리만 변환 가능",
|
||||
null, null, Symbol: "\uE783"));
|
||||
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
||||
}
|
||||
|
||||
var result = Convert(value, fromDef, toDef);
|
||||
var label = $"{FormatNum(value)} {fromDef.Names[0].ToUpper()} = {FormatNum(result)} {toDef.Names[0].ToUpper()}";
|
||||
items.Add(new LauncherItem(label, "Enter → 복사", null, ("copy", label), Symbol: "\uE8EF"));
|
||||
items.Add(new LauncherItem($"{FormatNum(result)} {toDef.Names[0].ToUpper()}", toDef.Display,
|
||||
null, ("copy", FormatNum(result)), Symbol: "\uE8EF"));
|
||||
}
|
||||
else
|
||||
{
|
||||
// 같은 카테고리 모든 단위로 변환
|
||||
var sameCat = Units.Where(u => u.Cat == fromDef.Cat && u != fromDef).ToList();
|
||||
items.Add(new LauncherItem($"{FormatNum(value)} {fromDef.Names[0].ToUpper()} 변환 결과",
|
||||
fromDef.Display, null, null, Symbol: "\uE8EF"));
|
||||
foreach (var toDef in sameCat)
|
||||
{
|
||||
var result = Convert(value, fromDef, toDef);
|
||||
var label = $"{FormatNum(result)} {toDef.Names[0].ToUpper()}";
|
||||
items.Add(new LauncherItem(label, toDef.Display, null, ("copy", FormatNum(result)), Symbol: "\uE8EF"));
|
||||
}
|
||||
}
|
||||
|
||||
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("Unit", "클립보드에 복사했습니다.");
|
||||
}
|
||||
catch { /* 비핵심 */ }
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ── 변환 로직 ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static double Convert(double value, UnitDef from, UnitDef to)
|
||||
{
|
||||
if (from.Cat == UnitCategory.Temperature)
|
||||
return ConvertTemp(value, from.Names[0].ToLowerInvariant(), to.Names[0].ToLowerInvariant());
|
||||
|
||||
// 선형 변환: value × from.ToBase / to.ToBase
|
||||
return value * from.ToBase / to.ToBase;
|
||||
}
|
||||
|
||||
private static double ConvertTemp(double value, string from, string to)
|
||||
{
|
||||
// 먼저 °C로
|
||||
var celsius = from switch
|
||||
{
|
||||
"c" or "°c" => value,
|
||||
"f" or "°f" => (value - 32) * 5 / 9,
|
||||
"k" => value - 273.15,
|
||||
_ => value,
|
||||
};
|
||||
// °C에서 목표로
|
||||
return to switch
|
||||
{
|
||||
"c" or "°c" => celsius,
|
||||
"f" or "°f" => celsius * 9 / 5 + 32,
|
||||
"k" => celsius + 273.15,
|
||||
_ => celsius,
|
||||
};
|
||||
}
|
||||
|
||||
private static UnitDef? FindUnit(string key) =>
|
||||
Units.FirstOrDefault(u => u.Names.Any(n => n.Equals(key, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
private static string FormatNum(double v)
|
||||
{
|
||||
if (double.IsNaN(v) || double.IsInfinity(v)) return v.ToString();
|
||||
if (Math.Abs(v) >= 1e12 || (Math.Abs(v) < 1e-4 && v != 0))
|
||||
return v.ToString("E3", System.Globalization.CultureInfo.InvariantCulture);
|
||||
if (v == Math.Floor(v) && Math.Abs(v) < 1e9)
|
||||
return $"{v:N0}";
|
||||
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user