JJServiceManagerEnt/Core/LogWindows.cs

497 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace JJServiceManager
{
/// <summary>서비스 한 개의 로그 파일 보기 (상단 정보줄 + 로그 목록)</summary>
public sealed class LogTailView : DockPanel, IDisposable
{
private readonly LogTailer _tailer;
private readonly TextBlock _path;
private readonly LogPanel _panel;
public ServiceEntry Entry { get; }
/// <summary>인코딩을 바꾸면 설정 저장용으로 호출됨</summary>
public event Action EntryChanged;
public LogTailView(ServiceEntry entry)
{
Entry = entry;
LastChildFill = true;
// ── 상단 정보줄
var bar = new Border
{
Background = Brushes2.Make("#F1F8FB"),
BorderBrush = Brushes2.Make("#B0BEC5"),
BorderThickness = new Thickness(0, 0, 0, 1),
Padding = new Thickness(6, 4, 6, 4),
Margin = new Thickness(0, 0, 0, 4)
};
SetDock(bar, Dock.Top);
var grid = new Grid();
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var title = new TextBlock
{
Text = entry.DisplayName,
FontWeight = FontWeights.Bold,
FontSize = 14,
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(0, 0, 12, 0),
Foreground = Brushes2.Make("#155E75")
};
Grid.SetColumn(title, 0);
_path = new TextBlock
{
Text = (entry.LogIsFolder ? "폴더: " : "파일: ") + entry.LogPath,
Foreground = Brushes.Gray,
VerticalAlignment = VerticalAlignment.Center,
TextTrimming = TextTrimming.CharacterEllipsis
};
Grid.SetColumn(_path, 1);
var encLabel = new TextBlock { Text = "인코딩", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(10, 0, 4, 0) };
Grid.SetColumn(encLabel, 2);
var enc = new ComboBox { Width = 90, VerticalAlignment = VerticalAlignment.Center };
enc.Items.Add(new ComboBoxItem { Content = "자동", Tag = "auto" });
enc.Items.Add(new ComboBoxItem { Content = "UTF-8", Tag = "utf-8" });
enc.Items.Add(new ComboBoxItem { Content = "CP949", Tag = "cp949" });
enc.SelectedIndex = (entry.LogEncoding ?? "auto") switch { "utf-8" => 1, "cp949" => 2, _ => 0 };
Grid.SetColumn(enc, 3);
var open = new Button { Content = "탐색기에서 열기", Margin = new Thickness(6, 0, 0, 0) };
Grid.SetColumn(open, 4);
open.Click += (s, e) => OpenInExplorer();
grid.Children.Add(title);
grid.Children.Add(_path);
grid.Children.Add(encLabel);
grid.Children.Add(enc);
grid.Children.Add(open);
bar.Child = grid;
Children.Add(bar);
// ── 로그 목록
_panel = new LogPanel { ShowTimestamp = false, MaxLines = 50000 };
Children.Add(_panel);
_tailer = new LogTailer(entry, (lines, level) => _panel.AddMany(lines, level));
_tailer.FileChanged += f => Dispatcher.BeginInvoke(new Action(() =>
{
_path.Text = entry.LogIsFolder ? $"폴더: {entry.LogPath} → {Path.GetFileName(f)}" : "파일: " + f;
_path.ToolTip = f;
}));
enc.SelectionChanged += (s, e) =>
{
if (enc.SelectedItem is ComboBoxItem item)
{
entry.LogEncoding = (string)item.Tag;
EntryChanged?.Invoke();
_panel.Add("──────── 인코딩 변경: " + item.Content + " ────────", LogLevel.Notice);
_tailer.Reload();
}
};
_tailer.Start();
}
/// <summary>로그 경로가 바뀌면 표시를 갱신하고 새 경로로 다시 읽음</summary>
public void PathChanged()
{
_path.Text = (Entry.LogIsFolder ? "폴더: " : "파일: ") + Entry.LogPath;
_path.ToolTip = null;
_panel.Add("──────── 로그 경로 변경: " + Entry.LogPath + " ────────", LogLevel.Notice);
_tailer.Reload();
}
private void OpenInExplorer()
{
try
{
var f = _tailer.CurrentFile;
if (!string.IsNullOrEmpty(f) && File.Exists(f))
Process.Start(new ProcessStartInfo("explorer.exe", $"/select,\"{f}\"") { UseShellExecute = true });
else if (Directory.Exists(Entry.LogPath))
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{Entry.LogPath}\"") { UseShellExecute = true });
else
{
var dir = Path.GetDirectoryName(Entry.LogPath);
if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir))
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{dir}\"") { UseShellExecute = true });
}
}
catch { }
}
public void Dispose() => _tailer.Dispose();
}
/// <summary>
/// 로그창 띄우기:
/// 1. 개별 창 2. 하나의 창에 탭 3. 창을 좌우로 나누어 비교
/// </summary>
public sealed class LogWindowManager
{
private readonly Func<AppSettings> _settings;
private readonly Action _save;
// 개별 창
private readonly Dictionary<string, Window> _individual = new Dictionary<string, Window>(StringComparer.OrdinalIgnoreCase);
// 탭
private Window _tabWindow;
private TabControl _tabs;
// 좌우 분할
private Window _splitWindow;
private SplitPane _left, _right;
public LogWindowManager(Func<AppSettings> settings, Action save)
{
_settings = settings;
_save = save;
}
public void Open(ServiceEntry entry)
{
switch (_settings().LogWindowMode)
{
case LogWindowMode.Tabs: OpenTab(entry); break;
case LogWindowMode.Split: OpenSplit(entry); break;
default: OpenIndividual(entry); break;
}
}
/// <summary>서비스 목록이 바뀌었을 때 (분할 창의 선택 목록 갱신)</summary>
public void ServicesChanged()
{
_left?.RefreshChoices(LogServices());
_right?.RefreshChoices(LogServices());
}
/// <summary>서비스의 로그 경로가 바뀌었을 때. 경로가 비었으면 그 서비스의 로그창을 닫음</summary>
public void EntryPathChanged(ServiceEntry entry)
{
if (string.IsNullOrWhiteSpace(entry.LogPath))
{
Close(entry.ServiceName);
ServicesChanged();
return;
}
foreach (var v in OpenViews(entry.ServiceName)) v.PathChanged();
ServicesChanged();
}
/// <summary>서비스가 목록에서 빠졌을 때 그 서비스의 로그창을 닫음</summary>
public void Close(string serviceName)
{
if (_individual.TryGetValue(serviceName, out var w)) w.Close();
if (_tabs != null)
{
foreach (var t in _tabs.Items.Cast<TabItem>().ToList())
{
if (t.Content is LogTailView v && Same(v.Entry.ServiceName, serviceName))
{
v.Dispose();
_tabs.Items.Remove(t);
}
}
if (_tabs.Items.Count == 0) _tabWindow?.Close();
}
if (_left != null && _left.ShowsName(serviceName)) _left.Clear();
if (_right != null && _right.ShowsName(serviceName)) _right.Clear();
}
private IEnumerable<LogTailView> OpenViews(string serviceName)
{
if (_individual.TryGetValue(serviceName, out var w) && w.Content is Border b && b.Child is LogTailView iv)
yield return iv;
if (_tabs != null)
foreach (TabItem t in _tabs.Items)
if (t.Content is LogTailView tv && Same(tv.Entry.ServiceName, serviceName)) yield return tv;
if (_left?.View != null && _left.ShowsName(serviceName)) yield return _left.View;
if (_right?.View != null && _right.ShowsName(serviceName)) yield return _right.View;
}
private static bool Same(string a, string b) => string.Equals(a, b, StringComparison.OrdinalIgnoreCase);
public void CloseAll()
{
foreach (var w in _individual.Values.ToList()) w.Close();
_tabWindow?.Close();
_splitWindow?.Close();
}
private List<ServiceEntry> LogServices()
=> _settings().Services.Where(s => !string.IsNullOrWhiteSpace(s.LogPath)).ToList();
private LogTailView NewView(ServiceEntry entry)
{
var v = new LogTailView(entry);
v.EntryChanged += _save;
return v;
}
private static Window NewWindow(string title, object content, double w = 960, double h = 620)
{
var win = new Window
{
Title = title,
Content = content,
Width = w,
Height = h,
FontFamily = new FontFamily("Malgun Gothic"),
FontSize = 13,
Background = Brushes2.Make("#FAFAFA"),
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
AppIcon.Apply(win);
return win;
}
private static void Activate(Window w)
{
if (w.WindowState == WindowState.Minimized) w.WindowState = WindowState.Normal;
w.Show();
w.Activate();
}
// ───────────────────────── 1. 개별 창 ─────────────────────────
private void OpenIndividual(ServiceEntry entry)
{
if (_individual.TryGetValue(entry.ServiceName, out var existing))
{
Activate(existing);
return;
}
var view = NewView(entry);
var win = NewWindow($"로그 - {entry.DisplayName}", new Border { Padding = new Thickness(6), Child = view });
win.Closed += (s, e) =>
{
view.Dispose();
_individual.Remove(entry.ServiceName);
};
_individual[entry.ServiceName] = win;
// 여러 창이 겹치지 않도록 조금씩 비켜서 띄움
int n = _individual.Count - 1;
win.WindowStartupLocation = WindowStartupLocation.Manual;
win.Left = Math.Max(0, (SystemParameters.WorkArea.Width - win.Width) / 2 + n * 28 % 200);
win.Top = Math.Max(0, (SystemParameters.WorkArea.Height - win.Height) / 2 + n * 28 % 200);
Activate(win);
}
// ───────────────────────── 2. 탭 ─────────────────────────
private void OpenTab(ServiceEntry entry)
{
if (_tabWindow == null)
{
_tabs = new TabControl { Margin = new Thickness(6) };
_tabWindow = NewWindow("로그 (탭)", _tabs);
_tabWindow.Closed += (s, e) =>
{
foreach (TabItem t in _tabs.Items)
(t.Content as LogTailView)?.Dispose();
_tabs = null;
_tabWindow = null;
};
}
foreach (TabItem t in _tabs.Items)
{
if (t.Content is LogTailView v && string.Equals(v.Entry.ServiceName, entry.ServiceName, StringComparison.OrdinalIgnoreCase))
{
_tabs.SelectedItem = t;
Activate(_tabWindow);
return;
}
}
var view = NewView(entry);
var tab = new TabItem { Content = view };
var header = new StackPanel { Orientation = Orientation.Horizontal };
header.Children.Add(new TextBlock { Text = entry.DisplayName, VerticalAlignment = VerticalAlignment.Center });
var close = new Button
{
Content = "✕",
MinWidth = 0,
MinHeight = 0,
Padding = new Thickness(4, 0, 4, 0),
Margin = new Thickness(8, 0, 0, 0),
FontSize = 10,
BorderThickness = new Thickness(0),
Background = Brushes.Transparent,
ToolTip = "탭 닫기"
};
close.Click += (s, e) =>
{
view.Dispose();
_tabs.Items.Remove(tab);
if (_tabs.Items.Count == 0) _tabWindow.Close();
};
header.Children.Add(close);
tab.Header = header;
_tabs.Items.Add(tab);
_tabs.SelectedItem = tab;
Activate(_tabWindow);
}
// ───────────────────────── 3. 좌우 분할 ─────────────────────────
private void OpenSplit(ServiceEntry entry)
{
if (_splitWindow == null)
{
var grid = new Grid { Margin = new Thickness(6) };
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(6) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
_left = new SplitPane(NewView, "왼쪽");
_right = new SplitPane(NewView, "오른쪽");
_left.RefreshChoices(LogServices());
_right.RefreshChoices(LogServices());
var splitter = new GridSplitter
{
Width = 6,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Background = Brushes2.Make("#CFD8DC"),
ResizeBehavior = GridResizeBehavior.PreviousAndNext
};
Grid.SetColumn(_left, 0);
Grid.SetColumn(splitter, 1);
Grid.SetColumn(_right, 2);
grid.Children.Add(_left);
grid.Children.Add(splitter);
grid.Children.Add(_right);
_splitWindow = NewWindow("로그 비교 (좌우)", grid, 1400, 700);
_splitWindow.Closed += (s, e) =>
{
_left.Dispose();
_right.Dispose();
_left = _right = null;
_splitWindow = null;
};
}
if (!_left.Shows(entry) && !_right.Shows(entry))
{
if (_left.IsEmpty) _left.Show(entry);
else if (_right.IsEmpty) _right.Show(entry);
else _right.Show(entry);
}
Activate(_splitWindow);
}
/// <summary>좌우 분할 창의 한쪽</summary>
private sealed class SplitPane : DockPanel, IDisposable
{
private readonly Func<ServiceEntry, LogTailView> _factory;
private readonly ComboBox _combo;
private readonly ContentControl _host;
private LogTailView _view;
private bool _updating;
public bool IsEmpty => _view == null;
public LogTailView View => _view;
private readonly UIElement _placeholder;
public SplitPane(Func<ServiceEntry, LogTailView> factory, string side)
{
_factory = factory;
var top = new DockPanel { Margin = new Thickness(0, 0, 0, 4) };
SetDock(top, Dock.Top);
var lbl = new TextBlock { Text = side + " 서비스:", VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0) };
SetDock(lbl, Dock.Left);
_combo = new ComboBox { DisplayMemberPath = nameof(ServiceEntry.DisplayName), MinWidth = 200 };
top.Children.Add(lbl);
top.Children.Add(_combo);
Children.Add(top);
_placeholder = new TextBlock
{
Text = "위에서 서비스를 선택하거나, 초기화면에서 Log 버튼을 누르세요.",
Foreground = Brushes.Gray,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
};
_host = new ContentControl { Content = _placeholder };
Children.Add(_host);
_combo.SelectionChanged += (s, e) =>
{
if (_updating) return;
if (_combo.SelectedItem is ServiceEntry se) Show(se);
};
}
public bool Shows(ServiceEntry e) => ShowsName(e.ServiceName);
public bool ShowsName(string serviceName)
=> _view != null && string.Equals(_view.Entry.ServiceName, serviceName, StringComparison.OrdinalIgnoreCase);
public void Clear()
{
_view?.Dispose();
_view = null;
_host.Content = _placeholder;
_updating = true;
_combo.SelectedItem = null;
_updating = false;
}
public void Show(ServiceEntry entry)
{
_view?.Dispose();
_view = _factory(entry);
_host.Content = _view;
SelectInCombo(entry);
}
public void RefreshChoices(List<ServiceEntry> services)
{
_updating = true;
_combo.ItemsSource = services;
_updating = false;
if (_view != null) SelectInCombo(_view.Entry);
}
private void SelectInCombo(ServiceEntry entry)
{
_updating = true;
_combo.SelectedItem = (_combo.ItemsSource as IEnumerable<ServiceEntry>)?
.FirstOrDefault(x => string.Equals(x.ServiceName, entry.ServiceName, StringComparison.OrdinalIgnoreCase));
_updating = false;
}
public void Dispose() => _view?.Dispose();
}
}
}