Files
AX-Copilot/src/AxCopilot/Handlers/AgeHandler.cs
lacvet e4e5bf7a7a [Phase L13] 시스템 정보·계산 도구 핸들러 4종 추가
DnsQueryHandler.cs (신규, ~200줄, prefix=dns):
- A/AAAA: .NET Dns.GetHostAddressesAsync() 직접 호출
- MX/TXT/NS/CNAME: nslookup 서브프로세스 + 출력 파싱
- PTR: Dns.GetHostEntryAsync()로 역방향 조회
- 사내 모드: 내부 IP(192.168/10/172.16-31) 허용, 외부 차단
- Enter → 비동기 실행, 결과 클립보드 복사 + 알림

PathHandler.cs (신규, ~180줄, prefix=path):
- Environment.GetEnvironmentVariable("PATH", Process/User/Machine)
- Directory.Exists 기반 경로 존재 여부 아이콘 구분
- which: .exe/.cmd/.bat/.ps1/.com 확장자 순서 탐색
- DistinctBy로 중복 경로 제거 (대소문자 무시)

DriveHandler.cs (신규, ~170줄, prefix=drive):
- DriveInfo.GetDrives() + IsReady 체크 + try/catch 방어
- █░ 시각적 사용량 바 (MakeBar 12~20칸 가변)
- TB/GB/MB/KB 자동 단위 포맷
- large 서브커맨드: UsedSpace 내림차순 정렬

AgeHandler.cs (신규, ~230줄, prefix=age):
- YYYYMMDD / YYYY-MM-DD / YYYY.MM.DD 형식 파싱
- 만 나이(생일 미경과 시 -1) + 한국식(연도 차이 +1)
- NextBirthday: 올해 생일 지났으면 내년으로 계산
- christmas/newyear 특수 키워드 Dictionary<string, Func>
- "next monday" 형식 다음 요일 D-day 파싱

App.xaml.cs: 4개 핸들러 Phase L13 블록 등록
docs/LAUNCHER_ROADMAP.md: Phase L13 완료 섹션 추가
빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 14:50:39 +09:00

285 lines
12 KiB
C#

using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
using AxCopilot.Themes;
namespace AxCopilot.Handlers;
/// <summary>
/// L13-4: 나이·D-day 계산기 핸들러. "age" 프리픽스로 사용합니다.
///
/// 예: age 1990-05-15 → 나이 계산 (만/한국식)
/// age 1990.05.15 → 점 구분자도 지원
/// age 19900515 → 숫자만 입력 (YYYYMMDD)
/// age 2025-12-25 → D-day 계산 (미래 날짜)
/// age next monday → 다음 월요일까지 D-day
/// age christmas → 크리스마스까지 D-day
/// Enter → 결과를 클립보드에 복사.
/// </summary>
public class AgeHandler : IActionHandler
{
public string? Prefix => "age";
public PluginMetadata Metadata => new(
"Age",
"나이·D-day 계산기 — 만 나이 · 한국 나이 · D-day",
"1.0",
"AX");
// 특수 날짜 키워드
private static readonly Dictionary<string, Func<DateTime, DateTime>> Keywords =
new(StringComparer.OrdinalIgnoreCase)
{
["christmas"] = t => new DateTime(t.Month > 12 || (t.Month == 12 && t.Day >= 26) ? t.Year + 1 : t.Year, 12, 25),
["xmas"] = t => new DateTime(t.Month > 12 || (t.Month == 12 && t.Day >= 26) ? t.Year + 1 : t.Year, 12, 25),
["newyear"] = t => new DateTime(t.Year + 1, 1, 1),
["new year"] = t => new DateTime(t.Year + 1, 1, 1),
["설날"] = t => GetNextLunarNewYear(t),
["chuseok"] = t => GetNextChuseok(t),
["추석"] = t => GetNextChuseok(t),
};
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List<LauncherItem>();
var today = DateTime.Today;
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem("나이·D-day 계산기",
"예: age 1990-05-15 / age 2025-12-25 / age christmas",
null, null, Symbol: "\uE787"));
items.Add(new LauncherItem("age 1990-01-01", "생년월일 → 나이", null, null, Symbol: "\uE787"));
items.Add(new LauncherItem("age 2025-12-25", "미래 날짜 D-day", null, null, Symbol: "\uE787"));
items.Add(new LauncherItem("age christmas", "크리스마스 D-day", null, null, Symbol: "\uE787"));
items.Add(new LauncherItem("age newyear", "신년 D-day", null, null, Symbol: "\uE787"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 특수 키워드 확인
foreach (var (kw, fn) in Keywords)
{
if (q.Equals(kw, StringComparison.OrdinalIgnoreCase))
{
var targetDate = fn(today);
items.AddRange(BuildDdayItems(targetDate, kw, today));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
}
// 요일 키워드: "next monday"
if (TryParseNextWeekday(q, out var weekdayDate))
{
items.AddRange(BuildDdayItems(weekdayDate, q, today));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
// 날짜 파싱 시도
if (!TryParseDate(q, out var date))
{
items.Add(new LauncherItem("날짜 형식 오류",
"예: 1990-05-15 / 1990.05.15 / 19900515 / 2025-12-25",
null, null, Symbol: "\uE783"));
return Task.FromResult<IEnumerable<LauncherItem>>(items);
}
if (date <= today)
{
// 과거 날짜 → 나이/경과 계산
items.AddRange(BuildAgeItems(date, today));
}
else
{
// 미래 날짜 → D-day
items.AddRange(BuildDdayItems(date, date.ToString("yyyy-MM-dd"), today));
}
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("Age", "클립보드에 복사했습니다.");
}
catch { /* 비핵심 */ }
}
return Task.CompletedTask;
}
// ── 나이 계산 ─────────────────────────────────────────────────────────────
private static IEnumerable<LauncherItem> BuildAgeItems(DateTime birth, DateTime today)
{
var ageInt = CalcAge(birth, today);
var ageKor = today.Year - birth.Year + 1; // 한국식 나이
var days = (today - birth).Days;
var months = (today.Year - birth.Year) * 12 + (today.Month - birth.Month);
var nextBirthday = NextBirthday(birth, today);
var daysToNext = (nextBirthday - today).Days;
var summary = $"""
생년월일: {birth:yyyy-MM-dd}
만 나이: {ageInt}세
한국 나이: {ageKor}세
경과 일수: {days:N0}일
경과 개월: {months:N0}개월
다음 생일: {nextBirthday:yyyy-MM-dd} (D-{daysToNext})
""";
yield return new LauncherItem(
$"만 {ageInt}세 (한국식 {ageKor}세)",
$"{birth:yyyy-MM-dd} · {days:N0}일 경과 · Enter 복사",
null, ("copy", summary), Symbol: "\uE787");
yield return new LauncherItem("만 나이", $"{ageInt}세", null, ("copy", ageInt.ToString()), Symbol: "\uE787");
yield return new LauncherItem("한국 나이", $"{ageKor}세", null, ("copy", ageKor.ToString()), Symbol: "\uE787");
yield return new LauncherItem("경과 일수", $"{days:N0}일", null, ("copy", days.ToString()), Symbol: "\uE787");
yield return new LauncherItem("경과 개월", $"{months:N0}개월", null, ("copy", months.ToString()), Symbol: "\uE787");
yield return new LauncherItem(
"다음 생일",
$"{nextBirthday:yyyy-MM-dd} · D-{daysToNext}",
null, ("copy", nextBirthday.ToString("yyyy-MM-dd")), Symbol: "\uE787");
// 요일
yield return new LauncherItem("태어난 요일", DayKor(birth.DayOfWeek), null, null, Symbol: "\uE787");
}
private static IEnumerable<LauncherItem> BuildDdayItems(DateTime target, string label, DateTime today)
{
var diff = (target - today).Days;
var absDiff = Math.Abs(diff);
var dLabel = diff > 0 ? $"D-{diff}" : diff == 0 ? "D-Day!" : $"D+{absDiff}";
yield return new LauncherItem(
dLabel,
$"{target:yyyy-MM-dd} ({label}) · {DayKor(target.DayOfWeek)}",
null, ("copy", dLabel), Symbol: "\uE787");
yield return new LauncherItem("날짜", target.ToString("yyyy-MM-dd"), null, ("copy", target.ToString("yyyy-MM-dd")), Symbol: "\uE787");
yield return new LauncherItem("요일", DayKor(target.DayOfWeek), null, null, Symbol: "\uE787");
yield return new LauncherItem("남은 일수", diff > 0 ? $"{diff:N0}일 후" : diff == 0 ? "오늘!" : $"{absDiff:N0}일 전", null, ("copy", absDiff.ToString()), Symbol: "\uE787");
yield return new LauncherItem("남은 주", $"{diff / 7}주 {diff % 7}일", null, null, Symbol: "\uE787");
}
// ── 파싱 헬퍼 ─────────────────────────────────────────────────────────────
private static bool TryParseDate(string s, out DateTime result)
{
result = default;
s = s.Trim();
// YYYYMMDD
if (s.Length == 8 && s.All(char.IsDigit))
{
return DateTime.TryParseExact(s, "yyyyMMdd",
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out result);
}
// 다양한 구분자 (-, ., /)
var normalized = s.Replace('.', '-').Replace('/', '-');
var formats = new[] { "yyyy-M-d", "yyyy-MM-dd", "yy-M-d", "M-d" };
foreach (var fmt in formats)
{
if (DateTime.TryParseExact(normalized, fmt,
System.Globalization.CultureInfo.InvariantCulture,
System.Globalization.DateTimeStyles.None, out result))
return true;
}
// "M월 d일" 한국어 형식
if (normalized.Contains('월'))
{
var parts = normalized.Split('월', '일');
if (parts.Length >= 2 &&
int.TryParse(parts[0].Trim(), out var m) &&
int.TryParse(parts[1].Trim(), out var d))
{
result = new DateTime(DateTime.Today.Year, m, d);
return true;
}
}
return false;
}
private static bool TryParseNextWeekday(string q, out DateTime result)
{
result = default;
var parts = q.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2 || !parts[0].Equals("next", StringComparison.OrdinalIgnoreCase))
return false;
DayOfWeek? dow = parts[1].ToLowerInvariant() switch
{
"monday" or "mon" or "월" or "월요일" => DayOfWeek.Monday,
"tuesday" or "tue" or "화" or "화요일" => DayOfWeek.Tuesday,
"wednesday" or "wed" or "수" or "수요일" => DayOfWeek.Wednesday,
"thursday" or "thu" or "목" or "목요일" => DayOfWeek.Thursday,
"friday" or "fri" or "금" or "금요일" => DayOfWeek.Friday,
"saturday" or "sat" or "토" or "토요일" => DayOfWeek.Saturday,
"sunday" or "sun" or "일" or "일요일" => DayOfWeek.Sunday,
_ => null,
};
if (dow == null) return false;
var today = DateTime.Today;
var daysAhead = ((int)dow.Value - (int)today.DayOfWeek + 7) % 7;
if (daysAhead == 0) daysAhead = 7; // "next"이므로 다음 주
result = today.AddDays(daysAhead);
return true;
}
// ── 날짜 계산 헬퍼 ────────────────────────────────────────────────────────
private static int CalcAge(DateTime birth, DateTime today)
{
var age = today.Year - birth.Year;
if (today.Month < birth.Month || (today.Month == birth.Month && today.Day < birth.Day))
age--;
return age;
}
private static DateTime NextBirthday(DateTime birth, DateTime today)
{
var thisYear = new DateTime(today.Year, birth.Month, birth.Day);
return thisYear >= today ? thisYear : thisYear.AddYears(1);
}
// 간략화된 양력 설날 근사값 (실제 음력 계산은 복잡하므로 고정 근사)
private static DateTime GetNextLunarNewYear(DateTime today)
{
// 설날은 대략 1월 말 ~ 2월 초이므로 2월 5일을 기준점으로 사용
var approx = new DateTime(today.Year, 2, 5);
return approx >= today ? approx : new DateTime(today.Year + 1, 2, 5);
}
private static DateTime GetNextChuseok(DateTime today)
{
// 추석은 대략 9월 말 ~ 10월 초이므로 9월 28일을 기준점으로 사용
var approx = new DateTime(today.Year, 9, 28);
return approx >= today ? approx : new DateTime(today.Year + 1, 9, 28);
}
private static string DayKor(DayOfWeek dow) => dow switch
{
DayOfWeek.Monday => "월요일",
DayOfWeek.Tuesday => "화요일",
DayOfWeek.Wednesday => "수요일",
DayOfWeek.Thursday => "목요일",
DayOfWeek.Friday => "금요일",
DayOfWeek.Saturday => "토요일",
DayOfWeek.Sunday => "일요일",
_ => "",
};
}