using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Threading; using Housing.Services; 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 { private const string OperatorHistoryFieldName = "Operator"; private const string SuppressedOperatorHistoryValue = "su"; private readonly LoginHistoryStore _loginHistoryStore; private readonly LoginSelectionOptions _loginOptions; private Popup? _openHistoryPopup; public StartupLoginWindowResult Result { get; private set; } = new(); public StartupLoginWindow() { _loginHistoryStore = LoginHistoryStore.Load(GetLoginHistoryPath()); _loginOptions = LoginSelectionOptions.Load(GetLoginOptionsPath()); RemoveSuppressedOperatorHistory(); InitializeComponent(); ApplyLoginOptions(); AttachLoginHistoryMenus(); AttachSelectionDropDowns(); PasswordBox.GotKeyboardFocus += (_, _) => CloseHistoryPopup(); LoginButton.GotKeyboardFocus += (_, _) => CloseHistoryPopup(); MakerComboBox.Focus(); } private async void LogInButton_Click(object sender, RoutedEventArgs e) { CloseHistoryPopup(); if (!TryReadRequiredFields(out var loginResult, out var password)) { return; } if (sender is Button button) { button.IsEnabled = false; } try { if (!await IsValidLoginAsync(loginResult.Operator, password)) { MessageBox.Show(this, "등록되지 않은 ID/PW입니다.", "Login", MessageBoxButton.OK, MessageBoxImage.Error); PasswordBox.Clear(); PasswordBox.Focus(); return; } Result = loginResult; SaveLoginHistory(loginResult); DialogResult = true; Close(); } catch (Exception ex) { MessageBox.Show(this, $"로그인 확인 실패: {ex.Message}", "Login", MessageBoxButton.OK, MessageBoxImage.Error); OperatorTextBox.Focus(); OperatorTextBox.SelectAll(); } finally { if (sender is Button loginButton) { loginButton.IsEnabled = true; } } } private async Task 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); } private bool TryReadRequiredFields(out StartupLoginWindowResult result, out string password) { result = new StartupLoginWindowResult(); password = string.Empty; if (!TryReadRequiredComboBox(MakerComboBox, "Maker", out var maker) || !TryReadRequiredComboBox(ModelComboBox, "Model", out var model) || !TryReadTextBox(OperatorTextBox, "ID", out var loginId) || !TryReadPasswordBox(PasswordBox, "PW", out password) || !TryReadRequiredComboBox(LineNoComboBox, "Line", out var lineNo) || !TryReadTextBox(LotNoTextBox, "Lot No", out var lotNo) || !TryReadRequiredComboBox(JigNoComboBox, "Jig No", out var jigNo)) { return false; } var variant1 = ReadOptionalComboBox(Variant1ComboBox); var variant2 = ReadOptionalComboBox(Variant2ComboBox); result = new StartupLoginWindowResult { Maker = maker, Model = model, Variant1 = variant1, Variant2 = variant2, Operator = loginId, LineNo = lineNo, LotNo = lotNo, JigNo = jigNo }; 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 values, bool allowBlank, string historyFieldName) { var items = new List(); 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(); } } private void AttachLoginHistoryMenus() { AttachHistoryMenu(OperatorTextBox, OperatorHistoryFieldName, PasswordBox); AttachHistoryMenu(LotNoTextBox, "LotNo", JigNoComboBox); } 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(); } 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; } private static string GetLoginHistoryPath() { return Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Housing", "LoginHistory.json"); } private sealed record LoginOptionItem(string DisplayText, string Value); }