using System.IO; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Windows; using AxCopilot.SDK; using AxCopilot.Services; namespace AxCopilot.Handlers; /// /// L27-4: 회의 링크 전용 관리 핸들러. "meet" 프리픽스로 사용합니다. /// /// 예: meet → 전체 회의 목록 /// meet 스탠드업 → 이름 검색 /// meet add 스탠드업 https://... → 회의 추가 /// meet del 스탠드업 → 회의 삭제 /// Enter → 기본 브라우저로 회의 링크 열기. /// 저장: %APPDATA%\AxCopilot\meet.json /// public class MeetHandler : IActionHandler { public string? Prefix => "meet"; public PluginMetadata Metadata => new( "회의 링크", "회의 링크 관리 — 추가 · 검색 · 즉시 열기", "1.0", "AX"); private record MeetEntry( [property: JsonPropertyName("name")] string Name, [property: JsonPropertyName("url")] string Url, [property: JsonPropertyName("service")] string Service); private static readonly string DataPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "AxCopilot", "meet.json"); private static readonly JsonSerializerOptions JsonOpt = new() { WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; public Task> GetItemsAsync(string query, CancellationToken ct) { var q = query.Trim(); var items = new List(); var meets = Load(); // ── add 명령 ───────────────────────────────────────────────────────── if (q.StartsWith("add ", StringComparison.OrdinalIgnoreCase)) { var parts = q[4..].Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); if (parts.Length < 2 || !Uri.TryCreate(parts[1], UriKind.Absolute, out _)) { items.Add(new LauncherItem("사용법: meet add {이름} {URL}", "예: meet add 스탠드업 https://teams.microsoft.com/...", null, null, Symbol: "\uE710")); } else { var name = parts[0]; var url = parts[1]; var svc = DetectService(url); items.Add(new LauncherItem( $"회의 추가: {name}", $"{svc} · {url}", null, ("add", $"{name}\t{url}\t{svc}"), Symbol: "\uE710")); } return Task.FromResult>(items); } // ── del 명령 ───────────────────────────────────────────────────────── if (q.StartsWith("del ", StringComparison.OrdinalIgnoreCase)) { var name = q[4..].Trim(); var found = meets.FirstOrDefault(m => m.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (found != null) { items.Add(new LauncherItem( $"회의 삭제: {found.Name}", $"{found.Service} · {found.Url}", null, ("del", found.Name), Symbol: "\uE74D")); } else { items.Add(new LauncherItem($"'{name}' 회의를 찾을 수 없습니다", "meet del {이름}", null, null, Symbol: "\uE783")); } return Task.FromResult>(items); } // ── 빈 쿼리 → 전체 목록 ────────────────────────────────────────────── if (string.IsNullOrWhiteSpace(q)) { if (meets.Count == 0) { items.Add(new LauncherItem("등록된 회의가 없습니다", "meet add {이름} {URL} 로 추가하세요", null, null, Symbol: "\uE8D6")); return Task.FromResult>(items); } items.Add(new LauncherItem( $"회의 {meets.Count}개 등록됨", "Enter: 브라우저로 열기 · meet add/del 로 관리", null, null, Symbol: "\uE8D6")); foreach (var m in meets) items.Add(MeetItem(m)); return Task.FromResult>(items); } // ── 검색 ────────────────────────────────────────────────────────────── var searched = meets.Where(m => m.Name.Contains(q, StringComparison.OrdinalIgnoreCase) || m.Service.Contains(q, StringComparison.OrdinalIgnoreCase) || m.Url.Contains(q, StringComparison.OrdinalIgnoreCase)).ToList(); if (searched.Count == 0) { items.Add(new LauncherItem($"'{q}' 회의를 찾을 수 없습니다", "meet add {이름} {URL} 로 추가하세요", null, null, Symbol: "\uE783")); } else { foreach (var m in searched) items.Add(MeetItem(m)); } return Task.FromResult>(items); } public Task ExecuteAsync(LauncherItem item, CancellationToken ct) { if (item.Data is ("add", string addData)) { var parts = addData.Split('\t'); if (parts.Length >= 3) { var meets = Load(); meets.RemoveAll(m => m.Name.Equals(parts[0], StringComparison.OrdinalIgnoreCase)); meets.Add(new MeetEntry(parts[0], parts[1], parts[2])); Save(meets); NotificationService.Notify("meet", $"'{parts[0]}' 회의가 추가되었습니다."); } } else if (item.Data is ("del", string delName)) { var meets = Load(); int removed = meets.RemoveAll(m => m.Name.Equals(delName, StringComparison.OrdinalIgnoreCase)); Save(meets); if (removed > 0) NotificationService.Notify("meet", $"'{delName}' 회의가 삭제되었습니다."); } else if (item.Data is ("open", string url)) { try { System.Diagnostics.Process.Start( new System.Diagnostics.ProcessStartInfo(url) { UseShellExecute = true }); } catch (Exception ex) { NotificationService.Notify("meet", $"열기 실패: {ex.Message}"); } } return Task.CompletedTask; } // ─── 헬퍼 ──────────────────────────────────────────────────────────────── private static LauncherItem MeetItem(MeetEntry m) { var icon = m.Service switch { "Zoom" => "\uE774", "Teams" => "\uE8D6", "Google Meet" => "\uE774", "Webex" => "\uE774", _ => "\uE774" }; return new LauncherItem( $"{m.Name} [{m.Service}]", m.Url, null, ("open", m.Url), Symbol: icon); } private static string DetectService(string url) { var lower = url.ToLowerInvariant(); if (lower.Contains("zoom.us") || lower.Contains("zoom.com")) return "Zoom"; if (lower.Contains("teams.microsoft.com") || lower.Contains("teams.live.com")) return "Teams"; if (lower.Contains("meet.google.com")) return "Google Meet"; if (lower.Contains("webex.com")) return "Webex"; if (lower.Contains("discord.gg") || lower.Contains("discord.com")) return "Discord"; if (lower.Contains("slack.com")) return "Slack"; return "기타"; } // ─── JSON 파일 I/O ─────────────────────────────────────────────────────── private static List Load() { try { if (!File.Exists(DataPath)) return []; var json = File.ReadAllText(DataPath); return JsonSerializer.Deserialize>(json) ?? []; } catch { return []; } } private static void Save(List list) { try { var dir = Path.GetDirectoryName(DataPath)!; if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); File.WriteAllText(DataPath, JsonSerializer.Serialize(list, JsonOpt)); } catch { } } }