JJServiceManagerEnt/Windows/SettingsWindow.cs

415 lines
19 KiB
C#

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using JJServiceManager.Remote;
namespace JJServiceManager
{
/// <summary>설정 화면 (탭: 일반 / 원격 접속 허용 / 관리 대상 PC)</summary>
public sealed class SettingsWindow : Window
{
private readonly AppSettings _s;
// 일반
private readonly CheckBox _startup, _hotkey, _opLog, _shutdownButtons, _confirmLocal, _confirmRemote;
private readonly TextBox _hotkeyBox;
private readonly ComboBox _logMode, _esc;
private ModifierKeys _mods;
private Key _key;
// 원격 접속 허용
private readonly CheckBox _server;
private readonly ComboBox _serverMode;
private readonly TextBox _serverPort;
private readonly TextBlock _serverPortHint, _pwState, _serverInfo;
private readonly PasswordBox _pwd1, _pwd2;
private int _sshPort, _tcpPort;
private int _shownMode = -1;
// 관리 대상 PC
private readonly CheckBox _manage;
private readonly ListBox _targets;
private readonly TextBox _addr, _hostname, _port;
private readonly ComboBox _mode;
private readonly PasswordBox _pwdTarget;
private readonly TextBlock _pwTargetState, _test;
private readonly Button _btnTest;
private readonly ObservableCollection<TargetItem> _items = new ObservableCollection<TargetItem>();
/// <summary>목록 표시용</summary>
private sealed class TargetItem : ObservableObject
{
public RemoteTarget T { get; }
public TargetItem(RemoteTarget t) => T = t;
public string Label => T.Label;
public string ModeText => (T.Mode == RemoteMode.Ssh ? "SSH" : "지정 포트") + " " + T.Port;
public void Changed()
{
OnPropertyChanged(nameof(Label));
OnPropertyChanged(nameof(ModeText));
}
}
public SettingsWindow(AppSettings current)
{
_s = current;
Title = "설정" + AppInstance.TitleTag;
Width = 780;
Height = 640;
MinHeight = 560;
ResizeMode = ResizeMode.CanResize;
FontFamily = new FontFamily("Malgun Gothic");
FontSize = 13;
Background = Brushes2.Make("#FAFAFA");
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ShowInTaskbar = false;
AppIcon.Apply(this);
var root = XamlLoader.LoadElement("Settings.xaml");
Content = root;
// ───────── 일반 ─────────
_startup = root.Find<CheckBox>("chkStartup");
_hotkey = root.Find<CheckBox>("chkHotkey");
_opLog = root.Find<CheckBox>("chkOpLog");
_shutdownButtons = root.Find<CheckBox>("chkShutdownButtons");
_confirmLocal = root.Find<CheckBox>("chkConfirmLocal");
_confirmRemote = root.Find<CheckBox>("chkConfirmRemote");
_hotkeyBox = root.Find<TextBox>("txtHotkey");
_logMode = root.Find<ComboBox>("cmbLogMode");
_esc = root.Find<ComboBox>("cmbEsc");
root.Find<TextBlock>("txtDataPath").Text = "설정 파일: " + SettingsStore.SettingsPath
+ "\n작업 로그 파일: " + OperationLogFile.Folder;
_startup.IsChecked = current.RunAtStartup;
_hotkey.IsChecked = current.HotkeyEnabled;
_opLog.IsChecked = current.ShowOperationLog;
_shutdownButtons.IsChecked = current.ShowShutdownButtons;
_confirmLocal.IsChecked = current.ConfirmLocalStartStop;
_confirmRemote.IsChecked = current.ConfirmRemoteStartStop;
_logMode.SelectedIndex = (int)current.LogWindowMode;
_esc.SelectedIndex = (int)current.EscAction;
_mods = (ModifierKeys)current.HotkeyModifiers;
_key = (Key)current.HotkeyKey;
ShowHotkey();
// 단축키: 클릭 후 직접 눌러서 지정
_hotkeyBox.GotKeyboardFocus += (s, e) => _hotkeyBox.Background = Brushes2.Make("#FFF8E1");
_hotkeyBox.LostKeyboardFocus += (s, e) => { _hotkeyBox.Background = Brushes2.Make("#FAFAFA"); ShowHotkey(); };
_hotkeyBox.PreviewKeyDown += OnHotkeyKeyDown;
// ───────── 원격 접속 허용 ─────────
_server = root.Find<CheckBox>("chkServer");
_serverMode = root.Find<ComboBox>("cmbServerMode");
_serverPort = root.Find<TextBox>("txtServerPort");
_serverPortHint = root.Find<TextBlock>("txtServerPortHint");
_pwd1 = root.Find<PasswordBox>("pwdServer");
_pwd2 = root.Find<PasswordBox>("pwdServer2");
_pwState = root.Find<TextBlock>("txtPwState");
_serverInfo = root.Find<TextBlock>("txtServerInfo");
_sshPort = current.RemoteServerSshPort;
_tcpPort = current.RemoteServerTcpPort;
_server.IsChecked = current.RemoteServerEnabled;
_serverMode.SelectionChanged += (s, e) => OnServerModeChanged();
_serverMode.SelectedIndex = (int)current.RemoteServerMode;
_pwState.Text = string.IsNullOrEmpty(current.RemotePasswordHash)
? "비밀번호가 설정되어 있지 않습니다. 원격 접속을 허용하려면 입력하세요."
: "비밀번호가 설정되어 있습니다. 바꿀 때만 입력하세요.";
UpdateServerInfo();
// ───────── 관리 대상 PC ─────────
_manage = root.Find<CheckBox>("chkManage");
_targets = root.Find<ListBox>("lstTargets");
_addr = root.Find<TextBox>("txtAddr");
_hostname = root.Find<TextBox>("txtHostname");
_port = root.Find<TextBox>("txtPort");
_mode = root.Find<ComboBox>("cmbMode");
_pwdTarget = root.Find<PasswordBox>("pwdTarget");
_pwTargetState = root.Find<TextBlock>("txtPwTarget");
_test = root.Find<TextBlock>("txtTest");
_btnTest = root.Find<Button>("btnTest");
_manage.IsChecked = current.RemoteManageEnabled;
foreach (var t in current.Targets) _items.Add(new TargetItem(t.Clone()));
_targets.ItemsSource = _items;
_targets.SelectionChanged += (s, e) => ShowTarget(_targets.SelectedItem as TargetItem);
_mode.SelectionChanged += (s, e) =>
{
// 방식을 바꾸면 기본 포트로 (직접 바꾼 포트가 아닐 때)
if (_port.Text == "" || _port.Text == "9988" || _port.Text == "2222")
_port.Text = _mode.SelectedIndex == 0 ? "2222" : "9988";
};
ClearForm();
root.Find<Button>("btnNew").Click += (s, e) => { _targets.SelectedItem = null; ClearForm(); };
root.Find<Button>("btnAddTarget").Click += (s, e) => AddTarget();
root.Find<Button>("btnUpdateTarget").Click += (s, e) => UpdateTarget();
root.Find<Button>("btnDeleteTarget").Click += (s, e) => DeleteTarget();
_btnTest.Click += async (s, e) => await TestTarget();
// ───────── 확인 / 취소 ─────────
root.Find<Button>("btnOk").Click += (s, e) => Save();
root.Find<Button>("btnCancel").Click += (s, e) => Close();
PreviewKeyDown += (s, e) =>
{
if (e.Key == Key.Escape && !_hotkeyBox.IsKeyboardFocused) Close();
};
}
// ───────────────────────── 단축키 ─────────────────────────
private void OnHotkeyKeyDown(object sender, KeyEventArgs e)
{
e.Handled = true;
var key = e.Key == Key.System ? e.SystemKey : e.Key;
var mods = Keyboard.Modifiers;
if (Keyboard.IsKeyDown(Key.LWin) || Keyboard.IsKeyDown(Key.RWin)) mods |= ModifierKeys.Windows;
// 보조키만 누른 상태: 진행 중 표시
if (key is Key.LeftCtrl or Key.RightCtrl or Key.LeftAlt or Key.RightAlt
or Key.LeftShift or Key.RightShift or Key.LWin or Key.RWin)
{
_hotkeyBox.Text = HotkeyManager.Format(mods, Key.None) + "…";
return;
}
// Tab은 포커스 이동에 사용
if (key == Key.Tab && mods == ModifierKeys.None)
{
e.Handled = false;
return;
}
if ((mods & (ModifierKeys.Control | ModifierKeys.Alt | ModifierKeys.Windows)) == 0)
{
_hotkeyBox.Text = "Ctrl / Alt / Win 중 하나와 함께 누르세요";
return;
}
_mods = mods;
_key = key;
ShowHotkey();
}
private void ShowHotkey() => _hotkeyBox.Text = HotkeyManager.Format(_mods, _key);
// ───────────────────────── 원격 접속 허용 ─────────────────────────
private void OnServerModeChanged()
{
// 방식마다 포트를 따로 기억
if (_shownMode == 0 && int.TryParse(_serverPort.Text, out var a)) _sshPort = a;
if (_shownMode == 1 && int.TryParse(_serverPort.Text, out var b)) _tcpPort = b;
_shownMode = _serverMode.SelectedIndex;
_serverPort.Text = (_shownMode == 0 ? _sshPort : _tcpPort).ToString();
_serverPortHint.Text = _shownMode == 0 ? "기본 2222 (Windows OpenSSH의 22번과 겹치지 않게)" : "기본 9988";
UpdateServerInfo();
}
private void UpdateServerInfo()
{
var ips = NetInfo.LocalIPv4();
_serverInfo.Text =
$"이 PC: {Environment.MachineName} IP: {(ips.Count == 0 ? "(확인 안 됨)" : string.Join(", ", ips))}\n" +
"관리 PC의 [관리 대상 PC]에 위 IP, 같은 통신 방식과 포트, 접속 비밀번호를 입력하면 됩니다.\n" +
"원격 접속 허용을 켜면 Windows 방화벽에 해당 TCP 포트를 자동으로 열고, 끄면 규칙을 지웁니다.\n" +
"원격에서는 이 PC에 등록된 서비스만 보고 Start / Stop 할 수 있습니다.";
}
// ───────────────────────── 관리 대상 PC ─────────────────────────
private void ClearForm()
{
_addr.Text = "";
_hostname.Text = "";
_mode.SelectedIndex = 1;
_port.Text = "9988";
_pwdTarget.Password = "";
_pwTargetState.Text = "대상 PC의 접속 비밀번호";
_test.Text = "";
}
private void ShowTarget(TargetItem item)
{
if (item == null) return;
var t = item.T;
_addr.Text = t.Address;
_hostname.Text = t.Hostname;
_mode.SelectedIndex = (int)t.Mode;
_port.Text = t.Port.ToString();
_pwdTarget.Password = "";
_pwTargetState.Text = string.IsNullOrEmpty(t.ProtectedPassword) ? "비밀번호 없음 - 입력하세요" : "저장됨 (바꿀 때만 입력)";
_test.Text = "";
}
/// <summary>입력값 검사. 문제가 있으면 메시지</summary>
private string ReadForm(out string addr, out RemoteMode mode, out int port)
{
addr = _addr.Text.Trim();
mode = (RemoteMode)Math.Max(0, _mode.SelectedIndex);
port = 0;
if (addr.Length == 0) return "대상 IP를 입력하세요.";
if (addr.Contains(' ')) return "대상 IP에 공백이 있습니다.";
if (!int.TryParse(_port.Text.Trim(), out port) || port < 1 || port > 65535) return "포트는 1~65535 사이 숫자로 입력하세요.";
return null;
}
private void AddTarget()
{
var err = ReadForm(out var addr, out var mode, out var port);
if (err == null && _pwdTarget.Password.Length == 0) err = "비밀번호를 입력하세요.";
if (err == null && _items.Any(i => string.Equals(i.T.Address, addr, StringComparison.OrdinalIgnoreCase) && i.T.Port == port))
err = "같은 IP와 포트가 이미 등록되어 있습니다.";
if (err != null) { ShowTest(err, false); return; }
var t = new RemoteTarget
{
Address = addr,
Mode = mode,
Port = port,
Hostname = _hostname.Text.Trim(),
ProtectedPassword = RemoteSecurity.Protect(_pwdTarget.Password)
};
var item = new TargetItem(t);
_items.Add(item);
_targets.SelectedItem = item;
ShowTest("추가했습니다. [확인]을 누르면 저장됩니다.", true);
}
private void UpdateTarget()
{
if (_targets.SelectedItem is not TargetItem item) { ShowTest("수정할 PC를 목록에서 고르세요.", false); return; }
var err = ReadForm(out var addr, out var mode, out var port);
if (err != null) { ShowTest(err, false); return; }
var t = item.T;
if (!string.Equals(t.Address, addr, StringComparison.OrdinalIgnoreCase)) t.Hostname = ""; // IP가 바뀌면 hostname 다시 확인
t.Address = addr;
t.Mode = mode;
t.Port = port;
if (_hostname.Text.Trim().Length > 0 && t.Hostname.Length == 0) t.Hostname = _hostname.Text.Trim();
if (_pwdTarget.Password.Length > 0) t.ProtectedPassword = RemoteSecurity.Protect(_pwdTarget.Password);
item.Changed();
ShowTarget(item);
ShowTest("수정했습니다. [확인]을 누르면 저장됩니다.", true);
}
private void DeleteTarget()
{
if (_targets.SelectedItem is not TargetItem item) { ShowTest("삭제할 PC를 목록에서 고르세요.", false); return; }
if (!Dialogs.Confirm(this, $"{item.Label} 을(를) 관리 대상에서 삭제할까요?", "삭제")) return;
_items.Remove(item);
ClearForm();
ShowTest("삭제했습니다. [확인]을 누르면 저장됩니다.", true);
}
private async System.Threading.Tasks.Task TestTarget()
{
var err = ReadForm(out var addr, out var mode, out var port);
if (err != null) { ShowTest(err, false); return; }
// 입력한 비밀번호가 없으면 저장된 비밀번호 사용
var pw = _pwdTarget.Password;
if (pw.Length == 0 && _targets.SelectedItem is TargetItem sel) pw = RemoteSecurity.Unprotect(sel.T.ProtectedPassword);
if (pw.Length == 0) { ShowTest("비밀번호를 입력하세요.", false); return; }
_btnTest.IsEnabled = false;
ShowTest($"{addr}:{port} 연결 중…", true);
try
{
var host = await RemoteClient.TestAsync(new RemoteTarget { Address = addr, Mode = mode, Port = port }, pw);
_hostname.Text = host;
if (_targets.SelectedItem is TargetItem item
&& string.Equals(item.T.Address, addr, StringComparison.OrdinalIgnoreCase))
{
item.T.Hostname = host;
item.Changed();
}
ShowTest($"연결 성공: {host}", true);
}
catch (Exception ex)
{
ShowTest("연결 실패: " + RemoteClient.Friendly(ex), false);
}
finally
{
_btnTest.IsEnabled = true;
}
}
private void ShowTest(string msg, bool ok)
{
_test.Text = msg;
_test.Foreground = ok ? Brushes2.Running : Brushes2.Stopped;
}
// ───────────────────────── 저장 ─────────────────────────
private void Save()
{
// 원격 접속 허용 검사
if (!int.TryParse(_serverPort.Text.Trim(), out var sp) || sp < 1 || sp > 65535)
{
Fail(1, "원격 접속 허용 포트는 1~65535 사이 숫자로 입력하세요.");
return;
}
if (_serverMode.SelectedIndex == 0) _sshPort = sp; else _tcpPort = sp;
string newHash = null, newSalt = null;
if (_pwd1.Password.Length > 0 || _pwd2.Password.Length > 0)
{
if (_pwd1.Password != _pwd2.Password) { Fail(1, "접속 비밀번호와 확인이 다릅니다."); return; }
if (_pwd1.Password.Length < 4) { Fail(1, "접속 비밀번호는 4자 이상으로 입력하세요."); return; }
(newHash, newSalt) = RemoteSecurity.HashPassword(_pwd1.Password);
}
bool hasPassword = newHash != null || !string.IsNullOrEmpty(_s.RemotePasswordHash);
if (_server.IsChecked == true && !hasPassword)
{
Fail(1, "원격 접속을 허용하려면 접속 비밀번호를 정하세요.");
return;
}
// 일반
_s.RunAtStartup = _startup.IsChecked == true;
_s.HotkeyEnabled = _hotkey.IsChecked == true;
_s.HotkeyModifiers = (int)_mods;
_s.HotkeyKey = (int)_key;
_s.ShowOperationLog = _opLog.IsChecked == true;
_s.ShowShutdownButtons = _shutdownButtons.IsChecked == true;
_s.ConfirmLocalStartStop = _confirmLocal.IsChecked == true;
_s.ConfirmRemoteStartStop = _confirmRemote.IsChecked == true;
_s.LogWindowMode = (LogWindowMode)Math.Max(0, _logMode.SelectedIndex);
_s.EscAction = (EscAction)Math.Max(0, _esc.SelectedIndex);
// 원격 접속 허용
_s.RemoteServerEnabled = _server.IsChecked == true;
_s.RemoteServerMode = (RemoteMode)Math.Max(0, _serverMode.SelectedIndex);
_s.RemoteServerSshPort = _sshPort;
_s.RemoteServerTcpPort = _tcpPort;
if (newHash != null)
{
_s.RemotePasswordHash = newHash;
_s.RemotePasswordSalt = newSalt;
}
// 관리 대상 PC
_s.RemoteManageEnabled = _manage.IsChecked == true;
_s.Targets = _items.Select(i => i.T.Clone()).ToList();
DialogResult = true;
}
private void Fail(int tabIndex, string msg)
{
if (Content is FrameworkElement root && root.FindName("tabs") is TabControl tc) tc.SelectedIndex = tabIndex;
Dialogs.Error(this, msg, "설정");
}
}
}