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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/WPFBeginner/Views/MainWindow.xaml.cs b/src/WPFBeginner/Views/MainWindow.xaml.cs
new file mode 100644
index 0000000..3fbdf77
--- /dev/null
+++ b/src/WPFBeginner/Views/MainWindow.xaml.cs
@@ -0,0 +1,27 @@
+using System.Windows;
+using System.Windows.Input;
+using WPFBeginner.ViewModels;
+
+namespace WPFBeginner.Views;
+
+public partial class MainWindow : Window
+{
+ public MainWindow()
+ {
+ InitializeComponent();
+ DataContext = App.GetService();
+ }
+
+ private void SettingsButton_Click(object sender, RoutedEventArgs e)
+ {
+ SettingsPopup.IsOpen = !SettingsPopup.IsOpen;
+ }
+
+ private void SearchTextBox_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.Key == Key.Enter && DataContext is MainViewModel viewModel)
+ {
+ viewModel.SearchCommand.Execute(null);
+ }
+ }
+}
diff --git a/src/WPFBeginner/Views/RegistWindow.xaml b/src/WPFBeginner/Views/RegistWindow.xaml
new file mode 100644
index 0000000..4fd0925
--- /dev/null
+++ b/src/WPFBeginner/Views/RegistWindow.xaml
@@ -0,0 +1,183 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/WPFBeginner/Views/RegistWindow.xaml.cs b/src/WPFBeginner/Views/RegistWindow.xaml.cs
new file mode 100644
index 0000000..2c2dc2e
--- /dev/null
+++ b/src/WPFBeginner/Views/RegistWindow.xaml.cs
@@ -0,0 +1,32 @@
+using System.Windows;
+using System.Windows.Input;
+using WPFBeginner.Models;
+using WPFBeginner.ViewModels;
+
+namespace WPFBeginner.Views;
+
+public partial class RegistWindow : Window
+{
+ public event EventHandler? MemberAdded;
+
+ public RegistWindow()
+ {
+ InitializeComponent();
+ var viewModel = App.GetService();
+ viewModel.MemberAdded += OnMemberAdded;
+ DataContext = viewModel;
+ }
+
+ private void OnMemberAdded(object? sender, Member member)
+ {
+ MemberAdded?.Invoke(this, member);
+ }
+
+ private void TitleBar_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
+ {
+ if (e.ClickCount == 1)
+ {
+ DragMove();
+ }
+ }
+}
diff --git a/src/WPFBeginner/WPFBeginner.csproj b/src/WPFBeginner/WPFBeginner.csproj
new file mode 100644
index 0000000..cf5a941
--- /dev/null
+++ b/src/WPFBeginner/WPFBeginner.csproj
@@ -0,0 +1,23 @@
+
+
+
+ WinExe
+ net8.0-windows
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/WPFBeginner/appsettings.json b/src/WPFBeginner/appsettings.json
new file mode 100644
index 0000000..f2fecbd
--- /dev/null
+++ b/src/WPFBeginner/appsettings.json
@@ -0,0 +1,46 @@
+{
+ "Login": {
+ "User": "admin",
+ "Password": "0000"
+ },
+ "Language": "ko-KR",
+ "Theme": "DefaultTheme",
+ "DefaultMembers": [
+ {
+ "Name": "최준영",
+ "Call": "02-2141-7523",
+ "EMail": "b17314.tdc.hanmacgroup@gmail.com",
+ "No": "b17314"
+ },
+ {
+ "Name": "엄지숙",
+ "Call": "-",
+ "EMail": "b23072@hanmaceng.co.kr",
+ "No": "b23072"
+ },
+ {
+ "Name": "정나래",
+ "Call": "-",
+ "EMail": "b23009@hanmaceng.co.kr",
+ "No": "b23009"
+ },
+ {
+ "Name": "강근아",
+ "Call": "02-2141-7317",
+ "EMail": "m21318.tdc.hanmacgroup@gmail.com",
+ "No": "m21318"
+ },
+ {
+ "Name": "김근형",
+ "Call": "02-2141-7508",
+ "EMail": "b20311.tdc.hanmacgroup@gmail.com",
+ "No": "b20311"
+ },
+ {
+ "Name": "정호진",
+ "Call": "-",
+ "EMail": "b23022@hanmaceng.co.kr",
+ "No": "b23022"
+ }
+ ]
+}
\ No newline at end of file