Files
suhyup_agent_program/ControlServer/Form1.cs
2026-07-03 16:43:53 +09:00

1288 lines
55 KiB
C#

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<TerminalInfo> terminalList = new List<TerminalInfo>();
// 동시성 제어를 위해 해시맵(Dictionary) 접근 시 락(Lock) 객체 분리 또는 보완
private readonly Dictionary<string, TcpClient> _connectedAgents = new Dictionary<string, TcpClient>();
private readonly Dictionary<string, AgentMonitorForm> _openedMonitorForms = new Dictionary<string, AgentMonitorForm>();
private readonly HashSet<string> _completedAgents = new HashSet<string>();
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<string> stragglers = new List<string>();
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);
}
}
}
}
}