Files
AX-Copilot-Codex/src/AxCopilot/Handlers/UnitHandler.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

285 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}