using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json.Nodes; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; using JJServiceManager.Remote; namespace JJServiceManager { /// 대상 PC: 원격 화면 연결 승인 창 (30초 안에 답하지 않으면 거부) public sealed class ApprovalWindow : Window { private readonly TaskCompletionSource _result = new TaskCompletionSource(); private readonly DispatcherTimer _timer; private readonly TextBlock _countdown; private int _left = 30; private ApprovalWindow(string who, bool control) { Title = "원격관리 연결 요청" + AppInstance.TitleTag; Width = 460; SizeToContent = SizeToContent.Height; ResizeMode = ResizeMode.NoResize; WindowStartupLocation = WindowStartupLocation.CenterScreen; 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 = control ? "원격 화면 보기 + 키보드·마우스 제어 요청" : "원격 화면 보기 요청", FontSize = 15, FontWeight = FontWeights.Bold, Foreground = Brushes2.Make(control ? "#B71C1C" : "#155E75"), Margin = new Thickness(0, 0, 0, 10) }); root.Children.Add(new TextBlock { Text = $"관리 PC [{who}] 가 이 PC 화면을 {(control ? "보고 조작" : "보")}려고 합니다.\n허용할까요?", TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 0, 0, 10) }); _countdown = new TextBlock { Foreground = Brushes.Gray }; root.Children.Add(_countdown); var buttons = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right, Margin = new Thickness(0, 12, 0, 0) }; var allow = new Button { Content = "허용", MinWidth = 90, FontWeight = FontWeights.Bold }; var deny = new Button { Content = "거부", MinWidth = 90, IsCancel = true }; allow.Click += (s, e) => Finish(true); deny.Click += (s, e) => Finish(false); buttons.Children.Add(allow); buttons.Children.Add(deny); root.Children.Add(buttons); Content = root; _timer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) }; _timer.Tick += (s, e) => { _left--; UpdateCountdown(); if (_left <= 0) Finish(false); }; UpdateCountdown(); Closed += (s, e) => { _timer.Stop(); _result.TrySetResult(false); }; Loaded += (s, e) => { _timer.Start(); Activate(); }; } private void UpdateCountdown() => _countdown.Text = $"{_left}초 뒤 자동으로 거부됩니다."; private void Finish(bool ok) { _result.TrySetResult(ok); Close(); } /// UI 스레드에서 호출 public static Task AskAsync(string who, bool control) { var w = new ApprovalWindow(who, control); w.Show(); return w._result.Task; } } /// 대상 PC: 원격 화면 연결 중 화면 위쪽 표시줄 (항상 표시) public sealed class RemoteIndicatorWindow : Window { private readonly TextBlock _text; private readonly Border _bar; public event Action DisconnectRequested; public RemoteIndicatorWindow() { WindowStyle = WindowStyle.None; ResizeMode = ResizeMode.NoResize; AllowsTransparency = true; Background = Brushes.Transparent; Topmost = true; ShowInTaskbar = false; ShowActivated = false; SizeToContent = SizeToContent.WidthAndHeight; FontFamily = new FontFamily("Malgun Gothic"); FontSize = 13; _bar = new Border { CornerRadius = new CornerRadius(0, 0, 8, 8), Padding = new Thickness(14, 6, 8, 6), Background = Brushes2.Make("#E65100") }; var panel = new StackPanel { Orientation = Orientation.Horizontal }; _text = new TextBlock { Foreground = Brushes.White, FontWeight = FontWeights.Bold, VerticalAlignment = VerticalAlignment.Center }; var stop = new Button { Content = "연결 끊기", Margin = new Thickness(12, 0, 0, 0), MinWidth = 0, MinHeight = 0, Padding = new Thickness(8, 2, 8, 2), Background = Brushes.White }; stop.Click += (s, e) => DisconnectRequested?.Invoke(); panel.Children.Add(_text); panel.Children.Add(stop); _bar.Child = panel; Content = _bar; SizeChanged += (s, e) => Reposition(); } public void Update(IReadOnlyList viewers) { if (viewers.Count == 0) { Hide(); return; } bool control = viewers.Any(v => v.Control); _bar.Background = Brushes2.Make(control ? "#C62828" : "#E65100"); _text.Text = $"● 원격 {(control ? "제어" : "보기")} 중: {string.Join(", ", viewers.Select(v => v.Who).Distinct())}"; if (!IsVisible) Show(); Reposition(); } private void Reposition() { var wa = SystemParameters.WorkArea; Left = wa.Left + (wa.Width - ActualWidth) / 2; Top = wa.Top; } } /// 관리 PC: 원격 화면 창 (보기 / 제어) public sealed class RemoteViewerWindow : Window { private readonly RemoteClient _client; private readonly string _label; private readonly Image _image; private readonly TextBlock _status, _overlay; private readonly StackPanel _monitorButtons; private readonly HashSet _pressedKeys = new HashSet(); private int _sid; private bool _control; private bool _ended; private int _monitor = 1; private int _frames; private DateTime _fpsStart = DateTime.UtcNow; private int _decoding; private DateTime _lastMove = DateTime.MinValue; public string ViewerKey { get; } public RemoteViewerWindow(string key, RemoteClient client, string label) { ViewerKey = key; _client = client; _label = label; Title = $"원격관리 - {label}" + AppInstance.TitleTag; Width = 1280; Height = 800; WindowStartupLocation = WindowStartupLocation.CenterScreen; FontFamily = new FontFamily("Malgun Gothic"); FontSize = 13; Background = Brushes2.Make("#263238"); AppIcon.Apply(this); // 한글 입력기는 대상 PC에서 처리하도록 이 창에서는 끔 InputMethod.SetIsInputMethodEnabled(this, false); var root = new DockPanel(); // ── 도구 모음 var bar = new DockPanel { Background = Brushes2.Make("#ECEFF1") }; DockPanel.SetDock(bar, Dock.Top); var close = new Button { Content = "연결 끊기" }; close.Click += (s, e) => Close(); DockPanel.SetDock(close, Dock.Right); bar.Children.Add(close); _status = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(12, 0, 12, 0), Foreground = Brushes2.Make("#37474F") }; DockPanel.SetDock(_status, Dock.Right); bar.Children.Add(_status); _monitorButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(4) }; bar.Children.Add(_monitorButtons); root.Children.Add(bar); // ── 화면 var stage = new Grid { Background = Brushes.Black, ClipToBounds = true }; _image = new Image { Stretch = Stretch.Uniform, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, Focusable = true, Cursor = Cursors.Arrow }; RenderOptions.SetBitmapScalingMode(_image, BitmapScalingMode.HighQuality); _overlay = new TextBlock { Text = "연결 중…", Foreground = Brushes.White, FontSize = 16, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, TextAlignment = TextAlignment.Center, TextWrapping = TextWrapping.Wrap }; stage.Children.Add(_image); stage.Children.Add(_overlay); root.Children.Add(stage); Content = root; _image.MouseMove += OnMouseMove; _image.MouseDown += OnMouseDown; _image.MouseUp += OnMouseUp; _image.MouseWheel += OnMouseWheel; PreviewKeyDown += OnKey; PreviewKeyUp += OnKey; Deactivated += (s, e) => ReleaseKeys(); _client.ScreenEvent += OnScreenEvent; Loaded += async (s, e) => await StartAsync(); Closed += (s, e) => Stop(); } // ───────────────────────── 시작 / 끝 ───────────────────────── private async Task StartAsync() { try { _overlay.Text = "대상 PC 정보를 확인하는 중…"; var info = await _client.RequestAsync("screenInfo", null, TimeSpan.FromSeconds(15)); if (!Json.Bool(info, "allowView")) { End("대상 PC에서 화면 보기를 허용하지 않았습니다.\n(대상 PC 설정 → 원격 접속 허용 → 화면 보기 허용)"); return; } var monitors = (info["monitors"] as JsonArray)?.OfType().ToList() ?? new List(); var primary = monitors.FirstOrDefault(m => Json.Bool(m, "primary")); _monitor = primary != null ? Json.Int(primary, "index", 1) : 1; foreach (var m in monitors) { int idx = Json.Int(m, "index", 1); AddMonitorButton($"모니터 {idx}" + (Json.Bool(m, "primary") ? " (주)" : ""), idx, $"{Json.Int(m, "width")} × {Json.Int(m, "height")}"); } if (monitors.Count > 1) AddMonitorButton("전체", 0, "모든 모니터를 한 화면에"); HighlightMonitor(); _overlay.Text = "대상 PC의 승인을 기다리는 중…"; var r = await _client.RequestAsync("screenStart", new JsonObject { ["monitor"] = _monitor }, TimeSpan.FromSeconds(45)); _sid = Json.Int(r, "sid"); _control = Json.Bool(r, "control"); _overlay.Text = ""; UpdateStatus(); _image.Cursor = _control ? Cursors.Cross : Cursors.Arrow; _image.Focus(); } catch (Exception ex) { End(RemoteClient.Friendly(ex)); } } private void AddMonitorButton(string text, int index, string tip) { var b = new Button { Content = text, Tag = index, ToolTip = tip, MinWidth = 70 }; b.Click += async (s, e) => { _monitor = index; HighlightMonitor(); try { await _client.RequestAsync("screenMonitor", new JsonObject { ["monitor"] = index }, TimeSpan.FromSeconds(10)); } catch { } _image.Focus(); }; _monitorButtons.Children.Add(b); } private void HighlightMonitor() { foreach (Button b in _monitorButtons.Children) { bool on = (int)b.Tag == _monitor; b.FontWeight = on ? FontWeights.Bold : FontWeights.Normal; b.Background = on ? Brushes2.Make("#BBDEFB") : Brushes.White; } } private void End(string reason) { _ended = true; _overlay.Text = reason; _status.Text = "연결 끝"; _image.Cursor = Cursors.Arrow; } private void Stop() { _client.ScreenEvent -= OnScreenEvent; ReleaseKeys(); if (_sid > 0 && !_ended) _ = _client.RequestAsync("screenStop", null, TimeSpan.FromSeconds(5)).ContinueWith(t => { _ = t.Exception; }); } private void UpdateStatus() { var secs = (DateTime.UtcNow - _fpsStart).TotalSeconds; double fps = secs > 0 ? _frames / secs : 0; if (secs > 3) { _frames = 0; _fpsStart = DateTime.UtcNow; } _status.Text = $"{(_control ? "● 제어 중" : "보기 전용")} · {fps:0.0}장/초"; _status.Foreground = _control ? Brushes2.Stopped : Brushes2.Make("#37474F"); } // ───────────────────────── 화면 받기 ───────────────────────── private void OnScreenEvent(JsonObject msg) { var ev = Json.Str(msg, "event"); int sid = Json.Int(msg, "sid"); if (ev == "screenEnd") { if (sid == 0 || sid == _sid) { var reason = Json.Str(msg, "reason"); Dispatcher.BeginInvoke(new Action(() => End("원격 화면 연결이 끝났습니다.\n" + reason))); } return; } if (ev != "frame" || sid != _sid || _ended) return; // 화면 그리기가 밀리면 이전 장면은 건너뜀 if (Interlocked.Exchange(ref _decoding, 1) == 1) return; try { var bytes = Convert.FromBase64String(Json.Str(msg, "data")); var bmp = new BitmapImage(); bmp.BeginInit(); bmp.CacheOption = BitmapCacheOption.OnLoad; bmp.StreamSource = new MemoryStream(bytes); bmp.EndInit(); bmp.Freeze(); Dispatcher.BeginInvoke(new Action(() => { _image.Source = bmp; _frames++; UpdateStatus(); Interlocked.Exchange(ref _decoding, 0); }), DispatcherPriority.Render); } catch { Interlocked.Exchange(ref _decoding, 0); } } // ───────────────────────── 입력 보내기 ───────────────────────── private bool CanControl => _control && !_ended && _sid > 0; private (double x, double y) Norm(MouseEventArgs e) { var p = e.GetPosition(_image); double w = Math.Max(1, _image.ActualWidth), h = Math.Max(1, _image.ActualHeight); return (Math.Clamp(p.X / w, 0, 1), Math.Clamp(p.Y / h, 0, 1)); } private void SendMouse(MouseEventArgs e, string action, string button = "", int delta = 0) { var (x, y) = Norm(e); _client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "mouse", ["action"] = action, ["button"] = button, ["x"] = Math.Round(x, 5), ["y"] = Math.Round(y, 5), ["delta"] = delta }); } private static string ButtonName(MouseButton b) => b switch { MouseButton.Right => "right", MouseButton.Middle => "middle", _ => "left" }; private void OnMouseMove(object sender, MouseEventArgs e) { if (!CanControl) return; // 너무 자주 보내지 않도록 (약 30회/초) if ((DateTime.UtcNow - _lastMove).TotalMilliseconds < 33) return; _lastMove = DateTime.UtcNow; SendMouse(e, "move"); } private void OnMouseDown(object sender, MouseButtonEventArgs e) { _image.Focus(); if (!CanControl) return; _image.CaptureMouse(); SendMouse(e, "down", ButtonName(e.ChangedButton)); e.Handled = true; } private void OnMouseUp(object sender, MouseButtonEventArgs e) { if (!CanControl) return; SendMouse(e, "up", ButtonName(e.ChangedButton)); if (Mouse.LeftButton == MouseButtonState.Released && Mouse.RightButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released) _image.ReleaseMouseCapture(); e.Handled = true; } private void OnMouseWheel(object sender, MouseWheelEventArgs e) { if (!CanControl) return; SendMouse(e, "wheel", "", e.Delta); e.Handled = true; } private void OnKey(object sender, KeyEventArgs e) { if (!CanControl) return; var key = e.Key switch { Key.System => e.SystemKey, Key.ImeProcessed => e.ImeProcessedKey, Key.DeadCharProcessed => e.DeadCharProcessedKey, _ => e.Key }; int vk = KeyInterop.VirtualKeyFromKey(key); if (vk <= 0) return; bool down = e.RoutedEvent == Keyboard.PreviewKeyDownEvent; if (down) _pressedKeys.Add(vk); else _pressedKeys.Remove(vk); _client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = down }); e.Handled = true; } /// 창을 벗어나면 눌린 키가 대상 PC에 남지 않도록 모두 뗌 private void ReleaseKeys() { if (_pressedKeys.Count == 0) return; foreach (var vk in _pressedKeys.ToList()) _client.SendNoReply(new JsonObject { ["op"] = "input", ["kind"] = "key", ["vk"] = vk, ["down"] = false }); _pressedKeys.Clear(); } } }