diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2909ad5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,60 @@ +# CLAUDE.md + +이 파일은 Claude Code (claude.ai/code)가 이 저장소에서 코드 작업을 할 때 참고하는 가이드입니다. + +## 프로젝트 개요 + +EduToy는 WPF(Windows Presentation Foundation) 기초를 학습하기 위한 교육용 프로젝트입니다. "WPF 기초를 위한 토이프로젝트"라는 이름으로도 불립니다. + +## 빌드 및 실행 + +```bash +# 프로젝트 빌드 +cd src/WPFBeginner +dotnet build + +# 프로젝트 실행 +dotnet run +``` + +## 기술 스택 + +- **.NET 8.0** - Windows WPF 애플리케이션 +- **CommunityToolkit.Mvvm 8.2.2** - MVVM 패턴 구현 +- **Microsoft.Extensions.DependencyInjection 8.0.0** - 의존성 주입 + +## 프로젝트 아키텍처 + +``` +src/WPFBeginner/ +├── Models/ # 데이터 모델 (Member, AppSettings) +├── ViewModels/ # MVVM ViewModel (LoginViewModel, MainViewModel, RegistViewModel) +├── Views/ # XAML Window (LoginWindow, MainWindow, RegistWindow) +├── Controls/ # 재사용 가능한 UserControl (InputPanel) +├── Services/ # 비즈니스 로직 서비스 (SettingsService, MemberService) +├── Converters/ # WPF 값 변환기 +└── Resources/ + ├── Themes/ # 테마 ResourceDictionary (DefaultTheme, LightTheme) + └── Languages/ # 다국어 리소스 (ko-KR, en-US) +``` + +## MVVM 패턴 + +- **View**: XAML만 사용, 코드 비하인드에 비즈니스 로직 없음 +- **ViewModel**: CommunityToolkit.Mvvm의 `[ObservableProperty]`, `[RelayCommand]` 사용 +- **DI**: App.xaml.cs에서 ServiceCollection으로 서비스 및 ViewModel 등록 + +## 주요 기능 + +- **로그인**: appsettings.json의 자격 증명으로 인증 (기본: admin/0000) +- **회원 관리**: DataGrid로 CRUD 작업 +- **테마 전환**: 어두운 테마(Default) / 밝은 테마(Light) +- **다국어 지원**: 한국어(ko-KR) / 영어(en-US) 동적 전환 + +## 설정 파일 + +`appsettings.json`: 로그인 정보, 언어, 테마, 기본 회원 데이터 저장 + +## 언어 + +프로젝트 문서 및 UI는 한국어를 기본으로 사용합니다. diff --git a/EduToy.sln b/EduToy.sln new file mode 100644 index 0000000..8b16763 --- /dev/null +++ b/EduToy.sln @@ -0,0 +1,29 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WPFBeginner", "src\WPFBeginner\WPFBeginner.csproj", "{20E73DF0-D43D-8913-E28F-6960CD8A512D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {20E73DF0-D43D-8913-E28F-6960CD8A512D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {20E73DF0-D43D-8913-E28F-6960CD8A512D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {20E73DF0-D43D-8913-E28F-6960CD8A512D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {20E73DF0-D43D-8913-E28F-6960CD8A512D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {20E73DF0-D43D-8913-E28F-6960CD8A512D} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9791FF85-3B94-4645-B4A9-22D1B17C40A2} + EndGlobalSection +EndGlobal diff --git a/History.md b/History.md new file mode 100644 index 0000000..67e00c2 --- /dev/null +++ b/History.md @@ -0,0 +1,203 @@ +# 프로젝트 개발 히스토리 + +이 문서는 EduToy WPF 프로젝트의 개발 과정을 기록합니다. + +--- + +## 1. 프로젝트 분석 및 CLAUDE.md 생성 + +### 요청 +- 코드베이스 분석 및 CLAUDE.md 파일 생성 + +### 작업 내용 +- 프로젝트 구조 탐색 +- "EduToy" - WPF 교육용 프로젝트로 확인 +- docs/sampleApp/WPFBeginner.exe (컴파일된 바이너리) 발견 +- CLAUDE.md 파일 영문으로 최초 작성 + +--- + +## 2. 한국어 전환 및 CLAUDE.md 번역 + +### 요청 +- 모든 대화를 한글로 진행 +- CLAUDE.md를 한글로 번역 + +### 작업 내용 +- CLAUDE.md 한국어로 번역 완료 +- 프로젝트 문서화 한국어 기준으로 변경 + +--- + +## 3. WPF 애플리케이션 구현 + +### 요청 +- PowerPoint 파일 (WPF 기초를 위한 토이프로젝트.pptx) 분석 +- sampleApp 참고하여 프로그램 구현 + +### 기술 스택 +- **.NET 8.0** Windows WPF +- **CommunityToolkit.Mvvm 8.2.2** - MVVM 패턴 +- **Microsoft.Extensions.DependencyInjection 8.0.0** - DI + +### 구현된 구조 +``` +src/WPFBeginner/ +├── Models/ +│ ├── Member.cs +│ └── AppSettings.cs +├── ViewModels/ +│ ├── LoginViewModel.cs +│ ├── MainViewModel.cs +│ └── RegistViewModel.cs +├── Views/ +│ ├── LoginWindow.xaml / .cs +│ ├── MainWindow.xaml / .cs +│ └── RegistWindow.xaml / .cs +├── Controls/ +│ └── InputPanel.xaml / .cs +├── Services/ +│ ├── SettingsService.cs +│ └── MemberService.cs +├── Converters/ +│ └── BoolToVisibilityConverter.cs +└── Resources/ + ├── Themes/ + │ ├── DefaultTheme.xaml + │ └── LightTheme.xaml + └── Languages/ + ├── ko-KR.xaml + └── en-US.xaml +``` + +### 주요 기능 +- 로그인 (appsettings.json 자격 증명) +- 회원 관리 (DataGrid CRUD) +- 테마 전환 (Dark/Light) +- 다국어 지원 (한국어/영어) + +--- + +## 4. 검색 영역 짤림 현상 수정 + +### 요청 +- 창 크기 줄였을 때 검색 영역이 짤리는 현상 수정 +- 엔터키로 검색 가능하도록 기능 개선 + +### 해결 방법 +- MinWidth="700" MinHeight="400" 설정 +- SearchTextBox_KeyDown 이벤트 핸들러 추가 + +### 수정 파일 +- `MainWindow.xaml` - MinWidth/MinHeight 추가 +- `MainWindow.xaml.cs` - KeyDown 이벤트 처리 +```csharp +private void SearchTextBox_KeyDown(object sender, KeyEventArgs e) +{ + if (e.Key == Key.Enter && DataContext is MainViewModel viewModel) + { + viewModel.SearchCommand.Execute(null); + } +} +``` + +--- + +## 5. 로그인 화면 디자인 개선 + +### 요청 +- 가로 스크롤 디자인 개선 +- 로그인 화면을 세련되게 변경 + +### 적용된 디자인 +- 그라데이션 배경 (#1a1a2e → #16213e) +- 둥근 모서리 (CornerRadius="12") +- 그림자 효과 (DropShadowEffect) +- 로고 아이콘 (원형 그라데이션) +- 모던한 입력 필드 스타일 +- 그라데이션 로그인 버튼 + +### 수정 파일 +- `LoginWindow.xaml` - 전체 디자인 재구성 + +--- + +## 6. 로그인 화면 짤림 현상 수정 + +### 요청 +- 로그인 화면 위아래 짤림 현상 수정 + +### 해결 방법 +- 외부 Border에 Margin="15" 추가 +- 창 크기 500x420 → 540x440으로 확대 + +### 수정 파일 +- `LoginWindow.xaml` +```xml + + + + + + ... + + +``` + +--- + +## 7. 테마 버튼 동작 수정 + +### 요청 +- 테마 버튼이 동작하지 않는 문제 수정 +- 로그인 화면 제외 이전 디자인 유지 + +### 문제 원인 +- MainWindow.xaml에 하드코딩된 색상 사용 +- DynamicResource 대신 직접 색상값 (#1a1a2e 등) 사용 + +### 해결 방법 +- MainWindow.xaml을 DynamicResource 사용하도록 복원 +- LoginWindow.xaml은 새 디자인 유지 (하드코딩) + +### 수정 파일 +- `MainWindow.xaml` +```xml + + + +``` + +### 테마 전환 구조 +1. **MainWindow.xaml** - 라디오 버튼 Command 바인딩 +2. **MainViewModel.cs** - SetDefaultThemeCommand, SetLightThemeCommand +3. **SettingsService.cs** - ApplyTheme() 메서드로 ResourceDictionary 교체 + +--- + +## 최종 결과 + +### 완료된 기능 +- ✅ MVVM 패턴 기반 WPF 애플리케이션 +- ✅ 의존성 주입 (DI) 구현 +- ✅ 로그인/회원관리 기능 +- ✅ 테마 전환 (Dark/Light) +- ✅ 다국어 지원 (한국어/영어) +- ✅ 세련된 로그인 화면 디자인 +- ✅ 반응형 레이아웃 (MinWidth/MinHeight) +- ✅ 엔터키 검색 기능 + +### 빌드 및 실행 +```bash +cd src/WPFBeginner +dotnet build +dotnet run +``` + +### 기본 로그인 정보 +- ID: admin +- Password: 0000 + +--- + +*마지막 업데이트: 2025-12-29* diff --git a/src/WPFBeginner/App.xaml b/src/WPFBeginner/App.xaml new file mode 100644 index 0000000..0d8e601 --- /dev/null +++ b/src/WPFBeginner/App.xaml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/WPFBeginner/App.xaml.cs b/src/WPFBeginner/App.xaml.cs new file mode 100644 index 0000000..e8192b5 --- /dev/null +++ b/src/WPFBeginner/App.xaml.cs @@ -0,0 +1,52 @@ +using System.Windows; +using Microsoft.Extensions.DependencyInjection; +using WPFBeginner.Services; +using WPFBeginner.ViewModels; +using WPFBeginner.Views; + +namespace WPFBeginner; + +public partial class App : Application +{ + private static IServiceProvider? _serviceProvider; + + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + + var services = new ServiceCollection(); + ConfigureServices(services); + _serviceProvider = services.BuildServiceProvider(); + + // Load settings first + var settingsService = _serviceProvider.GetRequiredService(); + settingsService.Load(); + + // Show login window + var loginWindow = new LoginWindow(); + loginWindow.Show(); + } + + private static void ConfigureServices(IServiceCollection services) + { + // Services + services.AddSingleton(); + services.AddSingleton(); + + // ViewModels + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } + + public static T GetService() where T : class + { + if (_serviceProvider == null) + { + throw new InvalidOperationException("Service provider is not initialized."); + } + + return _serviceProvider.GetRequiredService(); + } +} + diff --git a/src/WPFBeginner/AssemblyInfo.cs b/src/WPFBeginner/AssemblyInfo.cs new file mode 100644 index 0000000..cc29e7f --- /dev/null +++ b/src/WPFBeginner/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/src/WPFBeginner/Controls/InputPanel.xaml b/src/WPFBeginner/Controls/InputPanel.xaml new file mode 100644 index 0000000..44599c0 --- /dev/null +++ b/src/WPFBeginner/Controls/InputPanel.xaml @@ -0,0 +1,22 @@ + + + + + + + + diff --git a/src/WPFBeginner/Controls/InputPanel.xaml.cs b/src/WPFBeginner/Controls/InputPanel.xaml.cs new file mode 100644 index 0000000..b87dbcf --- /dev/null +++ b/src/WPFBeginner/Controls/InputPanel.xaml.cs @@ -0,0 +1,53 @@ +using System.Windows; +using System.Windows.Controls; + +namespace WPFBeginner.Controls; + +public partial class InputPanel : UserControl +{ + public static readonly DependencyProperty LabelTextProperty = + DependencyProperty.Register( + nameof(LabelText), + typeof(string), + typeof(InputPanel), + new PropertyMetadata(string.Empty)); + + public static readonly DependencyProperty TextProperty = + DependencyProperty.Register( + nameof(Text), + typeof(string), + typeof(InputPanel), + new FrameworkPropertyMetadata( + string.Empty, + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + + public static readonly DependencyProperty IsRequiredProperty = + DependencyProperty.Register( + nameof(IsRequired), + typeof(bool), + typeof(InputPanel), + new PropertyMetadata(false)); + + public string LabelText + { + get => (string)GetValue(LabelTextProperty); + set => SetValue(LabelTextProperty, value); + } + + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public bool IsRequired + { + get => (bool)GetValue(IsRequiredProperty); + set => SetValue(IsRequiredProperty, value); + } + + public InputPanel() + { + InitializeComponent(); + } +} diff --git a/src/WPFBeginner/Converters/BoolToVisibilityConverter.cs b/src/WPFBeginner/Converters/BoolToVisibilityConverter.cs new file mode 100644 index 0000000..e797da3 --- /dev/null +++ b/src/WPFBeginner/Converters/BoolToVisibilityConverter.cs @@ -0,0 +1,26 @@ +using System.Globalization; +using System.Windows; +using System.Windows.Data; + +namespace WPFBeginner.Converters; + +public class BoolToVisibilityConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is bool boolValue) + { + return boolValue ? Visibility.Visible : Visibility.Collapsed; + } + return Visibility.Collapsed; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is Visibility visibility) + { + return visibility == Visibility.Visible; + } + return false; + } +} diff --git a/src/WPFBeginner/Models/AppSettings.cs b/src/WPFBeginner/Models/AppSettings.cs new file mode 100644 index 0000000..b7ac69c --- /dev/null +++ b/src/WPFBeginner/Models/AppSettings.cs @@ -0,0 +1,15 @@ +namespace WPFBeginner.Models; + +public class AppSettings +{ + public LoginSettings Login { get; set; } = new(); + public string Language { get; set; } = "ko-KR"; + public string Theme { get; set; } = "DefaultTheme"; + public List DefaultMembers { get; set; } = new(); +} + +public class LoginSettings +{ + public string User { get; set; } = "admin"; + public string Password { get; set; } = "0000"; +} diff --git a/src/WPFBeginner/Models/Member.cs b/src/WPFBeginner/Models/Member.cs new file mode 100644 index 0000000..269501f --- /dev/null +++ b/src/WPFBeginner/Models/Member.cs @@ -0,0 +1,33 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace WPFBeginner.Models; + +public partial class Member : ObservableObject +{ + [ObservableProperty] + private string _name = string.Empty; + + [ObservableProperty] + private string _call = string.Empty; + + [ObservableProperty] + private string _eMail = string.Empty; + + [ObservableProperty] + private string _no = string.Empty; + + public Member() { } + + public Member(string name, string call, string email, string no) + { + Name = name; + Call = call; + EMail = email; + No = no; + } + + public Member Clone() + { + return new Member(Name, Call, EMail, No); + } +} diff --git a/src/WPFBeginner/Resources/Languages/en-US.xaml b/src/WPFBeginner/Resources/Languages/en-US.xaml new file mode 100644 index 0000000..6091c96 --- /dev/null +++ b/src/WPFBeginner/Resources/Languages/en-US.xaml @@ -0,0 +1,53 @@ + + + + Login + Welcome + User ID + Password + Login + Invalid ID or Password + + + Member Management + Search + Enter search keyword + Add + Delete + Reset + Settings + No results found + + + Name + Phone + Email + Employee No. + + + Language + 한국어 + English + Theme + Default (Dark) + Light + + + Member Registration + Name + Phone + Email + Employee No. + Register + Cancel + Name is required + Email is required + + + Close + OK + Cancel + + diff --git a/src/WPFBeginner/Resources/Languages/ko-KR.xaml b/src/WPFBeginner/Resources/Languages/ko-KR.xaml new file mode 100644 index 0000000..66cfc0f --- /dev/null +++ b/src/WPFBeginner/Resources/Languages/ko-KR.xaml @@ -0,0 +1,53 @@ + + + + 로그인 + 환영합니다 + 아이디 + 비밀번호 + 로그인 + 아이디 또는 비밀번호가 틀렸습니다 + + + 회원 관리 + 검색 + 검색어를 입력하세요 + 추가 + 삭제 + 초기화 + 설정 + 검색 결과가 없습니다 + + + 이름 + 연락처 + 이메일 + 사번 + + + 언어 + 한국어 + English + 테마 + 기본 (어두운) + 밝은 + + + 회원 등록 + 이름 + 연락처 + 이메일 + 사번 + 등록 + 취소 + 이름은 필수입니다 + 이메일은 필수입니다 + + + 닫기 + 확인 + 취소 + + diff --git a/src/WPFBeginner/Resources/Themes/DefaultTheme.xaml b/src/WPFBeginner/Resources/Themes/DefaultTheme.xaml new file mode 100644 index 0000000..e7debd9 --- /dev/null +++ b/src/WPFBeginner/Resources/Themes/DefaultTheme.xaml @@ -0,0 +1,192 @@ + + + + #2D2D30 + #3E3E42 + #007ACC + #FFFFFF + #A0A0A0 + #555555 + #FF5555 + #55FF55 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WPFBeginner/Resources/Themes/LightTheme.xaml b/src/WPFBeginner/Resources/Themes/LightTheme.xaml new file mode 100644 index 0000000..b86f37a --- /dev/null +++ b/src/WPFBeginner/Resources/Themes/LightTheme.xaml @@ -0,0 +1,192 @@ + + + + #FFFFFF + #F5F5F5 + #007ACC + #1E1E1E + #666666 + #CCCCCC + #D32F2F + #388E3C + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WPFBeginner/Services/MemberService.cs b/src/WPFBeginner/Services/MemberService.cs new file mode 100644 index 0000000..71dc771 --- /dev/null +++ b/src/WPFBeginner/Services/MemberService.cs @@ -0,0 +1,61 @@ +using System.Collections.ObjectModel; +using WPFBeginner.Models; + +namespace WPFBeginner.Services; + +public interface IMemberService +{ + ObservableCollection Members { get; } + void LoadDefaultMembers(List defaultMembers); + void Add(Member member); + void Remove(Member member); + void Reset(); + IEnumerable Search(string keyword); +} + +public class MemberService : IMemberService +{ + private readonly List _defaultMembers = new(); + public ObservableCollection Members { get; } = new(); + + public void LoadDefaultMembers(List defaultMembers) + { + _defaultMembers.Clear(); + _defaultMembers.AddRange(defaultMembers.Select(m => m.Clone())); + Reset(); + } + + public void Add(Member member) + { + Members.Add(member); + } + + public void Remove(Member member) + { + Members.Remove(member); + } + + public void Reset() + { + Members.Clear(); + foreach (var member in _defaultMembers) + { + Members.Add(member.Clone()); + } + } + + public IEnumerable Search(string keyword) + { + if (string.IsNullOrWhiteSpace(keyword)) + { + return Members; + } + + var lowerKeyword = keyword.ToLower(); + return Members.Where(m => + m.Name.ToLower().Contains(lowerKeyword) || + m.Call.ToLower().Contains(lowerKeyword) || + m.EMail.ToLower().Contains(lowerKeyword) || + m.No.ToLower().Contains(lowerKeyword)); + } +} diff --git a/src/WPFBeginner/Services/SettingsService.cs b/src/WPFBeginner/Services/SettingsService.cs new file mode 100644 index 0000000..1142d10 --- /dev/null +++ b/src/WPFBeginner/Services/SettingsService.cs @@ -0,0 +1,114 @@ +using System.IO; +using System.Text.Json; +using System.Windows; +using WPFBeginner.Models; + +namespace WPFBeginner.Services; + +public interface ISettingsService +{ + AppSettings Settings { get; } + void Load(); + void Save(); + void SetLanguage(string language); + void SetTheme(string theme); + string CurrentLanguage { get; } + string CurrentTheme { get; } +} + +public class SettingsService : ISettingsService +{ + private readonly string _settingsPath; + private AppSettings _settings = new(); + + public AppSettings Settings => _settings; + public string CurrentLanguage => _settings.Language; + public string CurrentTheme => _settings.Theme; + + public SettingsService() + { + _settingsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "appsettings.json"); + } + + public void Load() + { + try + { + if (File.Exists(_settingsPath)) + { + var json = File.ReadAllText(_settingsPath); + var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; + _settings = JsonSerializer.Deserialize(json, options) ?? new AppSettings(); + } + } + catch + { + _settings = new AppSettings(); + } + + ApplyLanguage(_settings.Language); + ApplyTheme(_settings.Theme); + } + + public void Save() + { + try + { + var options = new JsonSerializerOptions { WriteIndented = true }; + var json = JsonSerializer.Serialize(_settings, options); + File.WriteAllText(_settingsPath, json); + } + catch + { + // Ignore save errors + } + } + + public void SetLanguage(string language) + { + _settings.Language = language; + ApplyLanguage(language); + Save(); + } + + public void SetTheme(string theme) + { + _settings.Theme = theme; + ApplyTheme(theme); + Save(); + } + + private void ApplyLanguage(string language) + { + var dict = new ResourceDictionary(); + var langFile = language == "en-US" ? "en-US" : "ko-KR"; + dict.Source = new Uri($"pack://application:,,,/Resources/Languages/{langFile}.xaml"); + + // Remove existing language dictionary + var existingLang = Application.Current.Resources.MergedDictionaries + .FirstOrDefault(d => d.Source?.OriginalString.Contains("/Languages/") == true); + if (existingLang != null) + { + Application.Current.Resources.MergedDictionaries.Remove(existingLang); + } + + Application.Current.Resources.MergedDictionaries.Add(dict); + } + + private void ApplyTheme(string theme) + { + var dict = new ResourceDictionary(); + var themeFile = theme == "LightTheme" ? "LightTheme" : "DefaultTheme"; + dict.Source = new Uri($"pack://application:,,,/Resources/Themes/{themeFile}.xaml"); + + // Remove existing theme dictionary + var existingTheme = Application.Current.Resources.MergedDictionaries + .FirstOrDefault(d => d.Source?.OriginalString.Contains("/Themes/") == true); + if (existingTheme != null) + { + Application.Current.Resources.MergedDictionaries.Remove(existingTheme); + } + + Application.Current.Resources.MergedDictionaries.Add(dict); + } +} diff --git a/src/WPFBeginner/ViewModels/LoginViewModel.cs b/src/WPFBeginner/ViewModels/LoginViewModel.cs new file mode 100644 index 0000000..b44aa98 --- /dev/null +++ b/src/WPFBeginner/ViewModels/LoginViewModel.cs @@ -0,0 +1,62 @@ +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using WPFBeginner.Services; +using WPFBeginner.Views; + +namespace WPFBeginner.ViewModels; + +public partial class LoginViewModel : ObservableObject +{ + private readonly ISettingsService _settingsService; + + [ObservableProperty] + private string _userId = string.Empty; + + [ObservableProperty] + private string _password = string.Empty; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private bool _hasError; + + public string WelcomeMessage => _settingsService.CurrentLanguage == "en-US" + ? "Welcome" + : "환영합니다"; + + public LoginViewModel(ISettingsService settingsService) + { + _settingsService = settingsService; + } + + [RelayCommand] + private void Login(Window window) + { + var settings = _settingsService.Settings.Login; + + if (UserId == settings.User && Password == settings.Password) + { + HasError = false; + ErrorMessage = string.Empty; + + var mainWindow = new MainWindow(); + mainWindow.Show(); + window.Close(); + } + else + { + HasError = true; + ErrorMessage = _settingsService.CurrentLanguage == "en-US" + ? "Invalid ID or Password" + : "아이디 또는 비밀번호가 틀렸습니다"; + } + } + + [RelayCommand] + private void Close(Window window) + { + Application.Current.Shutdown(); + } +} diff --git a/src/WPFBeginner/ViewModels/MainViewModel.cs b/src/WPFBeginner/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..d8e7b4a --- /dev/null +++ b/src/WPFBeginner/ViewModels/MainViewModel.cs @@ -0,0 +1,165 @@ +using System.Collections.ObjectModel; +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using WPFBeginner.Models; +using WPFBeginner.Services; +using WPFBeginner.Views; + +namespace WPFBeginner.ViewModels; + +public partial class MainViewModel : ObservableObject +{ + private readonly ISettingsService _settingsService; + private readonly IMemberService _memberService; + + [ObservableProperty] + private string _searchKeyword = string.Empty; + + [ObservableProperty] + private Member? _selectedMember; + + [ObservableProperty] + private ObservableCollection _filteredMembers = new(); + + [ObservableProperty] + private bool _isKorean = true; + + [ObservableProperty] + private bool _isEnglish; + + [ObservableProperty] + private bool _isDefaultTheme = true; + + [ObservableProperty] + private bool _isLightTheme; + + [ObservableProperty] + private string _searchResultMessage = string.Empty; + + [ObservableProperty] + private bool _hasSearchResult = true; + + public MainViewModel(ISettingsService settingsService, IMemberService memberService) + { + _settingsService = settingsService; + _memberService = memberService; + + // Load default members + _memberService.LoadDefaultMembers(_settingsService.Settings.DefaultMembers); + + // Set initial language/theme based on settings + IsKorean = _settingsService.CurrentLanguage == "ko-KR"; + IsEnglish = _settingsService.CurrentLanguage == "en-US"; + IsDefaultTheme = _settingsService.CurrentTheme == "DefaultTheme"; + IsLightTheme = _settingsService.CurrentTheme == "LightTheme"; + + // Initialize filtered members + RefreshFilteredMembers(); + } + + private void RefreshFilteredMembers() + { + FilteredMembers = new ObservableCollection(_memberService.Members); + } + + [RelayCommand] + private void Search() + { + var results = _memberService.Search(SearchKeyword).ToList(); + FilteredMembers = new ObservableCollection(results); + + if (results.Count == 0) + { + HasSearchResult = false; + SearchResultMessage = _settingsService.CurrentLanguage == "en-US" + ? "No results found" + : "검색 결과가 없습니다"; + + MessageBox.Show(SearchResultMessage, + _settingsService.CurrentLanguage == "en-US" ? "Search" : "검색", + MessageBoxButton.OK, + MessageBoxImage.Information); + } + else + { + HasSearchResult = true; + SearchResultMessage = string.Empty; + } + } + + [RelayCommand] + private void AddMember() + { + var registWindow = new RegistWindow(); + registWindow.MemberAdded += OnMemberAdded; + registWindow.ShowDialog(); + } + + private void OnMemberAdded(object? sender, Member member) + { + _memberService.Add(member); + RefreshFilteredMembers(); + } + + [RelayCommand] + private void DeleteMember() + { + if (SelectedMember != null) + { + _memberService.Remove(SelectedMember); + RefreshFilteredMembers(); + SelectedMember = null; + } + } + + [RelayCommand] + private void Reset() + { + SearchKeyword = string.Empty; + _memberService.Reset(); + RefreshFilteredMembers(); + HasSearchResult = true; + SearchResultMessage = string.Empty; + } + + [RelayCommand] + private void SetKorean() + { + IsKorean = true; + IsEnglish = false; + _settingsService.SetLanguage("ko-KR"); + OnPropertyChanged(nameof(IsKorean)); + OnPropertyChanged(nameof(IsEnglish)); + } + + [RelayCommand] + private void SetEnglish() + { + IsKorean = false; + IsEnglish = true; + _settingsService.SetLanguage("en-US"); + OnPropertyChanged(nameof(IsKorean)); + OnPropertyChanged(nameof(IsEnglish)); + } + + [RelayCommand] + private void SetDefaultTheme() + { + IsDefaultTheme = true; + IsLightTheme = false; + _settingsService.SetTheme("DefaultTheme"); + OnPropertyChanged(nameof(IsDefaultTheme)); + OnPropertyChanged(nameof(IsLightTheme)); + } + + [RelayCommand] + private void SetLightTheme() + { + IsDefaultTheme = false; + IsLightTheme = true; + _settingsService.SetTheme("LightTheme"); + OnPropertyChanged(nameof(IsDefaultTheme)); + OnPropertyChanged(nameof(IsLightTheme)); + } +} diff --git a/src/WPFBeginner/ViewModels/RegistViewModel.cs b/src/WPFBeginner/ViewModels/RegistViewModel.cs new file mode 100644 index 0000000..73a9f15 --- /dev/null +++ b/src/WPFBeginner/ViewModels/RegistViewModel.cs @@ -0,0 +1,93 @@ +using System.Windows; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using WPFBeginner.Models; +using WPFBeginner.Services; + +namespace WPFBeginner.ViewModels; + +public partial class RegistViewModel : ObservableObject +{ + private readonly ISettingsService _settingsService; + + [ObservableProperty] + private string _name = string.Empty; + + [ObservableProperty] + private string _call = string.Empty; + + [ObservableProperty] + private string _eMail = string.Empty; + + [ObservableProperty] + private string _no = string.Empty; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private bool _hasError; + + public event EventHandler? MemberAdded; + + public RegistViewModel(ISettingsService settingsService) + { + _settingsService = settingsService; + } + + [RelayCommand] + private void Register(Window window) + { + // Validate required fields + if (string.IsNullOrWhiteSpace(Name)) + { + HasError = true; + ErrorMessage = _settingsService.CurrentLanguage == "en-US" + ? "Name is required" + : "이름은 필수입니다"; + return; + } + + if (string.IsNullOrWhiteSpace(EMail)) + { + HasError = true; + ErrorMessage = _settingsService.CurrentLanguage == "en-US" + ? "Email is required" + : "이메일은 필수입니다"; + return; + } + + HasError = false; + ErrorMessage = string.Empty; + + var member = new Member(Name, Call, EMail, No); + MemberAdded?.Invoke(this, member); + window.Close(); + } + + [RelayCommand] + private void Cancel(Window window) + { + window.Close(); + } + + [RelayCommand] + private void Minimize(Window window) + { + window.WindowState = WindowState.Minimized; + } + + [RelayCommand] + private void Maximize(Window window) + { + window.WindowState = window.WindowState == WindowState.Maximized + ? WindowState.Normal + : WindowState.Maximized; + } + + [RelayCommand] + private void Close(Window window) + { + window.Close(); + } +} diff --git a/src/WPFBeginner/Views/LoginWindow.xaml b/src/WPFBeginner/Views/LoginWindow.xaml new file mode 100644 index 0000000..23bb8fd --- /dev/null +++ b/src/WPFBeginner/Views/LoginWindow.xaml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/WPFBeginner/Views/LoginWindow.xaml.cs b/src/WPFBeginner/Views/LoginWindow.xaml.cs new file mode 100644 index 0000000..0b1df2a --- /dev/null +++ b/src/WPFBeginner/Views/LoginWindow.xaml.cs @@ -0,0 +1,30 @@ +using System.Windows; +using System.Windows.Input; +using WPFBeginner.ViewModels; + +namespace WPFBeginner.Views; + +public partial class LoginWindow : Window +{ + public LoginWindow() + { + InitializeComponent(); + DataContext = App.GetService(); + } + + private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) + { + if (e.ClickCount == 1) + { + DragMove(); + } + } + + private void PasswordBox_PasswordChanged(object sender, RoutedEventArgs e) + { + if (DataContext is LoginViewModel viewModel) + { + viewModel.Password = PasswordBox.Password; + } + } +} diff --git a/src/WPFBeginner/Views/MainWindow.xaml b/src/WPFBeginner/Views/MainWindow.xaml new file mode 100644 index 0000000..98b30f6 --- /dev/null +++ b/src/WPFBeginner/Views/MainWindow.xaml @@ -0,0 +1,143 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +