using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Security.Cryptography; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using static System.Windows.Forms.VisualStyles.VisualStyleElement; namespace ControlServer { public partial class Form1 : Form { private List terminalList = new List(); // 동시성 제어를 위해 해시맵(Dictionary) 접근 시 락(Lock) 객체 분리 또는 보완 private readonly Dictionary _connectedAgents = new Dictionary(); private readonly Dictionary _openedMonitorForms = new Dictionary(); private readonly HashSet _completedAgents = new HashSet(); private TcpListener _listener; private bool _isRunning = false; private System.Windows.Forms.Timer _statusTimer; private bool _isUpdatePending = false; private System.Windows.Forms.Timer _autoControlTimer; // 에이전트 PC 자동 ON/OFF 시간 private CancellationTokenSource _updateCts = null; // 60초 타이머 제어를 위한 토큰 소스 및 상태 플래그 private bool _isTimerRunning = false; private readonly object _syncLock = new object(); private readonly object _agentLock = new object(); private readonly DbFileManager _dbFileManager = new DbFileManager(); private Size _originalFormSize; // 폼 로드 시 초기 비율 저장 public Form1() { InitializeComponent(); this.Load += Form1_Load; this.FormClosing += Form1_FormClosing; // 서버 종료 시 자원 해제 this.WindowState = FormWindowState.Maximized; // 폼 최대화 설정 this.Resize += new EventHandler(Form1_Resize); // Resize 이벤트 연결 InitializeButtonDesign(); // 둥근 패널 설정 pannGray.BackColor = Color.White; pannBlue.BackColor = Color.White; pannGray.BackgroundImage = null; pannBlue.BackgroundImage = null; pannGray.CornerRadius = 30; // 숫자가 클수록 더 둥글어짐 pannBlue.CornerRadius = 30; // 텍스트 박스 스타일 및 정렬 설정 txtSearch.BorderStyle = BorderStyle.None; // 보더 선 제거 txtSearch.BackColor = Color.LightGray; // 배경색 회색 설정 (원하는 밝기로 조절 가능) txtSearch.TextAlign = HorizontalAlignment.Left; // 가로 왼쪽 정렬 txtSearch.PlaceholderText = "검색(단말기명, IP)"; // 히든 글씨 설정 // 자동 제어 타이머 초기화 (1초 주기) _autoControlTimer = new System.Windows.Forms.Timer(); _autoControlTimer.Interval = 1000; _autoControlTimer.Tick += AutoControlTimer_Tick; _autoControlTimer.Start(); InitDataGridView(); grdTotList.CellFormatting += grdTotList_CellFormatting; txtSearch.SizeChanged += TextBox_SizeChanged; btnDataUpdate.Click += btnDataUpdate_Click; grdTotList.CellDoubleClick += grdTotList_CellDoubleClick; grdTotList.CellMouseClick += grdTotList_CellMouseClick; // [누락 수정] 이벤트 바인딩 추가 } private void InitDataGridView() { // 기본 설정 grdTotList.RowHeadersVisible = false; // 왼쪽 화살표(행 헤더) 숨김 grdTotList.AllowUserToAddRows = false; // 빈 빈칸 추가 방지 grdTotList.AllowUserToResizeRows = false; // 행 높이 수동 조절 방지 grdTotList.AllowUserToResizeColumns = false; // 열 너비 수동 조절 방지 grdTotList.ReadOnly = true; // 읽기 전용 grdTotList.SelectionMode = DataGridViewSelectionMode.FullRowSelect; // 행 단위 선택 grdTotList.MultiSelect = false; // 다중 선택 방지 grdTotList.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; // 열 넓이도 그리드 넓이에 맞게 자동으로 늘어나게 설정 grdTotList.Columns[3].FillWeight = 60; // 통신설정 grdTotList.Columns[4].FillWeight = 60; // 프로그램 grdTotList.Columns[5].FillWeight = 140; grdTotList.EnableHeadersVisualStyles = false; // 커스텀 디자인을 적용하기 위해 반드시 false로 설정 // 전체 배경 및 테두리 설정 grdTotList.BackgroundColor = Color.White; // 그리드 바깥쪽 빈 공간 배경색 grdTotList.BorderStyle = BorderStyle.None; // 그리드 전체 테두리 제거 // 셀 테두리 설정 (가로선만 옅은 회색으로) grdTotList.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; grdTotList.GridColor = Color.FromArgb(230, 230, 230); // 헤더(제목 표시줄) 스타일 (진한 남색 배경, 흰색 글씨) DataGridViewCellStyle headerStyle = new DataGridViewCellStyle(); Color headerBgColor = Color.FromArgb(44, 62, 80); // #2C3E50 (진한 남색) Color headerFgColor = Color.White; headerStyle.BackColor = headerBgColor; headerStyle.ForeColor = headerFgColor; headerStyle.Font = new Font("맑은 고딕", 10F, FontStyle.Bold); headerStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; // ★ 행 클릭 시 헤더가 반전되거나 포커스 색상으로 변하는 것을 방지 headerStyle.SelectionBackColor = headerBgColor; headerStyle.SelectionForeColor = headerFgColor; grdTotList.ColumnHeadersDefaultCellStyle = headerStyle; grdTotList.ColumnHeadersHeight = 40; grdTotList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; // 행 선택 시 헤더 스타일 상속으로 인한 테두리 및 색상 왜곡 방지 grdTotList.EnableHeadersVisualStyles = false; // 기본 셀(데이터) 스타일 DataGridViewCellStyle cellStyle = new DataGridViewCellStyle(); cellStyle.BackColor = Color.White; cellStyle.ForeColor = Color.FromArgb(50, 50, 50); // 완전 검정보다 부드러운 짙은 회색 cellStyle.Font = new Font("맑은 고딕", 9.5F, FontStyle.Regular); cellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; // 선택했을 때 형광 파란색 대신 옅은 회색으로 표시 cellStyle.SelectionBackColor = Color.FromArgb(240, 245, 249); cellStyle.SelectionForeColor = Color.Black; grdTotList.DefaultCellStyle = cellStyle; // 행 높이 넉넉하게 설정 grdTotList.RowTemplate.Height = 45; grdTotList.ClearSelection(); } private void LoadSavedTerminals() { lblTotCnt.Text = grdTotList.RowCount.ToString() + "대"; } private void Form1_Load(object sender, EventArgs e) { _originalFormSize = this.Size; LoadSavedTerminals(); InitStatusTimer(); // 1. 단말기 등록 여부와 상관없이 "서버 포트 오픈"은 무조건 실행 System.Diagnostics.Debug.WriteLine("[서버 로드] StartServerAsync 호출 시도"); Task.Run(async () => await StartServerAsync()).ContinueWith(t => { if (t.IsFaulted) { string errMsg = t.Exception?.GetBaseException().Message; System.Diagnostics.Debug.WriteLine($"서버 구동 태스크 오류: {errMsg}"); } }); // 2. 단말기 상태 체크 부분 if (grdTotList.Rows.Count > 0) { Task.Run(() => CheckAllAliveStatus()); } else { System.Diagnostics.Debug.WriteLine("등록된 단말기가 없어 서버를 대기 상태로 유지합니다. 단말기를 등록해주세요."); } } // 폼 크기 변동 시 호출되는 이벤트 private void Form1_Resize(object sender, EventArgs e) { if (this.WindowState == FormWindowState.Minimized) return; int margin = (int)(this.Width * 0.05); int contentWidth = this.Width - (margin * 2); // 1. 그리드뷰 위치 및 크기 조정 grdTotList.Width = contentWidth; grdTotList.Left = margin; grdTotList.Height = (int)(this.Height * 0.6); pannGray.Width = (int)(grdTotList.Width * 0.2); pannBlue.Width = (int)(grdTotList.Width * 0.2); pannGray.Left = (int)(this.Width / 2 - pannGray.Width - 30); pannBlue.Left = (int)(this.Width / 2 + 30); lblGrayTitle.Width = (int)(grdTotList.Width * 0.2); lblBlueTitle.Width = (int)(grdTotList.Width * 0.2); lblTotCnt.Width = (int)(grdTotList.Width * 0.2); lblRunningCnt.Width = (int)(grdTotList.Width * 0.2); // 2. 상단/하단 컨트롤들도 동일하게 좌우 여백 적용 txtSearch.Left = margin; txtSearch.Width = (int)(contentWidth * 0.2); imgSearch.Left = (int)(txtSearch.Left + txtSearch.Width + 10); btnRegister.Left = (int)(grdTotList.Width + grdTotList.Left - btnRegister.Width); btnDataUpdate.Left = (int)(btnRegister.Left - btnDataUpdate.Width - 10); // 강제 중앙 정렬 함수를 호출 CenterControl(grdTotList); } // 특정 컨트롤을 폼의 가로 중앙에 배치하는 헬퍼 메서드 private void CenterControl(Control ctrl) { ctrl.Left = (this.Width - ctrl.Width) / 2; } // 버튼 디자인 초기화 private void InitializeButtonDesign() { // 버튼의 기본 테두리 제거를 위해 FlatStyle을 Flat으로 설정 btnDataUpdate.FlatStyle = FlatStyle.Flat; btnDataUpdate.FlatAppearance.BorderSize = 0; // 보더 선 제거 btnDataUpdate.BackColor = Color.White; // 원하시는 배경색 btnRegister.FlatStyle = FlatStyle.Flat; btnRegister.FlatAppearance.BorderSize = 0; // 보더 선 제거 btnRegister.BackColor = Color.RoyalBlue; // 둥글게 그리기 이벤트 연결 btnDataUpdate.Paint += Button_Paint; btnRegister.Paint += Button_Paint; } // 둥근 모서리 그리기 메서드 private void Button_Paint(object sender, PaintEventArgs e) { System.Windows.Forms.Button btn = (System.Windows.Forms.Button)sender; e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; // 둥근 사각형 그리기 int radius = 20; // 둥근 정도 System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath(); path.AddArc(0, 0, radius, radius, 180, 90); path.AddArc(btn.Width - radius, 0, radius, radius, 270, 90); path.AddArc(btn.Width - radius, btn.Height - radius, radius, radius, 0, 90); path.AddArc(0, btn.Height - radius, radius, radius, 90, 90); path.CloseFigure(); btn.Region = new Region(path); } private void InitStatusTimer() { _statusTimer = new System.Windows.Forms.Timer(); _statusTimer.Interval = 5000; _statusTimer.Tick += (s, e) => { // 타이머 틱은 UI 스레드에서 돌므로 비동기로 상태 체크 유도 Task.Run(() => CheckAllAliveStatus()); }; _statusTimer.Start(); } // [구조 개선] UI 전체를 Invoke로 묶어 돌리던 무거운 방식을 내부 값 갱신 시에만 분리하도록 최적화 private void CheckAllAliveStatus() { try { List<(int RowIndex, string Ip, string Mac)> targets = new List<(int, string, string)>(); this.Invoke(new Action(() => { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; string ip = row.Cells[2].Value?.ToString()?.Trim(); string mac = row.Cells[1].Value?.ToString()?.Replace("-", "").Replace(":", "").Replace(" ", "").ToUpper().Trim() ?? string.Empty; if (!string.IsNullOrEmpty(ip)) { targets.Add((row.Index, ip, mac)); } } })); foreach (var target in targets) { bool isAlive = false; lock (_agentLock) { // 복잡한 Poll 검사를 제거하고, 딕셔너리에 존재하며 Connected 상태인지 확인 if (_connectedAgents.TryGetValue(target.Ip, out TcpClient client)) { if (client != null && client.Connected) { isAlive = true; } } } // UI 업데이트 this.BeginInvoke(new Action(() => { if (target.RowIndex >= grdTotList.Rows.Count) return; var row = grdTotList.Rows[target.RowIndex]; if (isAlive) { row.Cells[4].Value = "🟢 Start"; row.Cells[4].Style.BackColor = Color.Green; } else { row.Cells[4].Value = "🟢 Stop"; row.Cells[4].Style.BackColor = Color.Red; } row.Cells[4].Style.ForeColor = Color.Black; row.Cells[4].Style.BackColor = Color.White; row.Cells[4].Style.Alignment = DataGridViewContentAlignment.MiddleCenter; })); } this.BeginInvoke(new Action(() => UpdateRunningCount())); } catch (Exception ex) { Console.WriteLine($"상태 체크 도중 예외 발생: {ex.Message}"); } } private async Task StartServerAsync() { if (_isRunning) return; try { _listener = new TcpListener(IPAddress.Any, 9999); _listener.Start(); _isRunning = true; System.Diagnostics.Debug.WriteLine("서버 구동 성공 (Port: 9999)... 에이전트 접속을 대기합니다."); while (_isRunning) { // Accept 직전 대기 상태 확인 System.Diagnostics.Debug.WriteLine("[대기 중] 새로운 에이전트의 접속 소켓 신호를 기다리는 중..."); // 클라이언트 접속 대기 (비동기) TcpClient client = await _listener.AcceptTcpClientAsync(); // 소켓 연결 자체가 성공했을 때 System.Diagnostics.Debug.WriteLine("[소켓 연결 성공] 누군가 9999 포트로 연결했습니다! 핸들러로 넘깁니다."); _ = Task.Run(() => HandleAgentAsync(client)); } } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"서버 루프 치명적 오류: {ex.Message}"); this.BeginInvoke(new Action(() => MessageBox.Show("서버 구동 오류: " + ex.Message))); } finally { _isRunning = false; try { _listener?.Stop(); } catch { } System.Diagnostics.Debug.WriteLine("서버 구동이 완전히 중지되었습니다. (_isRunning = false)"); } } // 동기 IO 함수들(StreamReader.ReadLine)을 비동기(ReadLineAsync)로 변경하여 리소스 낭비 방지 private async Task HandleAgentAsync(TcpClient client) { string clientIP = string.Empty; try { clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); System.Diagnostics.Debug.WriteLine($"[핸들러 진입] IP: {clientIP} 스트림 분석 시작"); using (NetworkStream stream = client.GetStream()) using (StreamReader reader = new StreamReader(stream, Encoding.UTF8)) { string receivedMac = await reader.ReadLineAsync(); System.Diagnostics.Debug.WriteLine($"[{clientIP}] 수신된 MAC 주소: '{receivedMac}'"); if (!string.IsNullOrEmpty(receivedMac)) { // 등록된 단말기인지 UI 스레드에서 먼저 판별 및 확인 bool isRegistered = false; this.Invoke(new Action(() => { isRegistered = UpdateAgentStatusByMac(receivedMac, clientIP); })); // 등록되지 않은 단말기라면 소켓 끊고 핸들러 종료! if (!isRegistered) { System.Diagnostics.Debug.WriteLine($"[{clientIP}] 미등록 단말기이므로 소켓을 강제 종료합니다."); try { client.Close(); } catch { } return; } // ---------------- 등록된 단말기만 아래 로직을 수행 ---------------- lock (_agentLock) { if (_connectedAgents.ContainsKey(clientIP)) { try { _connectedAgents[clientIP].Close(); } catch { } _connectedAgents.Remove(clientIP); } _connectedAgents[clientIP] = client; } System.Diagnostics.Debug.WriteLine($"[{clientIP}] 정상 등록 단말기 딕셔너리 추가 완료."); string terminalName = await Task.Run(() => GetTerminalNameByIp(clientIP)); if (string.IsNullOrEmpty(terminalName)) terminalName = $"Terminal_{clientIP.Replace(".", "_")}"; string initialDbData = _dbFileManager.CreateAndSaveTerminalFile(terminalName); await SendDataToSingleAgentAsync(client, initialDbData); // 에이전트로 데이터 전송 while (client.Connected) { System.Diagnostics.Debug.WriteLine($"[{clientIP}] 수신된 메시지 받기 전"); string agentMsg = await reader.ReadLineAsync(); System.Diagnostics.Debug.WriteLine($"[{clientIP}] 수신된 메시지: {(agentMsg ?? "NULL/연결종료")}"); if (agentMsg == null) { System.Diagnostics.Debug.WriteLine($"[{clientIP}] 읽기 종료(null). 루프 탈출."); break; } // (예: MAC|메시지) if (agentMsg.Contains("display_complete")) { // 기다리지 않고, 즉시 해당 에이전트에게 데이터 재전송 System.Diagnostics.Debug.WriteLine($"[{clientIP}] 완료 신호 수신. 즉시 데이터 갱신을 진행합니다."); string newData = _dbFileManager.CreateAndSaveTerminalFile(terminalName); await SendDataToSingleAgentAsync(client, newData); // 현재 접속된 소켓(client)을 통해 즉시 데이터 전송 // 모니터링 폼이 열려있다면 즉시 데이터 전달 lock (_syncLock) { if (_openedMonitorForms.TryGetValue(clientIP, out var monitorForm) && !monitorForm.IsDisposed) { monitorForm.BeginInvoke(new Action(() => monitorForm.UpdateMonitorData(newData))); } } } // 에이전트가 보내는 프로토콜 정의 else if (agentMsg.Contains("data_update")) { // 그리드의 마지막 통신 시간을 현재 시간으로 업데이트 this.BeginInvoke(new Action(() => { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; string gridIp = row.Cells[2].Value?.ToString()?.Trim(); // IP로 매칭 bool isMatch = (!string.IsNullOrEmpty(clientIP) && gridIp == clientIP); if (isMatch) { row.Cells[5].Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); break; } } })); } } } } } catch (Exception ex) { Console.WriteLine($"[{clientIP}] 핸들러 내부 루프 예외: " + ex.Message); } finally { lock (_agentLock) { if (!string.IsNullOrEmpty(clientIP) && _connectedAgents.ContainsKey(clientIP)) { _connectedAgents.Remove(clientIP); } } try { client.Close(); } catch { } Task.Run(() => CheckAllAliveStatus()); } } private void ProcessAgentCompletion(string agentIp) { lock (_syncLock) { // 1. 완료 목록에 추가 if (!_completedAgents.Contains(agentIp)) { _completedAgents.Add(agentIp); Console.WriteLine($"[완료 신호 수신] 단말기 IP: {agentIp} ({_completedAgents.Count}대 완료)"); } // 현재 연결 상태인 에이전트들의 총 개수 파악 int totalExpected = 0; lock (_agentLock) { totalExpected = _connectedAgents.Count; } if (totalExpected == 0) return; // 2. 첫 번째 완료 신호가 들어왔을 때 60초 타이머 기동 if (!_isTimerRunning) { _isTimerRunning = true; _updateCts = new CancellationTokenSource(); var token = _updateCts.Token; Task.Run(async () => { Console.WriteLine("첫 번째 완료 신호 감지. 60초 대기 타이머를 시작합니다..."); try { // 60초 동안 대기 (모든 단말 완료 시 외부에서 Cancel() 요청하여 조기 종료됨) await Task.Delay(60000, token); // 타임아웃 발생: 60초 내에 다 못 받은 상황 Console.WriteLine("[타임아웃] 60초 제한 시간이 지났습니다. 낙오 단말기 처리를 시작합니다."); List stragglers = new List(); lock (_syncLock) { lock (_agentLock) { foreach (var ip in _connectedAgents.Keys) { if (!_completedAgents.Contains(ip)) { stragglers.Add(ip); } } } } // 낙오 단말기 연결 끊기 및 상태 변경 foreach (var ip in stragglers) { Console.WriteLine($"[낙오 단말 끊기] 60초 내 미완료 단말 제거: {ip}"); lock (_agentLock) { if (_connectedAgents.TryGetValue(ip, out TcpClient client)) { try { client.Close(); } catch { } _connectedAgents.Remove(ip); } } // UI 스레드에서 해당 IP의 그리드 상태를 Stop(Red)으로 업데이트 this.BeginInvoke(new Action(() => { UpdateTerminalGridStatusToStop(ip); })); } } catch (TaskCanceledException) { // 60초 이전에 모든 단말기가 정상 완료되어 취소된 경우 Console.WriteLine("모든 단말기가 60초 내에 연출을 완료하여 대기를 조기 종료합니다."); } finally { // 3. 타이머가 끝났거나 조기 종료되었으므로 공통 다음 파일 전송 및 초기화 Console.WriteLine("다음 데이터 파일 갱신 처리를 시작합니다."); SendDataToAllAgents(); lock (_syncLock) { _completedAgents.Clear(); _isTimerRunning = false; _updateCts?.Dispose(); _updateCts = null; } // 상태 갱신 Task.Run(() => CheckAllAliveStatus()); } }); } } } // 낙오된 단말기를 UI 상에서 강제로 Stop 처리하기 위한 헬퍼 메서드 private void UpdateTerminalGridStatusToStop(string ip) { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; if (row.Cells[2].Value?.ToString()?.Trim() == ip) { row.Cells[4].Value = "🔴 Stop"; row.Cells[4].Style.BackColor = Color.White; row.Cells[4].Style.ForeColor = Color.Black; break; } } UpdateRunningCount(); } private bool UpdateAgentStatusByMac(string clientMac, string clientIP) { // 에이전트가 보내온 MAC 주소 정제 string cleanClientMac = string.IsNullOrEmpty(clientMac) ? string.Empty : clientMac.Replace("-", "").Replace(":", "").Replace(" ", "").ToUpper().Trim(); bool isMapped = false; // grdTotList 순회하며 등록된 단말기인지 확인 foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; string gridIp = row.Cells[2].Value?.ToString()?.Trim() ?? string.Empty; string gridMac = row.Cells[1].Value?.ToString()?.Replace("-", "").Replace(":", "").Replace(" ", "").ToUpper().Trim() ?? string.Empty; // 1순위: IP 주소 매칭 또는 2순위: MAC 주소 일치 if (gridIp == clientIP || (!string.IsNullOrEmpty(gridMac) && gridMac == cleanClientMac)) { if (gridIp != clientIP) { row.Cells[2].Value = clientIP; // 최신 IP로 갱신 } if (gridMac != cleanClientMac && !string.IsNullOrEmpty(cleanClientMac)) { row.Cells[1].Value = cleanClientMac; // MAC 주소 보정 Console.WriteLine($"[그리드 보정] IP: {clientIP} 단말기의 MAC 주소를 {cleanClientMac}으로 갱신했습니다."); } // 상태를 Start로 변경 row.Cells[4].Value = "🟢 Start"; row.Cells[4].Style.BackColor = Color.White; row.Cells[4].Style.ForeColor = Color.Black; row.Cells[4].Style.Alignment = DataGridViewContentAlignment.MiddleCenter; row.Cells[5].Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); isMapped = true; break; } } // ★ 미등록 에이전트 자동 추가 로직 제거 및 로그 출력 if (!isMapped) { System.Diagnostics.Debug.WriteLine($"[연결 거부] 등록되지 않은 단말기 접속 시도 - IP: {clientIP}, MAC: {cleanClientMac}"); } // 켜져 있는 대수 카운트 갱신 UpdateRunningCount(); return isMapped; // 매핑 성공 여부 반환 } private void UpdateRunningCount() { int startCount = 0; foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; if (row.Cells[4].Value?.ToString().Contains("Start") == true) { startCount++; } } lblRunningCnt.Text = startCount.ToString() + "대"; } private void btnRegister_MouseClick(object sender, MouseEventArgs e) { using (Form2 form = new Form2()) { if (form.ShowDialog() == DialogResult.OK) { TerminalInfo data = form.TerminalData; _dbFileManager.CreateAndSaveTerminalFile(data.TerminalName); // 등록 시점에 해당 단말기 이름으로 파일 생성 grdTotList.Rows.Add( data.TerminalName, data.Mac, data.IP, "설정", "Stop", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), data.Port, "5", // 에이전트 화면 페이지 표출 시간 디폴트값 "", // ON 시간 (초기값 없음) "" // OFF 시간 (초기값 없음) ); int rowIndex = grdTotList.Rows.Count - 1; //grdTotList.Rows[rowIndex].Cells[4].Style.BackColor = Color.Red; //grdTotList.Rows[rowIndex].Cells[4].Style.ForeColor = Color.White; //grdTotList.Rows[rowIndex].Cells[4].Style.Alignment = DataGridViewContentAlignment.MiddleCenter; //grdTotList.Rows[rowIndex].Cells[3].Style.BackColor = Color.LightGray; //grdTotList.Rows[rowIndex].Cells[3].Style.ForeColor = Color.Black; //grdTotList.Rows[rowIndex].Cells[3].Style.Alignment = DataGridViewContentAlignment.MiddleCenter; lblTotCnt.Text = grdTotList.RowCount.ToString() + "대"; if (!_isRunning) { _ = Task.Run(() => StartServerAsync()); } Task.Run(() => CheckAllAliveStatus()); } } } private void btnDataUpdate_Click(object sender, EventArgs e) { // 데이터 갱신 작업 실행 Task.Run(() => { SendDataToAllAgents(); this.BeginInvoke(new Action(() => { CheckAllAliveStatus(); })); }); } private async void grdTotList_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex < 3) { if (e.RowIndex < 0) return; string terminalName = grdTotList.Rows[e.RowIndex].Cells[0].Value?.ToString(); string targetIP = grdTotList.Rows[e.RowIndex].Cells[2].Value?.ToString(); // 실제 에이전트 연결 상태 확인 bool isConnected = false; lock (_agentLock) { if (!string.IsNullOrEmpty(targetIP) && _connectedAgents.TryGetValue(targetIP, out TcpClient client)) { isConnected = (client != null && client.Connected); } } if (!isConnected) { MessageBox.Show("현재 에이전트와 연결되어 있지 않습니다.\n연결 후 다시 시도해 주세요.", "연결 확인", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; // 연결되지 않았으면 여기서 중단 } // 1. 이미 열려있으면 업데이트만 수행 if (_openedMonitorForms.TryGetValue(targetIP, out AgentMonitorForm existingForm) && !existingForm.IsDisposed) { string updatedXml = await Task.Run(() => _dbFileManager.CreateAndSaveTerminalFile(terminalName)); existingForm.UpdateMonitorData(updatedXml); existingForm.Activate(); return; } // 2. 폼 생성 AgentMonitorForm monitorForm = new AgentMonitorForm(); monitorForm.Text = $"[가상 모니터링] {terminalName} ({targetIP})"; // 폼이 로드될 때 소켓 통신을 하지 않도록 설정하는 플래그를 추가하는 것이 좋습니다. // monitorForm.DisableSocketCommunication = true; monitorForm.Show(); // 3. 데이터 로드 및 적용 string currentXml = await Task.Run(() => _dbFileManager.CreateAndSaveTerminalFile(terminalName)); monitorForm.UpdateMonitorData(currentXml); lock (_syncLock) { _openedMonitorForms[targetIP] = monitorForm; } } else { } } private void grdTotList_CellMouseClick(object sender, DataGridViewCellMouseEventArgs e) { grdTotList.ClearSelection(); if (e.RowIndex < 0) return; var row = grdTotList.Rows[e.RowIndex]; string terminalName = row.Cells[0].Value?.ToString() ?? ""; string terminalIp = row.Cells[2].Value?.ToString() ?? ""; if (e.ColumnIndex == 3) // 설정 버튼 클릭 { string filePath = _dbFileManager.GetTerminalFilePath(terminalName); //파일 확보 using (Form3 form = new Form3(this, terminalName, terminalIp, filePath)) { form.ShowDialog(); } } else if (e.ColumnIndex == 4) // Start/Stop 상태 제어 버튼 클릭 { var grdRow = grdTotList.Rows[e.RowIndex]; string ip = grdRow.Cells[2].Value?.ToString(); string mac = grdRow.Cells[1].Value?.ToString(); string status = grdRow.Cells[4].Value?.ToString() ?? "Stop"; ExecuteAgentControl(ip, mac, status); } } // 그리드 셀 클릭 로직을 공통 함수로 분리 private void ExecuteAgentControl(string ip, string mac, string currentStatus) { // 현재 상태에 따라 적절한 동작 수행 if (currentStatus.Contains("Start")) { // 끄기 (Stop) SendStopCommandToAgent(ip); UpdateGridStatus(ip, "Stop", Color.Red); } else { // 켜기 (Start) if (!string.IsNullOrEmpty(mac)) { SendWakeOnLan(mac); UpdateGridStatus(ip, "Start", Color.Green); } } UpdateRunningCount(); } // 그리드 상태 업데이트 헬퍼 private void UpdateGridStatus(string ip, string status, Color color) { this.Invoke(new Action(() => { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.Cells[2].Value?.ToString() == ip) { row.Cells[4].Value = status; row.Cells[4].Style.ForeColor = Color.Black; row.Cells[4].Style.BackColor = Color.White; row.Cells[4].Style.ForeColor = color; if (status == "Start") { row.Cells[4].Value = "🟢 Start"; // 특수기호 사용 } else { row.Cells[4].Value = "🔴 Stop"; } break; } } })); } // 프로글매 자동 on/off private void AutoControlTimer_Tick(object sender, EventArgs e) { // 현재 시간(초단위까지) DateTime now = DateTime.Now; string currentTimeStr = now.ToString("HH:mm:ss"); foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; string ip = row.Cells[2].Value?.ToString(); string mac = row.Cells[1].Value?.ToString(); string onTimeStr = row.Cells[8].Value?.ToString(); // 예: "09:00:00" string offTimeStr = row.Cells[9].Value?.ToString(); // 예: "18:00:00" string currentStatus = row.Cells[4].Value?.ToString(); if (string.IsNullOrEmpty(ip)) continue; if (currentTimeStr == onTimeStr && currentStatus.Contains("Stop")) { // 켜기 명령 실행 SendWakeOnLan(mac); UpdateGridStatus(ip, "Start", Color.Green); } else if (currentTimeStr == offTimeStr && currentStatus.Contains("Start")) { // 끄기 명령 실행 SendStopCommandToAgent(ip); UpdateGridStatus(ip, "Stop", Color.Red); } } } private void SendStopCommandToAgent(string ip) { try { lock (_agentLock) { if (_connectedAgents.TryGetValue(ip, out TcpClient client) && client.Connected) { byte[] msg = Encoding.UTF8.GetBytes("STOP\n"); client.GetStream().Write(msg, 0, msg.Length); client.GetStream().Flush(); System.Diagnostics.Debug.WriteLine($"[서버] {ip}에게 STOP 명령 전송 완료!"); } else { MessageBox.Show("해당 에이전트는 현재 서버에 소켓으로 연결된 상태가 아닙니다."); } } } catch (Exception ex) { Console.WriteLine("종료 명령 전송 실패: " + ex.Message); } } private void SendWakeOnLan(string macAddress) { try { string cleanMac = macAddress.Replace("-", "").Replace(":", "").Replace(" ", "").Trim(); if (cleanMac.Length != 12) return; byte[] bytes = new byte[102]; for (int i = 0; i < 6; i++) bytes[i] = 0xFF; byte[] macBytes = new byte[6]; for (int i = 0; i < 6; i++) { macBytes[i] = Convert.ToByte(cleanMac.Substring(i * 2, 2), 16); } for (int i = 1; i <= 16; i++) { Array.Copy(macBytes, 0, bytes, i * 6, 6); } using (UdpClient client = new UdpClient()) { client.Connect(IPAddress.Parse("255.255.255.255"), 9); client.Send(bytes, bytes.Length); } Console.WriteLine($"[WOL 전송 완료] MAC: {cleanMac}"); } catch (Exception ex) { Console.WriteLine("WOL 패킷 송신 예외 오류: " + ex.Message); } } private void SendDataToAllAgents() { lock (_agentLock) { foreach (var kvp in _connectedAgents) { string agentIP = kvp.Key; TcpClient client = kvp.Value; if (client != null && client.Connected) { try { string terminalName = GetTerminalNameByIp(agentIP); string xmlData = _dbFileManager.CreateAndSaveTerminalFile(terminalName); //NetworkStream 대신 StreamWriter를 사용하여 명확하게 데이터 전송 using (NetworkStream stream = client.GetStream()) using (StreamWriter writer = new StreamWriter(stream, Encoding.UTF8) { AutoFlush = true }) { writer.WriteLine(xmlData); // Line 단위 전송 } Console.WriteLine($"[{terminalName}] 데이터 전송 성공."); } catch (Exception ex) { Console.WriteLine($"[{agentIP}] 데이터 전송 실패: {ex.Message}"); } } } } } // 단일 전송 비동기화 지원을 통한 네트워크 병목 방지 private async Task SendDataToSingleAgentAsync(TcpClient client, string message) { try { if (client != null && client.Connected) { byte[] data = Encoding.UTF8.GetBytes(message + "\n"); NetworkStream stream = client.GetStream(); await stream.WriteAsync(data, 0, data.Length); await stream.FlushAsync(); System.Diagnostics.Debug.WriteLine("[데이터 전송 완료] 성공"); } } catch (Exception ex) { Console.WriteLine($"단일 전송 실패: {ex.Message}"); } } private void UpdateVirtualMonitorFrame(string ip, string xmlData) { lock (_syncLock) { if (_openedMonitorForms.TryGetValue(ip, out var form) && form != null && !form.IsDisposed) { // 폼이 호출 가능(Load 완료)한 상태인지 확인 if (form.InvokeRequired) { form.BeginInvoke(new Action(() => { try { form.UpdateMonitorData(xmlData); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine("모니터 업데이트 실패: " + ex.Message); } })); } else { form.UpdateMonitorData(xmlData); } } else { System.Diagnostics.Debug.WriteLine($"[알림] {ip}에 해당하는 모니터링 폼이 아직 활성화되지 않았습니다."); } } } private string GetTerminalNameByIp(string ip) { string name = string.Empty; if (this.InvokeRequired) { this.Invoke(new Action(() => name = FindNameInGrid(ip))); } else { name = FindNameInGrid(ip); } return string.IsNullOrEmpty(name) ? $"Terminal_{ip.Replace(".", "_")}" : name; } private string FindNameInGrid(string ip) { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.Cells[2].Value?.ToString() == ip) { return row.Cells[0].Value?.ToString(); } } return string.Empty; } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { _isRunning = false; _statusTimer?.Stop(); try { _listener?.Stop(); } catch { } lock (_agentLock) { foreach (var client in _connectedAgents.Values) { try { client.Close(); } catch { } } _connectedAgents.Clear(); } } private void pannGray_Paint(object sender, PaintEventArgs e) { } private void pannBlue_Paint(object sender, PaintEventArgs e) { } public void SendDataToSingleAgent(string ip, string message) { // _connectedAgents 딕셔너리에서 해당 IP를 찾아 전송하는 로직 lock (_agentLock) // 안전한 접근을 위해 lock 사용 { if (_connectedAgents.ContainsKey(ip)) { TcpClient client = _connectedAgents[ip]; if (client != null && client.Connected) { try { byte[] data = Encoding.UTF8.GetBytes(message + "\n"); // 프로토콜에 맞게 줄바꿈 추가 NetworkStream stream = client.GetStream(); stream.Write(data, 0, data.Length); stream.Flush(); System.Diagnostics.Debug.WriteLine($"[성공] {ip}로 데이터 전송 완료."); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"[실패] {ip} 데이터 전송 중 오류: {ex.Message}"); } } } else { System.Diagnostics.Debug.WriteLine($"[알림] {ip}에 연결된 에이전트가 없습니다."); } } } // 그리드에서 데이터 읽어오기 (표출시간, ON시간, OFF시간) public string[] GetProgramTimesByIp(string ip) { foreach (DataGridViewRow row in grdTotList.Rows) { // IP가 일치하는 행을 찾음 if (row.Cells[2].Value?.ToString() == ip) { // 배열 생성 시 한 줄에 쉼표로 구분하여 작성 return new string[] { row.Cells[7].Value?.ToString(), row.Cells[8].Value?.ToString(), row.Cells[9].Value?.ToString() }; } } // 기본값 반환 return new string[] { "5", "", "" }; } // 그리드에 데이터 저장하기 public void SaveProgramTimesToGrid(string ip, string display, string on, string off) { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.Cells[2].Value?.ToString() == ip) { row.Cells[7].Value = display; row.Cells[8].Value = on; row.Cells[9].Value = off; break; } } } private void grdTotList_CellContentClick(object sender, DataGridViewCellEventArgs e) { } private void imgSearch_MouseClick(object sender, MouseEventArgs e) { // 1. 검색어 가져오기 (앞뒤 공백 제거) string search = txtSearch.Text.Trim(); // 2. 검색어가 빈 값이면 전체 리스트 노출 if (string.IsNullOrEmpty(search)) { foreach (DataGridViewRow row in grdTotList.Rows) { if (!row.IsNewRow) row.Visible = true; } return; // 전체 노출 후 로직 종료 } // 3. 검색어가 입력된 경우 필터링 진행 else { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; // 그리드 맨 밑의 빈 행(새 행)은 건너뜀 string terminalName = row.Cells[0].Value?.ToString() ?? ""; string ipAddress = row.Cells[2].Value?.ToString() ?? ""; // 단말기명 또는 IP에 검색어가 포함되어 있는지 확인 if (terminalName.ToLower().Contains(search) || ipAddress.Contains(search)) { row.Visible = true; // 조건에 맞으면 보여줌 } else { row.Visible = false; // 조건에 안 맞으면 숨김 } } } } private void txtSearch_TextChanged(object sender, EventArgs e) { // 1. 검색어 가져오기 (앞뒤 공백 제거) string search = txtSearch.Text.Trim(); // 2. 검색어가 빈 값이면 전체 리스트 노출 if (string.IsNullOrEmpty(search)) { foreach (DataGridViewRow row in grdTotList.Rows) { if (!row.IsNewRow) row.Visible = true; } return; // 전체 노출 후 로직 종료 } // 3. 검색어가 입력된 경우 필터링 진행 else { foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; // 그리드 맨 밑의 빈 행(새 행)은 건너뜀 string terminalName = row.Cells[0].Value?.ToString() ?? ""; string ipAddress = row.Cells[2].Value?.ToString() ?? ""; // 단말기명 또는 IP에 검색어가 포함되어 있는지 확인 if (terminalName.ToLower().Contains(search) || ipAddress.Contains(search)) { row.Visible = true; // 조건에 맞으면 보여줌 } else { row.Visible = false; // 조건에 안 맞으면 숨김 } } } } private void Form1_Load_1(object sender, EventArgs e) { } // 텍스트 박스 크기가 결정되거나 변경될 때 둥근 리전을 생성하여 적용 private void TextBox_SizeChanged(object sender, EventArgs e) { System.Windows.Forms.TextBox txt = (System.Windows.Forms.TextBox)sender; System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath(); // 둥근 모서리의 반지름 설정 (값이 클수록 더 둥글어집니다) int radius = 14; if (txt.Width > radius && txt.Height > radius) { path.AddArc(0, 0, radius, radius, 180, 90); path.AddArc(txt.Width - radius, 0, radius, radius, 270, 90); path.AddArc(txt.Width - radius, txt.Height - radius, radius, radius, 0, 90); path.AddArc(0, txt.Height - radius, radius, radius, 90, 90); path.CloseFigure(); txt.Region = new Region(path); } } private void grdTotList_CellFormatting(object sender, DataGridViewCellFormattingEventArgs e) { if (e.ColumnIndex == 4 && e.Value != null) { string status = e.Value.ToString(); if (status.Contains("Stop")) { e.CellStyle.ForeColor = Color.Red; e.CellStyle.SelectionForeColor = Color.Red; // 선택 시에도 빨간 글씨 유지 e.CellStyle.BackColor = Color.White; e.CellStyle.SelectionBackColor = Color.FromArgb(240, 245, 249); // 표 시안 배경색 유지 } else { e.CellStyle.ForeColor = Color.Green; e.CellStyle.SelectionForeColor = Color.Green; // 선택 시에도 초록 글씨 유지 e.CellStyle.BackColor = Color.White; e.CellStyle.SelectionBackColor = Color.FromArgb(240, 245, 249); } } } } }