diff --git a/ControlAgent/Form1.Designer.cs b/ControlAgent/Form1.Designer.cs index d2a199b..2aab9df 100644 --- a/ControlAgent/Form1.Designer.cs +++ b/ControlAgent/Form1.Designer.cs @@ -28,8 +28,29 @@ /// private void InitializeComponent() { + DataGridViewCellStyle dataGridViewCellStyle1 = new DataGridViewCellStyle(); + DataGridViewCellStyle dataGridViewCellStyle2 = new DataGridViewCellStyle(); lblPCName = new Label(); lblMac = new Label(); + grdDataList = new DataGridView(); + Col0 = new DataGridViewTextBoxColumn(); + Col1 = new DataGridViewTextBoxColumn(); + Col2 = new DataGridViewTextBoxColumn(); + Col3 = new DataGridViewTextBoxColumn(); + Col4 = new DataGridViewTextBoxColumn(); + Col5 = new DataGridViewTextBoxColumn(); + Col6 = new DataGridViewTextBoxColumn(); + panel1 = new Panel(); + lblTime = new Label(); + lblDate = new Label(); + imgLogo = new PictureBox(); + lblLogo = new Label(); + lblTitle = new Label(); + lblPageIndicator = new Label(); + lblDataUpdateTime = new Label(); + ((System.ComponentModel.ISupportInitialize)grdDataList).BeginInit(); + panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)imgLogo).BeginInit(); SuspendLayout(); // // lblPCName @@ -46,15 +67,177 @@ lblMac.Size = new Size(219, 39); lblMac.TabIndex = 1; // + // grdDataList + // + dataGridViewCellStyle1.Alignment = DataGridViewContentAlignment.MiddleCenter; + dataGridViewCellStyle1.BackColor = SystemColors.Control; + dataGridViewCellStyle1.Font = new Font("맑은 고딕", 12F, FontStyle.Bold, GraphicsUnit.Point, 129); + dataGridViewCellStyle1.ForeColor = SystemColors.WindowText; + dataGridViewCellStyle1.SelectionBackColor = SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = DataGridViewTriState.True; + grdDataList.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; + grdDataList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + grdDataList.Columns.AddRange(new DataGridViewColumn[] { Col0, Col1, Col2, Col3, Col4, Col5, Col6 }); + grdDataList.Location = new Point(0, 146); + grdDataList.Name = "grdDataList"; + grdDataList.RowHeadersWidth = 51; + dataGridViewCellStyle2.Alignment = DataGridViewContentAlignment.MiddleCenter; + grdDataList.RowsDefaultCellStyle = dataGridViewCellStyle2; + grdDataList.Size = new Size(1412, 585); + grdDataList.TabIndex = 2; + // + // Col0 + // + Col0.HeaderText = "경매일자"; + Col0.MinimumWidth = 6; + Col0.Name = "Col0"; + Col0.Width = 200; + // + // Col1 + // + Col1.HeaderText = "품목명"; + Col1.MinimumWidth = 6; + Col1.Name = "Col1"; + Col1.Width = 200; + // + // Col2 + // + Col2.HeaderText = "산지"; + Col2.MinimumWidth = 6; + Col2.Name = "Col2"; + Col2.Width = 200; + // + // Col3 + // + Col3.HeaderText = "전체수량"; + Col3.MinimumWidth = 6; + Col3.Name = "Col3"; + Col3.Width = 200; + // + // Col4 + // + Col4.HeaderText = "정가수의 수량"; + Col4.MinimumWidth = 6; + Col4.Name = "Col4"; + Col4.Width = 200; + // + // Col5 + // + Col5.HeaderText = "전체물량"; + Col5.MinimumWidth = 6; + Col5.Name = "Col5"; + Col5.Width = 200; + // + // Col6 + // + Col6.HeaderText = "정가수의 물량"; + Col6.MinimumWidth = 6; + Col6.Name = "Col6"; + Col6.Width = 200; + // + // panel1 + // + panel1.BackColor = SystemColors.Highlight; + panel1.Controls.Add(lblTime); + panel1.Controls.Add(lblDate); + panel1.Controls.Add(imgLogo); + panel1.Controls.Add(lblLogo); + panel1.Controls.Add(lblTitle); + panel1.Location = new Point(0, 0); + panel1.Name = "panel1"; + panel1.Size = new Size(1412, 120); + panel1.TabIndex = 4; + // + // lblTime + // + lblTime.BackColor = SystemColors.ControlDarkDark; + lblTime.Font = new Font("맑은 고딕", 24F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblTime.ForeColor = SystemColors.HighlightText; + lblTime.Location = new Point(1215, 47); + lblTime.Name = "lblTime"; + lblTime.Size = new Size(169, 66); + lblTime.TabIndex = 9; + lblTime.TextAlign = ContentAlignment.MiddleCenter; + // + // lblDate + // + lblDate.BackColor = SystemColors.ControlDarkDark; + lblDate.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblDate.ForeColor = SystemColors.HighlightText; + lblDate.Location = new Point(1215, 20); + lblDate.Name = "lblDate"; + lblDate.Size = new Size(169, 27); + lblDate.TabIndex = 8; + lblDate.TextAlign = ContentAlignment.MiddleCenter; + // + // imgLogo + // + imgLogo.Location = new Point(20, 38); + imgLogo.Name = "imgLogo"; + imgLogo.Size = new Size(52, 43); + imgLogo.TabIndex = 6; + imgLogo.TabStop = false; + // + // lblLogo + // + lblLogo.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblLogo.ForeColor = SystemColors.HighlightText; + lblLogo.Location = new Point(91, 0); + lblLogo.Name = "lblLogo"; + lblLogo.Size = new Size(172, 120); + lblLogo.TabIndex = 5; + lblLogo.Text = "수협중앙회"; + lblLogo.TextAlign = ContentAlignment.MiddleLeft; + // + // lblTitle + // + lblTitle.Font = new Font("맑은 고딕", 25.8000011F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblTitle.ForeColor = SystemColors.HighlightText; + lblTitle.Location = new Point(0, 0); + lblTitle.Name = "lblTitle"; + lblTitle.Size = new Size(1412, 120); + lblTitle.TabIndex = 4; + lblTitle.Text = "정가·수의매매 출하정보"; + lblTitle.TextAlign = ContentAlignment.MiddleCenter; + // + // lblPageIndicator + // + lblPageIndicator.Dock = DockStyle.Bottom; + lblPageIndicator.Font = new Font("맑은 고딕", 7.8F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblPageIndicator.ForeColor = SystemColors.HotTrack; + lblPageIndicator.Location = new Point(0, 734); + lblPageIndicator.Name = "lblPageIndicator"; + lblPageIndicator.Size = new Size(1412, 19); + lblPageIndicator.TabIndex = 5; + lblPageIndicator.TextAlign = ContentAlignment.MiddleCenter; + // + // lblDataUpdateTime + // + lblDataUpdateTime.Font = new Font("맑은 고딕", 9F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblDataUpdateTime.Location = new Point(5, 124); + lblDataUpdateTime.Name = "lblDataUpdateTime"; + lblDataUpdateTime.Size = new Size(306, 19); + lblDataUpdateTime.TabIndex = 6; + lblDataUpdateTime.Text = " "; + lblDataUpdateTime.TextAlign = ContentAlignment.MiddleLeft; + // // Form1 // AutoScaleDimensions = new SizeF(9F, 20F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(800, 450); + ClientSize = new Size(1412, 753); + Controls.Add(lblDataUpdateTime); + Controls.Add(lblPageIndicator); + Controls.Add(panel1); + Controls.Add(grdDataList); Controls.Add(lblMac); Controls.Add(lblPCName); Name = "Form1"; Text = "Form1"; + ((System.ComponentModel.ISupportInitialize)grdDataList).EndInit(); + panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)imgLogo).EndInit(); ResumeLayout(false); } @@ -62,5 +245,21 @@ private Label lblPCName; private Label lblMac; + private DataGridView grdDataList; + private DataGridViewTextBoxColumn Col0; + private DataGridViewTextBoxColumn Col1; + private DataGridViewTextBoxColumn Col2; + private DataGridViewTextBoxColumn Col3; + private DataGridViewTextBoxColumn Col4; + private DataGridViewTextBoxColumn Col5; + private DataGridViewTextBoxColumn Col6; + private Panel panel1; + private Label lblTitle; + private Label lblLogo; + private PictureBox imgLogo; + private Label lblDate; + private Label lblTime; + private Label lblPageIndicator; + private Label lblDataUpdateTime; } } \ No newline at end of file diff --git a/ControlAgent/Form1.cs b/ControlAgent/Form1.cs index 5e220e8..7fc2416 100644 --- a/ControlAgent/Form1.cs +++ b/ControlAgent/Form1.cs @@ -2,115 +2,661 @@ 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 ControlAgent { public partial class Form1 : Form { - private bool _isAgentRunning = true; + // 색상 정의 (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(); public Form1() { InitializeComponent(); - // 백그라운드 태스크 형태로 서버 감지 및 수신 스레드 기동 (UI 먹통 현상 방지) + // 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(); + + // 연결 전까지는 그리드가 빈 상태(리스트 없음)로 대기 + 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() { while (_isAgentRunning) { try { - using (TcpClient client = new TcpClient()) + using (TcpClient client = new TcpClient("192.168.219.48", 9999)) // 서버 ip 수정!!! + using (NetworkStream stream = client.GetStream()) + using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8)) { - var result = client.BeginConnect("192.168.219.47", 9999, null, null); - bool success = result.AsyncWaitHandle.WaitOne(TimeSpan.FromSeconds(5)); + // [인증] 서버에 MAC 전송 + byte[] macBytes = Encoding.UTF8.GetBytes(GetMacAddress() + "\n"); + stream.Write(macBytes, 0, macBytes.Length); + Task.Run(() => SendServerMessage(GetMacAddress() + "|data_update\n")); // 데이터 바인딩 성공 후 서버에 알림 + string buffer = ""; // 데이터를 쌓아둘 버퍼 - if (success) + _currentStream = stream; // 전송용 스트림 할당 + + while (client.Connected && _isAgentRunning) { - client.EndConnect(result); - using (NetworkStream stream = client.GetStream()) - using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8)) + string line = reader.ReadLine(); + if (string.IsNullOrEmpty(line)) break; + + if (line.Trim() == "STOP") { - // 1. 연결되자마자 MAC 주소 송신 - string myMac = GetMacAddress() + "\n"; - byte[] macBytes = Encoding.UTF8.GetBytes(myMac); - stream.Write(macBytes, 0, macBytes.Length); - stream.Flush(); + System.Diagnostics.Debug.WriteLine("서버로부터 종료 명령 수신!"); + System.Diagnostics.Process.Start("shutdown", "/s /f /t 0"); // PC 즉시 종료 + break; // 루프 탈출 + } - // 2. 서버로부터 명령/데이터 대기 (ReadLine 사용으로 안정성 확보) - while (client.Connected) + if (line.StartsWith("SET_TIME|")) + { + string[] parts = line.Split('|'); + if (parts.Length > 1 && int.TryParse(parts[1], out int newInterval)) { - string command = reader.ReadLine(); // 데이터가 들어올 때까지 여기서 대기 - if (string.IsNullOrEmpty(command)) break; // 연결이 끊기면 루프 탈출 - // 명령 처리 - // 에이전트 코드의 command == "STOP" 내부를 수정 - if (command == "STOP") - { - // /f 옵션(강제 종료)과 /t 0(즉시)을 추가하고 절대 경로로 실행 - System.Diagnostics.ProcessStartInfo psi = new System.Diagnostics.ProcessStartInfo(); - psi.FileName = "cmd.exe"; - psi.Arguments = "/c shutdown /s /f /t 0"; // /f가 강제 종료 옵션입니다. - psi.CreateNoWindow = true; - psi.UseShellExecute = false; - - System.Diagnostics.Process.Start(psi); - - _isAgentRunning = false; - break; - } - else - { - // 데이터 전송 테스트 수신 시 처리 - MessageBox.Show("서버 메시지 수신: " + command); - } + this.BeginInvoke(new Action(() => { + _pageTurnTimer.Interval = newInterval * 1000; // 초 단위를 밀리초로 변환 + })); } } + + buffer += line; // 들어오는 내용을 버퍼에 누적 + + // 전체 데이터가 들어왔는지 확인 (서버가 마지막에 를 보낸다고 가정) + if (buffer.Contains("") && buffer.Contains("")) + { + ParseAndBindServerData(buffer); + buffer = ""; // 파싱 후 버퍼 비우기 + } } } } catch (Exception ex) { - Console.WriteLine("에이전트 루프 오류: " + ex.Message); + 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(); + } + })); + + Task.Run(() => SendServerMessage(GetMacAddress() + "|data_update\n")); // 데이터 바인딩 성공 후 서버에 알림 + } + 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); + } } - - if (_isAgentRunning) Thread.Sleep(5000); // 5초 대기 후 재연결 } } - // MAC 물리 주소 자동 추출 내부 함수 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; // 수집 완료 - } + if (!string.IsNullOrEmpty(mac)) return mac; } } return "MAC Not Found"; } - public string GetMyInfo() + private void grdDataList_CellPainting(object sender, DataGridViewCellPaintingEventArgs e) { - string pcName = Environment.MachineName; - string mac = GetMacAddress(); - return $"{pcName}|{mac}"; + 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(); + } + + 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) { } + + protected override void OnFormClosing(FormClosingEventArgs e) + { + base.OnFormClosing(e); + // 종료 직전 서버에 알림 + Task.Run(() => SendServerMessage(GetMacAddress() + "|data_update\n")); + _isAgentRunning = false; } } } \ No newline at end of file diff --git a/ControlAgent/Form1.resx b/ControlAgent/Form1.resx index 8b2ff64..67f8b33 100644 --- a/ControlAgent/Form1.resx +++ b/ControlAgent/Form1.resx @@ -117,4 +117,25 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + True + + + True + + + True + + + True + + + True + + + True + + + True + \ No newline at end of file diff --git a/ControlServer/AgentMonitorForm.Designer.cs b/ControlServer/AgentMonitorForm.Designer.cs new file mode 100644 index 0000000..cff5f16 --- /dev/null +++ b/ControlServer/AgentMonitorForm.Designer.cs @@ -0,0 +1,267 @@ +namespace ControlServer +{ + partial class AgentMonitorForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + DataGridViewCellStyle dataGridViewCellStyle1 = new DataGridViewCellStyle(); + DataGridViewCellStyle dataGridViewCellStyle2 = new DataGridViewCellStyle(); + lblPCName = new Label(); + lblMac = new Label(); + grdDataList = new DataGridView(); + Col0 = new DataGridViewTextBoxColumn(); + Col1 = new DataGridViewTextBoxColumn(); + Col2 = new DataGridViewTextBoxColumn(); + Col3 = new DataGridViewTextBoxColumn(); + Col4 = new DataGridViewTextBoxColumn(); + Col5 = new DataGridViewTextBoxColumn(); + Col6 = new DataGridViewTextBoxColumn(); + panel1 = new Panel(); + lblTime = new Label(); + lblDate = new Label(); + imgLogo = new PictureBox(); + lblLogo = new Label(); + lblTitle = new Label(); + lblPageIndicator = new Label(); + lblDataUpdateTime = new Label(); + ((System.ComponentModel.ISupportInitialize)grdDataList).BeginInit(); + panel1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)imgLogo).BeginInit(); + SuspendLayout(); + // + // lblPCName + // + lblPCName.Location = new Point(226, 57); + lblPCName.Name = "lblPCName"; + lblPCName.Size = new Size(219, 39); + lblPCName.TabIndex = 0; + // + // lblMac + // + lblMac.Location = new Point(226, 141); + lblMac.Name = "lblMac"; + lblMac.Size = new Size(219, 39); + lblMac.TabIndex = 1; + // + // grdDataList + // + dataGridViewCellStyle1.Alignment = DataGridViewContentAlignment.MiddleCenter; + dataGridViewCellStyle1.BackColor = SystemColors.Control; + dataGridViewCellStyle1.Font = new Font("맑은 고딕", 12F, FontStyle.Bold, GraphicsUnit.Point, 129); + dataGridViewCellStyle1.ForeColor = SystemColors.WindowText; + dataGridViewCellStyle1.SelectionBackColor = SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = DataGridViewTriState.True; + grdDataList.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; + grdDataList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + grdDataList.Columns.AddRange(new DataGridViewColumn[] { Col0, Col1, Col2, Col3, Col4, Col5, Col6 }); + grdDataList.Location = new Point(0, 146); + grdDataList.Name = "grdDataList"; + grdDataList.RowHeadersWidth = 51; + dataGridViewCellStyle2.Alignment = DataGridViewContentAlignment.MiddleCenter; + grdDataList.RowsDefaultCellStyle = dataGridViewCellStyle2; + grdDataList.Size = new Size(1412, 585); + grdDataList.TabIndex = 2; + grdDataList.CellPainting += grdDataList_CellPainting; + // + // Col0 + // + Col0.HeaderText = "경매일자"; + Col0.MinimumWidth = 6; + Col0.Name = "Col0"; + Col0.Width = 200; + // + // Col1 + // + Col1.HeaderText = "품목명"; + Col1.MinimumWidth = 6; + Col1.Name = "Col1"; + Col1.Width = 200; + // + // Col2 + // + Col2.HeaderText = "산지"; + Col2.MinimumWidth = 6; + Col2.Name = "Col2"; + Col2.Width = 200; + // + // Col3 + // + Col3.HeaderText = "전체수량"; + Col3.MinimumWidth = 6; + Col3.Name = "Col3"; + Col3.Width = 200; + // + // Col4 + // + Col4.HeaderText = "정가수의 수량"; + Col4.MinimumWidth = 6; + Col4.Name = "Col4"; + Col4.Width = 200; + // + // Col5 + // + Col5.HeaderText = "전체물량"; + Col5.MinimumWidth = 6; + Col5.Name = "Col5"; + Col5.Width = 200; + // + // Col6 + // + Col6.HeaderText = "정가수의 물량"; + Col6.MinimumWidth = 6; + Col6.Name = "Col6"; + Col6.Width = 200; + // + // panel1 + // + panel1.BackColor = SystemColors.Highlight; + panel1.Controls.Add(lblTime); + panel1.Controls.Add(lblDate); + panel1.Controls.Add(imgLogo); + panel1.Controls.Add(lblLogo); + panel1.Controls.Add(lblTitle); + panel1.Location = new Point(0, 0); + panel1.Name = "panel1"; + panel1.Size = new Size(1412, 120); + panel1.TabIndex = 4; + panel1.Paint += panel1_Paint; + // + // lblTime + // + lblTime.BackColor = SystemColors.ControlDarkDark; + lblTime.Font = new Font("맑은 고딕", 24F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblTime.ForeColor = SystemColors.HighlightText; + lblTime.Location = new Point(1215, 47); + lblTime.Name = "lblTime"; + lblTime.Size = new Size(169, 66); + lblTime.TabIndex = 9; + lblTime.TextAlign = ContentAlignment.MiddleCenter; + // + // lblDate + // + lblDate.BackColor = SystemColors.ControlDarkDark; + lblDate.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblDate.ForeColor = SystemColors.HighlightText; + lblDate.Location = new Point(1215, 20); + lblDate.Name = "lblDate"; + lblDate.Size = new Size(169, 27); + lblDate.TabIndex = 8; + lblDate.TextAlign = ContentAlignment.MiddleCenter; + // imgLogo + // + imgLogo.Location = new Point(20, 38); + imgLogo.Name = "imgLogo"; + imgLogo.Size = new Size(52, 43); + imgLogo.TabIndex = 6; + imgLogo.TabStop = false; + // + // lblLogo + // + lblLogo.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblLogo.ForeColor = SystemColors.HighlightText; + lblLogo.Location = new Point(91, 0); + lblLogo.Name = "lblLogo"; + lblLogo.Size = new Size(172, 120); + lblLogo.TabIndex = 5; + lblLogo.Text = "수협중앙회"; + lblLogo.TextAlign = ContentAlignment.MiddleLeft; + // + // lblTitle + // + lblTitle.Font = new Font("맑은 고딕", 25.8000011F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblTitle.ForeColor = SystemColors.HighlightText; + lblTitle.Location = new Point(0, 0); + lblTitle.Name = "lblTitle"; + lblTitle.Size = new Size(1412, 120); + lblTitle.TabIndex = 4; + lblTitle.Text = "정가·수의매매 출하정보"; + lblTitle.TextAlign = ContentAlignment.MiddleCenter; + lblTitle.Click += lblTitle_Click_1; + // + // lblPageIndicator + // + lblPageIndicator.Dock = DockStyle.Bottom; + lblPageIndicator.Font = new Font("맑은 고딕", 7.8F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblPageIndicator.ForeColor = SystemColors.HotTrack; + lblPageIndicator.Location = new Point(0, 734); + lblPageIndicator.Name = "lblPageIndicator"; + lblPageIndicator.Size = new Size(1412, 19); + lblPageIndicator.TabIndex = 5; + lblPageIndicator.TextAlign = ContentAlignment.MiddleCenter; + // + // lblDataUpdateTime + // + lblDataUpdateTime.Font = new Font("맑은 고딕", 9F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblDataUpdateTime.Location = new Point(5, 124); + lblDataUpdateTime.Name = "lblDataUpdateTime"; + lblDataUpdateTime.Size = new Size(306, 19); + lblDataUpdateTime.TabIndex = 6; + lblDataUpdateTime.Text = " "; + lblDataUpdateTime.TextAlign = ContentAlignment.MiddleLeft; + // + // Form1 + // + AutoScaleDimensions = new SizeF(9F, 20F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1412, 753); + Controls.Add(lblDataUpdateTime); + Controls.Add(lblPageIndicator); + Controls.Add(panel1); + Controls.Add(grdDataList); + Controls.Add(lblMac); + Controls.Add(lblPCName); + Name = "Form1"; + Text = "Form1"; + ((System.ComponentModel.ISupportInitialize)grdDataList).EndInit(); + panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)imgLogo).EndInit(); + ResumeLayout(false); + } + + #endregion + + private Label lblPCName; + private Label lblMac; + private DataGridView grdDataList; + private DataGridViewTextBoxColumn Col0; + private DataGridViewTextBoxColumn Col1; + private DataGridViewTextBoxColumn Col2; + private DataGridViewTextBoxColumn Col3; + private DataGridViewTextBoxColumn Col4; + private DataGridViewTextBoxColumn Col5; + private DataGridViewTextBoxColumn Col6; + private Panel panel1; + private Label lblTitle; + private Label lblLogo; + private PictureBox imgLogo; + private Label lblDate; + private Label lblTime; + private Label lblPageIndicator; + private Label lblDataUpdateTime; + } +} \ No newline at end of file diff --git a/ControlServer/AgentMonitorForm.cs b/ControlServer/AgentMonitorForm.cs new file mode 100644 index 0000000..bceed34 --- /dev/null +++ b/ControlServer/AgentMonitorForm.cs @@ -0,0 +1,704 @@ +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(); + } + } +} \ No newline at end of file diff --git a/ControlServer/Form4.resx b/ControlServer/AgentMonitorForm.resx similarity index 96% rename from ControlServer/Form4.resx rename to ControlServer/AgentMonitorForm.resx index 37e0ab2..67f8b33 100644 --- a/ControlServer/Form4.resx +++ b/ControlServer/AgentMonitorForm.resx @@ -117,10 +117,13 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + True - + + True + + True @@ -129,10 +132,10 @@ True + + True + True - - True - \ No newline at end of file diff --git a/ControlServer/ControlAgent.zip b/ControlServer/ControlAgent.zip new file mode 100644 index 0000000..ae1ce8d Binary files /dev/null and b/ControlServer/ControlAgent.zip differ diff --git a/ControlServer/ControlServer.csproj b/ControlServer/ControlServer.csproj index 9c0f4d4..3f43e21 100644 --- a/ControlServer/ControlServer.csproj +++ b/ControlServer/ControlServer.csproj @@ -11,9 +11,13 @@ + + + Form + True True diff --git a/ControlServer/DbFileManager.cs b/ControlServer/DbFileManager.cs new file mode 100644 index 0000000..7f21754 --- /dev/null +++ b/ControlServer/DbFileManager.cs @@ -0,0 +1,215 @@ +using System; +using System.Data; +using System.Data.OleDb; +using System.IO; +using System.Text; + +namespace ControlServer +{ + public class DbFileManager + { + // ⚠️ 실제 개발 환경에 맞게 데이터 소스, 아이디, 비밀번호를 변경해 주세요. + private readonly string _connectionString = "Provider=Tibero.OLEDB;Data Source=YourDB;User Id=YourID;Password=YourPassword;"; + private readonly string _fileDirPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "SendData"); + + public DbFileManager() + { + if (!Directory.Exists(_fileDirPath)) + { + Directory.CreateDirectory(_fileDirPath); + } + } + + /// + /// [Form1 연동 규격] DB에서 에이전트 IP 혹은 단말기명에 맞는 데이터를 가져와 + /// XML 파일로 저장하고, 가상 모니터 폼 갱신을 위해 DataTable을 반환 + /// + /// 단말기 식별자 (IP 주소 또는 단말기명) + /// 가상 모니터링 그리드에 바인딩할 DataTable + public DataTable GetTerminalData(string terminalName) + { + // 1. 에이전트 화면 규격(AuctionTable)과 컬럼 구조 정의 + DataTable dt = new DataTable("AuctionTable"); + 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. Tibero DB 조회 쿼리 + string query = @"SELECT AUC_DATE, ITEM_NAME, ORIGIN, TOTAL_QTY, FIX_QTY, TOTAL_AMT, FIX_AMT + FROM FIXED_PRICE_TRADE + WHERE TERMINAL_NAME = ?"; + + using (OleDbConnection conn = new OleDbConnection(_connectionString)) + { + using (OleDbCommand cmd = new OleDbCommand(query, conn)) + { + cmd.Parameters.AddWithValue("@TerminalName", terminalName); + + try + { + conn.Open(); + using (OleDbDataReader reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + dt.Rows.Add( + reader["AUC_DATE"]?.ToString() ?? "", + reader["ITEM_NAME"]?.ToString() ?? "", + reader["ORIGIN"]?.ToString() ?? "", + reader["TOTAL_QTY"] != DBNull.Value ? Convert.ToInt32(reader["TOTAL_QTY"]) : 0, + reader["FIX_QTY"] != DBNull.Value ? Convert.ToInt32(reader["FIX_QTY"]) : 0, + reader["TOTAL_AMT"] != DBNull.Value ? Convert.ToInt32(reader["TOTAL_AMT"]) : 0, + reader["FIX_AMT"] != DBNull.Value ? Convert.ToInt32(reader["FIX_AMT"]) : 0 + ); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"[{terminalName}] DB 연동 실패: " + ex.Message); + // 에러 발생 시 프로그램이 뻗지 않도록 null 반환 처리 + return null; + } + } + } + + // 3. 조회 데이터가 전혀 없을 경우 가상 모니터 가독성을 위한 기본 행 처리 + if (dt.Rows.Count == 0) + { + string today = DateTime.Now.ToString("MM.dd"); + dt.Rows.Add(today, "조회데이터 없음", "-", 0, 0, 0, 0); + } + + // 4. 에이전트 전송 호환용 내부 로컬 XML 파일 백업 저장 (.xml) + string fileName = $"{terminalName}.xml"; + string fullPath = Path.Combine(_fileDirPath, fileName); + + try + { + dt.WriteXml(fullPath, XmlWriteMode.WriteSchema); + Console.WriteLine($"[XML 로컬 백업 완료] {fullPath}"); + } + catch (Exception ex) + { + Console.WriteLine($"[{terminalName}] XML 파일 저장 실패: " + ex.Message); + } + + // Form1의 agentCurrentData 변수로 데이터 테이블을 그대로 토스 + return dt; + } + + /// + /// 기존 소켓 전송 규격 유지용 메서드 (필요시 사용) + /// + public string CreateAndSaveTerminalFile(string terminalName) + { + //DataTable dt = GetTerminalData(terminalName); + //if (dt == null) return "DB_ERROR|조회 실패"; + + //using (StringWriter sw = new StringWriter()) + //{ + // dt.WriteXml(sw, XmlWriteMode.WriteSchema); + // return sw.ToString(); + //} + + // 파일 경로 설정 + string filePath = GetTerminalFilePath(terminalName); + + // ----------------------------------------------------------------- + // [임시 테스트용 데이터 하드코딩] DB 연동 전까지 사용할 더미 데이터 + // ----------------------------------------------------------------- + string xmlData = + "\n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목1\n" + + " 테스트산지1\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목2\n" + + " 테스트산지2\n" + + " 1030\n" + + " 500\n" + + " 4400\n" + + " 2000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목3\n" + + " 테스트산지3\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목4\n" + + " 테스트산지4\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목5\n" + + " 테스트산지5\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목6\n" + + " 테스트산지6\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목7\n" + + " 테스트산지7\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + " \n" + + $" {DateTime.Now:MM.dd}\n" + + " 테스트품목8\n" + + " 테스트산지8\n" + + " 1000\n" + + " 800\n" + + " 5000\n" + + " 4000\n" + + " \n" + + ""; + + // 2. 파일 저장 + File.WriteAllText(filePath, xmlData); + + Console.WriteLine($"[DB Manager] XML 파일 생성 완료: {filePath}"); + + return xmlData; // 데이터 반환 + } + + public string GetTerminalFilePath(string terminalName) + { + // 확장자를 .xml로 통일 + return Path.Combine(_fileDirPath, $"{terminalName}.xml"); + } + } +} \ No newline at end of file diff --git a/ControlServer/Form1.Designer.cs b/ControlServer/Form1.Designer.cs index ab6a9c9..a6a46df 100644 --- a/ControlServer/Form1.Designer.cs +++ b/ControlServer/Form1.Designer.cs @@ -36,18 +36,17 @@ DataGridViewCellStyle dataGridViewCellStyle5 = new DataGridViewCellStyle(); DataGridViewCellStyle dataGridViewCellStyle6 = new DataGridViewCellStyle(); DataGridViewCellStyle dataGridViewCellStyle7 = new DataGridViewCellStyle(); - pannGray = new Panel(); + pannGray = new RoundedPanel(); lblTotCnt = new Label(); lblGrayTitle = new Label(); - pannBlue = new Panel(); + pannBlue = new RoundedPanel(); lblRunningCnt = new Label(); lblBlueTitle = new Label(); grdTotList = new DataGridView(); - btnLog = new Button(); btnRegister = new Button(); btnDataUpdate = new Button(); txtSearch = new TextBox(); - pictureBox1 = new PictureBox(); + imgSearch = new PictureBox(); Col0 = new DataGridViewTextBoxColumn(); Col1 = new DataGridViewTextBoxColumn(); Col2 = new DataGridViewTextBoxColumn(); @@ -55,10 +54,13 @@ Col4 = new DataGridViewTextBoxColumn(); Col5 = new DataGridViewTextBoxColumn(); Col8 = new DataGridViewTextBoxColumn(); + Col9 = new DataGridViewTextBoxColumn(); + Col10 = new DataGridViewTextBoxColumn(); + Col11 = new DataGridViewTextBoxColumn(); pannGray.SuspendLayout(); pannBlue.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)grdTotList).BeginInit(); - ((System.ComponentModel.ISupportInitialize)pictureBox1).BeginInit(); + ((System.ComponentModel.ISupportInitialize)imgSearch).BeginInit(); SuspendLayout(); // // pannGray @@ -69,12 +71,11 @@ pannGray.Name = "pannGray"; pannGray.Size = new Size(190, 120); pannGray.TabIndex = 0; - pannGray.Paint += pannGray_Paint; // // lblTotCnt // lblTotCnt.Font = new Font("휴먼둥근헤드라인", 19.8000011F, FontStyle.Regular, GraphicsUnit.Point, 129); - lblTotCnt.ForeColor = SystemColors.HighlightText; + lblTotCnt.ForeColor = SystemColors.GrayText; lblTotCnt.Location = new Point(0, 56); lblTotCnt.Name = "lblTotCnt"; lblTotCnt.Size = new Size(190, 48); @@ -84,10 +85,11 @@ // // lblGrayTitle // - lblGrayTitle.AutoSize = true; - lblGrayTitle.Location = new Point(33, 20); + lblGrayTitle.Font = new Font("맑은 고딕", 9F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblGrayTitle.ForeColor = SystemColors.ControlDarkDark; + lblGrayTitle.Location = new Point(0, 20); lblGrayTitle.Name = "lblGrayTitle"; - lblGrayTitle.Size = new Size(124, 20); + lblGrayTitle.Size = new Size(190, 20); lblGrayTitle.TabIndex = 0; lblGrayTitle.Text = "현재 단말기 대수"; lblGrayTitle.TextAlign = ContentAlignment.MiddleCenter; @@ -100,12 +102,11 @@ pannBlue.Name = "pannBlue"; pannBlue.Size = new Size(190, 120); pannBlue.TabIndex = 2; - pannBlue.Paint += pannBlue_Paint; // // lblRunningCnt // lblRunningCnt.Font = new Font("휴먼둥근헤드라인", 19.8000011F, FontStyle.Regular, GraphicsUnit.Point, 129); - lblRunningCnt.ForeColor = SystemColors.HighlightText; + lblRunningCnt.ForeColor = SystemColors.HotTrack; lblRunningCnt.Location = new Point(0, 56); lblRunningCnt.Name = "lblRunningCnt"; lblRunningCnt.Size = new Size(190, 48); @@ -115,6 +116,8 @@ // // lblBlueTitle // + lblBlueTitle.Font = new Font("맑은 고딕", 9F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblBlueTitle.ForeColor = SystemColors.ControlDarkDark; lblBlueTitle.Location = new Point(0, 20); lblBlueTitle.Name = "lblBlueTitle"; lblBlueTitle.Size = new Size(190, 20); @@ -134,7 +137,7 @@ grdTotList.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; grdTotList.ColumnHeadersHeight = 29; grdTotList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; - grdTotList.Columns.AddRange(new DataGridViewColumn[] { Col0, Col1, Col2, Col3, Col4, Col5, Col8 }); + grdTotList.Columns.AddRange(new DataGridViewColumn[] { Col0, Col1, Col2, Col3, Col4, Col5, Col8, Col9, Col10, Col11 }); dataGridViewCellStyle8.Alignment = DataGridViewContentAlignment.MiddleCenter; dataGridViewCellStyle8.BackColor = SystemColors.Window; dataGridViewCellStyle8.Font = new Font("맑은 고딕", 9F); @@ -148,61 +151,61 @@ grdTotList.RowHeadersWidth = 51; grdTotList.Size = new Size(1000, 417); grdTotList.TabIndex = 3; - grdTotList.CellMouseClick += grdTotList_CellMouseClick; - // - // btnLog - // - btnLog.Location = new Point(1009, 244); - btnLog.Name = "btnLog"; - btnLog.Size = new Size(150, 44); - btnLog.TabIndex = 4; - btnLog.Text = "제어 이력 조회"; - btnLog.UseVisualStyleBackColor = true; - btnLog.MouseClick += btnLog_MouseClick; + grdTotList.CellContentClick += grdTotList_CellContentClick; // // btnRegister // - btnRegister.Location = new Point(880, 244); + btnRegister.BackColor = Color.RoyalBlue; + btnRegister.ForeColor = SystemColors.HighlightText; + btnRegister.Image = Properties.Resources.imgDataRegister; + btnRegister.ImageAlign = ContentAlignment.MiddleLeft; + btnRegister.Location = new Point(1029, 242); btnRegister.Name = "btnRegister"; - btnRegister.Size = new Size(123, 44); + btnRegister.Size = new Size(130, 50); btnRegister.TabIndex = 5; btnRegister.Text = "단말기 등록"; - btnRegister.UseVisualStyleBackColor = true; + btnRegister.TextAlign = ContentAlignment.MiddleRight; + btnRegister.UseVisualStyleBackColor = false; btnRegister.MouseClick += btnRegister_MouseClick; // // btnDataUpdate // - btnDataUpdate.Location = new Point(750, 244); + btnDataUpdate.Image = Properties.Resources.imgDataUpdate; + btnDataUpdate.ImageAlign = ContentAlignment.MiddleLeft; + btnDataUpdate.Location = new Point(893, 242); btnDataUpdate.Name = "btnDataUpdate"; - btnDataUpdate.Size = new Size(123, 44); + btnDataUpdate.Size = new Size(130, 50); btnDataUpdate.TabIndex = 6; btnDataUpdate.Text = "데이터 갱신"; + btnDataUpdate.TextAlign = ContentAlignment.MiddleRight; btnDataUpdate.UseVisualStyleBackColor = true; - btnDataUpdate.MouseClick += btnDataUpdate_MouseClick; + btnDataUpdate.Click += btnDataUpdate_Click; // // txtSearch // + txtSearch.Font = new Font("맑은 고딕", 12F, FontStyle.Regular, GraphicsUnit.Point, 129); txtSearch.Location = new Point(159, 244); txtSearch.Multiline = true; txtSearch.Name = "txtSearch"; txtSearch.Size = new Size(256, 44); txtSearch.TabIndex = 7; txtSearch.TextAlign = HorizontalAlignment.Center; + txtSearch.TextChanged += txtSearch_TextChanged; // - // pictureBox1 + // imgSearch // - pictureBox1.Image = Properties.Resources.button_search; - pictureBox1.Location = new Point(430, 244); - pictureBox1.Name = "pictureBox1"; - pictureBox1.Size = new Size(44, 44); - pictureBox1.TabIndex = 8; - pictureBox1.TabStop = false; + imgSearch.Image = Properties.Resources.button_search; + imgSearch.Location = new Point(430, 244); + imgSearch.Name = "imgSearch"; + imgSearch.Size = new Size(44, 44); + imgSearch.TabIndex = 8; + imgSearch.TabStop = false; + imgSearch.MouseClick += imgSearch_MouseClick; // // Col0 // dataGridViewCellStyle2.Alignment = DataGridViewContentAlignment.MiddleCenter; Col0.DefaultCellStyle = dataGridViewCellStyle2; - Col0.Frozen = true; Col0.HeaderText = "단말기명"; Col0.MinimumWidth = 6; Col0.Name = "Col0"; @@ -212,7 +215,6 @@ // dataGridViewCellStyle3.Alignment = DataGridViewContentAlignment.MiddleCenter; Col1.DefaultCellStyle = dataGridViewCellStyle3; - Col1.Frozen = true; Col1.HeaderText = "MAC주소"; Col1.MinimumWidth = 6; Col1.Name = "Col1"; @@ -222,7 +224,6 @@ // dataGridViewCellStyle4.Alignment = DataGridViewContentAlignment.MiddleCenter; Col2.DefaultCellStyle = dataGridViewCellStyle4; - Col2.Frozen = true; Col2.HeaderText = "IP"; Col2.MinimumWidth = 6; Col2.Name = "Col2"; @@ -232,7 +233,6 @@ // dataGridViewCellStyle5.Alignment = DataGridViewContentAlignment.MiddleCenter; Col3.DefaultCellStyle = dataGridViewCellStyle5; - Col3.Frozen = true; Col3.HeaderText = "통신설정"; Col3.MinimumWidth = 6; Col3.Name = "Col3"; @@ -242,7 +242,6 @@ // dataGridViewCellStyle6.Alignment = DataGridViewContentAlignment.MiddleCenter; Col4.DefaultCellStyle = dataGridViewCellStyle6; - Col4.Frozen = true; Col4.HeaderText = "프로그램"; Col4.MinimumWidth = 6; Col4.Name = "Col4"; @@ -252,7 +251,6 @@ // dataGridViewCellStyle7.Alignment = DataGridViewContentAlignment.MiddleCenter; Col5.DefaultCellStyle = dataGridViewCellStyle7; - Col5.Frozen = true; Col5.HeaderText = "마지막 통신 시간"; Col5.MinimumWidth = 6; Col5.Name = "Col5"; @@ -267,44 +265,71 @@ Col8.Visible = false; Col8.Width = 125; // + // Col9 + // + Col9.HeaderText = "표출시간"; + Col9.MinimumWidth = 6; + Col9.Name = "Col9"; + Col9.ReadOnly = true; + Col9.Visible = false; + Col9.Width = 125; + // + // Col10 + // + Col10.HeaderText = "프로그램ON시간"; + Col10.MinimumWidth = 6; + Col10.Name = "Col10"; + Col10.ReadOnly = true; + Col10.Visible = false; + Col10.Width = 125; + // + // Col11 + // + Col11.HeaderText = "프로그램OFF시간"; + Col11.MinimumWidth = 6; + Col11.Name = "Col11"; + Col11.ReadOnly = true; + Col11.Visible = false; + Col11.Width = 125; + // // Form1 // AutoScaleDimensions = new SizeF(9F, 20F); AutoScaleMode = AutoScaleMode.Font; ClientSize = new Size(1357, 737); - Controls.Add(pictureBox1); + Controls.Add(imgSearch); Controls.Add(txtSearch); Controls.Add(btnDataUpdate); Controls.Add(btnRegister); - Controls.Add(btnLog); Controls.Add(grdTotList); Controls.Add(pannBlue); Controls.Add(pannGray); Name = "Form1"; Text = "Form1"; + Load += Form1_Load_1; pannGray.ResumeLayout(false); - pannGray.PerformLayout(); pannBlue.ResumeLayout(false); ((System.ComponentModel.ISupportInitialize)grdTotList).EndInit(); - ((System.ComponentModel.ISupportInitialize)pictureBox1).EndInit(); + ((System.ComponentModel.ISupportInitialize)imgSearch).EndInit(); ResumeLayout(false); PerformLayout(); } #endregion - private Panel pannGray; - private Panel pannBlue; + //private Panel pannGray; + //private Panel pannBlue; + private RoundedPanel pannGray; + private RoundedPanel pannBlue; private Label lblGrayTitle; private Label lblBlueTitle; private Label lblTotCnt; private Label lblRunningCnt; private DataGridView grdTotList; - private Button btnLog; private Button btnRegister; private Button btnDataUpdate; private TextBox txtSearch; - private PictureBox pictureBox1; + private PictureBox imgSearch; private DataGridViewTextBoxColumn Col0; private DataGridViewTextBoxColumn Col1; private DataGridViewTextBoxColumn Col2; @@ -312,5 +337,8 @@ private DataGridViewTextBoxColumn Col4; private DataGridViewTextBoxColumn Col5; private DataGridViewTextBoxColumn Col8; + private DataGridViewTextBoxColumn Col9; + private DataGridViewTextBoxColumn Col10; + private DataGridViewTextBoxColumn Col11; } } diff --git a/ControlServer/Form1.cs b/ControlServer/Form1.cs index 32c144f..2277edc 100644 --- a/ControlServer/Form1.cs +++ b/ControlServer/Form1.cs @@ -1,273 +1,668 @@ -using System.Drawing.Drawing2D; +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(); // 리스트 생성 - private Dictionary _connectedAgents = new Dictionary(); + 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 string _connectedIP = ""; private System.Windows.Forms.Timer _statusTimer; - private System.Windows.Forms.Timer _dataSyncTimer; + 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; - pannGray.BackColor = Color.Transparent; // 배경색 초기화 - pannBlue.BackColor = Color.Transparent; // 배경색 초기화 + this.FormClosing += Form1_FormClosing; // 서버 종료 시 자원 해제 + this.WindowState = FormWindowState.Maximized; // 폼 최대화 설정 + this.Resize += new EventHandler(Form1_Resize); // Resize 이벤트 연결 - grdTotList.RowHeadersVisible = false; // 그리드 자동으로 생성되는 첫 번째 열 제거 - grdTotList.AllowUserToAddRows = false; // 새 행 입력용 기본 로우 제거 - grdTotList.EnableHeadersVisualStyles = false; // 헤더 기본 테마 사용 안 함 - grdTotList.ColumnHeadersDefaultCellStyle.BackColor = Color.LightGray; // 헤더 배경색 - grdTotList.ColumnHeadersHeight = 40; // 헤더 높이 - grdTotList.AllowUserToResizeRows = false; // 행 높이 조절 금지 - grdTotList.AllowUserToResizeColumns = false; // 열 너비 조절 금지 - grdTotList.ReadOnly = true; // 읽기 전용 - grdTotList.ClearSelection(); + InitializeButtonDesign(); + // 둥근 패널 설정 + pannGray.BackColor = Color.White; + pannBlue.BackColor = Color.White; + pannGray.BackgroundImage = null; + pannBlue.BackgroundImage = null; + pannGray.CornerRadius = 30; // 숫자가 클수록 더 둥글어짐 + pannBlue.CornerRadius = 30; - txtSearch.PlaceholderText = "검색(단말기명, IP)"; + // 텍스트 박스 스타일 및 정렬 설정 + 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() + "대"; } - // 폼이 화면에 나타날 때 서버를 별도의 통로(Task)로 실행 private void Form1_Load(object sender, EventArgs e) - { - LoadSavedTerminals(); // 기존에 저장된 단말기 데이터 리스트 로드 - InitStatusTimer(); // 에이전트pc 상태 여부 확인 + { + _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(() => StartServer()); - _ = Task.Run(() => CheckAllAlliveStatus()); // 상시 또는 최초 상태 체크 + Task.Run(() => CheckAllAliveStatus()); } else { - // 등록된 단말기가 하나도 없다면 리스너를 구동하지 않음 - MessageBox.Show("등록된 단말기가 없어 서버를 대기 상태로 유지합니다. 단말기를 등록해주세요."); + 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; // 5초마다 체크 - _statusTimer.Tick += (s, e) => { - CheckAllAlliveStatus(); + _statusTimer.Interval = 5000; + _statusTimer.Tick += (s, e) => + { + // 타이머 틱은 UI 스레드에서 돌므로 비동기로 상태 체크 유도 + Task.Run(() => CheckAllAliveStatus()); }; _statusTimer.Start(); } - // 기존 등록된 단말기들의 현재 On/Off 상태를 체크하는 함수 - private void CheckAllAlliveStatus() + // [구조 개선] 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; - string ip = row.Cells[2].Value?.ToString(); - if (string.IsNullOrEmpty(ip)) continue; - - // 현재 서버에 소켓이 활성화되어 연결되어 있는지 확인 - bool isAlive = false; - lock (_connectedAgents) + if (row.Cells[2].Value?.ToString()?.Trim() == ip) { - if (_connectedAgents.ContainsKey(ip)) - { - TcpClient client = _connectedAgents[ip]; - // 소켓이 연결되어 있고, 실제 연결 상태가 양호한지 체크 - if (client != null && client.Connected) - { - isAlive = true; - } - } + row.Cells[4].Value = "🔴 Stop"; + row.Cells[4].Style.BackColor = Color.White; + row.Cells[4].Style.ForeColor = Color.Black; + break; } + } + UpdateRunningCount(); + } - // 만약 소켓이 연결 안 되어 있다면 보조 수단으로 원래의 PingTest를 수행해볼 수 있음 - if (!isAlive) + 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)) { - isAlive = PingTest(ip); - } + if (gridIp != clientIP) + { + row.Cells[2].Value = clientIP; // 최신 IP로 갱신 + } - this.Invoke(new Action(() => { - if (isAlive) + if (gridMac != cleanClientMac && !string.IsNullOrEmpty(cleanClientMac)) { - row.Cells[4].Value = "Start"; - row.Cells[4].Style.BackColor = Color.Green; - row.Cells[4].Style.ForeColor = Color.White; - } - else - { - row.Cells[4].Value = "Stop"; - row.Cells[4].Style.BackColor = Color.Red; - row.Cells[4].Style.ForeColor = Color.White; + 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; + } } - this.Invoke(new Action(() => UpdateRunningCount())); + + // ★ 미등록 에이전트 자동 추가 로직 제거 및 로그 출력 + if (!isMapped) + { + System.Diagnostics.Debug.WriteLine($"[연결 거부] 등록되지 않은 단말기 접속 시도 - IP: {clientIP}, MAC: {cleanClientMac}"); + } + + // 켜져 있는 대수 카운트 갱신 + UpdateRunningCount(); + + return isMapped; // 매핑 성공 여부 반환 } - private bool PingTest(string ip) - { - if (string.IsNullOrEmpty(ip)) return false; - try - { - using (System.Net.NetworkInformation.Ping ping = new System.Net.NetworkInformation.Ping()) - { - var reply = ping.Send(ip, 1000); // 1초 타임아웃 - return reply.Status == System.Net.NetworkInformation.IPStatus.Success; - } - } - catch { return false; } - } - - // 서버 시작 함수 - private async Task StartServer() - { - try - { - if (_isRunning) return; // 이미 실행 중이면 중복 방지 - - _listener = new TcpListener(IPAddress.Any, 9999); - _listener.Start(); - MessageBox.Show("서버 연결 중..."); - _isRunning = true; - - while (_isRunning) - { - // 에이전트의 접속을 비동기로 대기 - TcpClient client = await _listener.AcceptTcpClientAsync(); - - // 에이전트 접속 처리 핸들러 실행 - _ = Task.Run(() => HandleAgent(client)); - } - } - catch (Exception ex) - { - // 리스너가 중단되었을 때의 예외 처리 제외 처리 - if (_isRunning) - { - MessageBox.Show("서버 구동 오류: " + ex.Message); - } - } - } - - // 에이전트에서 데이터 받기 - private void HandleAgent(TcpClient client) - { - try - { - string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); - - // using을 쓰지 않고 직접 스트림과 리더를 생성(함수가 끝나도 닫히지 않게) - NetworkStream stream = client.GetStream(); - System.IO.StreamReader reader = new System.IO.StreamReader(stream, Encoding.UTF8); - - // 에이전트가 보낸 첫 줄(MAC 주소) - string receivedMac = reader.ReadLine(); - - if (!string.IsNullOrEmpty(receivedMac)) - { - this.Invoke(new Action(() => { - // 기존 연결이 있다면 안전하게 닫고 교체 - if (_connectedAgents.ContainsKey(clientIP)) - { - try { _connectedAgents[clientIP].Close(); } catch { } - } - - _connectedAgents[clientIP] = client; // 소켓 유지 - UpdateAgentStatusByMac(receivedMac, clientIP); - })); - - // 서버 소켓이 닫히거나 에이전트가 끊어질 때까지 - // 이 스레드가 종료되지 않고 백그라운드에서 계속 대기하도록 루프를 돌려줌 - // (에이전트로부터 추가적인 메시지를 받을 때도 여기서 처리 가능) - while (client.Connected) - { - // 에이전트가 보낸 데이터가 없다면 여기서 대기 - // 에이전트가 연결을 끊으면 null이 들어와 루프를 빠져나감 - string agentMsg = reader.ReadLine(); - if (agentMsg == null) break; - } - } - } - catch (Exception ex) - { - Console.WriteLine("서버 핸들러 오류: " + ex.Message); - } - finally - { - // 연결이 최종적으로 끊기면 딕셔너리에서 제거 및 리소스 정리 - try - { - string clientIP = ((IPEndPoint)client.Client.RemoteEndPoint).Address.ToString(); - lock (_connectedAgents) - { - if (_connectedAgents.ContainsKey(clientIP)) - { - _connectedAgents.Remove(clientIP); - } - } - } - catch { } - client.Close(); - } - } - - // MAC 주소를 기반으로 에이전트 매칭 및 상태 업데이트 - private void UpdateAgentStatusByMac(string clientMac, string clientIP) - { - // 수신된 MAC에서 불필요한 문자 제거 - string cleanClientMac = clientMac.Replace("-", "").Replace(":", "").Replace(" ", "").ToUpper().Trim(); - - bool isMatched = false; - - this.Invoke(new Action(() => { - foreach (DataGridViewRow row in grdTotList.Rows) - { - string gridIp = row.Cells[2].Value.ToString(); - - if (gridIp == clientIP) - { - row.Cells[1].Value = cleanClientMac; - row.Cells[4].Value = "Start"; - row.Cells[4].Style.BackColor = Color.Green; - row.Cells[4].Style.ForeColor = Color.White; - row.Cells[5].Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); - - isMatched = true; - break; - } - } - - if (!isMatched) - { - Console.WriteLine($"매칭 실패! 수신된 MAC: {cleanClientMac}"); - } - - UpdateRunningCount(); - })); - } - - // 상단의 프로그램 실행 대수 갱신용 공통 메서드 private void UpdateRunningCount() { int startCount = 0; foreach (DataGridViewRow row in grdTotList.Rows) { if (row.IsNewRow) continue; - if (row.Cells[4].Value?.ToString() == "Start") + if (row.Cells[4].Value?.ToString().Contains("Start") == true) { startCount++; } @@ -277,175 +672,263 @@ namespace ControlServer private void btnRegister_MouseClick(object sender, MouseEventArgs e) { - // 단말기 등록 팝업 호출 - Form2 form = new Form2(); - - if (form.ShowDialog() == DialogResult.OK) + using (Form2 form = new Form2()) { - TerminalInfo data = form.TerminalData; - - grdTotList.Rows.Add( - data.TerminalName, // 단말기명 - data.Mac, // MAC 주소 - data.IP, // IP - "설정", // 통신설정 버튼 - "Stop", // 기본 상태 - DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), - data.Port // 포트번호 - ); - - int rowIndex = grdTotList.Rows.Count - 1; - - // 프로그램 열 스타일 지정 - DataGridViewCell cell4 = grdTotList.Rows[rowIndex].Cells[4]; - cell4.Style.BackColor = Color.Red; - cell4.Style.ForeColor = Color.White; - cell4.Style.Alignment = DataGridViewContentAlignment.MiddleCenter; - - // 통신설정 열 스타일 지정 - DataGridViewCell cell3 = grdTotList.Rows[rowIndex].Cells[3]; - cell3.Style.BackColor = Color.LightGray; - cell3.Style.ForeColor = Color.Black; - cell3.Style.Alignment = DataGridViewContentAlignment.MiddleCenter; - - // 현재 단말기 개수 업데이트 - lblTotCnt.Text = grdTotList.RowCount.ToString() + "대"; - - // 단말 등록 완료 후 리스너가 구동 중이 아니었다면 리스너 최초 구동 개시 - if (!_isRunning) + if (form.ShowDialog() == DialogResult.OK) { - _ = Task.Run(() => StartServer()); - } + TerminalInfo data = form.TerminalData; - // 기접속된 IP가 있는 장치라면 바로 연동 매칭 - if (!string.IsNullOrEmpty(_connectedIP) && _connectedIP == data.IP) - { - UpdateAgentStatusByMac(data.Mac, data.IP); + _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_MouseClick(object sender, MouseEventArgs e) + private void btnDataUpdate_Click(object sender, EventArgs e) { - // 1. 메시지 전송 - SendDataToAllAgents("데이터 전송 테스트(수동)"); + // 데이터 갱신 작업 실행 + Task.Run(() => + { + SendDataToAllAgents(); - // 2. 기존 로직 - Popup popup = new Popup("전체 단말기에 데이터 전송을 시도했습니다."); - popup.ShowDialog(); - CheckAllAlliveStatus(); + this.BeginInvoke(new Action(() => + { + CheckAllAliveStatus(); + })); + }); } - private void btnLog_MouseClick(object sender, MouseEventArgs e) + private async void grdTotList_CellDoubleClick(object sender, DataGridViewCellEventArgs e) { - Form4 form = new Form4(); - form.Show(); + 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; - // 통신설정 버튼 열 클릭 시 - if (e.ColumnIndex == 3) - { - var row = grdTotList.Rows[e.RowIndex]; - string TerminalName = row.Cells[0].Value?.ToString() ?? ""; - string TerminalIp = row.Cells[2].Value?.ToString() ?? ""; + var row = grdTotList.Rows[e.RowIndex]; + string terminalName = row.Cells[0].Value?.ToString() ?? ""; + string terminalIp = row.Cells[2].Value?.ToString() ?? ""; - Form3 form = new Form3(TerminalName, TerminalIp); - form.ShowDialog(); + 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) + else if (e.ColumnIndex == 4) // Start/Stop 상태 제어 버튼 클릭 { - DataGridViewRow row = grdTotList.Rows[e.RowIndex]; - DataGridViewCell cell4 = row.Cells[4]; + 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"; - string status = cell4.Value?.ToString() ?? ""; - string targetIP = row.Cells[2].Value?.ToString() ?? ""; - string targetMac = row.Cells[1].Value?.ToString() ?? ""; - - if (status == "Start") - { - // 에이전트 PC로 원격 강제 종료 패킷 전달 - SendStopCommandToAgent(targetIP); - - cell4.Value = "Stop"; - cell4.Style.BackColor = Color.Red; - cell4.Style.ForeColor = Color.White; - } - else - { - // WOL 매직 패킷 송신 - if (!string.IsNullOrEmpty(targetMac)) - { - SendWakeOnLan(targetMac); - - cell4.Value = "Start"; - cell4.Style.BackColor = Color.Green; - cell4.Style.ForeColor = Color.White; - } - else - { - MessageBox.Show("등록된 MAC 주소가 없습니다."); - } - } - - cell4.Style.Alignment = DataGridViewContentAlignment.MiddleCenter; - UpdateRunningCount(); + 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 { - if (_connectedAgents.ContainsKey(ip)) + lock (_agentLock) { - TcpClient client = _connectedAgents[ip]; - if (client.Connected) + if (_connectedAgents.TryGetValue(ip, out TcpClient client) && client.Connected) { - // 중요: 뒤에 "\n"을 추가하여 에이전트의 ReadLine()이 끝을 인식하게 합니다. 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("해당 에이전트는 현재 서버에 소켓으로 연결된 상태가 아닙니다."); } - } - else - { - MessageBox.Show("에이전트가 서버에 연결되어 있지 않습니다."); } } catch (Exception ex) { - MessageBox.Show("종료 명령 전송 실패: " + ex.Message); + Console.WriteLine("종료 명령 전송 실패: " + ex.Message); } } - // WOL 매직 패킷 전송 함수 private void SendWakeOnLan(string macAddress) { try { string cleanMac = macAddress.Replace("-", "").Replace(":", "").Replace(" ", "").Trim(); - - if (cleanMac.Length != 12) - { - MessageBox.Show("올바른 MAC 주소 형식이 아닙니다."); - return; - } + if (cleanMac.Length != 12) return; byte[] bytes = new byte[102]; - - for (int i = 0; i < 6; i++) - { - bytes[i] = 0xFF; - } + for (int i = 0; i < 6; i++) bytes[i] = 0xFF; byte[] macBytes = new byte[6]; for (int i = 0; i < 6; i++) @@ -463,41 +946,343 @@ namespace ControlServer client.Connect(IPAddress.Parse("255.255.255.255"), 9); client.Send(bytes, bytes.Length); } + Console.WriteLine($"[WOL 전송 완료] MAC: {cleanMac}"); } catch (Exception ex) { - MessageBox.Show("WOL 전송 실패: " + ex.Message); + Console.WriteLine("WOL 패킷 송신 예외 오류: " + ex.Message); } } - private void SendDataToAllAgents(string message) + private void SendDataToAllAgents() { - // 메시지 끝에 \n을 붙여서 에이전트가 ReadLine()으로 잘 읽게함 - byte[] data = Encoding.UTF8.GetBytes(message + "\n"); - - lock (_connectedAgents) // 여러 스레드에서 접근할 수 있으므로 lock 사용 + lock (_agentLock) { foreach (var kvp in _connectedAgents) { - try + string agentIP = kvp.Key; + TcpClient client = kvp.Value; + + if (client != null && client.Connected) { - TcpClient client = kvp.Value; - if (client != null && client.Connected) + try { - NetworkStream stream = client.GetStream(); - stream.Write(data, 0, data.Length); - stream.Flush(); + 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}"); } - } - catch (Exception ex) - { - Console.WriteLine($"전송 실패 ({kvp.Key}): {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); + } + } + } } } \ No newline at end of file diff --git a/ControlServer/Form1.resx b/ControlServer/Form1.resx index d7de2eb..5405830 100644 --- a/ControlServer/Form1.resx +++ b/ControlServer/Form1.resx @@ -138,4 +138,13 @@ True + + True + + + True + + + True + \ No newline at end of file diff --git a/ControlServer/Form2.Designer.cs b/ControlServer/Form2.Designer.cs index 51f9174..5288d5c 100644 --- a/ControlServer/Form2.Designer.cs +++ b/ControlServer/Form2.Designer.cs @@ -36,52 +36,54 @@ btnCancel = new Button(); txtBoxPort = new TextBox(); lbl5 = new Label(); + txtBoxMac = new TextBox(); + lbl2 = new Label(); + lblTitle = new Label(); + lblBox = new Label(); SuspendLayout(); // // lbl0 // - lbl0.BackColor = SystemColors.ControlDark; - lbl0.BorderStyle = BorderStyle.FixedSingle; - lbl0.Location = new Point(277, 106); + lbl0.BackColor = Color.White; + lbl0.Location = new Point(195, 116); lbl0.Name = "lbl0"; - lbl0.Size = new Size(293, 50); + lbl0.Size = new Size(270, 50); lbl0.TabIndex = 0; lbl0.Text = "단말기명"; lbl0.TextAlign = ContentAlignment.MiddleLeft; // // lbl1 // - lbl1.BackColor = SystemColors.ControlDark; - lbl1.BorderStyle = BorderStyle.FixedSingle; - lbl1.Location = new Point(277, 156); + lbl1.BackColor = Color.White; + lbl1.Location = new Point(195, 166); lbl1.Name = "lbl1"; - lbl1.Size = new Size(293, 50); + lbl1.Size = new Size(270, 50); lbl1.TabIndex = 1; lbl1.Text = "IP"; lbl1.TextAlign = ContentAlignment.MiddleLeft; // // txtBoxName // - txtBoxName.Location = new Point(572, 106); + txtBoxName.Location = new Point(471, 127); txtBoxName.Multiline = true; txtBoxName.Name = "txtBoxName"; - txtBoxName.Size = new Size(507, 50); + txtBoxName.Size = new Size(350, 32); txtBoxName.TabIndex = 4; // // txtBoxIP // - txtBoxIP.Location = new Point(572, 156); + txtBoxIP.Location = new Point(471, 177); txtBoxIP.Multiline = true; txtBoxIP.Name = "txtBoxIP"; - txtBoxIP.Size = new Size(507, 50); + txtBoxIP.Size = new Size(350, 32); txtBoxIP.TabIndex = 5; // // btnRegister // - btnRegister.BackColor = SystemColors.Desktop; + btnRegister.BackColor = Color.RoyalBlue; btnRegister.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); btnRegister.ForeColor = SystemColors.Window; - btnRegister.Location = new Point(460, 291); + btnRegister.Location = new Point(296, 366); btnRegister.Name = "btnRegister"; btnRegister.Size = new Size(174, 56); btnRegister.TabIndex = 9; @@ -92,7 +94,7 @@ // btnCancel // btnCancel.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); - btnCancel.Location = new Point(710, 291); + btnCancel.Location = new Point(535, 366); btnCancel.Name = "btnCancel"; btnCancel.Size = new Size(174, 56); btnCancel.TabIndex = 10; @@ -102,28 +104,67 @@ // // txtBoxPort // - txtBoxPort.Location = new Point(572, 206); + txtBoxPort.Location = new Point(471, 227); txtBoxPort.Multiline = true; txtBoxPort.Name = "txtBoxPort"; - txtBoxPort.Size = new Size(507, 50); + txtBoxPort.Size = new Size(350, 32); txtBoxPort.TabIndex = 12; // // lbl5 // - lbl5.BackColor = SystemColors.ControlDark; - lbl5.BorderStyle = BorderStyle.FixedSingle; - lbl5.Location = new Point(277, 206); + lbl5.BackColor = Color.White; + lbl5.Location = new Point(195, 216); lbl5.Name = "lbl5"; - lbl5.Size = new Size(293, 50); + lbl5.Size = new Size(270, 50); lbl5.TabIndex = 11; lbl5.Text = "Port"; lbl5.TextAlign = ContentAlignment.MiddleLeft; // + // txtBoxMac + // + txtBoxMac.Location = new Point(471, 276); + txtBoxMac.Multiline = true; + txtBoxMac.Name = "txtBoxMac"; + txtBoxMac.Size = new Size(350, 32); + txtBoxMac.TabIndex = 14; + // + // lbl2 + // + lbl2.BackColor = Color.White; + lbl2.Location = new Point(195, 266); + lbl2.Name = "lbl2"; + lbl2.Size = new Size(270, 50); + lbl2.TabIndex = 13; + lbl2.Text = "MAC"; + lbl2.TextAlign = ContentAlignment.MiddleLeft; + // + // lblTitle + // + lblTitle.AutoSize = true; + lblTitle.BackColor = Color.White; + lblTitle.Font = new Font("맑은 고딕", 18F, FontStyle.Bold, GraphicsUnit.Point, 129); + lblTitle.Location = new Point(92, 34); + lblTitle.Name = "lblTitle"; + lblTitle.Size = new Size(179, 41); + lblTitle.TabIndex = 15; + lblTitle.Text = "단말기 등록"; + // + // lblBox + // + lblBox.BackColor = Color.White; + lblBox.Location = new Point(56, 23); + lblBox.Name = "lblBox"; + lblBox.Size = new Size(953, 306); + lblBox.TabIndex = 16; + // // Form2 // AutoScaleDimensions = new SizeF(9F, 20F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(1357, 737); + ClientSize = new Size(1070, 461); + Controls.Add(lblTitle); + Controls.Add(txtBoxMac); + Controls.Add(lbl2); Controls.Add(txtBoxPort); Controls.Add(lbl5); Controls.Add(btnCancel); @@ -132,6 +173,7 @@ Controls.Add(txtBoxName); Controls.Add(lbl1); Controls.Add(lbl0); + Controls.Add(lblBox); Name = "Form2"; ResumeLayout(false); PerformLayout(); @@ -147,5 +189,9 @@ private Button btnCancel; private TextBox txtBoxPort; private Label lbl5; + private TextBox txtBoxMac; + private Label lbl2; + private Label lblTitle; + private Label lblBox; } } \ No newline at end of file diff --git a/ControlServer/Form2.cs b/ControlServer/Form2.cs index e3e82b9..1497501 100644 --- a/ControlServer/Form2.cs +++ b/ControlServer/Form2.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; +using System.Drawing.Drawing2D; using System.Text; using System.Windows.Forms; @@ -12,31 +13,106 @@ namespace ControlServer { public TerminalInfo TerminalData { get; private set; } = new TerminalInfo(); + // 둥근 모서리 반지름 설정 + private const int CornerRadius = 15; + public Form2() { InitializeComponent(); + // 1. 버튼 테두리 없애고 Flat 스타일로 변경 + ConfigureModernButtons(); + btnRegister.BackColor = Color.FromArgb(160, 180, 240); // 연해진 블루 btnRegister.Enabled = false; // 저장버튼 비활성화 + // 모든 텍스트박스의 값이 변할 때마다 실행될 이벤트 연결 txtBoxName.TextChanged += ValidateInputs; txtBoxIP.TextChanged += ValidateInputs; txtBoxPort.TextChanged += ValidateInputs; + txtBoxMac.TextChanged += ValidateInputs; + + // 2. 둥근 모서리를 그리기 위한 Paint 이벤트 연결 + if (lblBox != null) lblBox.Paint += lblBox_Paint; + btnRegister.Paint += Button_Paint; + btnCancel.Paint += Button_Paint; + } + + private void ConfigureModernButtons() + { + // 등록 버튼 스타일 설정 + btnRegister.FlatStyle = FlatStyle.Flat; + btnRegister.FlatAppearance.BorderSize = 0; // 보더라인 삭제 + + // 취소 버튼 스타일 설정 + btnCancel.BackColor = Color.FromArgb(230, 235, 240); + btnCancel.FlatStyle = FlatStyle.Flat; + btnCancel.FlatAppearance.BorderSize = 0; // 보더라인 삭제 + } + + // 컨트롤을 둥글게 만들기 위한 공통 경로 생성 메서드 + private GraphicsPath GetRoundPath(Rectangle bounds, int radius) + { + GraphicsPath path = new GraphicsPath(); + int diameter = radius * 2; + + path.AddArc(bounds.X, bounds.Y, diameter, diameter, 180, 90); + path.AddArc(bounds.Right - diameter, bounds.Y, diameter, diameter, 270, 90); + path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90); + path.AddArc(bounds.X, bounds.Bottom - diameter, diameter, diameter, 90, 90); + path.CloseAllFigures(); + + return path; + } + + // 흰색 배경 박스 둥글게 자르기 + private void lblBox_Paint(object? sender, PaintEventArgs e) + { + if (sender is Panel panel) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath path = GetRoundPath(new Rectangle(0, 0, panel.Width, panel.Height), CornerRadius)) + { + panel.Region = new Region(path); + } + } + } + + // 버튼 둥글게 자르기 + private void Button_Paint(object? sender, PaintEventArgs e) + { + if (sender is Button button) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + // 버튼은 박스보다 조금 더 작으므로 반지름을 10 정도로 주면 예쁩니다 + using (GraphicsPath path = GetRoundPath(new Rectangle(0, 0, button.Width, button.Height), 10)) + { + button.Region = new Region(path); + } + } } private void ValidateInputs(object? sender, EventArgs e) { - // 모든 텍스트박스가 공백이 아닐 때만 true bool isValid = !string.IsNullOrWhiteSpace(txtBoxName.Text) && !string.IsNullOrWhiteSpace(txtBoxIP.Text) && - !string.IsNullOrWhiteSpace(txtBoxPort.Text); + !string.IsNullOrWhiteSpace(txtBoxPort.Text) && + !string.IsNullOrWhiteSpace(txtBoxMac.Text); - // 저장 버튼 활성화 상태 조절 btnRegister.Enabled = isValid; + + // [팁] 비활성화 상태일 때 버튼 색상 피드백을 주면 더 자연스럽습니다. + if (isValid) + { + btnRegister.BackColor = Color.FromArgb(65, 105, 225); // 원래 로열 블루 색상 + } + else + { + btnRegister.BackColor = Color.FromArgb(160, 180, 240); // 연해진 블루 + } } private void btnCancel_MouseClick(object sender, MouseEventArgs e) { - // 취소 버튼 this.Close(); } @@ -44,17 +120,23 @@ namespace ControlServer { // 1. DB 중복 데이터 확인 - // 2. 중복 데이터 없을 경우 DB에 데이터 추가 - // DB 가져오면 데이터 넘겨주는게 아니라 테이블에 데이터 추가하고 Form1 화면에서 데이터 가져와 리스트 보여주는 형식으로 변경할 예정 - Popup popup = new Popup("등록되었습니다."); + // 2. 메시지박스 띄우기 (알림 아이콘과 OK 버튼 포함) + DialogResult result = MessageBox.Show( + "등록되었습니다.", + "알림", + MessageBoxButtons.OK, + MessageBoxIcon.Information + ); - if (popup.ShowDialog() == DialogResult.OK) + // 사용자가 메시지박스의 '확인'을 누르면 데이터 저장 진행 + if (result == DialogResult.OK) { this.TerminalData = new TerminalInfo { TerminalName = txtBoxName.Text.Trim(), IP = txtBoxIP.Text.Trim(), - Port = txtBoxPort.Text.Trim() + Port = txtBoxPort.Text.Trim(), + Mac = txtBoxMac.Text.Trim(), }; } @@ -62,4 +144,4 @@ namespace ControlServer this.Close(); } } -} +} \ No newline at end of file diff --git a/ControlServer/Form3.Designer.cs b/ControlServer/Form3.Designer.cs index aa0e3a8..0877cf7 100644 --- a/ControlServer/Form3.Designer.cs +++ b/ControlServer/Form3.Designer.cs @@ -28,6 +28,7 @@ /// private void InitializeComponent() { + DataGridViewCellStyle dataGridViewCellStyle1 = new DataGridViewCellStyle(); lblTerminalName = new Label(); lblTerminalIP = new Label(); label1 = new Label(); @@ -36,28 +37,42 @@ label3 = new Label(); label4 = new Label(); pannUpdate = new Panel(); - rdoUpdate5m = new RadioButton(); - rdoUpdate3m = new RadioButton(); - rdoUpdate1m = new RadioButton(); + label7 = new Label(); + txtBoxDisplay = new TextBox(); pannStartTime = new Panel(); + label8 = new Label(); + txtBoxOnSS = new TextBox(); label5 = new Label(); txtBoxOnMM = new TextBox(); txtBoxOnYY = new TextBox(); pannStopTime = new Panel(); + label9 = new Label(); + txtBoxOffSS = new TextBox(); label6 = new Label(); txtBoxOffMM = new TextBox(); txtBoxOffYY = new TextBox(); btnCancel = new Button(); btnUpdate = new Button(); + grdDataList = new DataGridView(); + lblBox = new Label(); + Col0 = new DataGridViewTextBoxColumn(); + Col1 = new DataGridViewTextBoxColumn(); + Col2 = new DataGridViewTextBoxColumn(); + Col3 = new DataGridViewTextBoxColumn(); + Col4 = new DataGridViewTextBoxColumn(); + Col5 = new DataGridViewTextBoxColumn(); + Col6 = new DataGridViewTextBoxColumn(); pannUpdate.SuspendLayout(); pannStartTime.SuspendLayout(); pannStopTime.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)grdDataList).BeginInit(); SuspendLayout(); // // lblTerminalName // + lblTerminalName.BackColor = Color.White; lblTerminalName.Font = new Font("맑은 고딕", 9F, FontStyle.Bold, GraphicsUnit.Point, 129); - lblTerminalName.Location = new Point(430, 42); + lblTerminalName.Location = new Point(256, 102); lblTerminalName.Name = "lblTerminalName"; lblTerminalName.Size = new Size(288, 50); lblTerminalName.TabIndex = 0; @@ -65,7 +80,8 @@ // // lblTerminalIP // - lblTerminalIP.Location = new Point(430, 118); + lblTerminalIP.BackColor = Color.White; + lblTerminalIP.Location = new Point(256, 178); lblTerminalIP.Name = "lblTerminalIP"; lblTerminalIP.Size = new Size(288, 50); lblTerminalIP.TabIndex = 1; @@ -73,7 +89,8 @@ // // label1 // - label1.Location = new Point(254, 42); + label1.BackColor = Color.White; + label1.Location = new Point(103, 102); label1.Name = "label1"; label1.Size = new Size(147, 50); label1.TabIndex = 2; @@ -82,7 +99,8 @@ // // label2 // - label2.Location = new Point(254, 118); + label2.BackColor = Color.White; + label2.Location = new Point(103, 178); label2.Name = "label2"; label2.Size = new Size(147, 50); label2.TabIndex = 3; @@ -91,20 +109,18 @@ // // lbl0 // - lbl0.BackColor = SystemColors.ControlDark; - lbl0.BorderStyle = BorderStyle.FixedSingle; - lbl0.Location = new Point(254, 223); + lbl0.BackColor = Color.White; + lbl0.Location = new Point(97, 285); lbl0.Name = "lbl0"; lbl0.Size = new Size(159, 50); lbl0.TabIndex = 4; - lbl0.Text = "갱신주기"; + lbl0.Text = "표출시간"; lbl0.TextAlign = ContentAlignment.MiddleLeft; // // label3 // - label3.BackColor = SystemColors.ControlDark; - label3.BorderStyle = BorderStyle.FixedSingle; - label3.Location = new Point(254, 273); + label3.BackColor = Color.White; + label3.Location = new Point(97, 335); label3.Name = "label3"; label3.Size = new Size(159, 50); label3.TabIndex = 5; @@ -113,9 +129,8 @@ // // label4 // - label4.BackColor = SystemColors.ControlDark; - label4.BorderStyle = BorderStyle.FixedSingle; - label4.Location = new Point(254, 323); + label4.BackColor = Color.White; + label4.Location = new Point(97, 385); label4.Name = "label4"; label4.Size = new Size(159, 50); label4.TabIndex = 6; @@ -124,59 +139,61 @@ // // pannUpdate // - pannUpdate.BorderStyle = BorderStyle.FixedSingle; - pannUpdate.Controls.Add(rdoUpdate5m); - pannUpdate.Controls.Add(rdoUpdate3m); - pannUpdate.Controls.Add(rdoUpdate1m); - pannUpdate.Location = new Point(413, 223); + pannUpdate.BackColor = Color.White; + pannUpdate.Controls.Add(label7); + pannUpdate.Controls.Add(txtBoxDisplay); + pannUpdate.Location = new Point(256, 285); pannUpdate.Name = "pannUpdate"; pannUpdate.Size = new Size(356, 50); pannUpdate.TabIndex = 7; // - // rdoUpdate5m + // label7 // - rdoUpdate5m.AutoSize = true; - rdoUpdate5m.Location = new Point(157, 12); - rdoUpdate5m.Name = "rdoUpdate5m"; - rdoUpdate5m.Size = new Size(53, 24); - rdoUpdate5m.TabIndex = 2; - rdoUpdate5m.TabStop = true; - rdoUpdate5m.Text = "5분"; - rdoUpdate5m.UseVisualStyleBackColor = true; + label7.AutoSize = true; + label7.Location = new Point(86, 14); + label7.Name = "label7"; + label7.Size = new Size(24, 20); + label7.TabIndex = 1; + label7.Text = "초"; // - // rdoUpdate3m + // txtBoxDisplay // - rdoUpdate3m.AutoSize = true; - rdoUpdate3m.Location = new Point(86, 12); - rdoUpdate3m.Name = "rdoUpdate3m"; - rdoUpdate3m.Size = new Size(53, 24); - rdoUpdate3m.TabIndex = 1; - rdoUpdate3m.TabStop = true; - rdoUpdate3m.Text = "3분"; - rdoUpdate3m.UseVisualStyleBackColor = true; - // - // rdoUpdate1m - // - rdoUpdate1m.AutoSize = true; - rdoUpdate1m.Location = new Point(16, 12); - rdoUpdate1m.Name = "rdoUpdate1m"; - rdoUpdate1m.Size = new Size(53, 24); - rdoUpdate1m.TabIndex = 0; - rdoUpdate1m.TabStop = true; - rdoUpdate1m.Text = "1분"; - rdoUpdate1m.UseVisualStyleBackColor = true; + txtBoxDisplay.Location = new Point(16, 11); + txtBoxDisplay.Name = "txtBoxDisplay"; + txtBoxDisplay.Size = new Size(64, 27); + txtBoxDisplay.TabIndex = 0; + txtBoxDisplay.Text = "5"; // // pannStartTime // - pannStartTime.BorderStyle = BorderStyle.FixedSingle; + pannStartTime.BackColor = Color.White; + pannStartTime.Controls.Add(label8); + pannStartTime.Controls.Add(txtBoxOnSS); pannStartTime.Controls.Add(label5); pannStartTime.Controls.Add(txtBoxOnMM); pannStartTime.Controls.Add(txtBoxOnYY); - pannStartTime.Location = new Point(413, 273); + pannStartTime.Location = new Point(256, 335); pannStartTime.Name = "pannStartTime"; pannStartTime.Size = new Size(356, 50); pannStartTime.TabIndex = 8; // + // label8 + // + label8.AutoSize = true; + label8.Location = new Point(171, 14); + label8.Name = "label8"; + label8.Size = new Size(12, 20); + label8.TabIndex = 4; + label8.Text = ":"; + // + // txtBoxOnSS + // + txtBoxOnSS.Location = new Point(185, 11); + txtBoxOnSS.Name = "txtBoxOnSS"; + txtBoxOnSS.Size = new Size(64, 27); + txtBoxOnSS.TabIndex = 3; + txtBoxOnSS.TextAlign = HorizontalAlignment.Center; + // // label5 // label5.AutoSize = true; @@ -204,15 +221,34 @@ // // pannStopTime // - pannStopTime.BorderStyle = BorderStyle.FixedSingle; + pannStopTime.BackColor = Color.White; + pannStopTime.Controls.Add(label9); + pannStopTime.Controls.Add(txtBoxOffSS); pannStopTime.Controls.Add(label6); pannStopTime.Controls.Add(txtBoxOffMM); pannStopTime.Controls.Add(txtBoxOffYY); - pannStopTime.Location = new Point(413, 323); + pannStopTime.Location = new Point(256, 385); pannStopTime.Name = "pannStopTime"; pannStopTime.Size = new Size(356, 50); pannStopTime.TabIndex = 9; // + // label9 + // + label9.AutoSize = true; + label9.Location = new Point(169, 14); + label9.Name = "label9"; + label9.Size = new Size(12, 20); + label9.TabIndex = 7; + label9.Text = ":"; + // + // txtBoxOffSS + // + txtBoxOffSS.Location = new Point(185, 11); + txtBoxOffSS.Name = "txtBoxOffSS"; + txtBoxOffSS.Size = new Size(64, 27); + txtBoxOffSS.TabIndex = 6; + txtBoxOffSS.TextAlign = HorizontalAlignment.Center; + // // label6 // label6.AutoSize = true; @@ -241,7 +277,7 @@ // btnCancel // btnCancel.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); - btnCancel.Location = new Point(559, 408); + btnCancel.Location = new Point(386, 497); btnCancel.Name = "btnCancel"; btnCancel.Size = new Size(174, 56); btnCancel.TabIndex = 12; @@ -251,10 +287,10 @@ // // btnUpdate // - btnUpdate.BackColor = SystemColors.Desktop; + btnUpdate.BackColor = Color.RoyalBlue; btnUpdate.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); btnUpdate.ForeColor = SystemColors.Window; - btnUpdate.Location = new Point(309, 408); + btnUpdate.Location = new Point(136, 497); btnUpdate.Name = "btnUpdate"; btnUpdate.Size = new Size(174, 56); btnUpdate.TabIndex = 11; @@ -262,11 +298,92 @@ btnUpdate.UseVisualStyleBackColor = false; btnUpdate.MouseClick += btnUpdate_MouseClick; // + // grdDataList + // + dataGridViewCellStyle1.Alignment = DataGridViewContentAlignment.MiddleLeft; + dataGridViewCellStyle1.BackColor = SystemColors.Control; + dataGridViewCellStyle1.Font = new Font("맑은 고딕", 7.8F, FontStyle.Regular, GraphicsUnit.Point, 129); + dataGridViewCellStyle1.ForeColor = SystemColors.WindowText; + dataGridViewCellStyle1.SelectionBackColor = SystemColors.Highlight; + dataGridViewCellStyle1.SelectionForeColor = SystemColors.HighlightText; + dataGridViewCellStyle1.WrapMode = DataGridViewTriState.True; + grdDataList.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle1; + grdDataList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.AutoSize; + grdDataList.Columns.AddRange(new DataGridViewColumn[] { Col0, Col1, Col2, Col3, Col4, Col5, Col6 }); + grdDataList.Location = new Point(704, 46); + grdDataList.Name = "grdDataList"; + grdDataList.RowHeadersWidth = 51; + grdDataList.Size = new Size(641, 679); + grdDataList.TabIndex = 13; + // + // lblBox + // + lblBox.BackColor = Color.White; + lblBox.Location = new Point(43, 46); + lblBox.Name = "lblBox"; + lblBox.Size = new Size(597, 542); + lblBox.TabIndex = 14; + // + // Col0 + // + Col0.HeaderText = "경매일자"; + Col0.MinimumWidth = 6; + Col0.Name = "Col0"; + Col0.ReadOnly = true; + Col0.Width = 125; + // + // Col1 + // + Col1.HeaderText = "품목명"; + Col1.MinimumWidth = 6; + Col1.Name = "Col1"; + Col1.ReadOnly = true; + Col1.Width = 125; + // + // Col2 + // + Col2.HeaderText = "산지"; + Col2.MinimumWidth = 6; + Col2.Name = "Col2"; + Col2.ReadOnly = true; + Col2.Width = 125; + // + // Col3 + // + Col3.HeaderText = "전체수량"; + Col3.MinimumWidth = 6; + Col3.Name = "Col3"; + Col3.ReadOnly = true; + Col3.Width = 80; + // + // Col4 + // + Col4.HeaderText = "정가수의 수량"; + Col4.MinimumWidth = 6; + Col4.Name = "Col4"; + Col4.ReadOnly = true; + // + // Col5 + // + Col5.HeaderText = "전체물량"; + Col5.MinimumWidth = 6; + Col5.Name = "Col5"; + Col5.ReadOnly = true; + Col5.Width = 80; + // + // Col6 + // + Col6.HeaderText = "정가수의 물량"; + Col6.MinimumWidth = 6; + Col6.Name = "Col6"; + Col6.ReadOnly = true; + // // Form3 // AutoScaleDimensions = new SizeF(9F, 20F); AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(1008, 563); + ClientSize = new Size(1357, 737); + Controls.Add(grdDataList); Controls.Add(btnCancel); Controls.Add(btnUpdate); Controls.Add(pannStopTime); @@ -279,14 +396,16 @@ Controls.Add(label1); Controls.Add(lblTerminalIP); Controls.Add(lblTerminalName); + Controls.Add(lblBox); Name = "Form3"; - Text = "Form3"; + Text = "S"; pannUpdate.ResumeLayout(false); pannUpdate.PerformLayout(); pannStartTime.ResumeLayout(false); pannStartTime.PerformLayout(); pannStopTime.ResumeLayout(false); pannStopTime.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)grdDataList).EndInit(); ResumeLayout(false); } @@ -302,9 +421,6 @@ private Panel pannUpdate; private Panel pannStartTime; private Panel pannStopTime; - private RadioButton rdoUpdate1m; - private RadioButton rdoUpdate5m; - private RadioButton rdoUpdate3m; private TextBox txtBoxOnYY; private TextBox txtBoxOnMM; private Label label5; @@ -313,5 +429,20 @@ private TextBox txtBoxOffYY; private Button btnCancel; private Button btnUpdate; + private TextBox txtBoxDisplay; + private Label label7; + private Label label8; + private TextBox txtBoxOnSS; + private Label label9; + private TextBox txtBoxOffSS; + private DataGridView grdDataList; + private Label lblBox; + private DataGridViewTextBoxColumn Col0; + private DataGridViewTextBoxColumn Col1; + private DataGridViewTextBoxColumn Col2; + private DataGridViewTextBoxColumn Col3; + private DataGridViewTextBoxColumn Col4; + private DataGridViewTextBoxColumn Col5; + private DataGridViewTextBoxColumn Col6; } } \ No newline at end of file diff --git a/ControlServer/Form3.cs b/ControlServer/Form3.cs index 7f7b793..863f05a 100644 --- a/ControlServer/Form3.cs +++ b/ControlServer/Form3.cs @@ -3,44 +3,220 @@ using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; +using System.Drawing.Drawing2D; // 둥근 모서리 그리기(GraphicsPath)에 필수 +using System.IO; using System.Text; using System.Windows.Forms; +using System.Xml.Linq; namespace ControlServer { public partial class Form3 : Form { - public Form3(string terminalName, string terminalIP) // 매개변수 추가 + private Form1 _mainForm; // 메인 폼 참조 변수 + private string _targetIP; + private string _filePath; + + // 둥근 모서리 반지름 설정 (정수형 int로 선언) + private const int CornerRadiusValue = 15; + + public Form3(Form1 mainForm, string terminalName, string terminalIP, string filePath) { InitializeComponent(); - // 가져온 데이터를 Form3의 라벨이나 텍스트박스에 표시 + // 1. 버튼 테두리 없애고 Flat 스타일 및 이벤트 연결 + ConfigureModernStyles(); + + // 2. 그리드 디자인을 오른쪽 시안 스타일에 맞게 초기화 + InitDataGridViewStyles(); + + _mainForm = mainForm; + _targetIP = terminalIP; + _filePath = filePath; + lblTerminalName.Text = terminalName; lblTerminalIP.Text = terminalIP; + string[] timeData = _mainForm.GetProgramTimesByIp(_targetIP); + + txtBoxDisplay.Text = string.IsNullOrEmpty(timeData[0]) ? "5" : timeData[0]; + + if (!string.IsNullOrEmpty(timeData[1]) && timeData[1].Contains(":")) + { + string[] onTime = timeData[1].Split(':'); + txtBoxOnYY.Text = onTime[0]; + txtBoxOnMM.Text = onTime[1]; + txtBoxOnSS.Text = onTime[2]; + } + + if (!string.IsNullOrEmpty(timeData[2]) && timeData[2].Contains(":")) + { + string[] offTime = timeData[2].Split(':'); + txtBoxOffYY.Text = offTime[0]; + txtBoxOffMM.Text = offTime[1]; + txtBoxOffSS.Text = offTime[2]; + } + txtBoxOnYY.TextChanged += ValidateInputs; txtBoxOnMM.TextChanged += ValidateInputs; + txtBoxOnSS.TextChanged += ValidateInputs; txtBoxOffYY.TextChanged += ValidateInputs; txtBoxOffMM.TextChanged += ValidateInputs; + txtBoxOffSS.TextChanged += ValidateInputs; + txtBoxOnYY.PlaceholderText = "YY"; txtBoxOnMM.PlaceholderText = "MM"; + txtBoxOnSS.PlaceholderText = "SS"; txtBoxOffYY.PlaceholderText = "YY"; txtBoxOffMM.PlaceholderText = "MM"; + txtBoxOffSS.PlaceholderText = "SS"; + + LoadAndParseFile(); + } + + // ⭐️ 오른쪽 그리드뷰 디자인 시안을 반영한 메서드 + // ⭐️ 오른쪽 그리드뷰 디자인 시안을 반영한 메서드 + private void InitDataGridViewStyles() + { + grdDataList.AllowUserToResizeRows = false; + grdDataList.AllowUserToResizeColumns = false; + grdDataList.RowHeadersVisible = false; // 왼쪽 화살표 열 숨김 + grdDataList.AllowUserToAddRows = false; // 빈 칸 추가 방지 + grdDataList.ReadOnly = true; // 읽기 전용 + grdDataList.SelectionMode = DataGridViewSelectionMode.FullRowSelect; // 행 선택 + grdDataList.MultiSelect = false; + + // 커스텀 헤더 스타일을 적용하기 위해 반드시 false 설정 + grdDataList.EnableHeadersVisualStyles = false; + + // 그리드 전체 배경 및 기본 테두리 제거 (시안처럼 하얗고 투명하게) + grdDataList.BackgroundColor = Color.White; + grdDataList.BorderStyle = BorderStyle.None; + + // 셀 구분선 설정 (옅은 회색 가로선만 남김) + grdDataList.CellBorderStyle = DataGridViewCellBorderStyle.SingleHorizontal; + grdDataList.GridColor = Color.FromArgb(240, 242, 245); + + // [상단 헤더 스타일 수정] + DataGridViewCellStyle headerStyle = new DataGridViewCellStyle(); + headerStyle.BackColor = Color.FromArgb(44, 62, 80); + headerStyle.ForeColor = Color.White; + headerStyle.Font = new Font("맑은 고딕", 9.5F, FontStyle.Bold); + headerStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + + // 행을 선택하더라도 헤더의 선택 배경색/글자색이 기본 배경색과 똑같이 유지되도록 고정 + headerStyle.SelectionBackColor = Color.FromArgb(44, 62, 80); + headerStyle.SelectionForeColor = Color.White; + + grdDataList.ColumnHeadersDefaultCellStyle = headerStyle; + grdDataList.ColumnHeadersHeight = 38; // 헤더 높이 여유롭게 조정 + grdDataList.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; + + // 행 선택 시 헤더 스타일 상속으로 인한 테두리 및 색상 왜곡 방지 + grdDataList.EnableHeadersVisualStyles = false; + + // [일반 데이터 셀 스타일 수정] + DataGridViewCellStyle cellStyle = new DataGridViewCellStyle(); + cellStyle.BackColor = Color.White; + cellStyle.ForeColor = Color.FromArgb(50, 50, 50); // 부드러운 차콜 블랙 + cellStyle.Font = new Font("맑은 고딕", 9F, FontStyle.Regular); + cellStyle.Alignment = DataGridViewContentAlignment.MiddleCenter; + + // 행 선택(클릭) 시 파란색 대신 부드러운 회청색으로 채우기 + cellStyle.SelectionBackColor = Color.FromArgb(242, 246, 252); + cellStyle.SelectionForeColor = Color.FromArgb(50, 50, 50); + + grdDataList.DefaultCellStyle = cellStyle; + + // 시안처럼 행 간격(높이)을 시원하게 벌려 가시성을 높임 + grdDataList.RowTemplate.Height = 38; + + grdDataList.ClearSelection(); + } + + private void ConfigureModernStyles() + { + // 수정 버튼 스타일 설정 (테두리 삭제) + btnUpdate.FlatStyle = FlatStyle.Flat; + btnUpdate.FlatAppearance.BorderSize = 0; + + // 취소 버튼 스타일 설정 (테두리 삭제 및 부드러운 회색) + btnCancel.FlatStyle = FlatStyle.Flat; + btnCancel.FlatAppearance.BorderSize = 0; + btnCancel.BackColor = Color.FromArgb(230, 235, 240); + btnCancel.ForeColor = Color.FromArgb(80, 80, 80); + + // 둥근 처리를 위한 Paint 이벤트 연결 + if (lblBox != null) lblBox.Paint += lblBox_Paint; + btnUpdate.Paint += Button_Paint; + btnCancel.Paint += Button_Paint; + } + + // 둥근 모서리 경로를 만들어주는 공통 메서드 + private GraphicsPath GetRoundPath(Rectangle bounds, int radius) + { + GraphicsPath path = new GraphicsPath(); + int diameter = radius * 2; + + path.AddArc(bounds.X, bounds.Y, diameter, diameter, 180, 90); + path.AddArc(bounds.Right - diameter, bounds.Y, diameter, diameter, 270, 90); + path.AddArc(bounds.Right - diameter, bounds.Bottom - diameter, diameter, diameter, 0, 90); + path.AddArc(bounds.X, bounds.Bottom - diameter, diameter, diameter, 90, 90); + path.CloseAllFigures(); + + return path; + } + + // 흰색 배경 박스 둥글게 자르기 + private void lblBox_Paint(object? sender, PaintEventArgs e) + { + if (sender is Control control) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath path = GetRoundPath(new Rectangle(0, 0, control.Width, control.Height), CornerRadiusValue)) + { + control.Region = new Region(path); + } + } + } + + // 버튼들 둥글게 자르기 + private void Button_Paint(object? sender, PaintEventArgs e) + { + if (sender is Button button) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + using (GraphicsPath path = GetRoundPath(new Rectangle(0, 0, button.Width, button.Height), 10)) + { + button.Region = new Region(path); + } + } } private void ValidateInputs(object? sender, EventArgs e) { - // 프로그램 시간 조건 체크 bool isOnYYEmpty = string.IsNullOrWhiteSpace(txtBoxOnYY.Text); bool isOnMMEmpty = string.IsNullOrWhiteSpace(txtBoxOnMM.Text); + bool isOnSSEmpty = string.IsNullOrWhiteSpace(txtBoxOnSS.Text); bool isOffYYEmpty = string.IsNullOrWhiteSpace(txtBoxOffYY.Text); - bool isOffMMEmpty = string.IsNullOrWhiteSpace(@txtBoxOffMM.Text); + bool isOffMMEmpty = string.IsNullOrWhiteSpace(txtBoxOffMM.Text); + bool isOffSSEmpty = string.IsNullOrWhiteSpace(txtBoxOffSS.Text); - // 둘 다 비어있거나(AND), 둘 다 비어있지 않거나(AND) - bool isTimeValid = (isOnYYEmpty && isOnMMEmpty && isOffYYEmpty && isOffMMEmpty) || (!isOnYYEmpty && !isOnMMEmpty && !isOffYYEmpty && !isOffMMEmpty); + bool isTimeValid = (isOnYYEmpty && isOnMMEmpty && isOnSSEmpty && isOffYYEmpty && isOffMMEmpty && isOffSSEmpty) || + (!isOnYYEmpty && !isOnMMEmpty && !isOnSSEmpty && !isOffYYEmpty && !isOffMMEmpty && !isOffSSEmpty); - // 저장 버튼 활성화 btnUpdate.Enabled = isTimeValid; + + if (isTimeValid) + { + btnUpdate.BackColor = Color.FromArgb(52, 152, 219); // 활성화 스카이블루 + btnUpdate.ForeColor = Color.White; + } + else + { + btnUpdate.BackColor = Color.FromArgb(180, 210, 235); // 비활성화 흐린 블루 + btnUpdate.ForeColor = Color.White; + } } private void btnCancel_MouseClick(object sender, MouseEventArgs e) @@ -50,8 +226,53 @@ namespace ControlServer private void btnUpdate_MouseClick(object sender, MouseEventArgs e) { - // 수정 - // 수정 시 DB 저장 + string displayTime = txtBoxDisplay.Text; + string onTime = txtBoxOnYY.Text + ":" + txtBoxOnMM.Text + ":" + txtBoxOnSS.Text; + string offTime = txtBoxOffYY.Text + ":" + txtBoxOffMM.Text + ":" + txtBoxOffSS.Text; + + _mainForm.SaveProgramTimesToGrid(_targetIP, displayTime, onTime, offTime); + + string protocol = $"SET_TIME|{displayTime}\n"; + _mainForm.SendDataToSingleAgent(_targetIP, protocol); + + MessageBox.Show("수정되었습니다.", "알림", MessageBoxButtons.OK, MessageBoxIcon.Information); + + this.Close(); + } + + private void LoadAndParseFile() + { + if (!File.Exists(_filePath)) return; + string fileContent = File.ReadAllText(_filePath); + if (string.IsNullOrWhiteSpace(fileContent)) return; + + ParseAndBindServerData(fileContent); + } + + private void ParseAndBindServerData(string rawData) + { + try + { + var doc = System.Xml.Linq.XDocument.Parse(rawData); + grdDataList.Rows.Clear(); + + foreach (var item in doc.Descendants("AuctionTable")) + { + grdDataList.Rows.Add( + item.Element("aucDate")?.Value, + item.Element("itemName")?.Value, + item.Element("origin")?.Value, + item.Element("totalQty")?.Value, + item.Element("fixPriceQty")?.Value, + item.Element("totalAmt")?.Value, + item.Element("fixPriceAmt")?.Value + ); + } + } + catch (Exception ex) + { + MessageBox.Show("XML 파싱 에러: " + ex.Message, "오류", MessageBoxButtons.OK, MessageBoxIcon.Error); + } } } -} +} \ No newline at end of file diff --git a/ControlServer/Form3.resx b/ControlServer/Form3.resx index 8b2ff64..67f8b33 100644 --- a/ControlServer/Form3.resx +++ b/ControlServer/Form3.resx @@ -117,4 +117,25 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + True + + + True + + + True + + + True + + + True + + + True + + + True + \ No newline at end of file diff --git a/ControlServer/Form4.Designer.cs b/ControlServer/Form4.Designer.cs deleted file mode 100644 index 674169c..0000000 --- a/ControlServer/Form4.Designer.cs +++ /dev/null @@ -1,166 +0,0 @@ -namespace ControlServer -{ - partial class Form4 - { - /// - /// Required designer variable. - /// - private System.ComponentModel.IContainer components = null; - - /// - /// Clean up any resources being used. - /// - /// true if managed resources should be disposed; otherwise, false. - protected override void Dispose(bool disposing) - { - if (disposing && (components != null)) - { - components.Dispose(); - } - base.Dispose(disposing); - } - - #region Windows Form Designer generated code - - /// - /// Required method for Designer support - do not modify - /// the contents of this method with the code editor. - /// - private void InitializeComponent() - { - DataGridViewCellStyle dataGridViewCellStyle7 = new DataGridViewCellStyle(); - DataGridViewCellStyle dataGridViewCellStyle12 = new DataGridViewCellStyle(); - DataGridViewCellStyle dataGridViewCellStyle8 = new DataGridViewCellStyle(); - DataGridViewCellStyle dataGridViewCellStyle9 = new DataGridViewCellStyle(); - DataGridViewCellStyle dataGridViewCellStyle10 = new DataGridViewCellStyle(); - DataGridViewCellStyle dataGridViewCellStyle11 = new DataGridViewCellStyle(); - grdLog = new DataGridView(); - Col5 = new DataGridViewTextBoxColumn(); - Col0 = new DataGridViewTextBoxColumn(); - Col3 = new DataGridViewTextBoxColumn(); - Col4 = new DataGridViewTextBoxColumn(); - Col6 = new DataGridViewTextBoxColumn(); - Col7 = new DataGridViewTextBoxColumn(); - btnPrev = new Button(); - ((System.ComponentModel.ISupportInitialize)grdLog).BeginInit(); - SuspendLayout(); - // - // grdLog - // - dataGridViewCellStyle7.Alignment = DataGridViewContentAlignment.MiddleCenter; - dataGridViewCellStyle7.BackColor = SystemColors.ButtonShadow; - dataGridViewCellStyle7.Font = new Font("맑은 고딕", 9F); - dataGridViewCellStyle7.ForeColor = SystemColors.WindowText; - dataGridViewCellStyle7.SelectionBackColor = SystemColors.Highlight; - dataGridViewCellStyle7.SelectionForeColor = SystemColors.HighlightText; - dataGridViewCellStyle7.WrapMode = DataGridViewTriState.True; - grdLog.ColumnHeadersDefaultCellStyle = dataGridViewCellStyle7; - grdLog.ColumnHeadersHeight = 29; - grdLog.ColumnHeadersHeightSizeMode = DataGridViewColumnHeadersHeightSizeMode.DisableResizing; - grdLog.Columns.AddRange(new DataGridViewColumn[] { Col5, Col0, Col3, Col4, Col6, Col7 }); - dataGridViewCellStyle12.Alignment = DataGridViewContentAlignment.MiddleCenter; - dataGridViewCellStyle12.BackColor = SystemColors.Window; - dataGridViewCellStyle12.Font = new Font("맑은 고딕", 9F); - dataGridViewCellStyle12.ForeColor = SystemColors.ControlText; - dataGridViewCellStyle12.SelectionBackColor = SystemColors.Highlight; - dataGridViewCellStyle12.SelectionForeColor = SystemColors.HighlightText; - dataGridViewCellStyle12.WrapMode = DataGridViewTriState.False; - grdLog.DefaultCellStyle = dataGridViewCellStyle12; - grdLog.Location = new Point(164, 87); - grdLog.Name = "grdLog"; - grdLog.RowHeadersWidth = 51; - grdLog.Size = new Size(1000, 638); - grdLog.TabIndex = 4; - // - // Col5 - // - dataGridViewCellStyle8.Alignment = DataGridViewContentAlignment.MiddleCenter; - Col5.DefaultCellStyle = dataGridViewCellStyle8; - Col5.Frozen = true; - Col5.HeaderText = "일시"; - Col5.MinimumWidth = 6; - Col5.Name = "Col5"; - Col5.Width = 220; - // - // Col0 - // - dataGridViewCellStyle9.Alignment = DataGridViewContentAlignment.MiddleCenter; - Col0.DefaultCellStyle = dataGridViewCellStyle9; - Col0.Frozen = true; - Col0.HeaderText = "단말기명"; - Col0.MinimumWidth = 6; - Col0.Name = "Col0"; - Col0.Width = 170; - // - // Col3 - // - dataGridViewCellStyle10.Alignment = DataGridViewContentAlignment.MiddleCenter; - Col3.DefaultCellStyle = dataGridViewCellStyle10; - Col3.Frozen = true; - Col3.HeaderText = "명령 유형"; - Col3.MinimumWidth = 6; - Col3.Name = "Col3"; - Col3.Width = 125; - // - // Col4 - // - dataGridViewCellStyle11.Alignment = DataGridViewContentAlignment.MiddleCenter; - Col4.DefaultCellStyle = dataGridViewCellStyle11; - Col4.Frozen = true; - Col4.HeaderText = "수행자"; - Col4.MinimumWidth = 6; - Col4.Name = "Col4"; - Col4.Width = 125; - // - // Col6 - // - Col6.Frozen = true; - Col6.HeaderText = "결과"; - Col6.MinimumWidth = 6; - Col6.Name = "Col6"; - Col6.Width = 125; - // - // Col7 - // - Col7.Frozen = true; - Col7.HeaderText = "상세내용"; - Col7.MinimumWidth = 6; - Col7.Name = "Col7"; - Col7.Width = 285; - // - // btnPrev - // - btnPrev.Font = new Font("맑은 고딕", 10.2F, FontStyle.Bold, GraphicsUnit.Point, 129); - btnPrev.Location = new Point(164, 12); - btnPrev.Name = "btnPrev"; - btnPrev.Size = new Size(174, 56); - btnPrev.TabIndex = 13; - btnPrev.Text = "이전"; - btnPrev.UseVisualStyleBackColor = true; - btnPrev.MouseClick += btnPrev_MouseClick; - // - // Form4 - // - AutoScaleDimensions = new SizeF(9F, 20F); - AutoScaleMode = AutoScaleMode.Font; - ClientSize = new Size(1357, 737); - Controls.Add(btnPrev); - Controls.Add(grdLog); - Name = "Form4"; - Text = "Form4"; - ((System.ComponentModel.ISupportInitialize)grdLog).EndInit(); - ResumeLayout(false); - } - - #endregion - - private DataGridView grdLog; - private DataGridViewTextBoxColumn Col5; - private DataGridViewTextBoxColumn Col0; - private DataGridViewTextBoxColumn Col3; - private DataGridViewTextBoxColumn Col4; - private DataGridViewTextBoxColumn Col6; - private DataGridViewTextBoxColumn Col7; - private Button btnPrev; - } -} \ No newline at end of file diff --git a/ControlServer/Form4.cs b/ControlServer/Form4.cs deleted file mode 100644 index 3745b0a..0000000 --- a/ControlServer/Form4.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Data; -using System.Drawing; -using System.Text; -using System.Windows.Forms; - -namespace ControlServer -{ - public partial class Form4 : Form - { - public Form4() - { - InitializeComponent(); - - grdLog.RowHeadersVisible = false; // 그리드 자동으로 생성되는 첫 번째 열 제거 - grdLog.AllowUserToAddRows = false; // 새 행 입력용 기본 로우 제거 - grdLog.EnableHeadersVisualStyles = false; // 헤더 기본 테마 사용 안 함 - grdLog.ColumnHeadersDefaultCellStyle.BackColor = Color.LightGray; // 헤더 배경색 - grdLog.ColumnHeadersHeight = 40; // 헤더 높이 - grdLog.RowHeadersVisible = false; // 행 헤더 제거 - grdLog.AllowUserToAddRows = false; // // 행 추가 금지 - grdLog.AllowUserToResizeRows = false; // 행 높이 조절 금지 - grdLog.AllowUserToResizeColumns = false; // 열 너비 조절 금지 - grdLog.ReadOnly = true; // 읽기 전용 - grdLog.ClearSelection(); - } - - private void btnPrev_MouseClick(object sender, MouseEventArgs e) - { - this.Close(); - } - } -} diff --git a/ControlServer/Properties/Resources.Designer.cs b/ControlServer/Properties/Resources.Designer.cs index a98024f..41cb486 100644 --- a/ControlServer/Properties/Resources.Designer.cs +++ b/ControlServer/Properties/Resources.Designer.cs @@ -69,5 +69,45 @@ namespace ControlServer.Properties { return ((System.Drawing.Bitmap)(obj)); } } + + /// + /// System.Drawing.Bitmap 형식의 지역화된 리소스를 찾습니다. + /// + internal static System.Drawing.Bitmap imgCountBox { + get { + object obj = ResourceManager.GetObject("imgCountBox", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// System.Drawing.Bitmap 형식의 지역화된 리소스를 찾습니다. + /// + internal static System.Drawing.Bitmap imgCountBox1 { + get { + object obj = ResourceManager.GetObject("imgCountBox1", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// System.Drawing.Bitmap 형식의 지역화된 리소스를 찾습니다. + /// + internal static System.Drawing.Bitmap imgDataRegister { + get { + object obj = ResourceManager.GetObject("imgDataRegister", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// System.Drawing.Bitmap 형식의 지역화된 리소스를 찾습니다. + /// + internal static System.Drawing.Bitmap imgDataUpdate { + get { + object obj = ResourceManager.GetObject("imgDataUpdate", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } } } diff --git a/ControlServer/Properties/Resources.resx b/ControlServer/Properties/Resources.resx index dab1ac7..bfad3b4 100644 --- a/ControlServer/Properties/Resources.resx +++ b/ControlServer/Properties/Resources.resx @@ -118,7 +118,19 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\imgDataRegister.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + ..\Resources\button-search.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + ..\Resources\imgDataUpdate.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\imgCountBox.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\imgCountBox1.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + \ No newline at end of file diff --git a/ControlServer/Resources/imgCountBox.png b/ControlServer/Resources/imgCountBox.png new file mode 100644 index 0000000..c063313 Binary files /dev/null and b/ControlServer/Resources/imgCountBox.png differ diff --git a/ControlServer/Resources/imgCountBox1.png b/ControlServer/Resources/imgCountBox1.png new file mode 100644 index 0000000..99407d2 Binary files /dev/null and b/ControlServer/Resources/imgCountBox1.png differ diff --git a/ControlServer/Resources/imgDataRegister.png b/ControlServer/Resources/imgDataRegister.png new file mode 100644 index 0000000..ff6c0e6 Binary files /dev/null and b/ControlServer/Resources/imgDataRegister.png differ diff --git a/ControlServer/Resources/imgDataUpdate.png b/ControlServer/Resources/imgDataUpdate.png new file mode 100644 index 0000000..452a866 Binary files /dev/null and b/ControlServer/Resources/imgDataUpdate.png differ diff --git a/ControlServer/RoundedPanel.cs b/ControlServer/RoundedPanel.cs new file mode 100644 index 0000000..a27bb8e --- /dev/null +++ b/ControlServer/RoundedPanel.cs @@ -0,0 +1,49 @@ +using System.Drawing; +using System.Drawing.Drawing2D; +using System.Windows.Forms; +using System.ComponentModel; + +namespace ControlServer +{ + public class RoundedPanel : Panel + { + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + public int CornerRadius { get; set; } = 30; + + public RoundedPanel() + { + this.DoubleBuffered = true; + // 컨트롤의 배경색이 바뀔 때 다시 그리도록 설정 + this.SetStyle(ControlStyles.AllPaintingInWmPaint | + ControlStyles.OptimizedDoubleBuffer | + ControlStyles.ResizeRedraw | + ControlStyles.UserPaint, true); + } + + protected override void OnPaint(PaintEventArgs e) + { + e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; + + // 배경을 투명하게 처리하고 둥근 영역만 남김 + using (GraphicsPath path = new GraphicsPath()) + { + path.AddArc(0, 0, CornerRadius, CornerRadius, 180, 90); + path.AddArc(Width - CornerRadius - 1, 0, CornerRadius, CornerRadius, 270, 90); + path.AddArc(Width - CornerRadius - 1, Height - CornerRadius - 1, CornerRadius, CornerRadius, 0, 90); + path.AddArc(0, Height - CornerRadius - 1, CornerRadius, CornerRadius, 90, 90); + path.CloseAllFigures(); + + // 영역(Region)을 설정하여 모서리 바깥쪽을 클릭 방지 및 투명 처리 + this.Region = new Region(path); + + // 배경 채우기 + using (SolidBrush brush = new SolidBrush(this.BackColor)) + { + e.Graphics.FillPath(brush, path); + } + } + + base.OnPaint(e); + } + } +} \ No newline at end of file