diff --git a/README.md b/README.md
index b399d57..7206233 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,8 @@ Windows 서비스 기동/종료 관리 프로그램 (C# WPF, .NET 8)
### 초기화면
- **추가**: Windows 서비스 전체 목록에서 관리할 서비스 선택
- **삭제**: 체크한 서비스를 목록에서 제외 (Windows 서비스 자체는 건드리지 않음)
-- **선택 Start / 선택 Stop**: 체크한 서비스를 차례로 시작/중지
-- **Windows 강제종료 / Windows 재시작**: 확인 후 즉시 실행 (설정에서 버튼 숨김 가능)
+- **Start / Stop**: 체크한 서비스를 차례로 시작/중지
+- **Windows 강제종료 / Windows 재시작**: 확인창에 표시된 무작위 숫자 6자리를 그대로 입력해야만 실행 (붙여넣기 불가, 설정에서 버튼 숨김 가능)
- **설정**
- 서비스마다 **Start / Stop / Status / Log** 버튼, 상태는 2초마다 자동 갱신 (초록=실행 중, 빨강=중지됨)
- 서비스 줄 **우클릭**: 로그 파일 지정 / 로그 폴더 지정 / 경로 해제
diff --git a/Views/MainWindow.xaml b/Views/MainWindow.xaml
index 253e27a..fe3bc03 100644
--- a/Views/MainWindow.xaml
+++ b/Views/MainWindow.xaml
@@ -20,8 +20,8 @@
-
-
+
+
diff --git a/Windows/CodeConfirmWindow.cs b/Windows/CodeConfirmWindow.cs
new file mode 100644
index 0000000..7e8795c
--- /dev/null
+++ b/Windows/CodeConfirmWindow.cs
@@ -0,0 +1,166 @@
+using System.Security.Cryptography;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Input;
+using System.Windows.Media;
+
+namespace JJServiceManager
+{
+ ///
+ /// 위험 작업 확인창: 무작위 숫자 6자리를 보여주고, 그 숫자를 정확히 입력해야만 확인됨
+ /// (Windows 강제종료 / Windows 재시작)
+ ///
+ public sealed class CodeConfirmWindow : Window
+ {
+ private readonly string _code;
+ private readonly TextBox _input;
+ private readonly Button _ok;
+ private readonly TextBlock _hint;
+
+ public CodeConfirmWindow(string title, string message, string actionText)
+ {
+ _code = RandomNumberGenerator.GetInt32(0, 1_000_000).ToString("D6");
+
+ Title = title;
+ Width = 440;
+ SizeToContent = SizeToContent.Height;
+ ResizeMode = ResizeMode.NoResize;
+ WindowStartupLocation = WindowStartupLocation.CenterOwner;
+ ShowInTaskbar = false;
+ Topmost = true;
+ FontFamily = new FontFamily("Malgun Gothic");
+ FontSize = 13;
+ Background = Brushes2.Make("#FAFAFA");
+ AppIcon.Apply(this);
+
+ var root = new StackPanel { Margin = new Thickness(20, 18, 20, 16) };
+
+ // 경고 문구
+ root.Children.Add(new TextBlock
+ {
+ Text = "⚠ " + message,
+ TextWrapping = TextWrapping.Wrap,
+ Foreground = Brushes2.Make("#B71C1C"),
+ FontWeight = FontWeights.SemiBold,
+ Margin = new Thickness(0, 0, 0, 14)
+ });
+
+ root.Children.Add(new TextBlock
+ {
+ Text = "계속하려면 아래 숫자 6자리를 그대로 입력하세요.",
+ Foreground = Brushes2.Make("#455A64"),
+ Margin = new Thickness(0, 0, 0, 8)
+ });
+
+ // 무작위 숫자 (복사할 수 없는 글자로 표시)
+ var codeBox = new Border
+ {
+ Background = Brushes2.Make("#FFF8E1"),
+ BorderBrush = Brushes2.Make("#FFB300"),
+ BorderThickness = new Thickness(1),
+ CornerRadius = new CornerRadius(4),
+ Padding = new Thickness(0, 8, 0, 8),
+ Margin = new Thickness(0, 0, 0, 12),
+ Child = new TextBlock
+ {
+ Text = string.Join(" ", _code.ToCharArray()),
+ FontFamily = new FontFamily("Consolas"),
+ FontSize = 30,
+ FontWeight = FontWeights.Bold,
+ Foreground = Brushes2.Make("#E65100"),
+ HorizontalAlignment = HorizontalAlignment.Center,
+ IsHitTestVisible = false
+ }
+ };
+ root.Children.Add(codeBox);
+
+ // 입력칸 (숫자만, 붙여넣기 불가)
+ _input = new TextBox
+ {
+ MaxLength = 6,
+ FontFamily = new FontFamily("Consolas"),
+ FontSize = 24,
+ Height = 42,
+ HorizontalContentAlignment = HorizontalAlignment.Center,
+ VerticalContentAlignment = VerticalAlignment.Center,
+ ToolTip = "위의 숫자 6자리를 입력"
+ };
+ InputMethod.SetIsInputMethodEnabled(_input, false);
+ _input.PreviewTextInput += (s, e) =>
+ {
+ foreach (var c in e.Text)
+ if (c < '0' || c > '9') { e.Handled = true; return; }
+ };
+ DataObject.AddPastingHandler(_input, (s, e) => e.CancelCommand());
+ _input.TextChanged += (s, e) => UpdateState();
+ root.Children.Add(_input);
+
+ _hint = new TextBlock
+ {
+ Text = " ",
+ Foreground = Brushes2.Make("#C62828"),
+ Margin = new Thickness(0, 6, 0, 0),
+ HorizontalAlignment = HorizontalAlignment.Center
+ };
+ root.Children.Add(_hint);
+
+ // 버튼
+ var buttons = new StackPanel
+ {
+ Orientation = Orientation.Horizontal,
+ HorizontalAlignment = HorizontalAlignment.Right,
+ Margin = new Thickness(0, 10, 0, 0)
+ };
+ _ok = new Button { Content = actionText, IsEnabled = false, MinWidth = 110 };
+ _ok.SetResourceReference(StyleProperty, "DangerButton");
+ _ok.Click += (s, e) => Submit();
+ var cancel = new Button { Content = "취소", MinWidth = 80, IsCancel = true };
+ buttons.Children.Add(_ok);
+ buttons.Children.Add(cancel);
+ root.Children.Add(buttons);
+
+ Content = root;
+
+ Loaded += (s, e) => _input.Focus();
+ PreviewKeyDown += (s, e) =>
+ {
+ if (e.Key == Key.Enter)
+ {
+ e.Handled = true;
+ Submit();
+ }
+ };
+ }
+
+ private void UpdateState()
+ {
+ var t = _input.Text;
+ _ok.IsEnabled = t == _code;
+ if (t.Length == 6 && t != _code) _hint.Text = "숫자가 다릅니다. 다시 입력하세요.";
+ else _hint.Text = " ";
+ }
+
+ private void Submit()
+ {
+ if (_input.Text == _code)
+ {
+ DialogResult = true;
+ }
+ else
+ {
+ _hint.Text = _input.Text.Length == 0 ? "숫자를 입력하세요." : "숫자가 다릅니다. 다시 입력하세요.";
+ _input.SelectAll();
+ _input.Focus();
+ }
+ }
+
+ /// 확인창을 띄우고, 숫자를 정확히 입력해 확인했으면 true
+ public static bool Ask(Window owner, string title, string message, string actionText)
+ {
+ var w = new CodeConfirmWindow(title, message, actionText);
+ if (owner != null && owner.IsVisible) w.Owner = owner;
+ else w.WindowStartupLocation = WindowStartupLocation.CenterScreen;
+ return w.ShowDialog() == true;
+ }
+ }
+}
diff --git a/Windows/MainWindow.cs b/Windows/MainWindow.cs
index addaf9e..f645fe7 100644
--- a/Windows/MainWindow.cs
+++ b/Windows/MainWindow.cs
@@ -454,8 +454,21 @@ namespace JJServiceManager
private void Shutdown(bool restart)
{
var what = restart ? "재시작" : "강제 종료";
- if (!Dialogs.Confirm(this, $"Windows를 지금 {what}합니다.\n저장하지 않은 작업은 모두 사라집니다.\n\n계속할까요?", "Windows " + what))
+ if (_modalOpen) return;
+ bool confirmed;
+ _modalOpen = true;
+ try
+ {
+ confirmed = CodeConfirmWindow.Ask(this, "Windows " + what,
+ $"Windows를 지금 {what}합니다.\n저장하지 않은 작업은 모두 사라집니다.",
+ restart ? "Windows 재시작" : "Windows 강제종료");
+ }
+ finally { _modalOpen = false; }
+ if (!confirmed)
+ {
+ Log($"Windows {what} 취소");
return;
+ }
Log($"Windows {what} 실행", LogLevel.Warning);
try
{