using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Data.SqlTypes; using System.Drawing; using System.Drawing.Drawing2D; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml.Linq; using static System.Windows.Forms.LinkLabel; using static System.Windows.Forms.VisualStyles.VisualStyleElement.TaskbarClock; namespace ControlServer { public partial class AgentMonitorForm : Form { // 색상 정의 (Hex 코드 대신 RGB 사용) private readonly Color HEADER_BG_COLOR = Color.FromArgb(36, 85, 178); private readonly Color HEADER_TEXT_COLOR = Color.White; private readonly Color ROW_EVEN_COLOR = Color.FromArgb(245, 248, 255); // 짝수 행 (연한 파란색 계열) private readonly Color ROW_ODD_COLOR = Color.White; // 홀수 행 (흰색) private readonly Color TEXT_BLUE_COLOR = Color.FromArgb(24, 60, 150); // 파란색 텍스트 private DataTable _dtSourceAll; // 전체 데이터를 담을 DataTable private DataTable _dtPaged; // 현재 페이지에 해당하는 데이터만 담을 DataTable private int _currentPage = 1; private const int PAGE_SIZE = 5; // 한 페이지당 5행씩 private int _dynamicFontSize = 12; // 동적으로 바뀔 폰트 크기 (기본값) private System.Windows.Forms.Timer _pageTurnTimer; private System.Windows.Forms.Timer _clockTimer; private bool _isAgentRunning = true; // 에이전트 루프 제어 플래그 private NetworkStream _currentStream = null; private readonly object _streamLock = new object(); private Button _btnMinimize; private Button _btnClose; public AgentMonitorForm() { InitializeComponent(); // 1. 폼 최대화 및 테두리 제거 (현황판/키오스크 스타일) this.WindowState = FormWindowState.Maximized; this.FormBorderStyle = FormBorderStyle.None; // 고정 도킹 해제하여 동적 배치 적용 if (panel1 != null) { panel1.Dock = DockStyle.None; panel1.Height = 140; // 전체 화면 비율에 맞춰 타이틀 패널 높이 조절 } // 라벨들의 개별 Dock 설정을 해제하여 디자이너에서 배치한 원래 위치(좌측/우측)를 유지하도록 합니다. if (lblTime != null) lblTime.Dock = DockStyle.None; if (lblDate != null) lblDate.Dock = DockStyle.None; if (lblTitle != null) lblTitle.Dock = DockStyle.None; if (lblDataUpdateTime != null) { lblDataUpdateTime.Dock = DockStyle.None; lblDataUpdateTime.AutoSize = true; } // 3.그리드가 기준시간 밑으로 오도록 도킹 해제 및 여백 초기화 grdDataList.Dock = DockStyle.None; grdDataList.Padding = new Padding(0); // 기존 오작동하던 패딩 제거 // 4. 화면 해상도가 바뀌거나 최대화될 때 컨트롤 위치를 전체적으로 재계산 this.SizeChanged += (s, e) => { int sideMargin = 25; // 좌우 고정 여백 int topMargin = 20; // 타이틀 패널 위쪽 살짝 여백 추가 int areaWidth = this.ClientSize.Width - (sideMargin * 2); // A. 타이틀 패널 위치 및 크기 조절 (그리드와 넓이 동기화 및 상단 여백) if (panel1 != null) { panel1.SetBounds(sideMargin, topMargin, areaWidth, panel1.Height); // 타이틀 내부 컨트롤 정렬 강제 호출 panel1_SizeChanged(null, null); } // B. 기준 시간 라벨 배치 (타이틀 패널 아래로 15픽셀 여백) if (lblDataUpdateTime != null && panel1 != null) { lblDataUpdateTime.Location = new Point(sideMargin, panel1.Bottom + 15); lblDataUpdateTime.BringToFront(); } // C. 그리드 배치 (기준시간 라벨 아랫변 + 15픽셀 여백) if (lblDataUpdateTime != null) { int gridTop = lblDataUpdateTime.Bottom + 15; int gridHeight = this.ClientSize.Height - gridTop - 60; if (areaWidth > 0 && gridHeight > 0) { grdDataList.SetBounds(sideMargin, gridTop, areaWidth, gridHeight); // 가용 높이에 맞게 행 높이 동적 계산 (한 번에 5개 고정) int headerHeight = grdDataList.ColumnHeadersHeight; int availableHeight = gridHeight - headerHeight; if (availableHeight > 0) { // 정밀도를 위해 가용한 높이를 PAGE_SIZE(5)로 나눈 몫을 계산 int dynamicRowHeight = availableHeight / PAGE_SIZE; // 앞으로 추가될 행의 기본 높이 설정 grdDataList.RowTemplate.Height = dynamicRowHeight; // 현재 이미 그리드에 바인딩되어 있는 행들의 높이도 일괄 변경 // 폰트 크기가 최소 12pt ~ 최대 24pt 사이에 머물도록 제한 _dynamicFontSize = Math.Clamp((int)(dynamicRowHeight * 0.16), 12, 24); foreach (DataGridViewRow row in grdDataList.Rows) { row.Height = dynamicRowHeight; } } //그리드 모서리 둥글게 깎기 UpdateGridRegion(); } } // 하단 페이지 점(● • •) 위치 정중앙 하단으로 재배치 if (lblPageIndicator != null) { lblPageIndicator.Dock = DockStyle.None; lblPageIndicator.AutoSize = true; lblPageIndicator.Location = new Point( (this.ClientSize.Width - lblPageIndicator.Width) / 2, this.ClientSize.Height - 40 ); lblPageIndicator.BringToFront(); } }; // DataGridView 내부 컬럼들도 우측 빈 공간(회색 여백) 없이 꽉 차게 채움 grdDataList.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill; grdDataList.ColumnHeadersVisible = true; // DataGridView의 기본 스타일 및 그리드선 숨기기 설정 grdDataList.CellPainting -= grdDataList_CellPainting; grdDataList.CellPainting += grdDataList_CellPainting; grdDataList.AutoGenerateColumns = false; // 자동 컬럼 생성 방지 grdDataList.RowHeadersVisible = false; // 행 헤더 숨기기 grdDataList.AllowUserToAddRows = false; // 빈 행 추가 방지 grdDataList.AllowUserToResizeColumns = false; // 컬럼 너비 조정 불가 grdDataList.AllowUserToResizeRows = false; // 행 높이 조정 불가 grdDataList.ReadOnly = true; // 읽기 전용 grdDataList.AllowUserToDeleteRows = false; // 행 삭제 방지 grdDataList.ColumnHeadersHeight = 81; // 헤더 높이를 81로 설정 (2줄 헤더 공간 확보) grdDataList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; grdDataList.EnableHeadersVisualStyles = false; // 커스텀 헤더 스타일 적용을 위해 필수 grdDataList.CellBorderStyle = DataGridViewCellBorderStyle.None; // 셀 테두리(그리드선) 제거 grdDataList.RowTemplate.Height = 100; // 데이터 행 높이를 100으로 설정 grdDataList.BorderStyle = BorderStyle.None; // 컨트롤 외곽선 제거 // 사용자가 컬럼을 클릭해도 정렬되지 않도록 설정 (안정적인 화면 유지) grdDataList.ColumnAdded += (s, e) => { e.Column.SortMode = DataGridViewColumnSortMode.NotSortable; }; // 시계 타이머 설정 (1초마다 업데이트) _clockTimer = new System.Windows.Forms.Timer(); _clockTimer.Interval = 1000; _clockTimer.Tick += ClockTimer_Tick; // 페이지 전환 타이머 설정 (5초마다 업데이트) _pageTurnTimer = new System.Windows.Forms.Timer(); _pageTurnTimer.Interval = 5000; _pageTurnTimer.Tick += PageTurnTimer_Tick; this.Load += Form1_Load; } // 데이터그리드뷰 영역을 둥글게 깎아주는 헬퍼 메서드 private void UpdateGridRegion() { if (grdDataList.Width <= 0 || grdDataList.Height <= 0) return; int radius = 15; using (GraphicsPath path = new GraphicsPath()) { path.AddArc(0, 0, radius, radius, 180, 90); path.AddArc(grdDataList.Width - radius, 0, radius, radius, 270, 90); path.AddArc(grdDataList.Width - radius, grdDataList.Height - radius, radius, radius, 0, 90); path.AddArc(0, grdDataList.Height - radius, radius, radius, 90, 90); path.CloseAllFigures(); grdDataList.Region = new Region(path); } } private void Form1_Load(object sender, EventArgs e) { // 컬럼 프로퍼티 매핑 초기화 선행 InitGridColumnProperties(); // 한 번 더 명시적으로 정렬 비활성화 foreach (DataGridViewColumn col in grdDataList.Columns) { col.SortMode = DataGridViewColumnSortMode.NotSortable; } // 시계 시작 _clockTimer.Start(); UpdateCurrentTime(); // [수정] XML 테스트 파일 생성 및 직접 바인딩 로직을 제거했습니다. // 연결 전까지는 그리드가 빈 상태(리스트 없음)로 대기합니다. grdDataList.DataSource = null; // 페이지 타이머 시작 _pageTurnTimer.Start(); // 백그라운드 소켓 통신 시작 (비동기 Task 활용으로 UI 블로킹 방지) Task.Run(() => StartAgentTask()); } // 그리드 내부 컬럼에 프로퍼티 명을 미리 지정해 주는 헬퍼 메서드 private void InitGridColumnProperties() { foreach (DataGridViewColumn col in grdDataList.Columns) { if (col.HeaderText.Contains("경매일자")) col.DataPropertyName = "aucDate"; else if (col.HeaderText.Contains("품목명")) col.DataPropertyName = "itemName"; else if (col.HeaderText.Contains("산지")) col.DataPropertyName = "origin"; else if (col.HeaderText.Contains("전체수량") || (col.HeaderText.Contains("전체") && col.Index == 3)) col.DataPropertyName = "totalQty"; else if (col.HeaderText.Contains("정가수의수량") || col.HeaderText.Contains("정가수의 수량") || col.Index == 4) col.DataPropertyName = "fixPriceQty"; else if (col.HeaderText.Contains("전체물량") || (col.HeaderText.Contains("전체") && col.Index == 5)) col.DataPropertyName = "totalAmt"; else if (col.HeaderText.Contains("정가수의물량") || col.HeaderText.Contains("정가수의 물량") || col.Index == 6) col.DataPropertyName = "fixPriceAmt"; if (col.Index >= 3 && col.Index <= 6) { col.ValueType = typeof(int); col.DefaultCellStyle.Format = "N0"; } } } // 서버와의 소켓 통신을 관리하는 백그라운드 메서드 private void StartAgentTask() { if (DisableSocketCommunication) return; // 외부 모드면 통신 루프 진입 차단 while (_isAgentRunning) { try { using (TcpClient client = new TcpClient("192.168.219.48", 9999)) using (NetworkStream stream = client.GetStream()) using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8)) { // [인증] 서버에 MAC 전송 byte[] macBytes = Encoding.UTF8.GetBytes(GetMacAddress() + "\n"); stream.Write(macBytes, 0, macBytes.Length); string buffer = ""; // 데이터를 쌓아둘 버퍼 _currentStream = stream; // 전송용 스트림 할당 while (client.Connected && _isAgentRunning) { string line = reader.ReadLine(); if (string.IsNullOrEmpty(line)) { break; } if (line.StartsWith("SET_TIME|")) { string[] parts = line.Split('|'); if (parts.Length > 1 && int.TryParse(parts[1], out int newInterval)) { this.BeginInvoke(new Action(() => { _pageTurnTimer.Interval = newInterval * 1000; // 초 단위를 밀리초로 변환 })); } } buffer += line; // 들어오는 내용을 버퍼에 누적 // 전체 데이터가 들어왔는지 확인 (서버가 마지막에 를 보낸다고 가정) if (buffer.Contains("") && buffer.Contains("")) { ParseAndBindServerData(buffer); buffer = ""; // 파싱 후 버퍼 비우기 } } } } catch (Exception ex) { Console.WriteLine("통신 끊김, 5초 후 재시도: " + ex.Message); Thread.Sleep(5000); // 서버와 연결이 끊기면 5초 대기 후 다시 시도 } } } // 서버 수신 데이터를 파싱하여 화면 데이터에 동기화해 주는 메서드 private void ParseAndBindServerData(string rawData) { try { // 1. XML 문자열 파싱 XDocument doc = XDocument.Parse(rawData); DataTable dt = new DataTable(); dt.Columns.Add("aucDate", typeof(string)); dt.Columns.Add("itemName", typeof(string)); dt.Columns.Add("origin", typeof(string)); dt.Columns.Add("totalQty", typeof(int)); dt.Columns.Add("fixPriceQty", typeof(int)); dt.Columns.Add("totalAmt", typeof(int)); dt.Columns.Add("fixPriceAmt", typeof(int)); // 2. 태그를 찾아서 행 추가 foreach (var row in doc.Descendants("AuctionTable")) { dt.Rows.Add( row.Element("aucDate")?.Value, row.Element("itemName")?.Value, row.Element("origin")?.Value, int.TryParse(row.Element("totalQty")?.Value.Replace(",", ""), out int tq) ? tq : 0, int.TryParse(row.Element("fixPriceQty")?.Value.Replace(",", ""), out int fq) ? fq : 0, int.TryParse(row.Element("totalAmt")?.Value.Replace(",", ""), out int ta) ? ta : 0, int.TryParse(row.Element("fixPriceAmt")?.Value.Replace(",", ""), out int fa) ? fa : 0 ); } // 3. UI 바인딩 this.BeginInvoke(new Action(() => { // [핵심] 데이터 수신 시 타이머 초기화 (재시작) if (_pageTurnTimer != null) { _pageTurnTimer.Stop(); // 현재 카운트 중지 } _dtSourceAll = dt; _dtPaged = _dtSourceAll.Clone(); _currentPage = 1; if (_dtSourceAll.Rows.Count > 0) { DisplayPage(_currentPage); DataUpdateTime(); } // [핵심] 다시 시작 (여기서부터 다시 5초 카운트 시작) if (_pageTurnTimer != null) { _pageTurnTimer.Start(); } })); } catch (Exception ex) { // 파싱 에러 상세 확인을 위해 에러 메시지 출력 Console.WriteLine("XML 파싱 에러: " + ex.Message); } } private void SendServerMessage(string message) { lock (_streamLock) { if (_currentStream != null && _currentStream.CanWrite) { try { byte[] buffer = Encoding.UTF8.GetBytes(message); _currentStream.Write(buffer, 0, buffer.Length); _currentStream.Flush(); Console.WriteLine("서버로 상태 전송 완료: " + message.Trim()); } catch (Exception ex) { Console.WriteLine("서버 상태 전송 오류: " + ex.Message); } } } } public string GetMacAddress() { foreach (NetworkInterface ni in NetworkInterface.GetAllNetworkInterfaces()) { if (ni.NetworkInterfaceType != NetworkInterfaceType.Loopback && ni.NetworkInterfaceType != NetworkInterfaceType.Tunnel && ni.OperationalStatus == OperationalStatus.Up) { string mac = ni.GetPhysicalAddress().ToString(); if (!string.IsNullOrEmpty(mac)) return mac; } } return "MAC Not Found"; } private void grdDataList_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { if (e.ColumnIndex < 0) return; // 현재 열이 수량/물량 그룹인지 확인 bool isDataGroup = (e.ColumnIndex >= 3 && e.ColumnIndex <= 6); if (e.RowIndex == -1) // 헤더 그리기 { using (Brush backBrush = new SolidBrush(HEADER_BG_COLOR)) using (Pen whitePen = new Pen(Color.White, 1)) using (Pen groupLinePen = new Pen(Color.White, 2)) using (Font boldFont = new Font(this.Font.FontFamily, 14, FontStyle.Bold)) // 폰트 크기 조정 { e.Graphics.FillRectangle(backBrush, e.CellBounds); if (isDataGroup) { // 1. 수탁수량/수탁물량 (상단) Rectangle topRect = new Rectangle(e.CellBounds.Left, e.CellBounds.Top, e.CellBounds.Width, e.CellBounds.Height / 2); if (e.ColumnIndex == 3) TextRenderer.DrawText(e.Graphics, "수 탁", boldFont, topRect, HEADER_TEXT_COLOR, TextFormatFlags.Right | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak); else if (e.ColumnIndex == 4) TextRenderer.DrawText(e.Graphics, "수 량", boldFont, topRect, HEADER_TEXT_COLOR, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak); else if (e.ColumnIndex == 5) TextRenderer.DrawText(e.Graphics, "수 탁", boldFont, topRect, HEADER_TEXT_COLOR, TextFormatFlags.Right | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak); else if (e.ColumnIndex == 6) TextRenderer.DrawText(e.Graphics, "물 량", boldFont, topRect, HEADER_TEXT_COLOR, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.WordBreak); //TextFormatFlags topAlign = (e.ColumnIndex == 3 || e.ColumnIndex == 5) ? TextFormatFlags.Right : TextFormatFlags.Left; // 수탁은 오른쪽, 수량/물량은 왼쪽 정렬 // 2. 중간 구분선 int midY = e.CellBounds.Top + (e.CellBounds.Height / 2); e.Graphics.DrawLine(whitePen, e.CellBounds.Left, midY, e.CellBounds.Right, midY); // 3. 하단 텍스트 (전체 / 정가수의) Rectangle subRect = new Rectangle(e.CellBounds.Left, midY, e.CellBounds.Width, e.CellBounds.Height / 2); string subText = grdDataList.Columns[e.ColumnIndex].HeaderText; TextRenderer.DrawText(e.Graphics, subText, boldFont, subRect, HEADER_TEXT_COLOR, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } else { // 일반 컬럼 (경매일자 등) - 세로 중앙 정렬을 위해 VerticalCenter 강조 TextRenderer.DrawText(e.Graphics, grdDataList.Columns[e.ColumnIndex].HeaderText, boldFont, e.CellBounds, HEADER_TEXT_COLOR, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); } e.Graphics.DrawRectangle(whitePen, e.CellBounds); e.Handled = true; } } else if (e.RowIndex >= 0) { // 데이터 행 그리기 로직 (기존 유지) Color rowBgColor = (e.RowIndex % 2 == 0) ? ROW_ODD_COLOR : ROW_EVEN_COLOR; if ((e.State & DataGridViewElementStates.Selected) == DataGridViewElementStates.Selected) rowBgColor = Color.FromArgb(215, 230, 255); using (Brush bgBrush = new SolidBrush(rowBgColor)) using (Pen bottomLinePen = new Pen(Color.FromArgb(235, 238, 245))) { e.Graphics.FillRectangle(bgBrush, e.CellBounds); string cellText = e.FormattedValue?.ToString() ?? ""; // 데이터 영역 세로 구분선 (헤더와 동일하게 그룹 구분) if (e.ColumnIndex == 3 || e.ColumnIndex == 5) e.Graphics.DrawLine(new Pen(Color.FromArgb(235, 238, 245), 2), e.CellBounds.Right - 1, e.CellBounds.Top, e.CellBounds.Right - 1, e.CellBounds.Bottom); TextRenderer.DrawText(e.Graphics, cellText, new Font(this.Font.FontFamily, _dynamicFontSize), e.CellBounds, Color.Black, TextFormatFlags.HorizontalCenter | TextFormatFlags.VerticalCenter); e.Graphics.DrawLine(bottomLinePen, e.CellBounds.Left, e.CellBounds.Bottom - 1, e.CellBounds.Right, e.CellBounds.Bottom - 1); e.Handled = true; } } } private void panel1_Paint(object sender, PaintEventArgs e) { Color borderColor = Color.FromArgb(36, 85, 178); int borderRadius = 15; int borderWidth = 2; // System.Drawing.Drawing2D 로 올바르게 수정되었습니다. e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; using (System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath()) using (Pen pen = new Pen(borderColor, borderWidth)) { path.AddArc(0, 0, borderRadius, borderRadius, 180, 90); path.AddArc(panel1.Width - borderRadius - 1, 0, borderRadius, borderRadius, 270, 90); path.AddArc(panel1.Width - borderRadius - 1, panel1.Height - borderRadius - 1, borderRadius, borderRadius, 0, 90); path.AddArc(0, panel1.Height - borderRadius - 1, borderRadius, borderRadius, 90, 90); path.CloseAllFigures(); panel1.Region = new Region(path); e.Graphics.DrawPath(pen, path); } } // 타이틀 패널 내부 컨트롤들의 정확한 위치 및 잘림 방지 private void panel1_SizeChanged(object sender, EventArgs e) { if (panel1 == null) return; int sideOffset = 25; // 우측/좌측 끝단 마진 // 1. 왼쪽 로고 텍스트 잘림 방지 및 세로 중앙 정렬 if (lblLogo != null) { lblLogo.AutoSize = false; // 자동 크기 해제 후 직접 넉넉하게 지정 lblLogo.Width = 200; // '수협중앙회'가 절대 잘리지 않을 크기 lblLogo.Height = panel1.Height; // 패널 높이만큼 꽉 채움 lblLogo.TextAlign = ContentAlignment.MiddleLeft; // 텍스트를 왼쪽 중앙에 정렬 lblLogo.Left = 100; // 패널 왼쪽에서 25픽셀 여백 lblLogo.Top = 0; // TextAlign이 중앙을 잡아주므로 Top은 0 } // 2. 타이틀 메인 텍스트 중앙 배치 if (lblTitle != null) { lblTitle.AutoSize = true; lblTitle.Left = (panel1.Width - lblTitle.Width) / 2; lblTitle.Top = (panel1.Height - lblTitle.Height) / 2; } // 3. 날짜 및 시간 영역 바짝 오른쪽 정렬 및 상하 밀착 배치 int totalClockHeight = (lblDate != null ? lblDate.Height : 0) + (lblTime != null ? lblTime.Height : 0) - 5; int clockTopStart = (panel1.Height - totalClockHeight) / 2; if (lblDate != null) { //lblDate.AutoSize = true; lblDate.Left = panel1.Width - lblDate.Width - sideOffset; lblDate.Top = clockTopStart; } if (lblTime != null) { //lblTime.AutoSize = true; lblTime.Left = panel1.Width - lblTime.Width - sideOffset; lblTime.Top = (lblDate != null) ? lblDate.Bottom - 2 : clockTopStart + 25; } } private void ClockTimer_Tick(object sender, EventArgs e) { UpdateCurrentTime(); } private void UpdateCurrentTime() { string dateText = DateTime.Now.ToString("yyyy.MM.dd (ddd)"); string timeText = DateTime.Now.ToString("HH:mm"); if (lblDate != null) lblDate.Text = dateText; if (lblTime != null) lblTime.Text = timeText; // 텍스트 변동에 따른 우측 정렬 유지 보장 panel1_SizeChanged(null, null); } private void DataUpdateTime() { string timeText = DateTime.Now.ToString("HH:mm:ss"); if (lblDataUpdateTime != null) lblDataUpdateTime.Text = timeText + "기준"; } private async Task DisplayPage(int page) { if (_dtSourceAll == null || _dtSourceAll.Rows.Count == 0 || _dtPaged == null) return; _dtPaged.Rows.Clear(); int startIndex = (page - 1) * PAGE_SIZE; int endIndex = Math.Min(startIndex + PAGE_SIZE, _dtSourceAll.Rows.Count); for (int i = startIndex; i < endIndex; i++) { _dtPaged.ImportRow(_dtSourceAll.Rows[i]); } //grdDataList.DataSource = null; grdDataList.DataSource = _dtPaged; // 강제 새로고침 grdDataList.Refresh(); grdDataList.Invalidate(); // 데이터가 새로 바인딩된 후 템플릿 높이를 행들에 강제 적용 foreach (DataGridViewRow row in grdDataList.Rows) { row.Height = grdDataList.RowTemplate.Height; } int totalPages = (int)Math.Ceiling((double)_dtSourceAll.Rows.Count / PAGE_SIZE); StringBuilder sb = new StringBuilder(); for (int i = 1; i <= totalPages; i++) { if (i == page) sb.Append("● "); else sb.Append("• "); } if (lblPageIndicator != null) { lblPageIndicator.Text = sb.ToString().TrimEnd(); lblPageIndicator.ForeColor = Color.FromArgb(36, 85, 178); } grdDataList.Invalidate(); //if (page == totalPages) //{ // string myMac = GetMacAddress(); // string responseMessage = $"{myMac}|display_complete\n"; // SendServerMessage(responseMessage); //} } private void PageTurnTimer_Tick(object sender, EventArgs e) { if (_dtSourceAll == null || _dtSourceAll.Rows.Count == 0) return; int totalPages = (int)Math.Ceiling((double)_dtSourceAll.Rows.Count / PAGE_SIZE); // 현재 마지막 페이지를 보여주고 있었다면 메시지 전송 if (_currentPage == totalPages) { string myMac = GetMacAddress(); string responseMessage = $"{myMac}|display_complete\n"; // 별도 스레드로 보내 UI 블로킹 방지 Task.Run(() => SendServerMessage(responseMessage)); } _currentPage++; if (_currentPage > totalPages) { _currentPage = 1; } DisplayPage(_currentPage); } private void lblTitle_Click(object sender, EventArgs e) { } private void lblTitle_Click_1(object sender, EventArgs e) { } public void UpdateMonitorData(string xmlData) { if (this.InvokeRequired) { this.Invoke(new Action(() => UpdateMonitorData(xmlData))); return; } // 외부 주입 모드일 때는 기존 소켓 통신 로직을 정지시키거나 데이터만 바로 파싱하여 표시 _isAgentRunning = false; // 소켓 루프 정지 (이미 열려있는 폼이라면) // ParseAndBindServerData는 이미 구현되어 있으므로 그대로 재사용 가능 ParseAndBindServerData(xmlData); System.Diagnostics.Debug.WriteLine($"[모니터링 갱신] 외부 데이터 바인딩 완료: {xmlData.Length} bytes"); } [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public bool DisableSocketCommunication { get; set; } = false; private void InitializeCustomButtons() { // 최소화 버튼 설정 _btnMinimize = new Button { Text = "—", Size = new Size(40, 40), FlatStyle = FlatStyle.Flat }; _btnMinimize.FlatAppearance.BorderSize = 0; _btnMinimize.Click += (s, e) => { this.WindowState = FormWindowState.Minimized; }; // 닫기 버튼 설정 _btnClose = new Button { Text = "✕", Size = new Size(40, 40), FlatStyle = FlatStyle.Flat }; _btnClose.FlatAppearance.BorderSize = 0; _btnClose.Click += (s, e) => { this.Close(); }; // 타이틀 패널(panel1)에 추가 panel1.Controls.Add(_btnMinimize); panel1.Controls.Add(_btnClose); // 위치 조정 (우측 상단 배치) _btnClose.Location = new Point(panel1.Width - 50, 5); _btnMinimize.Location = new Point(panel1.Width - 95, 5); _btnClose.BringToFront(); _btnMinimize.BringToFront(); } } }