using System.Collections.ObjectModel; using System.ServiceProcess; using System.Windows; using System.Windows.Media; using JJServiceManager.Remote; namespace JJServiceManager { /// /// 초기화면의 PC 하나 (내 PC 또는 관리 대상 PC)와 그 PC의 서비스 목록 /// public sealed class HostGroup : ObservableObject { public const string LocalId = "local"; private string _title = ""; private string _statusText = ""; private Brush _statusBrush = Brushes2.Unknown; private bool _isExpanded = true; private string _emptyText = ""; /// "local" 또는 대상 PC Id public string Id { get; init; } = LocalId; public bool IsLocal => Id == LocalId; /// 원격 PC일 때 접속 public RemoteClient Client { get; init; } public ObservableCollection Rows { get; } = new ObservableCollection(); /// 제목: "내 PC (HOSTNAME)" 또는 "192.168.0.10 / SERVER01" public string Title { get => _title; set => Set(ref _title, value); } /// 서비스 앞에 붙는 표시 (원격: "IP/hostname") public string Prefix => IsLocal ? "" : Title.Replace(" / ", "/"); public string StatusText { get => _statusText; set => Set(ref _statusText, value); } public Brush StatusBrush { get => _statusBrush; set => Set(ref _statusBrush, value); } public bool IsExpanded { get => _isExpanded; set => Set(ref _isExpanded, value); } public string EmptyText { get => _emptyText; set { if (Set(ref _emptyText, value)) OnPropertyChanged(nameof(EmptyVisibility)); } } public Visibility EmptyVisibility => Rows.Count == 0 && !string.IsNullOrEmpty(_emptyText) ? Visibility.Visible : Visibility.Collapsed; /// 마지막으로 기록한 연결 실패 메시지 (같은 메시지 반복 기록 방지) public string Tag { get; set; } /// 원격 PC 제목 줄의 [원격관리] 버튼 public Visibility RemoteButtonVisibility => IsLocal ? Visibility.Collapsed : Visibility.Visible; /// 중계 기기 제목 줄의 [비밀번호] 버튼 (대상 PC 접속 비밀번호는 기기마다 입력) public bool IsRelay => Id.StartsWith("relay-", System.StringComparison.OrdinalIgnoreCase); public Visibility PasswordButtonVisibility => IsRelay ? Visibility.Visible : Visibility.Collapsed; public Brush HeaderBrush => IsLocal ? Brushes2.Make("#E3F2FD") : IsRelay ? Brushes2.Make("#E8F5E9") : Brushes2.Make("#F3E5F5"); public HostGroup() { Rows.CollectionChanged += (s, e) => OnPropertyChanged(nameof(EmptyVisibility)); } public void TitleChanged() { OnPropertyChanged(nameof(Prefix)); foreach (var r in Rows) r.HostChanged(); } public override string ToString() => Title; } /// 초기화면 서비스 목록의 한 줄 public sealed class ServiceRow : ObservableObject { private bool _isChecked; private ServiceControllerStatus? _status; private bool _statusKnown; private bool _isBusy; public ServiceEntry Entry { get; } public HostGroup Host { get; } public bool IsRemote => Host != null && !Host.IsLocal; public ServiceRow(ServiceEntry entry, HostGroup host) { Entry = entry; Host = host; } public string DisplayName => string.IsNullOrEmpty(Entry.DisplayName) ? Entry.ServiceName : Entry.DisplayName; public string ServiceName => Entry.ServiceName; public string ServiceNameText => string.Equals(Entry.DisplayName, Entry.ServiceName, System.StringComparison.OrdinalIgnoreCase) ? "" : $"({Entry.ServiceName})"; /// 서비스 앞에 붙는 "IP/hostname" (내 PC는 없음) public string HostPrefix => IsRemote ? Host.Prefix : ""; public Visibility HostPrefixVisibility => IsRemote ? Visibility.Visible : Visibility.Collapsed; /// 로그에 쓰는 이름: "[IP/hostname] [서비스]" 또는 "[서비스]" public string LogName => IsRemote ? $"[{Host.Prefix}] [{DisplayName}]" : $"[{DisplayName}]"; public string Tooltip => IsRemote ? $"대상 PC: {Host.Title}\n서비스명: {Entry.ServiceName}" : $"서비스명: {Entry.ServiceName}\n우클릭: 로그 경로 지정"; public bool IsChecked { get => _isChecked; set => Set(ref _isChecked, value); } /// Start/Stop 진행 중이면 false public bool IsIdle => !_isBusy; public bool IsBusy { get => _isBusy; set { if (Set(ref _isBusy, value)) OnPropertyChanged(nameof(IsIdle)); } } public ServiceControllerStatus? Status { get => _status; set { if (_statusKnown && _status == value) return; _status = value; _statusKnown = true; OnPropertyChanged(); OnPropertyChanged(nameof(StatusText)); OnPropertyChanged(nameof(StatusBrush)); } } public bool StatusKnown => _statusKnown; /// 원격 PC 연결이 끊긴 경우 등 상태를 모를 때 public void MarkUnknown() { if (!_statusKnown) return; _statusKnown = false; OnPropertyChanged(nameof(Status)); OnPropertyChanged(nameof(StatusText)); OnPropertyChanged(nameof(StatusBrush)); } public string StatusText => _statusKnown ? ServiceOps.StatusText(_status) : (IsRemote ? "연결 안 됨" : "확인 중…"); public Brush StatusBrush => !_statusKnown ? Brushes2.Unknown : _status switch { ServiceControllerStatus.Running => Brushes2.Running, ServiceControllerStatus.Stopped => Brushes2.Stopped, null => Brushes2.Unknown, ServiceControllerStatus.Paused => Brushes2.Unknown, _ => Brushes2.Pending }; public bool HasLog => !string.IsNullOrWhiteSpace(Entry.LogPath); public string LogTooltip { get { if (IsRemote) return HasLog ? "대상 PC 로그: " + (Entry.LogIsFolder ? "폴더(최신 파일) " : "") + Entry.LogPath : "대상 PC에서 로그 경로가 지정되지 않았습니다."; return HasLog ? (Entry.LogIsFolder ? "폴더(최신 파일): " : "파일: ") + Entry.LogPath + "\n우클릭: 변경" : "로그 경로가 없습니다. 누르면 지정합니다. (우클릭: 변경)"; } } public FontWeight LogFontWeight => HasLog ? FontWeights.Bold : FontWeights.Normal; public void LogPathChanged() { OnPropertyChanged(nameof(HasLog)); OnPropertyChanged(nameof(LogTooltip)); OnPropertyChanged(nameof(LogFontWeight)); } public void HostChanged() { OnPropertyChanged(nameof(HostPrefix)); OnPropertyChanged(nameof(Tooltip)); OnPropertyChanged(nameof(LogName)); } /// 원격 목록을 새로 받았을 때 이름/로그 경로 갱신 public void UpdateFromRemote(RemoteServiceInfo info) { bool nameChanged = Entry.DisplayName != info.DisplayName; bool logChanged = Entry.LogPath != info.LogPath || Entry.LogIsFolder != info.LogIsFolder; Entry.DisplayName = info.DisplayName; Entry.LogPath = info.LogPath; Entry.LogIsFolder = info.LogIsFolder; if (nameChanged) { OnPropertyChanged(nameof(DisplayName)); OnPropertyChanged(nameof(ServiceNameText)); OnPropertyChanged(nameof(LogName)); } if (logChanged) LogPathChanged(); Status = info.Status.HasValue ? (ServiceControllerStatus?)info.Status.Value : null; } } }