[Phase L4-4/L4-6] 검색 히스토리 + 계산기 단위변환 단축 문법

L4-4 검색 히스토리 (↑/↓ 키 탐색):
- Services/SearchHistoryService.cs (신규 100줄): 50개 FIFO JSON 저장
  Add() · GetAll() · Clear(). 2자 미만/중복 최상단 무시
- ViewModels/LauncherViewModel.cs:
  _historyIndex·_isHistoryNavigation 필드 추가
  NavigateHistoryPrev() / NavigateHistoryNext() / SetInputFromHistory()
  InputText setter: 직접 입력 시 _historyIndex 초기화
  ExecuteSelectedAsync: 실행 전 히스토리 저장 (2자 이상)
  OnShown: _historyIndex = -1 초기화
- Views/LauncherWindow.Keyboard.cs:
  Key.Up/Down — Results.Count==0 분기: 히스토리 탐색 / 목록 탐색 분기

L4-3 클립보드 핀/카테고리: 기존 완전 구현 확인 (IsPinned, Category,
  TogglePin, Ctrl+P, #pin/#url/#코드/#경로 필터)

L4-6 계산기 단위 변환 단축 문법:
- Handlers/UnitConverter.cs:
  AutoSuggest(): "20km", "100f", "5lb" 등 목표 없이 주요 단위 자동 제안
  _suggestions 테이블: 길이/무게/속도/데이터/온도 40개 단위 매핑
  DateShortcut: "today+30d", "today-7w" → = 접두어에서 날짜 계산
- Handlers/CalculatorHandler.cs:
  DateShortcut.TryMatch 분기 추가 (통화 감지 전)
  UnitConverter.AutoSuggest 분기 추가 (명시 변환 후)
- 힌트 텍스트: "20km · 100°F · today+30d" 추가

docs/LAUNCHER_ROADMAP.md: Phase L4 계획 테이블 추가

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-04 11:15:11 +09:00
parent fc881124b9
commit 75cb4ba6e9
6 changed files with 330 additions and 1 deletions

View File

@@ -40,13 +40,17 @@ public class CalculatorHandler : IActionHandler
var trimmed = query.Trim();
// ─── today+Nd / today-Nd 날짜 단축 계산 (= 접두어에서 date 핸들러 위임) ─
if (DateShortcut.TryMatch(trimmed, out var dateItems))
return dateItems!;
// ─── 통화 변환 우선 감지 ──────────────────────────────────────────────
if (CurrencyConverter.IsCurrencyQuery(trimmed))
{
return await CurrencyConverter.ConvertAsync(trimmed, ct);
}
// ─── 단위 변환 우선 감지 ──────────────────────────────────────────────
// ─── 단위 변환 (명시적 목표: 100km in miles) ─────────────────────────
if (UnitConverter.TryConvert(trimmed, out var convertResult))
{
return
@@ -58,6 +62,11 @@ public class CalculatorHandler : IActionHandler
];
}
// ─── 단위 단축 자동 제안 (20km, 100f, 5lb …) ──────────────────────────
var autoSuggest = UnitConverter.AutoSuggest(trimmed);
if (autoSuggest.Count > 0)
return autoSuggest;
// ─── 수식 계산 ────────────────────────────────────────────────────────
try
{

View File

@@ -151,4 +151,131 @@ internal static class UnitConverter
return ((long)v).ToString("N0", System.Globalization.CultureInfo.CurrentCulture);
return v.ToString("G6", System.Globalization.CultureInfo.InvariantCulture);
}
// ─── 자동 제안 (목표 단위 없이 입력 시) ──────────────────────────────────
// 패턴: <숫자><단위> (공백 없어도 OK)
private static readonly Regex _autoPattern = new(
@"^(-?\d+(?:\.\d+)?)\s*([a-z°/²³µ]+)$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
// 단위 → 주요 변환 대상 목록 (사용자에게 자동 제안)
private static readonly Dictionary<string, string[]> _suggestions = new(StringComparer.OrdinalIgnoreCase)
{
// 길이
["km"] = ["miles", "m", "ft"],
["miles"] = ["km", "m", "ft"],
["m"] = ["km", "ft", "inches"],
["cm"] = ["inches", "m"],
["mm"] = ["inches", "cm"],
["ft"] = ["m", "km", "inches"],
["feet"] = ["m", "km", "inches"],
["in"] = ["cm", "m"],
["inches"]= ["cm", "m"],
// 무게
["kg"] = ["lb", "g"],
["lb"] = ["kg", "g"],
["lbs"] = ["kg", "g"],
["g"] = ["kg", "oz"],
["oz"] = ["g", "kg"],
// 속도
["km/h"] = ["mph", "m/s"],
["kmh"] = ["mph", "m/s"],
["kph"] = ["mph", "m/s"],
["mph"] = ["km/h", "m/s"],
["m/s"] = ["km/h", "mph"],
// 데이터
["mb"] = ["gb", "kb"],
["gb"] = ["mb", "tb"],
["tb"] = ["gb", "mb"],
["kb"] = ["mb", "b"],
// 온도 — 양방향
["c"] = ["f", "k"],
["°c"] = ["°f", "k"],
["celsius"]= ["fahrenheit", "kelvin"],
["f"] = ["c", "k"],
["°f"] = ["°c", "k"],
["fahrenheit"] = ["celsius", "kelvin"],
};
/// <summary>
/// "20km", "100f" 처럼 목표 단위 없이 입력하면 주요 변환 대상을 자동 제안합니다.
/// </summary>
public static List<LauncherItem> AutoSuggest(string input)
{
var m = _autoPattern.Match(input.Trim());
if (!m.Success) return new();
if (!double.TryParse(m.Groups[1].Value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var value))
return new();
var fromUnit = m.Groups[2].Value;
if (!_suggestions.TryGetValue(fromUnit, out var targets)) return new();
var items = new List<LauncherItem>();
foreach (var to in targets)
{
if (TryConvert($"{value} {fromUnit} in {to}", out var r) && r != null)
{
items.Add(new LauncherItem(
r,
$"{value} {fromUnit} → {to} · Enter로 복사",
null, r, Symbol: Symbols.Calculator));
}
}
return items;
}
}
// ─── = 접두어에서 today 날짜 단축 계산 ────────────────────────────────────────
/// <summary>
/// "today+30d", "today-7d", "today+2w" 등 날짜 단축 패턴을 = 핸들러에서 처리합니다.
/// DateCalcHandler의 로직을 재사용합니다.
/// </summary>
internal static class DateShortcut
{
private static readonly Regex _pattern = new(
@"^today\s*([+-])(\d+)([dDwWmMyY])$",
RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static bool TryMatch(string input, out IEnumerable<LauncherItem>? items)
{
items = null;
var m = _pattern.Match(input.Trim());
if (!m.Success) return false;
var sign = m.Groups[1].Value == "+" ? 1 : -1;
var val = int.Parse(m.Groups[2].Value) * sign;
var unit = m.Groups[3].Value.ToLowerInvariant();
var now = DateTime.Now;
var target = unit switch
{
"d" => now.AddDays(val),
"w" => now.AddDays(val * 7),
"m" => now.AddMonths(val),
"y" => now.AddYears(val),
_ => now.AddDays(val),
};
var diff = (target.Date - now.Date).Days;
var dayName = target.ToString("dddd", new System.Globalization.CultureInfo("ko-KR"));
var dateStr = target.ToString("yyyy-MM-dd");
var diffText = diff == 0 ? "오늘"
: diff > 0 ? $"+{diff}일 후"
: $"{-diff}일 전";
items = new List<LauncherItem>
{
new LauncherItem(
$"{dateStr} ({dayName})",
$"{input} → {diffText} · Enter로 복사",
null, dateStr, Symbol: Symbols.Calculator),
};
return true;
}
}