변경 목적: 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개를 확인했습니다.
350 lines
15 KiB
C#
350 lines
15 KiB
C#
using System.Globalization;
|
|
using System.Windows;
|
|
using AxCopilot.SDK;
|
|
using AxCopilot.Services;
|
|
|
|
namespace AxCopilot.Handlers;
|
|
|
|
/// <summary>
|
|
/// L23-1: 한국 공휴일·업무일 달력. "cal" 프리픽스로 사용합니다.
|
|
///
|
|
/// 예: cal → 이번달 달력·공휴일
|
|
/// cal next → 다음 공휴일 D-day (5개)
|
|
/// cal workdays → 이번달 업무일 수·잔여 업무일
|
|
/// cal today → 오늘 공휴일 여부
|
|
/// cal 2026-05 → 특정 월 조회
|
|
/// cal 2026-04-10 → 특정 날짜 조회
|
|
/// Enter → 복사
|
|
/// </summary>
|
|
public class CalHandler : IActionHandler
|
|
{
|
|
public string? Prefix => "cal";
|
|
|
|
public PluginMetadata Metadata => new(
|
|
"한국 달력",
|
|
"공휴일·업무일 달력 — 이번달 · 다음 공휴일 · 업무일 계산",
|
|
"1.0",
|
|
"AX");
|
|
|
|
// ── 공휴일 데이터 ─────────────────────────────────────────────────────────
|
|
private static readonly Dictionary<DateOnly, string> Holidays = new()
|
|
{
|
|
// 2024
|
|
{ new DateOnly(2024, 1, 1), "신정" },
|
|
{ new DateOnly(2024, 2, 9), "설날연휴" },
|
|
{ new DateOnly(2024, 2, 10), "설날" },
|
|
{ new DateOnly(2024, 2, 11), "설날연휴" },
|
|
{ new DateOnly(2024, 2, 12), "대체공휴일" },
|
|
{ new DateOnly(2024, 3, 1), "삼일절" },
|
|
{ new DateOnly(2024, 4, 10), "국회의원선거" },
|
|
{ new DateOnly(2024, 5, 5), "어린이날" },
|
|
{ new DateOnly(2024, 5, 6), "대체공휴일" },
|
|
{ new DateOnly(2024, 5, 15), "부처님오신날" },
|
|
{ new DateOnly(2024, 6, 6), "현충일" },
|
|
{ new DateOnly(2024, 8, 15), "광복절" },
|
|
{ new DateOnly(2024, 9, 16), "추석연휴" },
|
|
{ new DateOnly(2024, 9, 17), "추석" },
|
|
{ new DateOnly(2024, 9, 18), "추석연휴" },
|
|
{ new DateOnly(2024, 10, 3), "개천절" },
|
|
{ new DateOnly(2024, 10, 9), "한글날" },
|
|
{ new DateOnly(2024, 12, 25), "크리스마스" },
|
|
|
|
// 2025
|
|
{ new DateOnly(2025, 1, 1), "신정" },
|
|
{ new DateOnly(2025, 1, 28), "설날연휴" },
|
|
{ new DateOnly(2025, 1, 29), "설날" },
|
|
{ new DateOnly(2025, 1, 30), "설날연휴" },
|
|
{ new DateOnly(2025, 3, 1), "삼일절" },
|
|
{ new DateOnly(2025, 3, 3), "대체공휴일" },
|
|
{ new DateOnly(2025, 5, 5), "어린이날" },
|
|
{ new DateOnly(2025, 5, 6), "부처님오신날" },
|
|
{ new DateOnly(2025, 6, 6), "현충일" },
|
|
{ new DateOnly(2025, 8, 15), "광복절" },
|
|
{ new DateOnly(2025, 10, 3), "개천절" },
|
|
{ new DateOnly(2025, 10, 5), "추석연휴" },
|
|
{ new DateOnly(2025, 10, 6), "추석" },
|
|
{ new DateOnly(2025, 10, 7), "추석연휴" },
|
|
{ new DateOnly(2025, 10, 8), "대체공휴일" },
|
|
{ new DateOnly(2025, 10, 9), "한글날" },
|
|
{ new DateOnly(2025, 12, 25), "크리스마스" },
|
|
|
|
// 2026
|
|
{ new DateOnly(2026, 1, 1), "신정" },
|
|
{ new DateOnly(2026, 2, 17), "설날연휴" },
|
|
{ new DateOnly(2026, 2, 18), "설날" },
|
|
{ new DateOnly(2026, 2, 19), "설날연휴" },
|
|
{ new DateOnly(2026, 3, 1), "삼일절" },
|
|
{ new DateOnly(2026, 3, 2), "대체공휴일" },
|
|
{ new DateOnly(2026, 5, 5), "어린이날" },
|
|
{ new DateOnly(2026, 5, 24), "부처님오신날" },
|
|
{ new DateOnly(2026, 5, 25), "대체공휴일" },
|
|
{ new DateOnly(2026, 6, 6), "현충일" },
|
|
{ new DateOnly(2026, 6, 8), "대체공휴일" },
|
|
{ new DateOnly(2026, 8, 15), "광복절" },
|
|
{ new DateOnly(2026, 8, 17), "대체공휴일" },
|
|
{ new DateOnly(2026, 9, 24), "추석연휴" },
|
|
{ new DateOnly(2026, 9, 25), "추석" },
|
|
{ new DateOnly(2026, 9, 26), "추석연휴" },
|
|
{ new DateOnly(2026, 10, 3), "개천절" },
|
|
{ new DateOnly(2026, 10, 5), "대체공휴일" },
|
|
{ new DateOnly(2026, 10, 9), "한글날" },
|
|
{ new DateOnly(2026, 12, 25), "크리스마스" },
|
|
|
|
// 2027
|
|
{ new DateOnly(2027, 1, 1), "신정" },
|
|
{ new DateOnly(2027, 2, 7), "설날연휴" },
|
|
{ new DateOnly(2027, 2, 8), "설날" },
|
|
{ new DateOnly(2027, 2, 9), "설날연휴" },
|
|
{ new DateOnly(2027, 3, 1), "삼일절" },
|
|
{ new DateOnly(2027, 5, 5), "어린이날" },
|
|
{ new DateOnly(2027, 5, 13), "부처님오신날" },
|
|
{ new DateOnly(2027, 6, 6), "현충일" },
|
|
{ new DateOnly(2027, 6, 7), "대체공휴일" },
|
|
{ new DateOnly(2027, 8, 15), "광복절" },
|
|
{ new DateOnly(2027, 8, 16), "대체공휴일" },
|
|
{ new DateOnly(2027, 9, 13), "추석연휴" },
|
|
{ new DateOnly(2027, 9, 14), "추석" },
|
|
{ new DateOnly(2027, 9, 15), "추석연휴" },
|
|
{ new DateOnly(2027, 10, 3), "개천절" },
|
|
{ new DateOnly(2027, 10, 4), "대체공휴일" },
|
|
{ new DateOnly(2027, 10, 9), "한글날" },
|
|
{ new DateOnly(2027, 12, 25), "크리스마스" },
|
|
{ new DateOnly(2027, 12, 27), "대체공휴일" },
|
|
};
|
|
|
|
private static readonly string[] DayNames = ["일", "월", "화", "수", "목", "금", "토"];
|
|
|
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
|
|
|
private static bool IsHoliday(DateOnly d) =>
|
|
Holidays.ContainsKey(d) || d.DayOfWeek == DayOfWeek.Saturday || d.DayOfWeek == DayOfWeek.Sunday;
|
|
|
|
private static bool IsWorkday(DateOnly d) => !IsHoliday(d);
|
|
|
|
private static int CountWorkdays(int year, int month)
|
|
{
|
|
var days = DateTime.DaysInMonth(year, month);
|
|
var count = 0;
|
|
for (var i = 1; i <= days; i++)
|
|
if (IsWorkday(new DateOnly(year, month, i))) count++;
|
|
return count;
|
|
}
|
|
|
|
private static int CountWorkdaysFrom(DateOnly from, DateOnly to)
|
|
{
|
|
var count = 0;
|
|
var cur = from;
|
|
while (cur <= to)
|
|
{
|
|
if (IsWorkday(cur)) count++;
|
|
cur = cur.AddDays(1);
|
|
}
|
|
return count;
|
|
}
|
|
|
|
private static string DayOfWeekKor(DayOfWeek dow) => DayNames[(int)dow];
|
|
|
|
// ── GetItemsAsync ─────────────────────────────────────────────────────────
|
|
|
|
public Task<IEnumerable<LauncherItem>> GetItemsAsync(string query, CancellationToken ct)
|
|
{
|
|
var q = query.Trim();
|
|
var today = DateOnly.FromDateTime(DateTime.Today);
|
|
var items = new List<LauncherItem>();
|
|
|
|
if (string.IsNullOrWhiteSpace(q))
|
|
{
|
|
BuildMonthItems(today.Year, today.Month, today, items);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
var kw = q.ToLowerInvariant();
|
|
|
|
// next → 다음 공휴일 5개
|
|
if (kw == "next")
|
|
{
|
|
items.Add(new LauncherItem("다음 공휴일 5개", "D-N일 표시 · Enter: 클립보드 복사",
|
|
null, null, Symbol: "\uE787"));
|
|
var upcoming = Holidays.Keys
|
|
.Where(d => d > today)
|
|
.OrderBy(d => d)
|
|
.Take(5);
|
|
foreach (var d in upcoming)
|
|
{
|
|
var diff = d.DayNumber - today.DayNumber;
|
|
var name = Holidays[d];
|
|
var label = $"{d:yyyy-MM-dd} ({DayOfWeekKor(d.DayOfWeek)}) — {name} D-{diff}일";
|
|
items.Add(new LauncherItem(label, "Enter: 클립보드 복사",
|
|
null, ("copy", $"{d:yyyy-MM-dd} {name}"), Symbol: "\uE787"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// workdays → 이번달 업무일
|
|
if (kw == "workdays")
|
|
{
|
|
var total = CountWorkdays(today.Year, today.Month);
|
|
var remaining = CountWorkdaysFrom(today, new DateOnly(today.Year, today.Month,
|
|
DateTime.DaysInMonth(today.Year, today.Month)));
|
|
items.Add(new LauncherItem(
|
|
$"{today.Year}년 {today.Month}월 업무일 — 총 {total}일",
|
|
$"오늘 기준 잔여 업무일: {remaining}일",
|
|
null, ("copy", $"{today.Year}년 {today.Month}월 업무일: 총 {total}일, 잔여 {remaining}일"),
|
|
Symbol: "\uE787"));
|
|
|
|
var monthHolidays = Holidays.Where(kv =>
|
|
kv.Key.Year == today.Year && kv.Key.Month == today.Month)
|
|
.OrderBy(kv => kv.Key);
|
|
foreach (var kv in monthHolidays)
|
|
{
|
|
var label = $"🎌 {kv.Key.Day}일 ({DayOfWeekKor(kv.Key.DayOfWeek)}) — {kv.Value}";
|
|
items.Add(new LauncherItem(label, "Enter: 클립보드 복사",
|
|
null, ("copy", $"{kv.Key:yyyy-MM-dd} {kv.Value}"), Symbol: "\uE787"));
|
|
}
|
|
if (!monthHolidays.Any())
|
|
items.Add(new LauncherItem("이번 달 공휴일 없음", "", null, null, Symbol: "\uE787"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// today → 오늘 정보
|
|
if (kw == "today")
|
|
{
|
|
var isHol = Holidays.TryGetValue(today, out var holName);
|
|
var isWknd = today.DayOfWeek == DayOfWeek.Saturday || today.DayOfWeek == DayOfWeek.Sunday;
|
|
string status;
|
|
if (isHol) status = $"공휴일 ({holName})";
|
|
else if (isWknd) status = "주말";
|
|
else status = "평일 (업무일)";
|
|
|
|
items.Add(new LauncherItem(
|
|
$"{today:yyyy-MM-dd} ({DayOfWeekKor(today.DayOfWeek)}) — {status}",
|
|
"Enter: 클립보드 복사",
|
|
null, ("copy", $"{today:yyyy-MM-dd} ({DayOfWeekKor(today.DayOfWeek)}) {status}"),
|
|
Symbol: "\uE787"));
|
|
|
|
var next = Holidays.Keys.Where(d => d > today).OrderBy(d => d).FirstOrDefault();
|
|
if (next != default)
|
|
{
|
|
var diff = next.DayNumber - today.DayNumber;
|
|
items.Add(new LauncherItem(
|
|
$"다음 공휴일: {next:yyyy-MM-dd} ({DayOfWeekKor(next.DayOfWeek)}) — {Holidays[next]} D-{diff}일",
|
|
"", null, ("copy", $"{next:yyyy-MM-dd} {Holidays[next]}"), Symbol: "\uE787"));
|
|
}
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// yyyy-MM-dd 날짜 직접 조회
|
|
var dateFormats = new[] { "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd" };
|
|
if (DateOnly.TryParseExact(q, dateFormats, CultureInfo.InvariantCulture,
|
|
DateTimeStyles.None, out var specificDate))
|
|
{
|
|
var isHol = Holidays.TryGetValue(specificDate, out var hName);
|
|
var isWknd = specificDate.DayOfWeek == DayOfWeek.Saturday ||
|
|
specificDate.DayOfWeek == DayOfWeek.Sunday;
|
|
string status;
|
|
if (isHol) status = $"공휴일 ({hName})";
|
|
else if (isWknd) status = "주말";
|
|
else status = "평일 (업무일)";
|
|
items.Add(new LauncherItem(
|
|
$"{specificDate:yyyy-MM-dd} ({DayOfWeekKor(specificDate.DayOfWeek)}) — {status}",
|
|
"Enter: 클립보드 복사",
|
|
null, ("copy", $"{specificDate:yyyy-MM-dd} ({DayOfWeekKor(specificDate.DayOfWeek)}) {status}"),
|
|
Symbol: "\uE787"));
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// yyyy-MM or yyyy/MM or yyyyMM 월 조회
|
|
var monthFormats = new[] { "yyyy-MM", "yyyy/MM", "yyyyMM" };
|
|
if (DateOnly.TryParseExact(q + "-01", new[] { "yyyy-MM-dd", "yyyy/MM/dd", "yyyyMM-dd" },
|
|
CultureInfo.InvariantCulture, DateTimeStyles.None, out var monthDate) ||
|
|
TryParseYearMonth(q, out monthDate))
|
|
{
|
|
BuildMonthItems(monthDate.Year, monthDate.Month, today, items);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
// 기본 — 이번달
|
|
BuildMonthItems(today.Year, today.Month, today, items);
|
|
return Task.FromResult<IEnumerable<LauncherItem>>(items);
|
|
}
|
|
|
|
private static bool TryParseYearMonth(string q, out DateOnly result)
|
|
{
|
|
result = default;
|
|
// yyyy-MM, yyyy/MM, yyyyMM
|
|
var formats = new[] { "yyyy-MM", "yyyy/MM" };
|
|
foreach (var fmt in formats)
|
|
{
|
|
if (DateTime.TryParseExact(q, fmt, CultureInfo.InvariantCulture,
|
|
DateTimeStyles.None, out var dt))
|
|
{
|
|
result = new DateOnly(dt.Year, dt.Month, 1);
|
|
return true;
|
|
}
|
|
}
|
|
// yyyyMM (6자리)
|
|
if (q.Length == 6 && int.TryParse(q[..4], out var y) && int.TryParse(q[4..], out var m)
|
|
&& m >= 1 && m <= 12)
|
|
{
|
|
result = new DateOnly(y, m, 1);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static void BuildMonthItems(int year, int month, DateOnly today, List<LauncherItem> items)
|
|
{
|
|
var total = CountWorkdays(year, month);
|
|
var lastDay = new DateOnly(year, month, DateTime.DaysInMonth(year, month));
|
|
int remaining;
|
|
if (today.Year == year && today.Month == month)
|
|
remaining = CountWorkdaysFrom(today, lastDay);
|
|
else if (today < new DateOnly(year, month, 1))
|
|
remaining = total;
|
|
else
|
|
remaining = 0;
|
|
|
|
var header = $"{year}년 {month}월 · 업무일 {total}일";
|
|
if (today.Year == year && today.Month == month)
|
|
header += $" (잔여 {remaining}일)";
|
|
|
|
items.Add(new LauncherItem(header, "공휴일 목록 · Enter: 클립보드 복사",
|
|
null, ("copy", header), Symbol: "\uE787"));
|
|
|
|
var monthHolidays = Holidays
|
|
.Where(kv => kv.Key.Year == year && kv.Key.Month == month)
|
|
.OrderBy(kv => kv.Key)
|
|
.ToList();
|
|
|
|
if (monthHolidays.Count == 0)
|
|
{
|
|
items.Add(new LauncherItem("공휴일 없음", "", null, null, Symbol: "\uE787"));
|
|
}
|
|
else
|
|
{
|
|
foreach (var kv in monthHolidays)
|
|
{
|
|
var label = $"🎌 {kv.Key.Day}일 ({DayOfWeekKor(kv.Key.DayOfWeek)}) — {kv.Value}";
|
|
items.Add(new LauncherItem(label, "Enter: 클립보드 복사",
|
|
null, ("copy", $"{kv.Key:yyyy-MM-dd} {kv.Value}"), Symbol: "\uE787"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── ExecuteAsync ──────────────────────────────────────────────────────────
|
|
|
|
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
|
|
{
|
|
if (item.Data is ("copy", string text))
|
|
{
|
|
try
|
|
{
|
|
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
|
|
NotificationService.Notify("달력", "클립보드에 복사했습니다.");
|
|
}
|
|
catch { }
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
}
|