버튼 이름 변경 및 Windows 종료/재시작 숫자 확인 추가

- 선택 Start/선택 Stop 버튼을 Start/Stop으로 변경
- Windows 강제종료/재시작 시 무작위 숫자 6자리를 입력해야만 실행 (붙여넣기 불가)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013U497kUNL8ArhV7jkzCpKa
main
diadmin 2026-10-27 02:59:09 +00:00
parent 84cafc48a2
commit bb34662582
No known key found for this signature in database
4 changed files with 184 additions and 5 deletions

View File

@ -16,8 +16,8 @@ Windows 서비스 기동/종료 관리 프로그램 (C# WPF, .NET 8)
### 초기화면 ### 초기화면
- **추가**: Windows 서비스 전체 목록에서 관리할 서비스 선택 - **추가**: Windows 서비스 전체 목록에서 관리할 서비스 선택
- **삭제**: 체크한 서비스를 목록에서 제외 (Windows 서비스 자체는 건드리지 않음) - **삭제**: 체크한 서비스를 목록에서 제외 (Windows 서비스 자체는 건드리지 않음)
- **선택 Start / 선택 Stop**: 체크한 서비스를 차례로 시작/중지 - **Start / Stop**: 체크한 서비스를 차례로 시작/중지
- **Windows 강제종료 / Windows 재시작**: 확인 후 즉시 실행 (설정에서 버튼 숨김 가능) - **Windows 강제종료 / Windows 재시작**: 확인창에 표시된 무작위 숫자 6자리를 그대로 입력해야만 실행 (붙여넣기 불가, 설정에서 버튼 숨김 가능)
- **설정** - **설정**
- 서비스마다 **Start / Stop / Status / Log** 버튼, 상태는 2초마다 자동 갱신 (초록=실행 중, 빨강=중지됨) - 서비스마다 **Start / Stop / Status / Log** 버튼, 상태는 2초마다 자동 갱신 (초록=실행 중, 빨강=중지됨)
- 서비스 줄 **우클릭**: 로그 파일 지정 / 로그 폴더 지정 / 경로 해제 - 서비스 줄 **우클릭**: 로그 파일 지정 / 로그 폴더 지정 / 경로 해제

View File

@ -20,8 +20,8 @@
<Button x:Name="btnAdd" Content="추가" ToolTip="관리할 Windows 서비스를 선택합니다."/> <Button x:Name="btnAdd" Content="추가" ToolTip="관리할 Windows 서비스를 선택합니다."/>
<Button x:Name="btnDelete" Content="삭제" ToolTip="체크한 서비스를 목록에서 뺍니다. (Windows 서비스는 그대로 유지)"/> <Button x:Name="btnDelete" Content="삭제" ToolTip="체크한 서비스를 목록에서 뺍니다. (Windows 서비스는 그대로 유지)"/>
<Rectangle Width="1" Fill="#CFD8DC" Margin="8,4"/> <Rectangle Width="1" Fill="#CFD8DC" Margin="8,4"/>
<Button x:Name="btnStartChecked" Content="선택 Start" ToolTip="체크한 서비스를 차례로 시작합니다."/> <Button x:Name="btnStartChecked" Content="Start" ToolTip="체크한 서비스를 차례로 시작합니다."/>
<Button x:Name="btnStopChecked" Content="선택 Stop" ToolTip="체크한 서비스를 차례로 중지합니다."/> <Button x:Name="btnStopChecked" Content="Stop" ToolTip="체크한 서비스를 차례로 중지합니다."/>
</WrapPanel> </WrapPanel>
</DockPanel> </DockPanel>
</Border> </Border>

View File

@ -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
{
/// <summary>
/// 위험 작업 확인창: 무작위 숫자 6자리를 보여주고, 그 숫자를 정확히 입력해야만 확인됨
/// (Windows 강제종료 / Windows 재시작)
/// </summary>
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();
}
}
/// <summary>확인창을 띄우고, 숫자를 정확히 입력해 확인했으면 true</summary>
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;
}
}
}

View File

@ -454,8 +454,21 @@ namespace JJServiceManager
private void Shutdown(bool restart) private void Shutdown(bool restart)
{ {
var what = 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; return;
}
Log($"Windows {what} 실행", LogLevel.Warning); Log($"Windows {what} 실행", LogLevel.Warning);
try try
{ {