using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
using AxCopilot.SDK;
using AxCopilot.Services;
namespace AxCopilot.Handlers;
///
/// L25-1: 로컬 연락처 관리. "contact" 프리픽스로 사용합니다.
///
/// 예: contact → 전체 연락처 목록
/// contact 홍길동 → 이름 검색
/// contact add 홍길동 개발팀 010-1234-5678 hong@company.com → 추가
/// contact del 홍길동 → 삭제
/// contact <부서명> → 부서 필터
/// Enter → 이메일 또는 전화번호 클립보드 복사
/// 저장: %APPDATA%\AxCopilot\contacts.json
///
public class ContactHandler : IActionHandler
{
public string? Prefix => "contact";
public PluginMetadata Metadata => new(
"연락처",
"로컬 연락처 관리 — 추가 · 검색 · 삭제 · 복사",
"1.0",
"AX");
private static readonly string DataPath = System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"AxCopilot", "contacts.json");
private static readonly JsonSerializerOptions JsonOpts = new()
{
WriteIndented = true,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
};
private sealed class Contact
{
[JsonPropertyName("name")] public string Name { get; set; } = "";
[JsonPropertyName("dept")] public string Dept { get; set; } = "";
[JsonPropertyName("phone")] public string Phone { get; set; } = "";
[JsonPropertyName("email")] public string Email { get; set; } = "";
[JsonPropertyName("memo")] public string Memo { get; set; } = "";
}
public Task> GetItemsAsync(string query, CancellationToken ct)
{
var q = query.Trim();
var items = new List();
var contacts = LoadContacts();
if (string.IsNullOrWhiteSpace(q))
{
items.Add(new LauncherItem(
$"연락처 {contacts.Count}개",
"contact add 이름 부서 010-xxxx-xxxx email@company.com",
null, null, Symbol: "\uE8D4"));
if (contacts.Count == 0)
{
items.Add(new LauncherItem("저장된 연락처가 없습니다",
"contact add 이름 부서 전화 이메일 로 추가하세요",
null, null, Symbol: "\uE946"));
return Task.FromResult>(items);
}
foreach (var c in contacts)
items.Add(MakeContactItem(c));
return Task.FromResult>(items);
}
var parts = q.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var sub = parts[0].ToLowerInvariant();
// add 명령
if (sub == "add")
{
var addPart = parts.Length > 1 ? parts[1].Trim() : "";
if (string.IsNullOrWhiteSpace(addPart))
{
items.Add(new LauncherItem("이름을 입력하세요",
"예: contact add 홍길동 개발팀 010-1234-5678 hong@company.com",
null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
var (name, dept, phone, email, memo) = ParseAddArgs(addPart);
if (string.IsNullOrWhiteSpace(name))
{
items.Add(new LauncherItem("이름을 입력하세요",
"예: contact add 홍길동 개발팀 010-1234-5678 hong@company.com",
null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
var encoded = $"{name}|{dept}|{phone}|{email}|{memo}";
items.Add(new LauncherItem(
$"연락처 추가: {name}",
$"{dept} {phone} {email} · Enter로 추가",
null, ("add", encoded), Symbol: "\uE710"));
return Task.FromResult>(items);
}
// del 명령
if (sub is "del" or "delete" or "remove")
{
var delName = parts.Length > 1 ? parts[1].Trim() : "";
if (string.IsNullOrWhiteSpace(delName))
{
items.Add(new LauncherItem("삭제할 이름을 입력하세요",
"예: contact del 홍길동",
null, null, Symbol: "\uE783"));
return Task.FromResult>(items);
}
var matches = contacts.Where(c =>
c.Name.Contains(delName, StringComparison.OrdinalIgnoreCase)).ToList();
if (matches.Count == 0)
{
items.Add(new LauncherItem($"'{delName}' 연락처를 찾을 수 없습니다", "",
null, null, Symbol: "\uE783"));
}
else
{
foreach (var c in matches)
items.Add(new LauncherItem(
$"{c.Name} 삭제",
$"{c.Dept} {c.Phone} {c.Email} · Enter로 삭제",
null, ("del", c.Name), Symbol: "\uE8D4"));
}
return Task.FromResult>(items);
}
// 검색: 이름, 부서, 메모 포함
var byStartsWith = contacts
.Where(c => c.Name.StartsWith(q, StringComparison.OrdinalIgnoreCase) ||
c.Dept.StartsWith(q, StringComparison.OrdinalIgnoreCase))
.ToList();
var byContains = contacts
.Where(c => !byStartsWith.Contains(c) &&
(c.Name.Contains(q, StringComparison.OrdinalIgnoreCase) ||
c.Dept.Contains(q, StringComparison.OrdinalIgnoreCase) ||
c.Memo.Contains(q, StringComparison.OrdinalIgnoreCase)))
.ToList();
var results = byStartsWith.Concat(byContains).ToList();
if (results.Count > 0)
{
items.Add(new LauncherItem($"'{q}' 검색 결과 {results.Count}개", "",
null, null, Symbol: "\uE8D4"));
foreach (var c in results)
items.Add(MakeContactItem(c));
}
else
{
items.Add(new LauncherItem($"'{q}' 검색 결과 없음",
"contact add 로 새 연락처를 추가하세요",
null, null, Symbol: "\uE783"));
}
return Task.FromResult>(items);
}
public Task ExecuteAsync(LauncherItem item, CancellationToken ct)
{
switch (item.Data)
{
case ("copy_email", string email) when !string.IsNullOrWhiteSpace(email):
CopyToClipboard(email);
NotificationService.Notify("연락처", $"{email} 복사");
break;
case ("copy_phone", string phone) when !string.IsNullOrWhiteSpace(phone):
CopyToClipboard(phone);
NotificationService.Notify("연락처", $"{phone} 복사");
break;
case ("copy", string text) when !string.IsNullOrWhiteSpace(text):
CopyToClipboard(text);
NotificationService.Notify("연락처", $"{text} 복사");
break;
case ("add", string encoded):
var addParts = encoded.Split('|');
if (addParts.Length >= 5)
{
var contact = new Contact
{
Name = addParts[0],
Dept = addParts[1],
Phone = addParts[2],
Email = addParts[3],
Memo = addParts[4],
};
var list = LoadContacts();
list.RemoveAll(c => c.Name == contact.Name);
list.Add(contact);
SaveContacts(list);
NotificationService.Notify("연락처", $"{contact.Name} 추가됨");
}
break;
case ("del", string name):
var contacts = LoadContacts();
var removed = contacts.RemoveAll(c => c.Name == name);
if (removed > 0)
{
SaveContacts(contacts);
NotificationService.Notify("연락처", $"{name} 삭제됨");
}
break;
}
return Task.CompletedTask;
}
// ── 저장/불러오기 ─────────────────────────────────────────────────────────
private static List LoadContacts()
{
try
{
if (!System.IO.File.Exists(DataPath)) return new List();
var json = System.IO.File.ReadAllText(DataPath, System.Text.Encoding.UTF8);
return JsonSerializer.Deserialize>(json, JsonOpts) ?? new List();
}
catch { return new List(); }
}
private static void SaveContacts(List contacts)
{
try
{
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(DataPath)!);
System.IO.File.WriteAllText(DataPath,
JsonSerializer.Serialize(contacts, JsonOpts),
System.Text.Encoding.UTF8);
}
catch { /* 비핵심 */ }
}
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
private static LauncherItem MakeContactItem(Contact c)
{
var title = $"{c.Name}";
if (!string.IsNullOrWhiteSpace(c.Dept)) title += $" · {c.Dept}";
var sub = "";
if (!string.IsNullOrWhiteSpace(c.Phone)) sub += c.Phone;
if (!string.IsNullOrWhiteSpace(c.Email)) sub += (sub.Length > 0 ? " | " : "") + c.Email;
// 복사 우선순위: email > phone > name
(string action, string value) data;
if (!string.IsNullOrWhiteSpace(c.Email))
data = ("copy_email", c.Email);
else if (!string.IsNullOrWhiteSpace(c.Phone))
data = ("copy_phone", c.Phone);
else
data = ("copy", c.Name);
return new LauncherItem(title, sub, null, data, Symbol: "\uE8D4");
}
private static (string name, string dept, string phone, string email, string memo)
ParseAddArgs(string addPart)
{
var tokens = addPart.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (tokens.Length == 0) return ("", "", "", "", "");
var name = tokens[0];
var dept = "";
var phone = "";
var email = "";
var memoTokens = new List();
for (var i = 1; i < tokens.Length; i++)
{
var t = tokens[i];
if (string.IsNullOrWhiteSpace(phone) && IsPhoneLike(t))
phone = t;
else if (string.IsNullOrWhiteSpace(email) && t.Contains('@'))
email = t;
else if (string.IsNullOrWhiteSpace(dept) && i == 1)
dept = t;
else
memoTokens.Add(t);
}
return (name, dept, phone, email, string.Join(" ", memoTokens));
}
private static bool IsPhoneLike(string s)
{
var stripped = s.Replace("-", "").Replace(" ", "");
return stripped.Length >= 9 && stripped.All(c => char.IsDigit(c));
}
private static void CopyToClipboard(string text)
{
try
{
Application.Current.Dispatcher.Invoke(() => Clipboard.SetText(text));
}
catch { }
}
}