You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

483 lines
15 KiB

2 months ago
using System;
2 months ago
using System.IO;
2 months ago
using System.Windows;
2 months ago
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;
using Housing.Services;
2 months ago
namespace Housing.Login;
public sealed class StartupLoginWindowResult
{
public string Maker { get; set; } = string.Empty;
public string Model { get; set; } = string.Empty;
public string Variant1 { get; set; } = string.Empty;
public string Variant2 { get; set; } = string.Empty;
public string Operator { get; set; } = string.Empty;
public string LineNo { get; set; } = string.Empty;
public string LotNo { get; set; } = string.Empty;
public string JigNo { get; set; } = string.Empty;
}
public partial class StartupLoginWindow : Window
{
2 months ago
private const string OperatorHistoryFieldName = "Operator";
private const string SuppressedOperatorHistoryValue = "su";
private readonly LoginHistoryStore _loginHistoryStore;
private readonly LoginSelectionOptions _loginOptions;
2 months ago
private Popup? _openHistoryPopup;
2 months ago
public StartupLoginWindowResult Result { get; private set; } = new();
public StartupLoginWindow()
{
2 months ago
_loginHistoryStore = LoginHistoryStore.Load(GetLoginHistoryPath());
_loginOptions = LoginSelectionOptions.Load(GetLoginOptionsPath());
2 months ago
RemoveSuppressedOperatorHistory();
2 months ago
InitializeComponent();
ApplyLoginOptions();
2 months ago
AttachLoginHistoryMenus();
AttachSelectionDropDowns();
2 months ago
PasswordBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
LoginButton.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
MakerComboBox.Focus();
2 months ago
}
2 months ago
private async void LogInButton_Click(object sender, RoutedEventArgs e)
2 months ago
{
2 months ago
CloseHistoryPopup();
2 months ago
2 months ago
if (!TryReadRequiredFields(out var loginResult, out var password))
2 months ago
{
return;
}
2 months ago
if (sender is Button button)
2 months ago
{
2 months ago
button.IsEnabled = false;
2 months ago
}
2 months ago
try
2 months ago
{
if (!await IsValidLoginAsync(loginResult.Operator, password))
2 months ago
{
MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
2 months ago
PasswordBox.Clear();
PasswordBox.Focus();
return;
}
Result = loginResult;
SaveLoginHistory(loginResult);
DialogResult = true;
Close();
2 months ago
}
2 months ago
catch (Exception ex)
{
MessageBox.Show(this, $"로그인 확인 실패: {ex.Message}", "Login", MessageBoxButton.OK, MessageBoxImage.Error);
2 months ago
OperatorTextBox.Focus();
OperatorTextBox.SelectAll();
}
finally
{
if (sender is Button loginButton)
{
loginButton.IsEnabled = true;
}
}
}
2 months ago
private async Task<bool> IsValidLoginAsync(string loginId, string password)
{
if (_loginOptions.LoginAccounts.Count > 0)
{
return _loginOptions.LoginAccounts.Any(account =>
string.Equals(account.Id, loginId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(account.Password, password, StringComparison.Ordinal));
}
var loginSettings = LoginSettings.Load(GetDatabaseIniPath());
if (loginSettings.OfflinePreview)
{
return true;
}
var repository = new LoginAccountRepository(
DatabaseSettings.Load(GetDatabaseIniPath()),
loginSettings);
return await repository.ExistsAsync(loginId, password);
}
2 months ago
private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password)
2 months ago
{
result = new StartupLoginWindowResult();
2 months ago
password = string.Empty;
2 months ago
if (!TryReadRequiredComboBox(MakerComboBox, "Maker", out var maker) ||
!TryReadRequiredComboBox(ModelComboBox, "Model", out var model) ||
2 months ago
!TryReadTextBox(OperatorTextBox, "ID", out var loginId) ||
2 months ago
!TryReadPasswordBox(PasswordBox, "PW", out password) ||
!TryReadRequiredComboBox(LineNoComboBox, "Line", out var lineNo) ||
2 months ago
!TryReadTextBox(LotNoTextBox, "Lot No", out var lotNo) ||
!TryReadRequiredComboBox(JigNoComboBox, "Jig No", out var jigNo))
2 months ago
{
return false;
}
var variant1 = ReadOptionalComboBox(Variant1ComboBox);
var variant2 = ReadOptionalComboBox(Variant2ComboBox);
2 months ago
result = new StartupLoginWindowResult
2 months ago
{
2 months ago
Maker = maker,
Model = model,
Variant1 = variant1,
Variant2 = variant2,
2 months ago
Operator = loginId,
2 months ago
LineNo = lineNo,
LotNo = lotNo,
JigNo = jigNo
2 months ago
};
2 months ago
return true;
}
private void ApplyLoginOptions()
{
SetComboBoxOptions(MakerComboBox, _loginOptions.Makers, allowBlank: false, historyFieldName: "Maker");
SetComboBoxOptions(ModelComboBox, _loginOptions.Models, allowBlank: false, historyFieldName: "Model");
SetComboBoxOptions(Variant1ComboBox, _loginOptions.Variant1Values, allowBlank: true, historyFieldName: "Variant1");
SetComboBoxOptions(Variant2ComboBox, _loginOptions.Variant2Values, allowBlank: true, historyFieldName: "Variant2");
SetComboBoxOptions(LineNoComboBox, _loginOptions.LineValues, allowBlank: false, historyFieldName: "LineNo");
SetComboBoxOptions(JigNoComboBox, _loginOptions.JigNoValues, allowBlank: false, historyFieldName: "JigNo");
}
private void SetComboBoxOptions(
ComboBox comboBox,
IReadOnlyList<string> values,
bool allowBlank,
string historyFieldName)
{
var items = new List<LoginOptionItem>();
if (allowBlank)
{
items.Add(new LoginOptionItem("(None)", string.Empty));
}
foreach (var value in values.Where(value => !string.IsNullOrWhiteSpace(value)))
{
if (items.Any(item => string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase)))
{
continue;
}
items.Add(new LoginOptionItem(value, value));
}
comboBox.ItemsSource = items;
comboBox.DisplayMemberPath = nameof(LoginOptionItem.DisplayText);
var rememberedValue = _loginHistoryStore.GetValues(historyFieldName).FirstOrDefault();
if (!string.IsNullOrWhiteSpace(rememberedValue) &&
TrySelectComboBoxValue(comboBox, rememberedValue))
{
return;
}
if (allowBlank)
{
comboBox.SelectedIndex = 0;
}
else if (items.Count == 1)
{
comboBox.SelectedIndex = 0;
}
}
private static bool TrySelectComboBoxValue(ComboBox comboBox, string value)
{
for (var index = 0; index < comboBox.Items.Count; index++)
{
if (comboBox.Items[index] is LoginOptionItem item &&
string.Equals(item.Value, value, StringComparison.OrdinalIgnoreCase))
{
comboBox.SelectedIndex = index;
return true;
}
}
return false;
}
private void AttachSelectionDropDowns()
{
foreach (var comboBox in new[]
{
MakerComboBox,
ModelComboBox,
Variant1ComboBox,
Variant2ComboBox,
LineNoComboBox,
JigNoComboBox
})
{
comboBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup();
comboBox.DropDownOpened += (_, _) => CloseHistoryPopup();
}
}
2 months ago
private void AttachLoginHistoryMenus()
{
AttachHistoryMenu(OperatorTextBox, OperatorHistoryFieldName, PasswordBox);
AttachHistoryMenu(LotNoTextBox, "LotNo", JigNoComboBox);
2 months ago
}
private void AttachHistoryMenu(TextBox textBox, string fieldName, Control nextFocusControl)
{
textBox.GotKeyboardFocus += (_, _) => ShowHistoryMenu(textBox, fieldName, nextFocusControl);
textBox.PreviewMouseLeftButtonDown += (_, _) =>
{
if (textBox.IsKeyboardFocusWithin)
{
ShowHistoryMenu(textBox, fieldName, nextFocusControl);
}
};
}
private void ShowHistoryMenu(TextBox textBox, string fieldName, Control nextFocusControl)
{
var values = _loginHistoryStore.GetValues(fieldName);
if (values.Count == 0)
{
CloseHistoryPopup();
return;
}
CloseHistoryPopup();
var menuWidth = Math.Max(160, textBox.ActualWidth);
var stackPanel = new StackPanel();
var popup = new Popup
{
PlacementTarget = textBox,
Placement = PlacementMode.Bottom,
HorizontalOffset = 0,
StaysOpen = true,
AllowsTransparency = true,
Focusable = false
};
foreach (var value in values)
{
var item = new Button
{
Content = value,
MinWidth = menuWidth,
Padding = new Thickness(10, 6, 10, 6),
BorderThickness = new Thickness(0),
Background = textBox.Background,
Foreground = textBox.Foreground,
HorizontalContentAlignment = HorizontalAlignment.Left
};
item.Click += (_, _) =>
{
textBox.Text = value;
textBox.CaretIndex = textBox.Text.Length;
CloseHistoryPopup();
FocusControl(nextFocusControl);
};
stackPanel.Children.Add(item);
}
popup.Child = new Border
{
Width = menuWidth,
MaxHeight = 180,
Background = textBox.Background,
BorderBrush = textBox.BorderBrush,
BorderThickness = new Thickness(1),
Child = new ScrollViewer
{
MaxHeight = 180,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
Content = stackPanel
}
};
_openHistoryPopup = popup;
popup.IsOpen = true;
}
private void CloseHistoryPopup()
{
if (_openHistoryPopup is not null)
{
_openHistoryPopup.IsOpen = false;
_openHistoryPopup = null;
}
}
private void FocusControl(Control control)
{
Dispatcher.BeginInvoke(new Action(() =>
{
control.Focus();
if (control is TextBox textBox)
{
textBox.CaretIndex = textBox.Text.Length;
}
else if (control is PasswordBox passwordBox)
{
passwordBox.Focus();
}
}), DispatcherPriority.Input);
}
private void SaveLoginHistory(StartupLoginWindowResult result)
{
_loginHistoryStore.Remember("Maker", result.Maker);
_loginHistoryStore.Remember("Model", result.Model);
_loginHistoryStore.Remember("Variant1", result.Variant1);
_loginHistoryStore.Remember("Variant2", result.Variant2);
RememberOperatorHistory(result.Operator);
_loginHistoryStore.Remember("LineNo", result.LineNo);
_loginHistoryStore.Remember("LotNo", result.LotNo);
_loginHistoryStore.Remember("JigNo", result.JigNo);
_loginHistoryStore.Save();
}
private void RememberOperatorHistory(string operatorId)
{
if (IsSuppressedOperatorHistoryValue(operatorId))
{
_loginHistoryStore.Forget(OperatorHistoryFieldName, operatorId);
return;
}
_loginHistoryStore.Remember(OperatorHistoryFieldName, operatorId);
}
private void RemoveSuppressedOperatorHistory()
{
try
{
if (_loginHistoryStore.Forget(OperatorHistoryFieldName, SuppressedOperatorHistoryValue))
{
_loginHistoryStore.Save();
}
}
catch
{
// Login history cleanup is optional and should not block the login window.
}
}
private static bool IsSuppressedOperatorHistoryValue(string value)
{
return string.Equals(value.Trim(), SuppressedOperatorHistoryValue, StringComparison.OrdinalIgnoreCase);
}
private bool TryReadRequiredComboBox(ComboBox comboBox, string fieldName, out string value)
{
value = ReadComboBoxValue(comboBox);
if (!string.IsNullOrWhiteSpace(value))
{
return true;
}
MessageBox.Show(
this,
$"{fieldName} value is required. Edit LoginOptions.json and select a value.",
"Login",
MessageBoxButton.OK,
MessageBoxImage.Warning);
comboBox.Focus();
comboBox.IsDropDownOpen = true;
return false;
}
private static string ReadOptionalComboBox(ComboBox comboBox)
{
return ReadComboBoxValue(comboBox);
}
private static string ReadComboBoxValue(ComboBox comboBox)
{
return comboBox.SelectedItem is LoginOptionItem item
? item.Value.Trim()
: comboBox.Text.Trim();
}
2 months ago
private bool TryReadTextBox(TextBox textBox, string fieldName, out string value)
{
value = textBox.Text.Trim();
if (!string.IsNullOrWhiteSpace(value))
{
return true;
}
MessageBox.Show(this, $"{fieldName}을(를) 입력하세요.", "Login", MessageBoxButton.OK, MessageBoxImage.Warning);
textBox.Focus();
textBox.SelectAll();
return false;
}
private bool TryReadPasswordBox(PasswordBox passwordBox, string fieldName, out string value)
{
value = passwordBox.Password;
if (!string.IsNullOrWhiteSpace(value))
{
return true;
}
MessageBox.Show(this, $"{fieldName}을(를) 입력하세요.", "Login", MessageBoxButton.OK, MessageBoxImage.Warning);
passwordBox.Focus();
passwordBox.SelectAll();
return false;
}
private static string GetDatabaseIniPath()
{
return Path.Combine(AppContext.BaseDirectory, "Database.ini");
}
private static string GetLoginOptionsPath()
{
var directory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Housing");
var filePath = Path.Combine(directory, "LoginOptions.json");
if (File.Exists(filePath))
{
return filePath;
}
Directory.CreateDirectory(directory);
var bundledPath = Path.Combine(AppContext.BaseDirectory, "LoginOptions.json");
if (File.Exists(bundledPath))
{
File.Copy(bundledPath, filePath);
}
else
{
LoginSelectionOptions.CreateDefaultFile(filePath);
}
return filePath;
}
2 months ago
private static string GetLoginHistoryPath()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"Housing",
"LoginHistory.json");
2 months ago
}
private sealed record LoginOptionItem(string DisplayText, string Value);
2 months ago
}