using Newtonsoft.Json; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Threading; using WeldingCommon.Data.Services.History; namespace WeldingCommon.Data.History { public class SavedHistoryService : HistoryService, IHistoryWriter { private readonly Func _projector; private readonly string _filePath; private readonly Func _keyToString; private readonly Func _stringToKey; private readonly DispatcherTimer _saveTimer; private volatile bool _dirty; public SavedHistoryService(string filePath, Func projector, int maxCount = 50, int pumpMs = 100, int saveDebounceMs = 500, Func keyToString = null, Func stringToKey = null) : base(maxCount, pumpMs) { _filePath = filePath ?? throw new ArgumentNullException(nameof(filePath)); _projector = projector ?? throw new ArgumentNullException(nameof(projector)); _keyToString = keyToString ?? (k => k.ToString()); _stringToKey = stringToKey ?? DefaultStringToKey; Directory.CreateDirectory(Path.GetDirectoryName(_filePath)); LoadFromDisk(); // base.Seed 사용 _saveTimer = new DispatcherTimer(DispatcherPriority.Background) { Interval = TimeSpan.FromMilliseconds(saveDebounceMs) }; _saveTimer.Tick += SaveTimer_Tick; } public void Append(TKey key, TIn snapshot) { var view = _projector(snapshot); base.Append(key, view); // 큐에 들어감 → Pump 후 컬렉션 반영 } // Clear는 base 구현 그대로 사용(= TView 기준) public override void Clear(TKey key) { base.Clear(key); MarkDirtyAndScheduleSave(); } protected override void OnChanged() { MarkDirtyAndScheduleSave(); } private void MarkDirtyAndScheduleSave() { _dirty = true; if (!_saveTimer.IsEnabled) _saveTimer.Start(); } private void SaveTimer_Tick(object sender, EventArgs e) { if (!_dirty) { _saveTimer.Stop(); return; } SaveToDisk(); _dirty = false; _saveTimer.Stop(); } private void LoadFromDisk() { if (!File.Exists(_filePath)) return; try { var json = File.ReadAllText(_filePath, Encoding.UTF8); var data = JsonConvert.DeserializeObject>>(json); if (data == null) return; foreach (var kv in data) { var key = _stringToKey(kv.Key); var list = kv.Value ?? new List(); // 저장 포맷을 "최신이 앞"으로 가정 Seed(key, list); } } catch { // 파일 손상/버전 문제면 무시하고 시작 } } private void SaveToDisk() { Dictionary> snapshot; lock (_lock) { snapshot = _collections.ToDictionary( kv => _keyToString(kv.Key), kv => kv.Value.Take(500000).ToList()); // 여기서 maxCount 대신 큰 수 쓰지 말고, RollingCollection이 max를 유지하면 그냥 ToList도 OK } var json = JsonConvert.SerializeObject(snapshot, Formatting.Indented); var temp = _filePath + ".tmp"; File.WriteAllText(temp, json, Encoding.UTF8); if (File.Exists(_filePath)) { var backup = _filePath + ".bak"; File.Replace(temp, _filePath, backup, ignoreMetadataErrors: true); } else { File.Move(temp, _filePath); } } private TKey DefaultStringToKey(string s) { var t = typeof(TKey); if (t.IsEnum) return (TKey)Enum.Parse(t, s); if (t == typeof(string)) return (TKey)(object)s; throw new InvalidOperationException( $"TKey가 enum/string이 아니면 stringToKey 델리게이트를 제공해야 합니다. TKey={t.FullName}"); } public override void Dispose() { _saveTimer.Stop(); _saveTimer.Tick -= SaveTimer_Tick; if (_dirty) SaveToDisk(); base.Dispose(); } } }