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>
285 lines
12 KiB
C#
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 => "일요일",
|
|
_ => "",
|
|
};
|
|
}
|