415 lines
19 KiB
C#
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, "설정");
|
|
}
|
|
}
|
|
}
|