using System.IO; using System.Linq; using System.Text; using System.Text.Json; using System.Windows; namespace AxCopilot.Services; /// /// 격려 문구, 명언, 업계 상식, IT/AI, 과학, 역사, 생활영어, 드라마/영화 대사를 카테고리별로 관리합니다. /// 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 _motivational = new(LoadMotivational); private static readonly Lazy _famous = new(LoadFamous); private static readonly Lazy _displaySemi = new(() => LoadSimple("display_semiconductor.json")); private static readonly Lazy _itAi = new(() => LoadSimple("it_ai.json")); private static readonly Lazy _science = new(() => LoadSimple("science.json")); private static readonly Lazy _history = new(() => LoadSimple("history.json")); private static readonly Lazy _english = new(() => LoadSimple("english.json")); private static readonly Lazy _movies = new(LoadMovies); private static readonly Lazy> _todayEvents = new(LoadTodayEvents); /// 카테고리 정의: (key, label, countFunc). 설정 UI에서 다중 체크로 표시됨. public static readonly (string Key, string Label, Func 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(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(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(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(reader.ReadToEnd(), opts) ?? []; } catch { return []; } } private static Dictionary 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>(reader.ReadToEnd()) ?? new(); } catch { return new(); } } // ─── 공개 API ────────────────────────────────────────────────────── /// /// 활성화된 카테고리에서 랜덤으로 하나를 반환합니다. /// 날짜 카테고리가 활성화되어 있으면 첫 호출 시 오늘의 이벤트를 우선 반환합니다. /// public static (string Text, string? Author) GetRandom(IEnumerable? enabledCategories = null) { var enabled = enabledCategories?.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? new HashSet { "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); } /// 시간대에 따른 인사 접두사를 반환합니다 (줄바꿈 포함). /// /// 외부 파일(%APPDATA%\AxCopilot\Quotes\greetings.json) 우선 로드. /// 외부 파일이 없으면 내장 리소스(Assets/Quotes/greetings.json) 사용. /// 개발자가 외부 JSON만 수정하면 재빌드 없이 인사문구 변경/추가 가능. /// 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? _greetingsCache; private static DateTime _greetingsCacheTime; /// 인사문구를 로드합니다. 외부 파일 → 내장 리소스 순서. private static Dictionary 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>(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>(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); } /// 오늘 날짜에 해당하는 이벤트 수를 반환합니다. public static int GetTodayMatchCount() { var todayKey = DateTime.Now.ToString("MM-dd"); return _todayEvents.Value.TryGetValue(todayKey, out var events) ? events.Length : 0; } /// 로드된 전체 문구 수를 반환합니다. 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; }