2026-03-30 17:09:29 +09:00

146 lines
4.7 KiB
C#

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<TKey, TIn, TView> : HistoryService<TKey, TView>, IHistoryWriter<TKey, TIn>
{
private readonly Func<TIn, TView> _projector;
private readonly string _filePath;
private readonly Func<TKey, string> _keyToString;
private readonly Func<string, TKey> _stringToKey;
private readonly DispatcherTimer _saveTimer;
private volatile bool _dirty;
public SavedHistoryService(string filePath, Func<TIn, TView> projector, int maxCount = 50, int pumpMs = 100, int saveDebounceMs = 500, Func<TKey, string> keyToString = null, Func<string, TKey> 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<Dictionary<string, List<TView>>>(json);
if (data == null) return;
foreach (var kv in data) {
var key = _stringToKey(kv.Key);
var list = kv.Value ?? new List<TView>();
// 저장 포맷을 "최신이 앞"으로 가정
Seed(key, list);
}
} catch {
// 파일 손상/버전 문제면 무시하고 시작
}
}
private void SaveToDisk()
{
Dictionary<string, List<TView>> 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();
}
}
}