diff --git a/EETGW.sln b/EETGW.sln
index 07357ec..ec4773d 100644
--- a/EETGW.sln
+++ b/EETGW.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36310.24 d17.14
+# Visual Studio Express 15 for Windows Desktop
+VisualStudioVersion = 15.0.36324.19
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EETGW", "Project\EETGW.csproj", "{65F3E762-800C-499E-862F-A535642EC59F}"
EndProject
@@ -44,7 +44,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YARTE", "Sub\YARTE\YARTE.cs
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Console_SendMail", "Sub\Console_SendMail\Console_SendMail.csproj", "{8C94D335-7468-4964-AA24-1E3313CF7ABA}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmkorRestfulService", "SubProject\AmkorRestfulService\AmkorRestfulService.csproj", "{58CFC90C-5068-46A2-A8DE-0E92EE9E0990}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NoticeServer", "SubProject\ChatServer\NoticeServer.csproj", "{8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -168,14 +168,14 @@ Global
{8C94D335-7468-4964-AA24-1E3313CF7ABA}.Release|Any CPU.Build.0 = Release|Any CPU
{8C94D335-7468-4964-AA24-1E3313CF7ABA}.Release|x86.ActiveCfg = Release|Any CPU
{8C94D335-7468-4964-AA24-1E3313CF7ABA}.Release|x86.Build.0 = Release|Any CPU
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Debug|x86.ActiveCfg = Debug|x86
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Debug|x86.Build.0 = Debug|x86
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Release|Any CPU.Build.0 = Release|Any CPU
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Release|x86.ActiveCfg = Release|x86
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990}.Release|x86.Build.0 = Release|x86
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Debug|x86.Build.0 = Debug|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Release|x86.ActiveCfg = Release|Any CPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -195,7 +195,6 @@ Global
{CAFE5CD0-C055-4C77-9253-8D5EE9558D43} = {6C7EC99E-7367-4255-A039-EF5E8D75A2F6}
{3869B8C1-1290-4864-B72D-D771475F914D} = {6C7EC99E-7367-4255-A039-EF5E8D75A2F6}
{DB5EE9C8-EACF-4231-877E-B9DFD7A714DE} = {28105E67-9D33-4627-8E26-FCE67700622F}
- {58CFC90C-5068-46A2-A8DE-0E92EE9E0990} = {6C7EC99E-7367-4255-A039-EF5E8D75A2F6}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B5B1FD72-356F-4840-83E8-B070AC21C8D9}
diff --git a/Project/ChatClientService.cs b/Project/ChatClientService.cs
new file mode 100644
index 0000000..7849fe6
--- /dev/null
+++ b/Project/ChatClientService.cs
@@ -0,0 +1,283 @@
+using System;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using Newtonsoft.Json;
+
+namespace Project
+{
+ ///
+ /// Background chat client service with auto-reconnect
+ ///
+ public class ChatClientService : IDisposable
+ {
+ private TcpClient tcpClient;
+ private NetworkStream stream;
+ private Thread receiveThread;
+ private Thread reconnectThread;
+ private bool isRunning;
+ private bool isConnected;
+
+ private string serverIp;
+ private int serverPort;
+ private string nickName;
+ private string employeeId;
+ private string hostName;
+ private string userGroup;
+
+ /// Connection status changed event
+ public event EventHandler ConnectionStatusChanged;
+
+ /// Message received event
+ public event EventHandler MessageReceived;
+
+ /// Connection status
+ public bool IsConnected => isConnected;
+
+ /// User nickname
+ public string NickName => nickName;
+
+ /// User employee ID
+ public string EmployeeId => employeeId;
+
+ /// User group
+ public string UserGroup => userGroup;
+
+ public ChatClientService(string serverIp, int serverPort)
+ {
+ this.serverIp = serverIp;
+ this.serverPort = serverPort;
+ this.hostName = System.Net.Dns.GetHostName();
+ }
+
+ ///
+ /// Start background service
+ ///
+ public void Start(string nickName, string employeeId, string userGroup)
+ {
+ if (isRunning)
+ return;
+
+ this.nickName = nickName;
+ this.employeeId = employeeId;
+ this.userGroup = userGroup;
+ isRunning = true;
+
+ // Start reconnect thread
+ reconnectThread = new Thread(ReconnectLoop)
+ {
+ IsBackground = true,
+ Name = "ChatReconnectThread"
+ };
+ reconnectThread.Start();
+ }
+
+ ///
+ /// Stop background service
+ ///
+ public void Stop()
+ {
+ isRunning = false;
+ Disconnect();
+
+ try
+ {
+ reconnectThread?.Join(2000);
+ receiveThread?.Join(2000);
+ }
+ catch { }
+ }
+
+ ///
+ /// Auto-reconnect loop
+ ///
+ private void ReconnectLoop()
+ {
+ while (isRunning)
+ {
+ if (!isConnected)
+ {
+ try
+ {
+ Connect();
+ }
+ catch
+ {
+ // Retry after 5 seconds
+ Thread.Sleep(5000);
+ }
+ }
+ else
+ {
+ Thread.Sleep(1000);
+ }
+ }
+ }
+
+ ///
+ /// Connect to server
+ ///
+ private void Connect()
+ {
+ try
+ {
+ tcpClient = new TcpClient();
+ tcpClient.Connect(serverIp, serverPort);
+ stream = tcpClient.GetStream();
+ isConnected = true;
+
+ // Notify connection status
+ ConnectionStatusChanged?.Invoke(this, true);
+
+ // Send connect message
+ var connectMsg = new ChatMessage
+ {
+ Type = MessageType.Connect,
+ NickName = nickName,
+ EmployeeId = employeeId,
+ HostName = hostName,
+ UserGroup = userGroup,
+ Timestamp = DateTime.Now
+ };
+ SendMessage(connectMsg);
+
+ // Start receive thread
+ receiveThread = new Thread(ReceiveLoop)
+ {
+ IsBackground = true,
+ Name = "ChatReceiveThread"
+ };
+ receiveThread.Start();
+ }
+ catch
+ {
+ Disconnect();
+ throw;
+ }
+ }
+
+ ///
+ /// Disconnect from server
+ ///
+ private void Disconnect()
+ {
+ if (!isConnected)
+ return;
+
+ isConnected = false;
+
+ try
+ {
+ stream?.Close();
+ tcpClient?.Close();
+ }
+ catch { }
+
+ // Notify connection status
+ ConnectionStatusChanged?.Invoke(this, false);
+ }
+
+ ///
+ /// Message receive loop
+ ///
+ private void ReceiveLoop()
+ {
+ byte[] buffer = new byte[8192];
+
+ try
+ {
+ while (isRunning && isConnected && tcpClient.Connected)
+ {
+ int bytesRead = stream.Read(buffer, 0, buffer.Length);
+ if (bytesRead == 0)
+ {
+ // Connection closed
+ break;
+ }
+
+ string jsonData = Encoding.UTF8.GetString(buffer, 0, bytesRead);
+ var message = JsonConvert.DeserializeObject(jsonData);
+
+ if (message != null)
+ {
+ // Filter messages by group (dev can see all)
+ // UserListResponse and system messages should always be received
+ if (userGroup == "dev" ||
+ message.UserGroup == userGroup ||
+ message.Type == MessageType.Notice ||
+ message.Type == MessageType.UserListResponse ||
+ message.Type == MessageType.Pong)
+ {
+ MessageReceived?.Invoke(this, message);
+ }
+ }
+ }
+ }
+ catch
+ {
+ // Connection error
+ }
+ finally
+ {
+ Disconnect();
+ }
+ }
+
+ ///
+ /// Send message
+ ///
+ public bool SendMessage(ChatMessage message)
+ {
+ if (!isConnected || tcpClient == null || !tcpClient.Connected)
+ return false;
+
+ try
+ {
+ message.NickName = nickName;
+ message.UserGroup = userGroup;
+ message.Timestamp = DateTime.Now;
+
+ string jsonData = JsonConvert.SerializeObject(message);
+ byte[] data = Encoding.UTF8.GetBytes(jsonData);
+ stream.Write(data, 0, data.Length);
+ stream.Flush();
+ return true;
+ }
+ catch
+ {
+ Disconnect();
+ return false;
+ }
+ }
+
+ ///
+ /// Send chat message
+ ///
+ public bool SendChatMessage(string content)
+ {
+ var message = new ChatMessage
+ {
+ Type = MessageType.Chat,
+ Content = content
+ };
+ return SendMessage(message);
+ }
+
+ ///
+ /// Request user list
+ ///
+ public bool RequestUserList()
+ {
+ var message = new ChatMessage
+ {
+ Type = MessageType.UserListRequest
+ };
+ return SendMessage(message);
+ }
+
+ public void Dispose()
+ {
+ Stop();
+ }
+ }
+}
diff --git a/Project/ChatMessage.cs b/Project/ChatMessage.cs
new file mode 100644
index 0000000..20134f7
--- /dev/null
+++ b/Project/ChatMessage.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace Project
+{
+ ///
+ /// Chat message type
+ ///
+ public enum MessageType
+ {
+ /// Client connect
+ Connect = 0,
+ /// Client disconnect
+ Disconnect = 1,
+ /// Normal chat message
+ Chat = 2,
+ /// Server notice
+ Notice = 3,
+ /// Whisper message
+ Whisper = 4,
+ /// User list request
+ UserListRequest = 5,
+ /// User list response
+ UserListResponse = 6,
+ /// Ping (keep alive)
+ Ping = 7,
+ /// Pong (ping response)
+ Pong = 8
+ }
+
+ ///
+ /// Chat message protocol
+ ///
+ [Serializable]
+ public class ChatMessage
+ {
+ /// Message type
+ public MessageType Type { get; set; }
+
+ /// Sender nickname
+ public string NickName { get; set; }
+
+ /// Sender employee ID (사번)
+ public string EmployeeId { get; set; }
+
+ /// Sender IP
+ public string IpAddress { get; set; }
+
+ /// Sender hostname
+ public string HostName { get; set; }
+
+ /// Message content
+ public string Content { get; set; }
+
+ /// Target employee ID (for 1:1 chat)
+ public string TargetEmployeeId { get; set; }
+
+ /// Target nickname (for whisper, null for broadcast)
+ public string TargetNickName { get; set; }
+
+ /// Send time
+ public DateTime Timestamp { get; set; }
+
+ /// User group (for filtering)
+ public string UserGroup { get; set; }
+
+ public ChatMessage()
+ {
+ Timestamp = DateTime.Now;
+ }
+
+ public override string ToString()
+ {
+ return $"[{Timestamp:HH:mm:ss}] {NickName}: {Content}";
+ }
+ }
+}
diff --git a/Project/Dialog/fChat.Designer.cs b/Project/Dialog/fChat.Designer.cs
new file mode 100644
index 0000000..af5c4b8
--- /dev/null
+++ b/Project/Dialog/fChat.Designer.cs
@@ -0,0 +1,184 @@
+namespace Project.Dialog
+{
+ partial class fChat
+ {
+ ///
+ /// 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()
+ {
+ this.components = new System.ComponentModel.Container();
+ this.txtChatDisplay = new System.Windows.Forms.TextBox();
+ this.txtInput = new System.Windows.Forms.TextBox();
+ this.btnSend = new System.Windows.Forms.Button();
+ this.lblStatus = new System.Windows.Forms.Label();
+ this.timerStatus = new System.Windows.Forms.Timer(this.components);
+ this.panelTop = new System.Windows.Forms.Panel();
+ this.panelBottom = new System.Windows.Forms.Panel();
+ this.lblRecipient = new System.Windows.Forms.Label();
+ this.cboRecipient = new System.Windows.Forms.ComboBox();
+ this.panelTop.SuspendLayout();
+ this.panelBottom.SuspendLayout();
+ this.SuspendLayout();
+ //
+ // txtChatDisplay
+ //
+ this.txtChatDisplay.BackColor = System.Drawing.Color.White;
+ this.txtChatDisplay.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.txtChatDisplay.Font = new System.Drawing.Font("Consolas", 9F);
+ this.txtChatDisplay.Location = new System.Drawing.Point(0, 30);
+ this.txtChatDisplay.Multiline = true;
+ this.txtChatDisplay.Name = "txtChatDisplay";
+ this.txtChatDisplay.ReadOnly = true;
+ this.txtChatDisplay.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
+ this.txtChatDisplay.Size = new System.Drawing.Size(500, 340);
+ this.txtChatDisplay.TabIndex = 0;
+ //
+ // txtInput
+ //
+ this.txtInput.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.txtInput.Font = new System.Drawing.Font("Malgun Gothic", 9F);
+ this.txtInput.Location = new System.Drawing.Point(0, 0);
+ this.txtInput.Name = "txtInput";
+ this.txtInput.Size = new System.Drawing.Size(420, 23);
+ this.txtInput.TabIndex = 1;
+ this.txtInput.KeyDown += new System.Windows.Forms.KeyEventHandler(this.txtInput_KeyDown);
+ //
+ // btnSend
+ //
+ this.btnSend.Dock = System.Windows.Forms.DockStyle.Right;
+ this.btnSend.Location = new System.Drawing.Point(420, 0);
+ this.btnSend.Name = "btnSend";
+ this.btnSend.Size = new System.Drawing.Size(80, 30);
+ this.btnSend.TabIndex = 2;
+ this.btnSend.Text = "Send";
+ this.btnSend.UseVisualStyleBackColor = true;
+ this.btnSend.Click += new System.EventHandler(this.btnSend_Click);
+ //
+ // lblStatus
+ //
+ this.lblStatus.AutoSize = true;
+ this.lblStatus.Dock = System.Windows.Forms.DockStyle.Fill;
+ this.lblStatus.Font = new System.Drawing.Font("Malgun Gothic", 9F, System.Drawing.FontStyle.Bold);
+ this.lblStatus.ForeColor = System.Drawing.Color.Green;
+ this.lblStatus.Location = new System.Drawing.Point(5, 5);
+ this.lblStatus.Name = "lblStatus";
+ this.lblStatus.Padding = new System.Windows.Forms.Padding(5);
+ this.lblStatus.Size = new System.Drawing.Size(83, 25);
+ this.lblStatus.TabIndex = 3;
+ this.lblStatus.Text = "Connected";
+ this.lblStatus.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
+ //
+ // timerStatus
+ //
+ this.timerStatus.Enabled = true;
+ this.timerStatus.Interval = 1000;
+ this.timerStatus.Tick += new System.EventHandler(this.timerStatus_Tick);
+ //
+ // lblRecipient
+ //
+ this.lblRecipient.AutoSize = true;
+ this.lblRecipient.Dock = System.Windows.Forms.DockStyle.Left;
+ this.lblRecipient.Font = new System.Drawing.Font("Malgun Gothic", 9F);
+ this.lblRecipient.Location = new System.Drawing.Point(5, 5);
+ this.lblRecipient.Name = "lblRecipient";
+ this.lblRecipient.Padding = new System.Windows.Forms.Padding(0, 5, 5, 0);
+ this.lblRecipient.Size = new System.Drawing.Size(45, 20);
+ this.lblRecipient.TabIndex = 5;
+ this.lblRecipient.Text = "To:";
+ this.lblRecipient.TextAlign = System.Drawing.ContentAlignment.MiddleLeft;
+ //
+ // cboRecipient
+ //
+ this.cboRecipient.Dock = System.Windows.Forms.DockStyle.Left;
+ this.cboRecipient.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
+ this.cboRecipient.Font = new System.Drawing.Font("Malgun Gothic", 9F);
+ this.cboRecipient.FormattingEnabled = true;
+ this.cboRecipient.Location = new System.Drawing.Point(50, 5);
+ this.cboRecipient.Name = "cboRecipient";
+ this.cboRecipient.Size = new System.Drawing.Size(200, 23);
+ this.cboRecipient.TabIndex = 6;
+ //
+ // panelTop
+ //
+ this.panelTop.Controls.Add(this.cboRecipient);
+ this.panelTop.Controls.Add(this.lblRecipient);
+ this.panelTop.Controls.Add(this.lblStatus);
+ this.panelTop.Dock = System.Windows.Forms.DockStyle.Top;
+ this.panelTop.Location = new System.Drawing.Point(0, 0);
+ this.panelTop.Name = "panelTop";
+ this.panelTop.Padding = new System.Windows.Forms.Padding(5);
+ this.panelTop.Size = new System.Drawing.Size(500, 30);
+ this.panelTop.TabIndex = 4;
+ //
+ // panelBottom
+ //
+ this.panelBottom.Controls.Add(this.txtInput);
+ this.panelBottom.Controls.Add(this.btnSend);
+ this.panelBottom.Dock = System.Windows.Forms.DockStyle.Bottom;
+ this.panelBottom.Location = new System.Drawing.Point(0, 370);
+ this.panelBottom.Name = "panelBottom";
+ this.panelBottom.Size = new System.Drawing.Size(500, 30);
+ this.panelBottom.TabIndex = 5;
+ //
+ // fChat
+ //
+ this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 12F);
+ this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
+ this.ClientSize = new System.Drawing.Size(500, 400);
+ this.Controls.Add(this.txtChatDisplay);
+ this.Controls.Add(this.panelBottom);
+ this.Controls.Add(this.panelTop);
+ this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.Sizable;
+ this.KeyPreview = true;
+ this.MaximizeBox = true;
+ this.MinimizeBox = true;
+ this.MinimumSize = new System.Drawing.Size(400, 300);
+ this.Name = "fChat";
+ this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
+ this.Text = "Chat";
+ this.FormClosing += new System.Windows.Forms.FormClosingEventHandler(this.fChat_FormClosing);
+ this.Load += new System.EventHandler(this.fChat_Load);
+ this.panelTop.ResumeLayout(false);
+ this.panelTop.PerformLayout();
+ this.panelBottom.ResumeLayout(false);
+ this.panelBottom.PerformLayout();
+ this.ResumeLayout(false);
+ this.PerformLayout();
+
+ }
+
+ #endregion
+
+ private System.Windows.Forms.TextBox txtChatDisplay;
+ private System.Windows.Forms.TextBox txtInput;
+ private System.Windows.Forms.Button btnSend;
+ private System.Windows.Forms.Label lblStatus;
+ private System.Windows.Forms.Timer timerStatus;
+ private System.Windows.Forms.Panel panelTop;
+ private System.Windows.Forms.Panel panelBottom;
+ private System.Windows.Forms.Label lblRecipient;
+ private System.Windows.Forms.ComboBox cboRecipient;
+ }
+}
diff --git a/Project/Dialog/fChat.cs b/Project/Dialog/fChat.cs
new file mode 100644
index 0000000..fc72a5f
--- /dev/null
+++ b/Project/Dialog/fChat.cs
@@ -0,0 +1,412 @@
+using FCOMMON;
+using System;
+using System.Drawing;
+using System.Windows.Forms;
+
+namespace Project.Dialog
+{
+ ///
+ /// User info for recipient selection
+ ///
+ public class ChatUserInfo
+ {
+ public string EmployeeId { get; set; }
+ public string NickName { get; set; }
+ public string UserGroup { get; set; }
+
+ public override string ToString()
+ {
+ return $"{NickName} ({EmployeeId})";
+ }
+ }
+
+ public partial class fChat : fBase
+ {
+ private ChatClientService chatService;
+ private string targetEmployeeId;
+ private string targetNickName;
+
+ ///
+ /// Constructor for new chat or specific recipient
+ ///
+ public fChat(ChatClientService service, string targetEmployeeId = null, string targetNickName = null)
+ {
+ InitializeComponent();
+ this.chatService = service;
+ this.targetEmployeeId = targetEmployeeId;
+ this.targetNickName = targetNickName;
+ this.KeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) this.Close(); };
+ }
+
+ private void fChat_Load(object sender, EventArgs e)
+ {
+ // Subscribe to message received event
+ if (chatService != null)
+ {
+ chatService.MessageReceived += OnMessageReceived;
+ }
+
+ // Load user list for recipient selection
+ LoadUserList();
+
+ // If target is specified, set it
+ if (!string.IsNullOrEmpty(targetEmployeeId))
+ {
+ SetRecipient(targetEmployeeId, targetNickName);
+ }
+
+ // Load recent messages (if any)
+ RefreshConnectionStatus();
+
+ // Request user list from server
+ if (chatService != null && chatService.IsConnected)
+ {
+ System.Diagnostics.Debug.WriteLine("[DEBUG] Requesting user list from server...");
+ chatService.RequestUserList();
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Cannot request user list - Service: {chatService != null}, Connected: {chatService?.IsConnected}");
+ }
+ }
+
+ ///
+ /// Load user list into combobox
+ ///
+ private void LoadUserList()
+ {
+ cboRecipient.Items.Clear();
+
+ // Add placeholder
+ cboRecipient.Items.Add(new ChatUserInfo
+ {
+ EmployeeId = "",
+ NickName = "Select recipient...",
+ UserGroup = ""
+ });
+
+ // In a real implementation, this would load from server
+ // For now, user will need to type employee ID manually
+ // The server's UserListResponse should populate this
+
+ cboRecipient.SelectedIndex = 0;
+ }
+
+ ///
+ /// Set specific recipient
+ ///
+ private void SetRecipient(string employeeId, string nickName)
+ {
+ targetEmployeeId = employeeId;
+ targetNickName = nickName;
+
+ // Try to find in combobox
+ bool found = false;
+ for (int i = 0; i < cboRecipient.Items.Count; i++)
+ {
+ var user = cboRecipient.Items[i] as ChatUserInfo;
+ if (user != null && user.EmployeeId == employeeId)
+ {
+ cboRecipient.SelectedIndex = i;
+ found = true;
+ break;
+ }
+ }
+
+ // If not found, add it
+ if (!found && !string.IsNullOrEmpty(employeeId))
+ {
+ var user = new ChatUserInfo
+ {
+ EmployeeId = employeeId,
+ NickName = nickName ?? employeeId,
+ UserGroup = ""
+ };
+ cboRecipient.Items.Add(user);
+ cboRecipient.SelectedItem = user;
+ }
+
+ // Update form title
+ this.Text = $"Chat - {nickName ?? employeeId}";
+ }
+
+ private void fChat_FormClosing(object sender, FormClosingEventArgs e)
+ {
+ // Unsubscribe events
+ if (chatService != null)
+ {
+ chatService.MessageReceived -= OnMessageReceived;
+ }
+ }
+
+ ///
+ /// Message received event handler
+ ///
+ private void OnMessageReceived(object sender, ChatMessage message)
+ {
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(() => OnMessageReceived(sender, message)));
+ return;
+ }
+
+ // Get current recipient from combobox
+ var selectedUser = cboRecipient.SelectedItem as ChatUserInfo;
+ string currentTargetId = selectedUser?.EmployeeId ?? targetEmployeeId;
+
+ // Filter messages - only show messages between current user and target
+ string myEmployeeId = chatService.EmployeeId;
+
+ // Show message if:
+ // 1. It's from target to me
+ // 2. It's from me to target (echo from server)
+ // 3. It's a notice (broadcast)
+ bool shouldDisplay = false;
+
+ if (message.Type == MessageType.Notice)
+ {
+ shouldDisplay = true;
+
+ // If it's a join/leave notice, refresh user list
+ if (message.Content.Contains("has joined") || message.Content.Contains("has left"))
+ {
+ System.Diagnostics.Debug.WriteLine("[DEBUG] User joined/left, refreshing user list...");
+ if (chatService != null && chatService.IsConnected)
+ {
+ chatService.RequestUserList();
+ }
+ }
+ }
+ else if (message.Type == MessageType.UserListResponse)
+ {
+ // Handle user list response
+ HandleUserListResponse(message);
+ return;
+ }
+ else if (!string.IsNullOrEmpty(currentTargetId))
+ {
+ // From target to me
+ if (message.EmployeeId == currentTargetId && message.TargetEmployeeId == myEmployeeId)
+ {
+ shouldDisplay = true;
+ }
+ // From me to target (echo)
+ else if (message.EmployeeId == myEmployeeId && message.TargetEmployeeId == currentTargetId)
+ {
+ shouldDisplay = true;
+ }
+ }
+
+ if (shouldDisplay)
+ {
+ AppendMessage(message);
+ }
+ }
+
+ ///
+ /// Handle user list response from server
+ ///
+ private void HandleUserListResponse(ChatMessage message)
+ {
+ try
+ {
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] UserListResponse received: {message.Content}");
+
+ // Parse user list from Content (format: "employeeId1:nickName1:userGroup1,employeeId2:nickName2:userGroup2,...")
+ if (string.IsNullOrEmpty(message.Content))
+ {
+ System.Diagnostics.Debug.WriteLine("[DEBUG] UserListResponse content is empty");
+ return;
+ }
+
+ cboRecipient.Items.Clear();
+
+ // Add placeholder
+ cboRecipient.Items.Add(new ChatUserInfo
+ {
+ EmployeeId = "",
+ NickName = "Select recipient...",
+ UserGroup = ""
+ });
+
+ string[] users = message.Content.Split(',');
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Parsing {users.Length} users");
+
+ foreach (var userStr in users)
+ {
+ if (string.IsNullOrWhiteSpace(userStr))
+ continue;
+
+ string[] parts = userStr.Split(':');
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] User parts: {string.Join(", ", parts)}");
+
+ if (parts.Length >= 2)
+ {
+ string empId = parts[0].Trim();
+ string nick = parts[1].Trim();
+
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Processing user: {empId} - {nick}, My ID: {chatService.EmployeeId}");
+
+ // Don't add myself
+ if (empId != chatService.EmployeeId)
+ {
+ var user = new ChatUserInfo
+ {
+ EmployeeId = empId,
+ NickName = nick,
+ UserGroup = parts.Length > 2 ? parts[2].Trim() : ""
+ };
+ cboRecipient.Items.Add(user);
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Added user: {user}");
+ }
+ else
+ {
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Skipped myself: {empId}");
+ }
+ }
+ }
+
+ System.Diagnostics.Debug.WriteLine($"[DEBUG] Total items in combo: {cboRecipient.Items.Count}");
+
+ // Restore previous selection if exists
+ if (!string.IsNullOrEmpty(targetEmployeeId))
+ {
+ SetRecipient(targetEmployeeId, targetNickName);
+ }
+ else
+ {
+ cboRecipient.SelectedIndex = 0;
+ }
+ }
+ catch (Exception ex)
+ {
+ System.Diagnostics.Debug.WriteLine($"[ERROR] HandleUserListResponse: {ex.Message}");
+ MessageBox.Show($"Error loading user list: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ ///
+ /// Append message to chat display
+ ///
+ private void AppendMessage(ChatMessage message)
+ {
+ string timeStr = message.Timestamp.ToString("HH:mm:ss");
+ string displayMsg = "";
+
+ switch (message.Type)
+ {
+ case MessageType.Chat:
+ displayMsg = $"[{timeStr}] {message.NickName}: {message.Content}";
+ break;
+
+ case MessageType.Notice:
+ displayMsg = $"[{timeStr}] [NOTICE] {message.Content}";
+ break;
+
+ case MessageType.Whisper:
+ displayMsg = $"[{timeStr}] [WHISPER from {message.NickName}] {message.Content}";
+ break;
+
+ default:
+ return;
+ }
+
+ txtChatDisplay.AppendText(displayMsg + Environment.NewLine);
+ txtChatDisplay.ScrollToCaret();
+ }
+
+ ///
+ /// Send button click
+ ///
+ private void btnSend_Click(object sender, EventArgs e)
+ {
+ SendMessage();
+ }
+
+ ///
+ /// Input text key down (Enter to send)
+ ///
+ private void txtInput_KeyDown(object sender, KeyEventArgs e)
+ {
+ if (e.KeyCode == Keys.Enter)
+ {
+ e.SuppressKeyPress = true;
+ SendMessage();
+ }
+ }
+
+ ///
+ /// Send chat message
+ ///
+ private void SendMessage()
+ {
+ string content = txtInput.Text.Trim();
+ if (string.IsNullOrEmpty(content))
+ return;
+
+ if (chatService == null || !chatService.IsConnected)
+ {
+ MessageBox.Show("Not connected to chat server.", "Chat", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Get target from combobox
+ var selectedUser = cboRecipient.SelectedItem as ChatUserInfo;
+ string currentTargetId = selectedUser?.EmployeeId ?? targetEmployeeId;
+
+ if (string.IsNullOrEmpty(currentTargetId))
+ {
+ MessageBox.Show("Please select a recipient.", "Chat", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ // Create message with target
+ var message = new ChatMessage
+ {
+ Type = MessageType.Chat,
+ Content = content,
+ TargetEmployeeId = currentTargetId
+ };
+
+ bool sent = chatService.SendMessage(message);
+ if (sent)
+ {
+ // Note: Server will echo the message back, so we don't display it here
+ // This prevents duplicate messages
+
+ // Clear input
+ txtInput.Text = "";
+ txtInput.Focus();
+ }
+ else
+ {
+ MessageBox.Show("Failed to send message.", "Chat", MessageBoxButtons.OK, MessageBoxIcon.Error);
+ }
+ }
+
+ ///
+ /// Refresh connection status display
+ ///
+ private void RefreshConnectionStatus()
+ {
+ if (chatService != null && chatService.IsConnected)
+ {
+ lblStatus.Text = $"Connected - {chatService.NickName} ({chatService.UserGroup})";
+ lblStatus.ForeColor = Color.Green;
+ }
+ else
+ {
+ lblStatus.Text = "Disconnected";
+ lblStatus.ForeColor = Color.Red;
+ }
+ }
+
+ ///
+ /// Timer to update status periodically
+ ///
+ private void timerStatus_Tick(object sender, EventArgs e)
+ {
+ RefreshConnectionStatus();
+ }
+ }
+}
diff --git a/Project/Dialog/fChat.resx b/Project/Dialog/fChat.resx
new file mode 100644
index 0000000..a9e5900
--- /dev/null
+++ b/Project/Dialog/fChat.resx
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ 17, 17
+
+
diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj
index 1ec57f3..14ae08e 100644
--- a/Project/EETGW.csproj
+++ b/Project/EETGW.csproj
@@ -314,6 +314,10 @@
fMsgWindow.cs
+
+
+ fChat.cs
+
Form
@@ -478,6 +482,8 @@
+
+
Form
@@ -507,6 +513,9 @@
fMsgWindow.cs
+
+ fChat.cs
+
fLogin.cs
diff --git a/Project/EETGW.csproj.user b/Project/EETGW.csproj.user
index f6caabb..0789c32 100644
--- a/Project/EETGW.csproj.user
+++ b/Project/EETGW.csproj.user
@@ -1,4 +1,4 @@
-
+
ftp://10.131.36.205:2121/Install/GroupWare/|ftp://10.131.36.205:2121/Install/|ftp://10.131.36.205/Install/|게시\
diff --git a/Project/Properties/Settings.Designer.cs b/Project/Properties/Settings.Designer.cs
index d1d2bf2..72968ee 100644
--- a/Project/Properties/Settings.Designer.cs
+++ b/Project/Properties/Settings.Designer.cs
@@ -12,7 +12,7 @@ namespace Project.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
- [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.9.0.0")]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
diff --git a/Project/Properties/Settings.settings b/Project/Properties/Settings.settings
index 7f4e81e..4d038f7 100644
--- a/Project/Properties/Settings.settings
+++ b/Project/Properties/Settings.settings
@@ -4,7 +4,7 @@
<?xml version="1.0" encoding="utf-16"?>
-<SerializableConnectionString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ConnectionString>Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True</ConnectionString>
<ProviderName>System.Data.SqlClient</ProviderName>
</SerializableConnectionString>
@@ -12,7 +12,7 @@
<?xml version="1.0" encoding="utf-16"?>
-<SerializableConnectionString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+<SerializableConnectionString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<ConnectionString>Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True</ConnectionString>
<ProviderName>System.Data.SqlClient</ProviderName>
</SerializableConnectionString>
diff --git a/Project/app.config b/Project/app.config
index 7f92479..8cbec29 100644
--- a/Project/app.config
+++ b/Project/app.config
@@ -3,16 +3,26 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
diff --git a/Project/fMain.Designer.cs b/Project/fMain.Designer.cs
index 0158d91..4303cfb 100644
--- a/Project/fMain.Designer.cs
+++ b/Project/fMain.Designer.cs
@@ -41,6 +41,7 @@
this.toolStripStatusLabel1 = new System.Windows.Forms.ToolStripStatusLabel();
this.sbLoginUseTime = new System.Windows.Forms.ToolStripStatusLabel();
this.sbWeb = new System.Windows.Forms.ToolStripStatusLabel();
+ this.sbChat = new System.Windows.Forms.ToolStripStatusLabel();
this.menuStrip1 = new System.Windows.Forms.MenuStrip();
this.btSetting = new System.Windows.Forms.ToolStripMenuItem();
this.로그인ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
@@ -191,7 +192,8 @@
this.sbLogin,
this.toolStripStatusLabel1,
this.sbLoginUseTime,
- this.sbWeb});
+ this.sbWeb,
+ this.sbChat});
this.statusStrip1.Location = new System.Drawing.Point(1, 622);
this.statusStrip1.Name = "statusStrip1";
this.statusStrip1.Size = new System.Drawing.Size(1094, 22);
@@ -228,6 +230,15 @@
this.sbWeb.Size = new System.Drawing.Size(31, 17);
this.sbWeb.Text = "WEB";
//
+ // sbChat
+ //
+ this.sbChat.ForeColor = System.Drawing.Color.Gray;
+ this.sbChat.IsLink = true;
+ this.sbChat.Name = "sbChat";
+ this.sbChat.Size = new System.Drawing.Size(42, 17);
+ this.sbChat.Text = "Notice";
+ this.sbChat.Click += new System.EventHandler(this.sbChat_Click);
+ //
// menuStrip1
//
this.menuStrip1.Font = new System.Drawing.Font("맑은 고딕", 10F);
@@ -1343,6 +1354,7 @@
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem17;
private System.Windows.Forms.ToolStripMenuItem webview2TestToolStripMenuItem;
private System.Windows.Forms.ToolStripStatusLabel sbWeb;
+ private System.Windows.Forms.ToolStripStatusLabel sbChat;
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem18;
}
}
diff --git a/Project/fMain.cs b/Project/fMain.cs
index 33b7e43..f7b6890 100644
--- a/Project/fMain.cs
+++ b/Project/fMain.cs
@@ -20,11 +20,20 @@ namespace Project
string SearchKey = string.Empty;
private IDisposable webApp;
bool webok = false;
+ private ChatClientService chatService;
+ private System.Windows.Forms.Timer chatBlinkTimer;
+ private bool chatHasNewMessage = false;
+ private Dictionary chatMessageSenders = new Dictionary(); // employeeId -> nickName
public fMain()
{
InitializeComponent();
+
+ // Initialize chat blink timer
+ chatBlinkTimer = new System.Windows.Forms.Timer();
+ chatBlinkTimer.Interval = 500; // 500ms blink
+ chatBlinkTimer.Tick += ChatBlinkTimer_Tick;
this.KeyDown += (s1, e1) =>
{
if (e1.KeyCode == Keys.F12) btSetting.PerformClick();
@@ -93,6 +102,10 @@ namespace Project
}
}
+ // Stop chat service
+ chatService?.Stop();
+ chatBlinkTimer?.Stop();
+
FCOMMON.Pub.log.Add("Program Close");
FCOMMON.Pub.log.Flush();
}
@@ -193,6 +206,9 @@ namespace Project
Func_Login();
+ // Start chat service after login
+ StartChatService();
+
///즐겨찾기 목록 갱신
Update_FavoriteSite();
@@ -1558,8 +1574,214 @@ namespace Project
fdashboard.RefreshView();
Console.WriteLine( "view update");
}
-
+
}
}
+
+ #region Chat Functions
+
+ ///
+ /// Start chat service after login
+ ///
+ private void StartChatService()
+ {
+ try
+ {
+ // Get user info
+ string nickName = FCOMMON.info.Login.nameK ?? "Unknown";
+ string employeeId = FCOMMON.info.Login.no ?? "Unknown";
+ string userGroup = FCOMMON.info.Login.gcode ?? "default";
+
+ // Initialize chat service
+ //var chatServerIP = "127.0.0.1";// : "10.131.11.45";
+ var chatServerIP = "10.131.11.45";
+ chatService = new ChatClientService(chatServerIP, 5000);
+ chatService.ConnectionStatusChanged += ChatService_ConnectionStatusChanged;
+ chatService.MessageReceived += ChatService_MessageReceived;
+
+ // Start background service
+ chatService.Start(nickName, employeeId, userGroup);
+
+ FCOMMON.Pub.log.AddI($"Chat service started: {nickName} ({employeeId}, {userGroup})");
+ }
+ catch (Exception ex)
+ {
+ FCOMMON.Pub.log.AddE($"Failed to start chat service: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Chat connection status changed
+ ///
+ private void ChatService_ConnectionStatusChanged(object sender, bool isConnected)
+ {
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(() => ChatService_ConnectionStatusChanged(sender, isConnected)));
+ return;
+ }
+
+ if (isConnected)
+ {
+ sbChat.ForeColor = Color.Green;
+ sbChat.Text = "Notice ●";
+ }
+ else
+ {
+ sbChat.ForeColor = Color.Gray;
+ sbChat.Text = "Notice ○";
+ }
+ }
+
+ ///
+ /// Chat message received
+ ///
+ private void ChatService_MessageReceived(object sender, ChatMessage message)
+ {
+ if (InvokeRequired)
+ {
+ BeginInvoke(new Action(() => ChatService_MessageReceived(sender, message)));
+ return;
+ }
+
+ // Track message senders (only messages directed to me)
+ string myEmployeeId = chatService.EmployeeId;
+ if ((message.Type == MessageType.Chat || message.Type == MessageType.Whisper) &&
+ !string.IsNullOrEmpty(message.EmployeeId) &&
+ message.EmployeeId != myEmployeeId &&
+ message.TargetEmployeeId == myEmployeeId)
+ {
+ // Add or update sender
+ if (!chatMessageSenders.ContainsKey(message.EmployeeId))
+ {
+ chatMessageSenders[message.EmployeeId] = message.NickName ?? message.EmployeeId;
+ }
+
+ // Start blinking
+ chatHasNewMessage = true;
+ chatBlinkTimer.Start();
+ }
+ }
+
+ ///
+ /// Chat blink timer tick
+ ///
+ private void ChatBlinkTimer_Tick(object sender, EventArgs e)
+ {
+ if (!chatHasNewMessage)
+ {
+ chatBlinkTimer.Stop();
+ sbChat.ForeColor = chatService?.IsConnected == true ? Color.Green : Color.Gray;
+ return;
+ }
+
+ // Toggle color
+ sbChat.ForeColor = sbChat.ForeColor == Color.Red ? Color.Green : Color.Red;
+ }
+
+ ///
+ /// sbChat status label click event
+ ///
+ private void sbChat_Click(object sender, EventArgs e)
+ {
+ // Stop blinking
+ chatHasNewMessage = false;
+ chatBlinkTimer.Stop();
+ sbChat.ForeColor = chatService?.IsConnected == true ? Color.Green : Color.Gray;
+
+ // Show chat dialog
+ if (chatService == null)
+ {
+ MessageBox.Show("Chat service is not running.", "Chat", MessageBoxButtons.OK, MessageBoxIcon.Warning);
+ return;
+ }
+
+ // If there are senders who sent messages, show selection dialog
+ if (chatMessageSenders.Count > 0)
+ {
+ // Create simple selection form
+ using (var selectForm = new Form())
+ {
+ selectForm.Text = "Select Sender";
+ selectForm.Size = new Size(400, 300);
+ selectForm.StartPosition = FormStartPosition.CenterParent;
+ selectForm.FormBorderStyle = FormBorderStyle.FixedDialog;
+ selectForm.MaximizeBox = false;
+ selectForm.MinimizeBox = false;
+
+ var listBox = new ListBox
+ {
+ Dock = DockStyle.Fill,
+ Font = new Font("Malgun Gothic", 10)
+ };
+
+ var buttonPanel = new Panel
+ {
+ Dock = DockStyle.Bottom,
+ Height = 50
+ };
+
+ var btnOk = new Button
+ {
+ Text = "Open Chat",
+ DialogResult = DialogResult.OK,
+ Size = new Size(100, 30),
+ Location = new Point(140, 10)
+ };
+
+ var btnNew = new Button
+ {
+ Text = "New Chat",
+ DialogResult = DialogResult.Retry,
+ Size = new Size(100, 30),
+ Location = new Point(250, 10)
+ };
+
+ buttonPanel.Controls.Add(btnOk);
+ buttonPanel.Controls.Add(btnNew);
+
+ selectForm.Controls.Add(listBox);
+ selectForm.Controls.Add(buttonPanel);
+ selectForm.AcceptButton = btnOk;
+
+ // Populate list with senders
+ foreach (var sender2 in chatMessageSenders)
+ {
+ listBox.Items.Add(new { EmployeeId = sender2.Key, NickName = sender2.Value, Display = $"{sender2.Value} ({sender2.Key})" });
+ }
+ listBox.DisplayMember = "Display";
+
+ if (listBox.Items.Count > 0)
+ listBox.SelectedIndex = 0;
+
+ var result = selectForm.ShowDialog(this);
+
+ if (result == DialogResult.OK && listBox.SelectedItem != null)
+ {
+ // Open chat with selected sender
+ dynamic selected = listBox.SelectedItem;
+ var chatForm = new Dialog.fChat(chatService, selected.EmployeeId, selected.NickName);
+ chatForm.Show();
+
+ // Remove from senders list (chat opened)
+ chatMessageSenders.Remove(selected.EmployeeId);
+ }
+ else if (result == DialogResult.Retry)
+ {
+ // Open new chat (user will select recipient in chat window)
+ var chatForm = new Dialog.fChat(chatService);
+ chatForm.Show();
+ }
+ }
+ }
+ else
+ {
+ // No senders, open new chat
+ var chatForm = new Dialog.fChat(chatService);
+ chatForm.Show();
+ }
+ }
+
+ #endregion
}
}
diff --git a/Project/fMain.resx b/Project/fMain.resx
index 1964ad6..764f88c 100644
--- a/Project/fMain.resx
+++ b/Project/fMain.resx
@@ -184,16 +184,16 @@
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
- dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpVLbSqJRGPVqruY9Zp7gf4F5jbkSEhFE
- 00pFS4IyFSVTxHN4yAIPlCc8lGCpReJFqKAoaKJzNVeSMIgya/begw5SNAxzsfbP/vjWWt+3/s0D8F94
- s/geQqEQTk9P4Xa7Ybfbv/Kq1SooyuUySqUSisUiCoUCstksUqkULi8vEY1GcXFxgWAwyHrn8znG4zEs
- FkuLV6lUsFgsMJvN3sXLywt8Pt+3yWSCTqeD4+PjgdFoFPHu7u5YYTQa4eHhAcPhELQ2GAxwfX2NXq+H
- dDrNemKxGFqtFpvIYDB8oSuxvW5ubnB/f49MJsPd3t4iHo9zlHx2dsbRNTweD0fJVquVC4fDbP+Dg4PP
- K4FcLodGo8Ey6Pf7LAPqTMntdnvlTATx9PRE3bG3t/dxJUBxdXWF6XSKfD4PkjSXTCapE0fHJWFxlHx0
- dMR5vV5KJpTfvJUAadTUajV0u11QMnWORCJoNptUkDmTVVCv16FUKn++EqAIBAI0aTidTthsNpo0SNI4
- PDzE/v4+tFotEokEtra23hb4GxQKxQe5XA6JRPJ9WWOH3+//RMZzORwOmjTMZjP0ej1Nmrmq1Wrs7OxA
- JpORdvBEItF0TYCEpaHfk5MTqclkeiZk6bJhic3NTalYLH4mZKlAIPixrLODvGkNSZr9rsfHR5yfn7Ok
- VSoVtre3QchwuVzsGZNe8Pn83prAEjqdjoW1u7v7agKhUCjd2NigZHL9U19r+neA9wvhROqXtIFlogAA
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpZLPahpRGMVdddX3aJ9gXqCv0ZVgkAGJ
+ ozEqMUpAExUlE2UwziQhZtRANMRoxP+g+SeKi6BCgkJM0K66EoUiSk+5FyJYSwPt4jBwued3vu/cUQBQ
+ /I+WDt6TLMs4OjqCKIoQBOGr4u7uDkQ3Nzcol8solUrI5/PIZDJIpVK4uLhALBbD6ekpTk5O6N3pdIrB
+ YACe59uK29tbzGYzTCaTv2o0GuHw8PDbcDjE09MTdnd3e263m1VcX1/Tg36/j2q1itfXV5CzXq+HQqGA
+ breLq6sreicej6PdbtOJXC7Xl3kHxWIR9/f3SKfTTKVSwfn5OUPM4XCYIWtIksQQs8/nYyKRCN3fbrd/
+ ngOy2SyazSbt4Pn5mXZAkon58fFxnhwOh/Hw8EDSYbVaP84BRIlEAuPxGLlcDrIsM8lkkiQxZFye5xli
+ 3tnZYQ4ODogZb745IBaLWer1OjqdDoiZJJ+dnaHVahEgTZYkCY1GAyaT6ecSgCgUCpGmsb+/D7/fT5qG
+ 2+2Gw+HA1tYWbDYbLi8vsba29mfAezIajR/0ej1WV1e/LwCOj48/SZIUDAQCpGl4vV44nU7SNE3d2NjA
+ +vo6dDod3Z1l2fECQBRFC/nu7e1pPR7Pi9Pp1P6eznGcVqPRvLAsq11ZWfmxABAEwcLzPH2uWq2GaDRK
+ mzabzTAYDOA4DsFgkP7GgiBAqVR2FwBv2t7epmVtbm4uTaBWq7UqlYqY50+4BPgX/QLhROqXAnK3iwAA
AABJRU5ErkJggg==
@@ -254,34 +254,34 @@
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
- dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAALrSURBVDhPbZLZU9NXGIb5V3rn2FuttiJCUVtxuanI
- AGFJ2EQWl1GMIERASkQnhCQQQogRDSEhLHEERRmlUBKWH0sSI+DODxEdtcNF9danBzrOqOHiPVfnfd7z
- fe+JAqIOFl/98UCRbW5/kc23v9BGQmEr+wqEjln5Xei3/Bb2HrWwJ6+Z+GwjMWkXX6z51rR+CHODMH9W
- mwdRVXWhrOwi84KHDI2HdE0naRUuFOddiHvE55qJVmiF7SuAMM+qzfe461tg/sVb5p6/Ye7ZCuGnyzx8
- 8orgwhKBeZnXb96zurrK5pik7wCFrYvtI4850dBPhkhOr3CvS1HuIlUkp5R1kHzOuT5WXHZTJCChoFV2
- jz+n8X6Yu+EVbgdX6A8sc2vmFV5pie6JRdxjL7k29ISEgpZIgFiW7PQ9paTDj3XoMcY7QXReibqeSWo9
- 41R7JGq9QayDC2KplkiA2LTsECOccgxjGgix/M8nlj58RBZafP+Rl+/+ReOWaBqYE41sABA1yfaheY7b
- 76PvD6Ptlqh0jVHe7qe0bZSSNh9lHRL6vrCoszkSIDqWrYOPyG8ZoO5WmNreEFXdAco7Z1E7pzntkDjj
- mKLuZkj8BXMkYE+eRW66EyLH0Ed1TxCNZ5ZS1zQl7RInr09SdHWcIvs4f/YE2J2zQQu7c5tlQ1+AzMu9
- nHfPcNY5xekbEsXXJjhmGyO3xc9Rq58qAf01uzESEJ9jlnXeGVJrOsVT/08tEKlrpiyLj3TTCErTqNjJ
- FHFZpkiA+ByytksiqcJJsX2C/LVUix+l+W8UhhGS64dJ1Q2jbpskVrUBIDarSa5xT3JYfYM88VxV8+h6
- aop+mCNX/uKPSw9I1A5xxjbBLpWRTdGJ8jeAXapGucYzxaXeIFpviJruIJWdATQds5Q5ZkSV05y1i9Es
- Y8Qojfyw5VDbN4AYpal1Z6aJ6EwjOzIM/JJu4Oe0Bran6dmu0LMttZ6fUurZmqJja7Lu5hczEPUfnqi7
- iiOVMjYAAAAASUVORK5CYII=
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAL2SURBVDhPbdHJU9t1HMbx/ivenHq1tirSImVHL7V2
+ gLAkrAkErVNoypZCRVJ0QkhCQjbS2BASwpJOoaUyLQVJCPxYkhhZWtvyo0gdW4eDePXtEMcDhsPzuT2v
+ Z77zPQGc+KR+4J18pWM9T+kI5tU5yK2zk1NrJ0dhI1thI0tuJbPGQkZ1P+kVBlKLv35+2DtM4uQrHb15
+ SsffKvM0so4RpO0jlF33U6r2U6IeprjNi6TFS77SQXqVmRSJhiNAntKxpjL/wIPgJhvPf2P92SvWf9kj
+ /nSXn568JLq5Q2RD5NdXr9nf3+dk6qX/AXX27cG5Lb7snaT0up+SNl8iklYvRS1eCpuHKLjmSTwrrcKU
+ DOTW2kVf+Bl9D+M8iO9xL7rHZGSXu6svCQg7jC5u41t4wa2ZJ+TWWpOBHIVN9ASf0jgUwjazheF+FG1A
+ oHtsiS5/mBt+ga5AFNv0JtkKSzKQrbCJ7rktvnLPYpyKsfvHX+y8OUB8c8D26wNe/P4nap+AaWqdLPkx
+ QJbcKjpnNvjC+RDdZBzNqEC7d4HWwRBNrnkaXUGahwR0E3Eya/qTgcwai2ib/hm5dYruu3G6xmN0jEZo
+ HV5D5Vnhilugwb1M950YGdXmZCCj2iKa7seo1E9wYyyK2r9Gk3eFxkGBy98voRwIo3SG+WYswvnKY37h
+ fFW/qJ+IUPbtOC2+Va56lrlyW6D+1iIKxwJV1hA1thAd3hU+ruhLBtIrzaI2sEpR5zAN7n9XawfCiVK5
+ JUiJcQ6pcZ7WwWXSyo3JQFqFSdSMCFxq81DvXER+uGoJITX/iEQ/R0HPLEXaWVSuJc7JjgHOlZvETt8S
+ n6luU20NIeufT6wW6mb5/LvHXLj5iIuaGRoci5yVGXg75aJ4BDgr6xM7/cvcHI+iCcToHI3SPhxBPbRG
+ s3uVJtcKV50Cly0LpEoNvPXup64jQKrUaP+ozEhKmYEPS/V8UKLn/eJezhTrOCPRcbqoh/cKezhVqOVU
+ gfbOf+XD/AOeqLuKeIA3fgAAAABJRU5ErkJggg==
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
- dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHuSURBVDhPfZJbbxJRFIXPn+irpv+CZOIv88XamD7W
- Ng1t41trNHINhHsgAYFwC4SrzAyIIwEBMWkTdKZyyfLsQxw7HezDeph99vrW3mcO63Q6aLfbaDabaDQa
- qNfrqNVqqFarqFQqewDYY2KtVgubzcam0WgkIMVi8VEIo2QyDIdDkaxpmgmhGp8CuVzuvxBGI1MzpVGh
- XC5XF4sFVquVqBOwUCggnU7vhDBK/dvIzb/5yEPDMPD92y/k4zNxNhgMkM1mkUqlbBBGydREWq/XIPOP
- mYHzQxUnzxXkYltIv9+nKRCPxy0QxnfU5/O5aFoulyLZ+VLF1fEA7840XBz2TIiqqjQFwuGwCWGlUukZ
- 31GfzbZNcuNGJF+ffMGHi682iKIoNAUCgYCACEo+n5f4jvp0OhVNvfYtnAcK3p7+gzgPVHyMbM+73S5N
- AZ/Pt2fukslkJL6jPplMbJD3Tg1vjvq4PPrE/85aTBGNRuHxeJ6aABLfT0omk/p4PDYhZy9kXL7q4fq0
- h58L4755nzwWACmRSEixWEynl0gQtXWDq9efcWesTLPb7RZmkg1AikQiUigU0uklEoREZl63mEkW430F
- g0EHv2mdHpgsyzvNJMvHQ/n9fofX69XJ7HK5bGaSrfBQPNXBzU92nQFgfwCJli/+LKS33wAAAABJRU5E
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHuSURBVDhPhZLdb9JgGMX7T+xW439BQvzLvHEuZpdz
+ y4JbvNuMxpaPlED5CE2KhbS0gYwvgYKIBATEZCZoqxRyzPMS6hCGF+fm6XN+57xvX65er6NWq6FSqaBc
+ LuPm5galUgnFYhGmaR4A4PaJq1arWC6XWxoMBgyi6/peCEfJZOj3+yy51+t5EJqZpolcLncvhKPKtExp
+ NDAMozibzeC6LpsTUNM0KIqyE8JR6nrRMIzfuq73HcfB1y8/kU9N2LdutwtVVSHL8haEo+R15cViATJ/
+ mzh4eWTh9EkLueQK0ul0qAVSqdQGhDNN055Op2xpPp+z5MAzC1cnXbw57+HiqO1BLMuiFojH4x6EKxQK
+ jzVNsyeT1VKzfMuSr08/4d3F5y1Iq9WiFhBFkUEYJZ/P+1VVtcfjMVtq174jcNjC67O/kMChhffS6nuj
+ 0aAWCIfDB95ZstmsX1EUezQabUHeBnp4ddzB5fEHuO6CtUgkEggGgw83blSWZX8mk7GHw6EHOX/axOXz
+ Nq7P2vgxc+6aH3lHuKt0Ou1PJpM2vUR2cdVbXL34iF+O65kFQWDmnQCSJEn+WCxm00tc/2IyS5K0Yb4X
+ QIpGoz5RFG16YM1mc6d5L4AUiUR8oVDIJjPP81vm/wJIgiD4eJ5/8O98rT+Jli/+ECJFiAAAAABJRU5E
rkJggg==
@@ -369,32 +369,32 @@
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
- dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpVLbSqJRGPVqruY9Zp7gf4F5jbkSEhFE
- 00pFS4IyFSVTxHN4yAIPlCc8lGCpReJFqKAoaKJzNVeSMIgya/begw5SNAxzsfbP/vjWWt+3/s0D8F94
- s/geQqEQTk9P4Xa7Ybfbv/Kq1SooyuUySqUSisUiCoUCstksUqkULi8vEY1GcXFxgWAwyHrn8znG4zEs
- FkuLV6lUsFgsMJvN3sXLywt8Pt+3yWSCTqeD4+PjgdFoFPHu7u5YYTQa4eHhAcPhELQ2GAxwfX2NXq+H
- dDrNemKxGFqtFpvIYDB8oSuxvW5ubnB/f49MJsPd3t4iHo9zlHx2dsbRNTweD0fJVquVC4fDbP+Dg4PP
- K4FcLodGo8Ey6Pf7LAPqTMntdnvlTATx9PRE3bG3t/dxJUBxdXWF6XSKfD4PkjSXTCapE0fHJWFxlHx0
- dMR5vV5KJpTfvJUAadTUajV0u11QMnWORCJoNptUkDmTVVCv16FUKn++EqAIBAI0aTidTthsNpo0SNI4
- PDzE/v4+tFotEokEtra23hb4GxQKxQe5XA6JRPJ9WWOH3+//RMZzORwOmjTMZjP0ej1Nmrmq1Wrs7OxA
- JpORdvBEItF0TYCEpaHfk5MTqclkeiZk6bJhic3NTalYLH4mZKlAIPixrLODvGkNSZr9rsfHR5yfn7Ok
- VSoVtre3QchwuVzsGZNe8Pn83prAEjqdjoW1u7v7agKhUCjd2NigZHL9U19r+neA9wvhROqXtIFlogAA
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpZLPahpRGMVdddX3aJ9gXqCv0ZVgkAGJ
+ ozEqMUpAExUlE2UwziQhZtRANMRoxP+g+SeKi6BCgkJM0K66EoUiSk+5FyJYSwPt4jBwued3vu/cUQBQ
+ /I+WDt6TLMs4OjqCKIoQBOGr4u7uDkQ3Nzcol8solUrI5/PIZDJIpVK4uLhALBbD6ekpTk5O6N3pdIrB
+ YACe59uK29tbzGYzTCaTv2o0GuHw8PDbcDjE09MTdnd3e263m1VcX1/Tg36/j2q1itfXV5CzXq+HQqGA
+ breLq6sreicej6PdbtOJXC7Xl3kHxWIR9/f3SKfTTKVSwfn5OUPM4XCYIWtIksQQs8/nYyKRCN3fbrd/
+ ngOy2SyazSbt4Pn5mXZAkon58fFxnhwOh/Hw8EDSYbVaP84BRIlEAuPxGLlcDrIsM8lkkiQxZFye5xli
+ 3tnZYQ4ODogZb745IBaLWer1OjqdDoiZJJ+dnaHVahEgTZYkCY1GAyaT6ecSgCgUCpGmsb+/D7/fT5qG
+ 2+2Gw+HA1tYWbDYbLi8vsba29mfAezIajR/0ej1WV1e/LwCOj48/SZIUDAQCpGl4vV44nU7SNE3d2NjA
+ +vo6dDod3Z1l2fECQBRFC/nu7e1pPR7Pi9Pp1P6eznGcVqPRvLAsq11ZWfmxABAEwcLzPH2uWq2GaDRK
+ mzabzTAYDOA4DsFgkP7GgiBAqVR2FwBv2t7epmVtbm4uTaBWq7UqlYqY50+4BPgX/QLhROqXAnK3iwAA
AABJRU5ErkJggg==
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29m
- dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpVLbSqJRGPVqruY9Zp7gf4F5jbkSEhFE
- 00pFS4IyFSVTxHN4yAIPlCc8lGCpReJFqKAoaKJzNVeSMIgya/begw5SNAxzsfbP/vjWWt+3/s0D8F94
- s/geQqEQTk9P4Xa7Ybfbv/Kq1SooyuUySqUSisUiCoUCstksUqkULi8vEY1GcXFxgWAwyHrn8znG4zEs
- FkuLV6lUsFgsMJvN3sXLywt8Pt+3yWSCTqeD4+PjgdFoFPHu7u5YYTQa4eHhAcPhELQ2GAxwfX2NXq+H
- dDrNemKxGFqtFpvIYDB8oSuxvW5ubnB/f49MJsPd3t4iHo9zlHx2dsbRNTweD0fJVquVC4fDbP+Dg4PP
- K4FcLodGo8Ey6Pf7LAPqTMntdnvlTATx9PRE3bG3t/dxJUBxdXWF6XSKfD4PkjSXTCapE0fHJWFxlHx0
- dMR5vV5KJpTfvJUAadTUajV0u11QMnWORCJoNptUkDmTVVCv16FUKn++EqAIBAI0aTidTthsNpo0SNI4
- PDzE/v4+tFotEokEtra23hb4GxQKxQe5XA6JRPJ9WWOH3+//RMZzORwOmjTMZjP0ej1Nmrmq1Wrs7OxA
- JpORdvBEItF0TYCEpaHfk5MTqclkeiZk6bJhic3NTalYLH4mZKlAIPixrLODvGkNSZr9rsfHR5yfn7Ok
- VSoVtre3QchwuVzsGZNe8Pn83prAEjqdjoW1u7v7agKhUCjd2NigZHL9U19r+neA9wvhROqXtIFlogAA
+ dHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAIwSURBVDhPpZLPahpRGMVdddX3aJ9gXqCv0ZVgkAGJ
+ ozEqMUpAExUlE2UwziQhZtRANMRoxP+g+SeKi6BCgkJM0K66EoUiSk+5FyJYSwPt4jBwued3vu/cUQBQ
+ /I+WDt6TLMs4OjqCKIoQBOGr4u7uDkQ3Nzcol8solUrI5/PIZDJIpVK4uLhALBbD6ekpTk5O6N3pdIrB
+ YACe59uK29tbzGYzTCaTv2o0GuHw8PDbcDjE09MTdnd3e263m1VcX1/Tg36/j2q1itfXV5CzXq+HQqGA
+ breLq6sreicej6PdbtOJXC7Xl3kHxWIR9/f3SKfTTKVSwfn5OUPM4XCYIWtIksQQs8/nYyKRCN3fbrd/
+ ngOy2SyazSbt4Pn5mXZAkon58fFxnhwOh/Hw8EDSYbVaP84BRIlEAuPxGLlcDrIsM8lkkiQxZFye5xli
+ 3tnZYQ4ODogZb745IBaLWer1OjqdDoiZJJ+dnaHVahEgTZYkCY1GAyaT6ecSgCgUCpGmsb+/D7/fT5qG
+ 2+2Gw+HA1tYWbDYbLi8vsba29mfAezIajR/0ej1WV1e/LwCOj48/SZIUDAQCpGl4vV44nU7SNE3d2NjA
+ +vo6dDod3Z1l2fECQBRFC/nu7e1pPR7Pi9Pp1P6eznGcVqPRvLAsq11ZWfmxABAEwcLzPH2uWq2GaDRK
+ mzabzTAYDOA4DsFgkP7GgiBAqVR2FwBv2t7epmVtbm4uTaBWq7UqlYqY50+4BPgX/QLhROqXAnK3iwAA
AABJRU5ErkJggg==
@@ -416,52 +416,52 @@
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
- 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
+ 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
- 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
+ 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
- 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
- AT99qmQqX2rFAAAAAElFTkSuQmCC
+ YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIHSURBVDhPY2CAgZnGrAxLtTMZluosYFim08KwWFcdLL5K
+ i4dhmVYhwzLteQzLdFoZlqlLwfWggKU6qxmW6fyH48U6PxiWaHoyLNO8jiK+VOsJwyotCTTNWjYgSYG1
+ lv/9Tub8T7lc/z/3ZMX/eXNiP9S05/9LvdzwP/Jc2X/VbV5QQ7T7UA1YpjOBf435seQr9d+Kb/X8L7nZ
+ 8//QzJz/jzrT/l+dlPkTJAbDJrtDTzAs0bmGasBSXcHiW911MEUdh+r/Xy+N+L+/1f7v9UKvzz3bCuEG
+ FN/qugBSj2oAAwND8c3uuTBFlZc6lp0vDDl4Odnt28Ugtf/bKpz2Fl/s+AiSK7jZ8w5dLxjAXFB0s6cE
+ xH/RGiH+uMzz/5UMrT9Psm3UCm/26hff6vpcdKvnPLpeMCi+02NcfKPrOIz/uNjT81Gpx38wLvHyAKu5
+ 2V1afKu7BUUjMii+0y0GYz+p8MqCGfCk0jsTJFZ/v54j99ZEPhRNuMCjCs++J+Ve/0H4cblXL7o8QfC4
+ zHMj3AulHhvQ5fGC//vrOV50RZ1+0xP3H4RfdkaeAomhq0MB/w70a/490Nv3/0DvmfsHen//P9D7HxlD
+ xc6A1ezv0UBo3DaR/e+B3vmne+r/oWvChUFq/x7omQfSy/B3f28vugJiMUgvw78DfbZ/D/Qs/3+gdxUp
+ GKQHpBcAOYOqX5UHGvcAAAAASUVORK5CYII=
diff --git a/SubProject/AmkorRestfulService b/SubProject/AmkorRestfulService
deleted file mode 160000
index cd4e137..0000000
--- a/SubProject/AmkorRestfulService
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit cd4e1379bc7b1fd493d1e70ca073fb47eb07d9a5
diff --git a/SubProject/ChatServer/App.config b/SubProject/ChatServer/App.config
new file mode 100644
index 0000000..8645d93
--- /dev/null
+++ b/SubProject/ChatServer/App.config
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5000
+
+
+ 1000
+
+
+
+
diff --git a/SubProject/ChatServer/ChatClient.cs b/SubProject/ChatServer/ChatClient.cs
new file mode 100644
index 0000000..213e9cb
--- /dev/null
+++ b/SubProject/ChatServer/ChatClient.cs
@@ -0,0 +1,174 @@
+using System;
+using System.Net.Sockets;
+using System.Text;
+using System.Threading;
+using Newtonsoft.Json;
+
+namespace NoticeServer
+{
+ ///
+ /// 연결된 채팅 클라이언트 정보 및 통신 관리
+ ///
+ public class ChatClient
+ {
+ private TcpClient tcpClient;
+ private NetworkStream stream;
+ private Thread receiveThread;
+ private bool isConnected;
+
+ /// 클라이언트 고유 ID
+ public string ClientId { get; private set; }
+
+ /// 닉네임
+ public string NickName { get; set; }
+
+ /// 사원번호 (Employee ID)
+ public string EmployeeId { get; set; }
+
+ /// 사용자 그룹
+ public string UserGroup { get; set; }
+
+ /// IP 주소
+ public string IpAddress { get; private set; }
+
+ /// 호스트명
+ public string HostName { get; set; }
+
+ /// 연결 시간
+ public DateTime ConnectedTime { get; private set; }
+
+ /// 마지막 활동 시간
+ public DateTime LastActivity { get; set; }
+
+ /// 메시지 수신 이벤트
+ public event EventHandler MessageReceived;
+
+ /// 연결 해제 이벤트
+ public event EventHandler Disconnected;
+
+ public ChatClient(TcpClient client)
+ {
+ tcpClient = client;
+ stream = client.GetStream();
+ isConnected = true;
+ ClientId = Guid.NewGuid().ToString();
+ ConnectedTime = DateTime.Now;
+ LastActivity = DateTime.Now;
+
+ var endpoint = client.Client.RemoteEndPoint.ToString();
+ IpAddress = endpoint.Split(':')[0];
+
+ StartReceiving();
+ }
+
+ ///
+ /// 메시지 수신 스레드 시작
+ ///
+ private void StartReceiving()
+ {
+ receiveThread = new Thread(ReceiveLoop)
+ {
+ IsBackground = true,
+ Name = $"ChatClient-{ClientId}"
+ };
+ receiveThread.Start();
+ }
+
+ ///
+ /// 메시지 수신 루프
+ ///
+ private void ReceiveLoop()
+ {
+ byte[] buffer = new byte[4096];
+
+ try
+ {
+ while (isConnected && tcpClient.Connected)
+ {
+ int bytesRead = stream.Read(buffer, 0, buffer.Length);
+ if (bytesRead == 0)
+ {
+ // Connection closed
+ break;
+ }
+
+ string jsonData = Encoding.UTF8.GetString(buffer, 0, bytesRead);
+ var message = JsonConvert.DeserializeObject(jsonData);
+
+ if (message != null)
+ {
+ LastActivity = DateTime.Now;
+
+ // Set nickname, employeeId, userGroup from Connect message
+ if (message.Type == MessageType.Connect && !string.IsNullOrEmpty(message.NickName))
+ {
+ NickName = message.NickName;
+ EmployeeId = message.EmployeeId;
+ UserGroup = message.UserGroup;
+ HostName = message.HostName;
+ }
+
+ MessageReceived?.Invoke(this, message);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Client {NickName ?? ClientId} receive error: {ex.Message}");
+ }
+ finally
+ {
+ Disconnect();
+ }
+ }
+
+ ///
+ /// Send message
+ ///
+ public bool SendMessage(ChatMessage message)
+ {
+ if (!isConnected || !tcpClient.Connected)
+ return false;
+
+ try
+ {
+ string jsonData = JsonConvert.SerializeObject(message);
+ byte[] data = Encoding.UTF8.GetBytes(jsonData);
+ stream.Write(data, 0, data.Length);
+ stream.Flush();
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Client {NickName ?? ClientId} send error: {ex.Message}");
+ Disconnect();
+ return false;
+ }
+ }
+
+ ///
+ /// 연결 해제
+ ///
+ public void Disconnect()
+ {
+ if (!isConnected)
+ return;
+
+ isConnected = false;
+
+ try
+ {
+ stream?.Close();
+ tcpClient?.Close();
+ }
+ catch { }
+
+ Disconnected?.Invoke(this, ClientId);
+ }
+
+ public override string ToString()
+ {
+ return $"{NickName ?? "Unknown"} ({IpAddress})";
+ }
+ }
+}
diff --git a/SubProject/ChatServer/ChatMessage.cs b/SubProject/ChatServer/ChatMessage.cs
new file mode 100644
index 0000000..bb9227d
--- /dev/null
+++ b/SubProject/ChatServer/ChatMessage.cs
@@ -0,0 +1,76 @@
+using System;
+
+namespace NoticeServer
+{
+ ///
+ /// 채팅 메시지 타입
+ ///
+ public enum MessageType
+ {
+ /// 클라이언트 연결
+ Connect,
+ /// 클라이언트 연결 해제
+ Disconnect,
+ /// 일반 채팅 메시지
+ Chat,
+ /// 서버 공지
+ Notice,
+ /// 귓속말
+ Whisper,
+ /// 사용자 목록 요청
+ UserListRequest,
+ /// 사용자 목록 응답
+ UserListResponse,
+ /// 핑 (연결 유지)
+ Ping,
+ /// 퐁 (핑 응답)
+ Pong
+ }
+
+ ///
+ /// 채팅 메시지 프로토콜
+ ///
+ [Serializable]
+ public class ChatMessage
+ {
+ /// 메시지 타입
+ public MessageType Type { get; set; }
+
+ /// 발신자 닉네임
+ public string NickName { get; set; }
+
+ /// 발신자 사원번호 (Employee ID)
+ public string EmployeeId { get; set; }
+
+ /// 발신자 IP
+ public string IpAddress { get; set; }
+
+ /// 발신자 호스트명
+ public string HostName { get; set; }
+
+ /// 메시지 내용
+ public string Content { get; set; }
+
+ /// 수신자 사원번호 (1:1 채팅용)
+ public string TargetEmployeeId { get; set; }
+
+ /// 수신자 닉네임 (귓속말용, null이면 전체)
+ public string TargetNickName { get; set; }
+
+ /// 전송 시간
+ public DateTime Timestamp { get; set; }
+
+ /// 사용자 그룹
+ public string UserGroup { get; set; }
+
+ public ChatMessage()
+ {
+ Timestamp = DateTime.Now;
+ }
+
+ public override string ToString()
+ {
+ return $"[{Timestamp:HH:mm:ss}] {NickName}: {Content}";
+ }
+ }
+}
diff --git a/SubProject/ChatServer/NoticeServer.csproj b/SubProject/ChatServer/NoticeServer.csproj
new file mode 100644
index 0000000..fda8916
--- /dev/null
+++ b/SubProject/ChatServer/NoticeServer.csproj
@@ -0,0 +1,73 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}
+ Exe
+ NoticeServer
+ NoticeServer
+ v4.8
+ 512
+ true
+ true
+
+
+ x86
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+
+ ..\..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
+
+
+
+
+
+ Component
+
+
+ True
+ True
+ Settings.settings
+
+
+
+
+
+
+
+
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
\ No newline at end of file
diff --git a/SubProject/ChatServer/NoticeService.cs b/SubProject/ChatServer/NoticeService.cs
new file mode 100644
index 0000000..113dd6e
--- /dev/null
+++ b/SubProject/ChatServer/NoticeService.cs
@@ -0,0 +1,116 @@
+using System;
+using System.ServiceProcess;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Diagnostics;
+
+namespace NoticeServer
+{
+ public partial class NoticeService : ServiceBase
+ {
+ private CancellationTokenSource _cancellationTokenSource;
+ private Task _serviceTask;
+ private static TcpChatServer _tcpServer;
+
+ public NoticeService()
+ {
+ InitializeComponent();
+ ServiceName = "EETGWNoticeService";
+ CanStop = true;
+ CanPauseAndContinue = false;
+ AutoLog = true;
+ }
+
+ protected override void OnStart(string[] args)
+ {
+ try
+ {
+ EventLog.WriteEntry(ServiceName, "Notice 서비스가 시작됩니다.", EventLogEntryType.Information);
+
+ _cancellationTokenSource = new CancellationTokenSource();
+ _serviceTask = Task.Run(() => DoWork(_cancellationTokenSource.Token));
+
+ EventLog.WriteEntry(ServiceName, "Notice 서비스가 성공적으로 시작되었습니다.", EventLogEntryType.Information);
+ }
+ catch (Exception ex)
+ {
+ EventLog.WriteEntry(ServiceName, $"서비스 시작 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
+ throw;
+ }
+ }
+
+ protected override void OnStop()
+ {
+ try
+ {
+ EventLog.WriteEntry(ServiceName, "Notice 서비스를 중지합니다.", EventLogEntryType.Information);
+
+ _cancellationTokenSource?.Cancel();
+
+ // Stop server
+ _tcpServer?.Stop();
+
+ if (_serviceTask != null)
+ {
+ // 최대 30초 대기
+ if (!_serviceTask.Wait(TimeSpan.FromSeconds(30)))
+ {
+ EventLog.WriteEntry(ServiceName, "서비스 중지 시간 초과", EventLogEntryType.Warning);
+ }
+ }
+
+ EventLog.WriteEntry(ServiceName, "Notice 서비스가 중지되었습니다.", EventLogEntryType.Information);
+ }
+ catch (Exception ex)
+ {
+ EventLog.WriteEntry(ServiceName, $"서비스 중지 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
+ }
+ finally
+ {
+ _cancellationTokenSource?.Dispose();
+ _serviceTask?.Dispose();
+ }
+ }
+
+ private void DoWork(CancellationToken cancellationToken)
+ {
+ EventLog.WriteEntry(ServiceName, "Notice 서비스 작업을 시작합니다.", EventLogEntryType.Information);
+
+ try
+ {
+ // Read settings
+ int tcpPort = Properties.Settings.Default.TcpPort;
+ int maxClients = Properties.Settings.Default.MaxClients;
+
+ // Start server
+ _tcpServer = new TcpChatServer(tcpPort, maxClients);
+ _tcpServer.Start();
+
+ EventLog.WriteEntry(ServiceName, $"Notice 서버가 포트 {tcpPort}에서 시작되었습니다.", EventLogEntryType.Information);
+
+ // Wait for cancellation
+ while (!cancellationToken.IsCancellationRequested)
+ {
+ if (cancellationToken.WaitHandle.WaitOne(1000))
+ {
+ break; // 취소 요청 시 종료
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ EventLog.WriteEntry(ServiceName, $"서버 실행 중 오류 발생: {ex.Message}\n{ex.StackTrace}", EventLogEntryType.Error);
+ }
+
+ EventLog.WriteEntry(ServiceName, "Notice 서비스 작업이 종료되었습니다.", EventLogEntryType.Information);
+ }
+
+ private void InitializeComponent()
+ {
+ //
+ // NoticeService
+ //
+ this.ServiceName = "EETGWNoticeService";
+ }
+ }
+}
diff --git a/SubProject/ChatServer/Program.cs b/SubProject/ChatServer/Program.cs
new file mode 100644
index 0000000..b8f8e6c
--- /dev/null
+++ b/SubProject/ChatServer/Program.cs
@@ -0,0 +1,288 @@
+using System;
+using System.Configuration;
+using System.Diagnostics;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.ServiceProcess;
+using System.Threading;
+
+namespace NoticeServer
+{
+ partial class Program
+ {
+ static TcpChatServer tcpServer;
+ static bool isRunning = true;
+
+ static void Main(string[] args)
+ {
+ // 명령행 인수 처리
+ if (args.Length > 0)
+ {
+ string command = args[0].ToLower();
+ switch (command)
+ {
+ case "-install":
+ case "/install":
+ InstallService();
+ return;
+ case "-uninstall":
+ case "/uninstall":
+ UninstallService();
+ return;
+ case "-console":
+ case "/console":
+ RunAsConsole();
+ return;
+ case "-help":
+ case "/help":
+ case "/?":
+ ShowHelp();
+ return;
+ }
+ }
+
+ // 서비스로 실행
+ if (Environment.UserInteractive)
+ {
+ // 대화형 모드에서는 콘솔로 실행
+ RunAsConsole();
+ }
+ else
+ {
+ // 서비스로 실행
+ ServiceBase[] ServicesToRun;
+ ServicesToRun = new ServiceBase[]
+ {
+ new NoticeService()
+ };
+ ServiceBase.Run(ServicesToRun);
+ }
+ }
+
+ private static void ShowHelp()
+ {
+ Console.WriteLine("EETGW Notice Server (Chat Server)");
+ Console.WriteLine("사용법:");
+ Console.WriteLine(" NoticeServer.exe - 서비스로 실행 (또는 대화형 모드에서 콘솔 실행)");
+ Console.WriteLine(" NoticeServer.exe -console - 콘솔 모드로 실행");
+ Console.WriteLine(" NoticeServer.exe -install - 서비스 설치");
+ Console.WriteLine(" NoticeServer.exe -uninstall - 서비스 제거");
+ Console.WriteLine(" NoticeServer.exe -help - 도움말 표시");
+ }
+
+ private static void InstallService()
+ {
+ try
+ {
+ string servicePath = $"\"{System.Reflection.Assembly.GetExecutingAssembly().Location}\"";
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "sc.exe",
+ Arguments = $"create EETGWNoticeService binPath= \"{servicePath}\" DisplayName= \"EETGW Notice Service\" start= auto",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ process.WaitForExit();
+ if (process.ExitCode == 0)
+ {
+ Console.WriteLine("서비스가 성공적으로 설치되었습니다.");
+ Console.WriteLine("서비스를 시작하려면 다음 명령을 실행하세요:");
+ Console.WriteLine("net start EETGWNoticeService");
+ }
+ else
+ {
+ string error = process.StandardError.ReadToEnd();
+ Console.WriteLine($"서비스 설치 실패: {error}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"서비스 설치 중 오류 발생: {ex.Message}");
+ }
+ }
+
+ private static void UninstallService()
+ {
+ try
+ {
+ ProcessStartInfo startInfo = new ProcessStartInfo
+ {
+ FileName = "sc.exe",
+ Arguments = "delete EETGWNoticeService",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true
+ };
+
+ using (Process process = Process.Start(startInfo))
+ {
+ process.WaitForExit();
+ if (process.ExitCode == 0)
+ {
+ Console.WriteLine("서비스가 성공적으로 제거되었습니다.");
+ }
+ else
+ {
+ string error = process.StandardError.ReadToEnd();
+ Console.WriteLine($"서비스 제거 실패: {error}");
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"서비스 제거 중 오류 발생: {ex.Message}");
+ }
+ }
+
+ private static void RunAsConsole()
+ {
+ // 중복실행 방지 체크
+ if (!CheckSingleInstance())
+ {
+ return; // 프로그램 종료
+ }
+
+ Console.OutputEncoding = System.Text.Encoding.UTF8;
+ Console.Title = "EETGW Notice Server (Chat Server)";
+ Console.WriteLine("========================================");
+ Console.WriteLine(" EETGW Notice Server v1.0 (콘솔 모드)");
+ Console.WriteLine("========================================\n");
+
+ // Read settings
+ int tcpPort = Properties.Settings.Default.TcpPort;
+ int maxClients = Properties.Settings.Default.MaxClients;
+
+ // Get local IP address
+ string localIP = GetLocalIPAddress();
+ string hostName = Dns.GetHostName();
+
+ Console.WriteLine($"Server IP: {localIP}");
+ Console.WriteLine($"Host Name: {hostName}");
+ Console.WriteLine($"TCP Port: {tcpPort}");
+ Console.WriteLine($"Max Clients: {maxClients}\n");
+
+ // Start server
+ tcpServer = new TcpChatServer(tcpPort, maxClients);
+ tcpServer.Start();
+
+ Console.WriteLine("\nServer started successfully.");
+ Console.WriteLine("Commands: status, clear, quit, help");
+ Console.WriteLine("종료하려면 Ctrl+C를 누르거나 'quit'를 입력하세요.\n");
+
+ // Ctrl+C handler
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ Console.WriteLine("\n서버를 종료합니다...");
+ e.Cancel = true;
+ isRunning = false;
+ };
+
+ // Command processing loop
+ while (isRunning)
+ {
+ string command = Console.ReadLine()?.Trim().ToLower();
+
+ switch (command)
+ {
+ case "status":
+ case "s":
+ tcpServer.PrintStatus();
+ break;
+
+ case "clear":
+ case "cls":
+ Console.Clear();
+ Console.WriteLine("EETGW Notice Server - Commands: status, clear, quit\n");
+ break;
+
+ case "quit":
+ case "exit":
+ case "q":
+ isRunning = false;
+ break;
+
+ case "help":
+ case "h":
+ case "?":
+ PrintHelp();
+ break;
+
+ case "":
+ break;
+
+ default:
+ Console.WriteLine($"Unknown command: {command}");
+ Console.WriteLine("Available commands: status, clear, quit, help");
+ break;
+ }
+
+ Thread.Sleep(100);
+ }
+
+ // Server shutdown
+ Console.WriteLine("\nShutting down server...");
+ tcpServer.Stop();
+
+ Console.WriteLine("Server stopped.");
+ Thread.Sleep(1000);
+ }
+
+ ///
+ /// 중복실행 방지 체크
+ ///
+ /// 단일 인스턴스인 경우 true, 중복실행인 경우 false
+ static bool CheckSingleInstance()
+ {
+ string processName = Process.GetCurrentProcess().ProcessName;
+ Process[] processes = Process.GetProcessesByName(processName);
+
+ if (processes.Length > 1)
+ {
+ // 중복실행 감지
+ string message = $"⚠️ 프로그램이 이미 실행 중입니다!\n\n" +
+ "동시에 여러 개의 프로그램을 실행할 수 없습니다.\n";
+
+ Console.WriteLine(message);
+ return false;
+ }
+
+ return true; // 단일 인스턴스
+ }
+
+ static string GetLocalIPAddress()
+ {
+ try
+ {
+ var host = Dns.GetHostEntry(Dns.GetHostName());
+ var ipAddress = host.AddressList
+ .FirstOrDefault(ip => ip.AddressFamily == AddressFamily.InterNetwork);
+
+ if (ipAddress != null)
+ return ipAddress.ToString();
+ }
+ catch { }
+
+ return "127.0.0.1";
+ }
+
+ static void PrintHelp()
+ {
+ Console.WriteLine("\n========================================");
+ Console.WriteLine("Available Commands:");
+ Console.WriteLine("========================================");
+ Console.WriteLine("status (s) - Show server status and client list");
+ Console.WriteLine("clear (cls) - Clear screen");
+ Console.WriteLine("quit (q) - Stop server");
+ Console.WriteLine("help (h) - Show this help");
+ Console.WriteLine("========================================\n");
+ }
+ }
+}
diff --git a/SubProject/ChatServer/Properties/AssemblyInfo.cs b/SubProject/ChatServer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..339928c
--- /dev/null
+++ b/SubProject/ChatServer/Properties/AssemblyInfo.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+// 어셈블리에 대한 일반 정보는 다음 특성 집합을 통해
+// 제어됩니다. 어셈블리와 관련된 정보를 수정하려면
+// 이러한 특성 값을 변경하세요.
+[assembly: AssemblyTitle("ChatServer")]
+[assembly: AssemblyDescription("GroupWare Chat Server - TCP/UDP 기반 다중 사용자 채팅 서버")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("ChatServer")]
+[assembly: AssemblyCopyright("Copyright © 2025")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// ComVisible을 false로 설정하면 이 어셈블리의 형식이 COM 구성 요소에
+// 표시되지 않습니다. COM에서 이 어셈블리의 형식에 액세스하려면
+// 해당 형식에 대해 ComVisible 특성을 true로 설정하세요.
+[assembly: ComVisible(false)]
+
+// 이 프로젝트가 COM에 노출되는 경우 다음 GUID는 typelib의 ID를 나타냅니다.
+[assembly: Guid("8e9a4b1c-6d5f-4e2a-9f3b-1c8d7e6a5b4f")]
+
+// 어셈블리의 버전 정보는 다음 네 가지 값으로 구성됩니다.
+//
+// 주 버전
+// 부 버전
+// 빌드 번호
+// 수정 버전
+//
+[assembly: AssemblyVersion("25.11.12.1300")]
+[assembly: AssemblyFileVersion("25.11.12.1300")]
diff --git a/SubProject/ChatServer/Properties/Settings.Designer.cs b/SubProject/ChatServer/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..567a4b9
--- /dev/null
+++ b/SubProject/ChatServer/Properties/Settings.Designer.cs
@@ -0,0 +1,50 @@
+//------------------------------------------------------------------------------
+//
+// 이 코드는 도구를 사용하여 생성되었습니다.
+// 런타임 버전:4.0.30319.42000
+//
+// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
+// 이러한 변경 내용이 손실됩니다.
+//
+//------------------------------------------------------------------------------
+
+namespace ChatServer.Properties {
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default {
+ get {
+ return defaultInstance;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("5000")]
+ public int TcpPort {
+ get {
+ return ((int)(this["TcpPort"]));
+ }
+ set {
+ this["TcpPort"] = value;
+ }
+ }
+
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("1000")]
+ public int MaxClients {
+ get {
+ return ((int)(this["MaxClients"]));
+ }
+ set {
+ this["MaxClients"] = value;
+ }
+ }
+ }
+}
diff --git a/SubProject/ChatServer/Properties/Settings.settings b/SubProject/ChatServer/Properties/Settings.settings
new file mode 100644
index 0000000..c93109f
--- /dev/null
+++ b/SubProject/ChatServer/Properties/Settings.settings
@@ -0,0 +1,12 @@
+
+
+
+
+
+ 5000
+
+
+ 1000
+
+
+
\ No newline at end of file
diff --git a/SubProject/ChatServer/TcpChatServer.cs b/SubProject/ChatServer/TcpChatServer.cs
new file mode 100644
index 0000000..b0f27d3
--- /dev/null
+++ b/SubProject/ChatServer/TcpChatServer.cs
@@ -0,0 +1,421 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Threading;
+
+namespace NoticeServer
+{
+ ///
+ /// TCP 채팅 서버
+ ///
+ public class TcpChatServer
+ {
+ private TcpListener tcpListener;
+ private Thread acceptThread;
+ private bool isRunning;
+ private int tcpPort;
+ private int maxClients;
+
+ private readonly object clientsLock = new object();
+ private List clients = new List();
+
+ public TcpChatServer(int tcpPort, int maxClients = 100)
+ {
+ this.tcpPort = tcpPort;
+ this.maxClients = maxClients;
+ }
+
+ ///
+ /// TCP 채팅 서버 시작
+ ///
+ public void Start()
+ {
+ if (isRunning)
+ return;
+
+ try
+ {
+ tcpListener = new TcpListener(IPAddress.Any, tcpPort);
+ tcpListener.Start();
+ isRunning = true;
+
+ acceptThread = new Thread(AcceptLoop)
+ {
+ IsBackground = true,
+ Name = "TcpAcceptThread"
+ };
+ acceptThread.Start();
+
+ Console.WriteLine($"[TCP] Chat server started (Port: {tcpPort})");
+ Console.WriteLine($"[TCP] Max clients: {maxClients}");
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Failed to start TCP server: {ex.Message}");
+ isRunning = false;
+ }
+ }
+
+ ///
+ /// 클라이언트 연결 수락 루프
+ ///
+ private void AcceptLoop()
+ {
+ while (isRunning)
+ {
+ try
+ {
+ TcpClient tcpClient = tcpListener.AcceptTcpClient();
+
+ lock (clientsLock)
+ {
+ if (clients.Count >= maxClients)
+ {
+ Console.WriteLine("[REJECTED] Maximum clients exceeded");
+ tcpClient.Close();
+ continue;
+ }
+ }
+
+ var client = new ChatClient(tcpClient);
+ client.MessageReceived += OnClientMessageReceived;
+ client.Disconnected += OnClientDisconnected;
+
+ lock (clientsLock)
+ {
+ clients.Add(client);
+ }
+
+ Console.WriteLine($"[CONNECTED] New client: {client.IpAddress} (ID: {client.ClientId})");
+ }
+ catch (SocketException)
+ {
+ // Server shutdown
+ break;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[ERROR] Client accept error: {ex.Message}");
+ }
+ }
+ }
+
+ ///
+ /// 클라이언트 메시지 수신 이벤트 핸들러
+ ///
+ private void OnClientMessageReceived(object sender, ChatMessage message)
+ {
+ var client = sender as ChatClient;
+ if (client == null)
+ return;
+
+ Console.WriteLine($"[RECEIVED] {client.NickName ?? client.IpAddress}: {message.Type} - {message.Content}");
+
+ switch (message.Type)
+ {
+ case MessageType.Connect:
+ HandleConnect(client, message);
+ break;
+
+ case MessageType.Chat:
+ HandleChat(client, message);
+ break;
+
+ case MessageType.Whisper:
+ HandleWhisper(client, message);
+ break;
+
+ case MessageType.UserListRequest:
+ HandleUserListRequest(client);
+ break;
+
+ case MessageType.Ping:
+ HandlePing(client);
+ break;
+
+ case MessageType.Disconnect:
+ client.Disconnect();
+ break;
+ }
+ }
+
+ ///
+ /// 연결 처리
+ ///
+ private void HandleConnect(ChatClient client, ChatMessage message)
+ {
+ // Check employee ID duplication
+ lock (clientsLock)
+ {
+ if (!string.IsNullOrEmpty(message.EmployeeId) &&
+ clients.Any(c => c.ClientId != client.ClientId && c.EmployeeId == message.EmployeeId))
+ {
+ var errorMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = "Employee ID already connected.",
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(errorMsg);
+ client.Disconnect();
+ return;
+ }
+ }
+
+ Console.WriteLine($"[LOGIN] {message.NickName} ({message.EmployeeId}, {message.UserGroup}) from {client.IpAddress}, {message.HostName}");
+
+ // Welcome message
+ var welcomeMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = $"Welcome to the chat server, {message.NickName}!",
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(welcomeMsg);
+
+ // Notify other users
+ var noticeMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = $"{message.NickName} ({message.EmployeeId}) has joined the chat.",
+ Timestamp = DateTime.Now
+ };
+ BroadcastMessage(noticeMsg, client.ClientId);
+ }
+
+ ///
+ /// 채팅 메시지 처리 (1:1 messaging)
+ ///
+ private void HandleChat(ChatClient client, ChatMessage message)
+ {
+ // Set sender information
+ message.NickName = client.NickName;
+ message.EmployeeId = client.EmployeeId;
+ message.UserGroup = client.UserGroup;
+ message.IpAddress = client.IpAddress;
+ message.HostName = client.HostName;
+ message.Timestamp = DateTime.Now;
+
+ // Send to target recipient (1:1 messaging)
+ if (!string.IsNullOrEmpty(message.TargetEmployeeId))
+ {
+ lock (clientsLock)
+ {
+ // Find target by employee ID
+ var targetClient = clients.FirstOrDefault(c => c.EmployeeId == message.TargetEmployeeId);
+ if (targetClient != null)
+ {
+ // Send to target
+ targetClient.SendMessage(message);
+ // Echo back to sender
+ client.SendMessage(message);
+ Console.WriteLine($"[CHAT] {client.EmployeeId} -> {message.TargetEmployeeId}: {message.Content}");
+ }
+ else
+ {
+ var errorMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = $"User '{message.TargetEmployeeId}' not found or offline.",
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(errorMsg);
+ }
+ }
+ }
+ else
+ {
+ // No target specified, send error
+ var errorMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = "Please select a recipient.",
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(errorMsg);
+ }
+ }
+
+ ///
+ /// Whisper message handler
+ ///
+ private void HandleWhisper(ChatClient client, ChatMessage message)
+ {
+ message.NickName = client.NickName;
+ message.IpAddress = client.IpAddress;
+ message.Timestamp = DateTime.Now;
+
+ lock (clientsLock)
+ {
+ var targetClient = clients.FirstOrDefault(c => c.NickName == message.TargetNickName);
+ if (targetClient != null)
+ {
+ targetClient.SendMessage(message);
+ Console.WriteLine($"[WHISPER] {client.NickName} -> {message.TargetNickName}: {message.Content}");
+ }
+ else
+ {
+ var errorMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = $"User '{message.TargetNickName}' not found.",
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(errorMsg);
+ }
+ }
+ }
+
+ ///
+ /// 사용자 목록 요청 처리
+ ///
+ private void HandleUserListRequest(ChatClient client)
+ {
+ lock (clientsLock)
+ {
+ // Format: "employeeId1:nickName1:userGroup1,employeeId2:nickName2:userGroup2,..."
+ // Filter: dev can see all, others can only see same group
+ var filteredClients = clients
+ .Where(c => !string.IsNullOrEmpty(c.NickName) && !string.IsNullOrEmpty(c.EmployeeId))
+ .Where(c => client.UserGroup == "dev" || c.UserGroup == client.UserGroup);
+
+ var userListStr = string.Join(",", filteredClients
+ .Select(c => $"{c.EmployeeId}:{c.NickName}:{c.UserGroup ?? "default"}"));
+
+ var response = new ChatMessage
+ {
+ Type = MessageType.UserListResponse,
+ Content = userListStr,
+ Timestamp = DateTime.Now
+ };
+
+ client.SendMessage(response);
+
+ Console.WriteLine($"[USER_LIST] Sent to {client.EmployeeId} ({client.UserGroup}): {userListStr}");
+ }
+ }
+
+ ///
+ /// Ping 처리
+ ///
+ private void HandlePing(ChatClient client)
+ {
+ var pongMsg = new ChatMessage
+ {
+ Type = MessageType.Pong,
+ Timestamp = DateTime.Now
+ };
+ client.SendMessage(pongMsg);
+ }
+
+ ///
+ /// 클라이언트 연결 해제 이벤트 핸들러
+ ///
+ private void OnClientDisconnected(object sender, string clientId)
+ {
+ ChatClient disconnectedClient = null;
+
+ lock (clientsLock)
+ {
+ disconnectedClient = clients.FirstOrDefault(c => c.ClientId == clientId);
+ if (disconnectedClient != null)
+ {
+ clients.Remove(disconnectedClient);
+ }
+ }
+
+ if (disconnectedClient != null)
+ {
+ Console.WriteLine($"[DISCONNECTED] {disconnectedClient.NickName ?? disconnectedClient.IpAddress} (ID: {clientId})");
+
+ if (!string.IsNullOrEmpty(disconnectedClient.NickName))
+ {
+ var noticeMsg = new ChatMessage
+ {
+ Type = MessageType.Notice,
+ Content = $"{disconnectedClient.NickName} has left the chat.",
+ Timestamp = DateTime.Now
+ };
+ BroadcastMessage(noticeMsg);
+ }
+ }
+ }
+
+ ///
+ /// 모든 클라이언트에게 메시지 전송
+ ///
+ private void BroadcastMessage(ChatMessage message, string excludeClientId = null)
+ {
+ lock (clientsLock)
+ {
+ foreach (var client in clients.ToList())
+ {
+ if (excludeClientId != null && client.ClientId == excludeClientId)
+ continue;
+
+ if (!string.IsNullOrEmpty(client.NickName))
+ {
+ client.SendMessage(message);
+ }
+ }
+ }
+ }
+
+ ///
+ /// 서버 상태 출력
+ ///
+ public void PrintStatus()
+ {
+ Console.WriteLine("\n========================================");
+ Console.WriteLine($"Current Clients: {clients.Count}/{maxClients}");
+ Console.WriteLine("========================================");
+
+ lock (clientsLock)
+ {
+ foreach (var client in clients)
+ {
+ if (!string.IsNullOrEmpty(client.NickName))
+ {
+ Console.WriteLine($"- {client.NickName} ({client.EmployeeId}, {client.UserGroup})");
+ Console.WriteLine($" IP: {client.IpAddress}, Host: {client.HostName}");
+ Console.WriteLine($" Connected: {client.ConnectedTime:yyyy-MM-dd HH:mm:ss}");
+ Console.WriteLine($" Last Activity: {client.LastActivity:HH:mm:ss}");
+ }
+ }
+ }
+
+ Console.WriteLine("========================================\n");
+ }
+
+ ///
+ /// TCP 서버 중지
+ ///
+ public void Stop()
+ {
+ if (!isRunning)
+ return;
+
+ isRunning = false;
+
+ // Disconnect all clients
+ lock (clientsLock)
+ {
+ foreach (var client in clients.ToList())
+ {
+ client.Disconnect();
+ }
+ clients.Clear();
+ }
+
+ try
+ {
+ tcpListener?.Stop();
+ }
+ catch { }
+
+ Console.WriteLine("[TCP] Chat server stopped");
+ }
+ }
+}
diff --git a/SubProject/ChatServer/packages.config b/SubProject/ChatServer/packages.config
new file mode 100644
index 0000000..efd7b64
--- /dev/null
+++ b/SubProject/ChatServer/packages.config
@@ -0,0 +1,4 @@
+
+
+
+