270 lines
12 KiB
C#
270 lines
12 KiB
C#
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Windows;
|
|
|
|
namespace AxCopilot.Services;
|
|
|
|
/// <summary>
|
|
/// 격려 문구, 명언, 업계 상식, IT/AI, 과학, 역사, 생활영어, 드라마/영화 대사를 카테고리별로 관리합니다.
|
|
/// </summary>
|
|
public static class QuoteService
|
|
{
|
|
private record FamousQuote(string Text, string Author);
|
|
private record MovieQuote(string Text, string Movie, int Year, string? Character, long? Audience);
|
|
|
|
// 카테고리별 콘텐츠 (Lazy 로딩)
|
|
private static readonly Lazy<string[]> _motivational = new(LoadMotivational);
|
|
private static readonly Lazy<FamousQuote[]> _famous = new(LoadFamous);
|
|
private static readonly Lazy<string[]> _displaySemi = new(() => LoadSimple("display_semiconductor.json"));
|
|
private static readonly Lazy<string[]> _itAi = new(() => LoadSimple("it_ai.json"));
|
|
private static readonly Lazy<string[]> _science = new(() => LoadSimple("science.json"));
|
|
private static readonly Lazy<string[]> _history = new(() => LoadSimple("history.json"));
|
|
private static readonly Lazy<string[]> _english = new(() => LoadSimple("english.json"));
|
|
private static readonly Lazy<MovieQuote[]> _movies = new(LoadMovies);
|
|
private static readonly Lazy<Dictionary<string, string[]>> _todayEvents = new(LoadTodayEvents);
|
|
|
|
/// <summary>카테고리 정의: (key, label, countFunc). 설정 UI에서 다중 체크로 표시됨.</summary>
|
|
public static readonly (string Key, string Label, Func<int> Count)[] Categories =
|
|
[
|
|
("motivational", "격려 문구", () => _motivational.Value.Length),
|
|
("famous", "유명 명언", () => _famous.Value.Length),
|
|
("display_semiconductor", "디스플레이/반도체 상식", () => _displaySemi.Value.Length),
|
|
("it_ai", "IT, AI 기술", () => _itAi.Value.Length),
|
|
("science", "일반 과학", () => _science.Value.Length),
|
|
("history", "역사", () => _history.Value.Length),
|
|
("english", "생활영어", () => _english.Value.Length),
|
|
("movies", "드라마/영화 명대사", () => _movies.Value.Length),
|
|
("today_events", "오늘의 역사/기념일", () => GetTodayEventCount()),
|
|
];
|
|
|
|
// ─── 로더 ──────────────────────────────────────────────────────────
|
|
|
|
private static string[] LoadMotivational()
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/Assets/Quotes/motivational.json");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream == null) return [];
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
return JsonSerializer.Deserialize<string[]>(reader.ReadToEnd()) ?? [];
|
|
}
|
|
catch (Exception ex) { LogService.Warn($"격려 문구 로드 실패: {ex.Message}"); return []; }
|
|
}
|
|
|
|
private static FamousQuote[] LoadFamous()
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/Assets/Quotes/famous.json");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream == null) return [];
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
return JsonSerializer.Deserialize<FamousQuote[]>(reader.ReadToEnd(), opts) ?? [];
|
|
}
|
|
catch (Exception ex) { LogService.Warn($"명언 로드 실패: {ex.Message}"); return []; }
|
|
}
|
|
|
|
private static string[] LoadSimple(string fileName)
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri($"pack://application:,,,/Assets/Quotes/{fileName}");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream == null) return [];
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
return JsonSerializer.Deserialize<string[]>(reader.ReadToEnd()) ?? [];
|
|
}
|
|
catch (Exception ex) { LogService.Warn($"{fileName} 로드 실패: {ex.Message}"); return []; }
|
|
}
|
|
|
|
private static MovieQuote[] LoadMovies()
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/Assets/Quotes/movies.json");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream == null) return [];
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
return JsonSerializer.Deserialize<MovieQuote[]>(reader.ReadToEnd(), opts) ?? [];
|
|
}
|
|
catch { return []; }
|
|
}
|
|
|
|
private static Dictionary<string, string[]> LoadTodayEvents()
|
|
{
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/Assets/Quotes/today_events.json");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream == null) return new();
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
return JsonSerializer.Deserialize<Dictionary<string, string[]>>(reader.ReadToEnd()) ?? new();
|
|
}
|
|
catch { return new(); }
|
|
}
|
|
|
|
// ─── 공개 API ──────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// 활성화된 카테고리에서 랜덤으로 하나를 반환합니다.
|
|
/// 날짜 카테고리가 활성화되어 있으면 첫 호출 시 오늘의 이벤트를 우선 반환합니다.
|
|
/// </summary>
|
|
public static (string Text, string? Author) GetRandom(IEnumerable<string>? enabledCategories = null)
|
|
{
|
|
var enabled = enabledCategories?.ToHashSet(StringComparer.OrdinalIgnoreCase)
|
|
?? new HashSet<string> { "motivational" };
|
|
|
|
// 시간대별 인사 접두사
|
|
var greeting = GetTimeGreeting();
|
|
|
|
// 날짜 이벤트 우선 (해당 카테고리 선택 시)
|
|
if (enabled.Contains("today_events"))
|
|
{
|
|
var todayKey = DateTime.Now.ToString("MM-dd");
|
|
if (_todayEvents.Value.TryGetValue(todayKey, out var events) && events.Length > 0)
|
|
{
|
|
var evt = events[Random.Shared.Next(events.Length)];
|
|
return (greeting + evt, "오늘의 역사");
|
|
}
|
|
}
|
|
|
|
var pool = new List<(string Text, string? Author)>();
|
|
|
|
if (enabled.Contains("motivational"))
|
|
foreach (var q in _motivational.Value) pool.Add((q, null));
|
|
|
|
if (enabled.Contains("famous"))
|
|
foreach (var q in _famous.Value) pool.Add((q.Text, q.Author));
|
|
|
|
if (enabled.Contains("display_semiconductor"))
|
|
foreach (var q in _displaySemi.Value) pool.Add((q, "디스플레이/반도체"));
|
|
|
|
if (enabled.Contains("it_ai"))
|
|
foreach (var q in _itAi.Value) pool.Add((q, "IT/AI"));
|
|
|
|
if (enabled.Contains("science"))
|
|
foreach (var q in _science.Value) pool.Add((q, "과학"));
|
|
|
|
if (enabled.Contains("history"))
|
|
foreach (var q in _history.Value) pool.Add((q, "역사"));
|
|
|
|
if (enabled.Contains("english"))
|
|
foreach (var q in _english.Value) pool.Add((q, "생활영어"));
|
|
|
|
if (enabled.Contains("movies"))
|
|
foreach (var q in _movies.Value)
|
|
{
|
|
var medal = q.Audience >= 10_000_000 ? " 🥇" : q.Audience >= 7_000_000 ? " 🥈" : q.Audience >= 5_000_000 ? " 🥉" : "";
|
|
var info = $"《{q.Movie}》({q.Year}){(q.Character != null ? $" — {q.Character}" : "")}{medal}";
|
|
pool.Add(($"\"{q.Text}\"", info));
|
|
}
|
|
|
|
if (pool.Count == 0)
|
|
return (greeting + "오늘도 열심히 해주셔서 고맙습니다!", null);
|
|
|
|
var (text, author) = pool[Random.Shared.Next(pool.Count)];
|
|
return (greeting + text, author);
|
|
}
|
|
|
|
/// <summary>시간대에 따른 인사 접두사를 반환합니다 (줄바꿈 포함).</summary>
|
|
/// <remarks>
|
|
/// 외부 파일(%APPDATA%\AxCopilot\Quotes\greetings.json) 우선 로드.
|
|
/// 외부 파일이 없으면 내장 리소스(Assets/Quotes/greetings.json) 사용.
|
|
/// 개발자가 외부 JSON만 수정하면 재빌드 없이 인사문구 변경/추가 가능.
|
|
/// </remarks>
|
|
private static string GetTimeGreeting()
|
|
{
|
|
var hour = DateTime.Now.Hour;
|
|
string key;
|
|
if (hour < 9) key = "morning";
|
|
else if (hour >= 12 && hour < 15) key = "lunch";
|
|
else if (hour >= 18) key = "evening";
|
|
else return "";
|
|
|
|
var greetings = LoadGreetings();
|
|
if (greetings.TryGetValue(key, out var list) && list.Length > 0)
|
|
return list[Random.Shared.Next(list.Length)] + "\n\n";
|
|
|
|
return "";
|
|
}
|
|
|
|
private static Dictionary<string, string[]>? _greetingsCache;
|
|
private static DateTime _greetingsCacheTime;
|
|
|
|
/// <summary>인사문구를 로드합니다. 외부 파일 → 내장 리소스 순서.</summary>
|
|
private static Dictionary<string, string[]> LoadGreetings()
|
|
{
|
|
// 5분간 캐시 (파일 수정 반영을 위해 주기적 리로드)
|
|
if (_greetingsCache != null && (DateTime.Now - _greetingsCacheTime).TotalMinutes < 5)
|
|
return _greetingsCache;
|
|
|
|
var opts = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
|
|
|
// 1) 외부 파일 우선
|
|
try
|
|
{
|
|
var externalPath = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
|
"AxCopilot", "Quotes", "greetings.json");
|
|
if (File.Exists(externalPath))
|
|
{
|
|
var json = File.ReadAllText(externalPath, Encoding.UTF8);
|
|
var result = JsonSerializer.Deserialize<Dictionary<string, string[]>>(json, opts);
|
|
if (result != null)
|
|
{
|
|
_greetingsCache = result;
|
|
_greetingsCacheTime = DateTime.Now;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) { LogService.Warn($"외부 인사문구 로드 실패: {ex.Message}"); }
|
|
|
|
// 2) 내장 리소스 폴백
|
|
try
|
|
{
|
|
var uri = new Uri("pack://application:,,,/Assets/Quotes/greetings.json");
|
|
var stream = Application.GetResourceStream(uri)?.Stream;
|
|
if (stream != null)
|
|
{
|
|
using var reader = new StreamReader(stream, Encoding.UTF8);
|
|
var result = JsonSerializer.Deserialize<Dictionary<string, string[]>>(reader.ReadToEnd(), opts);
|
|
if (result != null)
|
|
{
|
|
_greetingsCache = result;
|
|
_greetingsCacheTime = DateTime.Now;
|
|
return result;
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex) { LogService.Warn($"내장 인사문구 로드 실패: {ex.Message}"); }
|
|
|
|
return _greetingsCache = new();
|
|
}
|
|
|
|
private static int GetTodayEventCount()
|
|
{
|
|
// 전체 이벤트 수 반환 (날짜별 합계)
|
|
return _todayEvents.Value.Values.Sum(v => v.Length);
|
|
}
|
|
|
|
/// <summary>오늘 날짜에 해당하는 이벤트 수를 반환합니다.</summary>
|
|
public static int GetTodayMatchCount()
|
|
{
|
|
var todayKey = DateTime.Now.ToString("MM-dd");
|
|
return _todayEvents.Value.TryGetValue(todayKey, out var events) ? events.Length : 0;
|
|
}
|
|
|
|
/// <summary>로드된 전체 문구 수를 반환합니다.</summary>
|
|
public static int TotalCount =>
|
|
_motivational.Value.Length + _famous.Value.Length +
|
|
_displaySemi.Value.Length + _itAi.Value.Length +
|
|
_science.Value.Length + _history.Value.Length +
|
|
_english.Value.Length + _movies.Value.Length;
|
|
}
|