add chatserver & client
This commit is contained in:
23
EETGW.sln
23
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}
|
||||
|
||||
283
Project/ChatClientService.cs
Normal file
283
Project/ChatClientService.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Project
|
||||
{
|
||||
/// <summary>
|
||||
/// Background chat client service with auto-reconnect
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>Connection status changed event</summary>
|
||||
public event EventHandler<bool> ConnectionStatusChanged;
|
||||
|
||||
/// <summary>Message received event</summary>
|
||||
public event EventHandler<ChatMessage> MessageReceived;
|
||||
|
||||
/// <summary>Connection status</summary>
|
||||
public bool IsConnected => isConnected;
|
||||
|
||||
/// <summary>User nickname</summary>
|
||||
public string NickName => nickName;
|
||||
|
||||
/// <summary>User employee ID</summary>
|
||||
public string EmployeeId => employeeId;
|
||||
|
||||
/// <summary>User group</summary>
|
||||
public string UserGroup => userGroup;
|
||||
|
||||
public ChatClientService(string serverIp, int serverPort)
|
||||
{
|
||||
this.serverIp = serverIp;
|
||||
this.serverPort = serverPort;
|
||||
this.hostName = System.Net.Dns.GetHostName();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start background service
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop background service
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
isRunning = false;
|
||||
Disconnect();
|
||||
|
||||
try
|
||||
{
|
||||
reconnectThread?.Join(2000);
|
||||
receiveThread?.Join(2000);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-reconnect loop
|
||||
/// </summary>
|
||||
private void ReconnectLoop()
|
||||
{
|
||||
while (isRunning)
|
||||
{
|
||||
if (!isConnected)
|
||||
{
|
||||
try
|
||||
{
|
||||
Connect();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Retry after 5 seconds
|
||||
Thread.Sleep(5000);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to server
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from server
|
||||
/// </summary>
|
||||
private void Disconnect()
|
||||
{
|
||||
if (!isConnected)
|
||||
return;
|
||||
|
||||
isConnected = false;
|
||||
|
||||
try
|
||||
{
|
||||
stream?.Close();
|
||||
tcpClient?.Close();
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Notify connection status
|
||||
ConnectionStatusChanged?.Invoke(this, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message receive loop
|
||||
/// </summary>
|
||||
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<ChatMessage>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send message
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send chat message
|
||||
/// </summary>
|
||||
public bool SendChatMessage(string content)
|
||||
{
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Type = MessageType.Chat,
|
||||
Content = content
|
||||
};
|
||||
return SendMessage(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Request user list
|
||||
/// </summary>
|
||||
public bool RequestUserList()
|
||||
{
|
||||
var message = new ChatMessage
|
||||
{
|
||||
Type = MessageType.UserListRequest
|
||||
};
|
||||
return SendMessage(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
76
Project/ChatMessage.cs
Normal file
76
Project/ChatMessage.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace Project
|
||||
{
|
||||
/// <summary>
|
||||
/// Chat message type
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>Client connect</summary>
|
||||
Connect = 0,
|
||||
/// <summary>Client disconnect</summary>
|
||||
Disconnect = 1,
|
||||
/// <summary>Normal chat message</summary>
|
||||
Chat = 2,
|
||||
/// <summary>Server notice</summary>
|
||||
Notice = 3,
|
||||
/// <summary>Whisper message</summary>
|
||||
Whisper = 4,
|
||||
/// <summary>User list request</summary>
|
||||
UserListRequest = 5,
|
||||
/// <summary>User list response</summary>
|
||||
UserListResponse = 6,
|
||||
/// <summary>Ping (keep alive)</summary>
|
||||
Ping = 7,
|
||||
/// <summary>Pong (ping response)</summary>
|
||||
Pong = 8
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chat message protocol
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>Message type</summary>
|
||||
public MessageType Type { get; set; }
|
||||
|
||||
/// <summary>Sender nickname</summary>
|
||||
public string NickName { get; set; }
|
||||
|
||||
/// <summary>Sender employee ID (사번)</summary>
|
||||
public string EmployeeId { get; set; }
|
||||
|
||||
/// <summary>Sender IP</summary>
|
||||
public string IpAddress { get; set; }
|
||||
|
||||
/// <summary>Sender hostname</summary>
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>Message content</summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>Target employee ID (for 1:1 chat)</summary>
|
||||
public string TargetEmployeeId { get; set; }
|
||||
|
||||
/// <summary>Target nickname (for whisper, null for broadcast)</summary>
|
||||
public string TargetNickName { get; set; }
|
||||
|
||||
/// <summary>Send time</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>User group (for filtering)</summary>
|
||||
public string UserGroup { get; set; }
|
||||
|
||||
public ChatMessage()
|
||||
{
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Timestamp:HH:mm:ss}] {NickName}: {Content}";
|
||||
}
|
||||
}
|
||||
}
|
||||
184
Project/Dialog/fChat.Designer.cs
generated
Normal file
184
Project/Dialog/fChat.Designer.cs
generated
Normal file
@@ -0,0 +1,184 @@
|
||||
namespace Project.Dialog
|
||||
{
|
||||
partial class fChat
|
||||
{
|
||||
/// <summary>
|
||||
/// Required designer variable.
|
||||
/// </summary>
|
||||
private System.ComponentModel.IContainer components = null;
|
||||
|
||||
/// <summary>
|
||||
/// Clean up any resources being used.
|
||||
/// </summary>
|
||||
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && (components != null))
|
||||
{
|
||||
components.Dispose();
|
||||
}
|
||||
base.Dispose(disposing);
|
||||
}
|
||||
|
||||
#region Windows Form Designer generated code
|
||||
|
||||
/// <summary>
|
||||
/// Required method for Designer support - do not modify
|
||||
/// the contents of this method with the code editor.
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
412
Project/Dialog/fChat.cs
Normal file
412
Project/Dialog/fChat.cs
Normal file
@@ -0,0 +1,412 @@
|
||||
using FCOMMON;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace Project.Dialog
|
||||
{
|
||||
/// <summary>
|
||||
/// User info for recipient selection
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for new chat or specific recipient
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load user list into combobox
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set specific recipient
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message received event handler
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle user list response from server
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Append message to chat display
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send button click
|
||||
/// </summary>
|
||||
private void btnSend_Click(object sender, EventArgs e)
|
||||
{
|
||||
SendMessage();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Input text key down (Enter to send)
|
||||
/// </summary>
|
||||
private void txtInput_KeyDown(object sender, KeyEventArgs e)
|
||||
{
|
||||
if (e.KeyCode == Keys.Enter)
|
||||
{
|
||||
e.SuppressKeyPress = true;
|
||||
SendMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send chat message
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refresh connection status display
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Timer to update status periodically
|
||||
/// </summary>
|
||||
private void timerStatus_Tick(object sender, EventArgs e)
|
||||
{
|
||||
RefreshConnectionStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
64
Project/Dialog/fChat.resx
Normal file
64
Project/Dialog/fChat.resx
Normal file
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" use="required" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
<xsd:attribute ref="xml:space" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
<xsd:attribute ref="xml:space" msdata:Ordinal="5" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<metadata name="timerStatus.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<value>17, 17</value>
|
||||
</metadata>
|
||||
</root>
|
||||
@@ -314,6 +314,10 @@
|
||||
<Compile Include="Dialog\fMsgWindow.Designer.cs">
|
||||
<DependentUpon>fMsgWindow.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialog\fChat.cs" />
|
||||
<Compile Include="Dialog\fChat.Designer.cs">
|
||||
<DependentUpon>fChat.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="Dialog\fLogin.cs">
|
||||
<SubType>Form</SubType>
|
||||
</Compile>
|
||||
@@ -478,6 +482,8 @@
|
||||
</Compile>
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Pub.cs" />
|
||||
<Compile Include="ChatMessage.cs" />
|
||||
<Compile Include="ChatClientService.cs" />
|
||||
<Compile Include="Setting.cs" />
|
||||
<Compile Include="StateMachine\_TMDisplay.cs">
|
||||
<SubType>Form</SubType>
|
||||
@@ -507,6 +513,9 @@
|
||||
<EmbeddedResource Include="Dialog\fMsgWindow.resx">
|
||||
<DependentUpon>fMsgWindow.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialog\fChat.resx">
|
||||
<DependentUpon>fChat.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Dialog\fLogin.resx">
|
||||
<DependentUpon>fLogin.cs</DependentUpon>
|
||||
</EmbeddedResource>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<PublishUrlHistory>ftp://10.131.36.205:2121/Install/GroupWare/|ftp://10.131.36.205:2121/Install/|ftp://10.131.36.205/Install/|게시\</PublishUrlHistory>
|
||||
|
||||
2
Project/Properties/Settings.Designer.cs
generated
2
Project/Properties/Settings.Designer.cs
generated
@@ -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())));
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<Settings>
|
||||
<Setting Name="gwcs" Type="(Connection string)" Scope="Application">
|
||||
<DesignTimeValue Profile="(Default)"><?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&amp;DJ+ug-D;Encrypt=False;TrustServerCertificate=True</ConnectionString>
|
||||
<ProviderName>System.Data.SqlClient</ProviderName>
|
||||
</SerializableConnectionString></DesignTimeValue>
|
||||
@@ -12,7 +12,7 @@
|
||||
</Setting>
|
||||
<Setting Name="CS" Type="(Connection string)" Scope="Application">
|
||||
<DesignTimeValue Profile="(Default)"><?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&amp;DJ+ug-D;Encrypt=False;TrustServerCertificate=True</ConnectionString>
|
||||
<ProviderName>System.Data.SqlClient</ProviderName>
|
||||
</SerializableConnectionString></DesignTimeValue>
|
||||
|
||||
@@ -3,16 +3,26 @@
|
||||
<configSections>
|
||||
</configSections>
|
||||
<connectionStrings>
|
||||
<add name="Project.Properties.Settings.gwcs" 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" providerName="System.Data.SqlClient" />
|
||||
<add name="EEEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntities1" connectionString="metadata=res://*/ModelMain.csdl|res://*/ModelMain.ssdl|res://*/ModelMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesMain" connectionString="metadata=res://*/AdoNetEFMain.csdl|res://*/AdoNetEFMain.ssdl|res://*/AdoNetEFMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
|
||||
<add name="S1ACCESS300Entities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesPurchase" connectionString="metadata=res://*/ModelPurchase.csdl|res://*/ModelPurchase.ssdl|res://*/ModelPurchase.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesCommon" connectionString="metadata=res://*/ModelCommon.csdl|res://*/ModelCommon.ssdl|res://*/ModelCommon.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesJobreport" connectionString="metadata=res://*/ModelJobreport.csdl|res://*/ModelJobreport.ssdl|res://*/ModelJobreport.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesProject" connectionString="metadata=res://*/ModelProject.csdl|res://*/ModelProject.ssdl|res://*/ModelProject.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="Project.Properties.Settings.CS" 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" providerName="System.Data.SqlClient" />
|
||||
<add name="Project.Properties.Settings.gwcs" 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"
|
||||
providerName="System.Data.SqlClient" />
|
||||
<add name="EEEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntities1" connectionString="metadata=res://*/ModelMain.csdl|res://*/ModelMain.ssdl|res://*/ModelMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework""
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesMain" connectionString="metadata=res://*/AdoNetEFMain.csdl|res://*/AdoNetEFMain.ssdl|res://*/AdoNetEFMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework""
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="S1ACCESS300Entities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesPurchase" connectionString="metadata=res://*/ModelPurchase.csdl|res://*/ModelPurchase.ssdl|res://*/ModelPurchase.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesCommon" connectionString="metadata=res://*/ModelCommon.csdl|res://*/ModelCommon.ssdl|res://*/ModelCommon.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesJobreport" connectionString="metadata=res://*/ModelJobreport.csdl|res://*/ModelJobreport.ssdl|res://*/ModelJobreport.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesProject" connectionString="metadata=res://*/ModelProject.csdl|res://*/ModelProject.ssdl|res://*/ModelProject.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="Project.Properties.Settings.CS" 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"
|
||||
providerName="System.Data.SqlClient" />
|
||||
</connectionStrings>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
|
||||
14
Project/fMain.Designer.cs
generated
14
Project/fMain.Designer.cs
generated
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
222
Project/fMain.cs
222
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<string, string> chatMessageSenders = new Dictionary<string, string>(); // 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();
|
||||
|
||||
@@ -1561,5 +1577,211 @@ namespace Project
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#region Chat Functions
|
||||
|
||||
/// <summary>
|
||||
/// Start chat service after login
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chat connection status changed
|
||||
/// </summary>
|
||||
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 ○";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chat message received
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Chat blink timer tick
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// sbChat status label click event
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,16 +184,16 @@
|
||||
<data name="mn_purchase.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
@@ -254,34 +254,34 @@
|
||||
<data name="휴가관리ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
<data name="personalInventoryToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
@@ -369,32 +369,32 @@
|
||||
<data name="toolStripMenuItem7.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripMenuItem16.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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==
|
||||
</value>
|
||||
</data>
|
||||
@@ -416,52 +416,52 @@
|
||||
<data name="toolStripButton1.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButton2.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
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
|
||||
</value>
|
||||
</data>
|
||||
<data name="toolStripButton3.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAICSURBVDhPY4CDmcasDEu1MxmW6ixgWKbTwrBYVx0svkqL
|
||||
h2GZViHDMu15QPFWhmXqUmBxDLBUZzVQwX84Xqzzg2GJpifDMs3rKOJLtZ4ADZWA6oKCpVo2IEmBtZb/
|
||||
/U7m/E+5XP8/92TF/3lzYj/UdOT/S73c8D/yXNl/1W1eUEO0+6A6oWCZzgT+NebHkq/Ufyu+1fO/5GbP
|
||||
/0Mzc/4/6kz7f3VS5k+QGAyb7A49wbBE5xpUJxQs1RUsvtVdB1PUcaj+//XSiP/7W+3/Xi/0+tyzrRBu
|
||||
QPGtrgsg9VCdCFB8s3suTFHlpY5l5wtDDl5Odvt2MUjt/7YKp73FFzs+guQKbva8g2pBBTAXFN3sKQHx
|
||||
X7RGiD8u8/x/JUPrz5NsG7XCm736QNs/F93qOQ/WgA6K7/QYF9/oOg7lMjwu9vR8VOrxH4xLvDxAYkBX
|
||||
lgItagErwAaK73SLQZkMTyq8smAGPKn0zgSJ1d+v58i9NZEPrIAQeFTh2fek3Os/CD8u9+qFChMPgP7f
|
||||
CPdCqccGqDBx4P/+eo4XXVGn3/TE/Qfhl52Rp0BiUGns4N+Bfs2/B3r7/h/oPXP/QO9vIP0fGUPFzoDV
|
||||
7O/RgGoDatw2kR0oOP9Mb/0/dE24MEjt3wM980B6Gf7u7+3FpogYDNILdHqfLdC05UCBVaRgkJ5/B/ps
|
||||
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=
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
|
||||
Submodule SubProject/AmkorRestfulService deleted from cd4e1379bc
27
SubProject/ChatServer/App.config
Normal file
27
SubProject/ChatServer/App.config
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<configuration>
|
||||
<configSections>
|
||||
<sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
|
||||
<section name="ChatServer.Properties.Settings" type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
|
||||
</sectionGroup>
|
||||
</configSections>
|
||||
<startup>
|
||||
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" />
|
||||
</startup>
|
||||
<appSettings>
|
||||
<!-- TCP 채팅 서버 포트 -->
|
||||
<add key="TcpPort" value="5000" />
|
||||
<!-- 최대 동시 접속자 수 -->
|
||||
<add key="MaxClients" value="100" />
|
||||
</appSettings>
|
||||
<userSettings>
|
||||
<ChatServer.Properties.Settings>
|
||||
<setting name="TcpPort" serializeAs="String">
|
||||
<value>5000</value>
|
||||
</setting>
|
||||
<setting name="MaxClients" serializeAs="String">
|
||||
<value>1000</value>
|
||||
</setting>
|
||||
</ChatServer.Properties.Settings>
|
||||
</userSettings>
|
||||
</configuration>
|
||||
174
SubProject/ChatServer/ChatClient.cs
Normal file
174
SubProject/ChatServer/ChatClient.cs
Normal file
@@ -0,0 +1,174 @@
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NoticeServer
|
||||
{
|
||||
/// <summary>
|
||||
/// 연결된 채팅 클라이언트 정보 및 통신 관리
|
||||
/// </summary>
|
||||
public class ChatClient
|
||||
{
|
||||
private TcpClient tcpClient;
|
||||
private NetworkStream stream;
|
||||
private Thread receiveThread;
|
||||
private bool isConnected;
|
||||
|
||||
/// <summary>클라이언트 고유 ID</summary>
|
||||
public string ClientId { get; private set; }
|
||||
|
||||
/// <summary>닉네임</summary>
|
||||
public string NickName { get; set; }
|
||||
|
||||
/// <summary>사원번호 (Employee ID)</summary>
|
||||
public string EmployeeId { get; set; }
|
||||
|
||||
/// <summary>사용자 그룹</summary>
|
||||
public string UserGroup { get; set; }
|
||||
|
||||
/// <summary>IP 주소</summary>
|
||||
public string IpAddress { get; private set; }
|
||||
|
||||
/// <summary>호스트명</summary>
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>연결 시간</summary>
|
||||
public DateTime ConnectedTime { get; private set; }
|
||||
|
||||
/// <summary>마지막 활동 시간</summary>
|
||||
public DateTime LastActivity { get; set; }
|
||||
|
||||
/// <summary>메시지 수신 이벤트</summary>
|
||||
public event EventHandler<ChatMessage> MessageReceived;
|
||||
|
||||
/// <summary>연결 해제 이벤트</summary>
|
||||
public event EventHandler<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메시지 수신 스레드 시작
|
||||
/// </summary>
|
||||
private void StartReceiving()
|
||||
{
|
||||
receiveThread = new Thread(ReceiveLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"ChatClient-{ClientId}"
|
||||
};
|
||||
receiveThread.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메시지 수신 루프
|
||||
/// </summary>
|
||||
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<ChatMessage>(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();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Send message
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연결 해제
|
||||
/// </summary>
|
||||
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})";
|
||||
}
|
||||
}
|
||||
}
|
||||
76
SubProject/ChatServer/ChatMessage.cs
Normal file
76
SubProject/ChatServer/ChatMessage.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
|
||||
namespace NoticeServer
|
||||
{
|
||||
/// <summary>
|
||||
/// 채팅 메시지 타입
|
||||
/// </summary>
|
||||
public enum MessageType
|
||||
{
|
||||
/// <summary>클라이언트 연결</summary>
|
||||
Connect,
|
||||
/// <summary>클라이언트 연결 해제</summary>
|
||||
Disconnect,
|
||||
/// <summary>일반 채팅 메시지</summary>
|
||||
Chat,
|
||||
/// <summary>서버 공지</summary>
|
||||
Notice,
|
||||
/// <summary>귓속말</summary>
|
||||
Whisper,
|
||||
/// <summary>사용자 목록 요청</summary>
|
||||
UserListRequest,
|
||||
/// <summary>사용자 목록 응답</summary>
|
||||
UserListResponse,
|
||||
/// <summary>핑 (연결 유지)</summary>
|
||||
Ping,
|
||||
/// <summary>퐁 (핑 응답)</summary>
|
||||
Pong
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채팅 메시지 프로토콜
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class ChatMessage
|
||||
{
|
||||
/// <summary>메시지 타입</summary>
|
||||
public MessageType Type { get; set; }
|
||||
|
||||
/// <summary>발신자 닉네임</summary>
|
||||
public string NickName { get; set; }
|
||||
|
||||
/// <summary>발신자 사원번호 (Employee ID)</summary>
|
||||
public string EmployeeId { get; set; }
|
||||
|
||||
/// <summary>발신자 IP</summary>
|
||||
public string IpAddress { get; set; }
|
||||
|
||||
/// <summary>발신자 호스트명</summary>
|
||||
public string HostName { get; set; }
|
||||
|
||||
/// <summary>메시지 내용</summary>
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>수신자 사원번호 (1:1 채팅용)</summary>
|
||||
public string TargetEmployeeId { get; set; }
|
||||
|
||||
/// <summary>수신자 닉네임 (귓속말용, null이면 전체)</summary>
|
||||
public string TargetNickName { get; set; }
|
||||
|
||||
/// <summary>전송 시간</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>사용자 그룹</summary>
|
||||
public string UserGroup { get; set; }
|
||||
|
||||
public ChatMessage()
|
||||
{
|
||||
Timestamp = DateTime.Now;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"[{Timestamp:HH:mm:ss}] {NickName}: {Content}";
|
||||
}
|
||||
}
|
||||
}
|
||||
73
SubProject/ChatServer/NoticeServer.csproj
Normal file
73
SubProject/ChatServer/NoticeServer.csproj
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<ProjectGuid>{8E9A4B1C-6D5F-4E2A-9F3B-1C8D7E6A5B4F}</ProjectGuid>
|
||||
<OutputType>Exe</OutputType>
|
||||
<RootNamespace>NoticeServer</RootNamespace>
|
||||
<AssemblyName>NoticeServer</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
<Deterministic>true</Deterministic>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<PlatformTarget>x86</PlatformTarget>
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<ErrorReport>prompt</ErrorReport>
|
||||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.ServiceProcess" />
|
||||
<Reference Include="System.Xml.Linq" />
|
||||
<Reference Include="System.Data.DataSetExtensions" />
|
||||
<Reference Include="Microsoft.CSharp" />
|
||||
<Reference Include="System.Data" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="NoticeService.cs">
|
||||
<SubType>Component</SubType>
|
||||
</Compile>
|
||||
<Compile Include="Properties\Settings.Designer.cs">
|
||||
<AutoGen>True</AutoGen>
|
||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||
<DependentUpon>Settings.settings</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="TcpChatServer.cs" />
|
||||
<Compile Include="ChatClient.cs" />
|
||||
<Compile Include="ChatMessage.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="Properties\Settings.settings">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
||||
116
SubProject/ChatServer/NoticeService.cs
Normal file
116
SubProject/ChatServer/NoticeService.cs
Normal file
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
288
SubProject/ChatServer/Program.cs
Normal file
288
SubProject/ChatServer/Program.cs
Normal file
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 중복실행 방지 체크
|
||||
/// </summary>
|
||||
/// <returns>단일 인스턴스인 경우 true, 중복실행인 경우 false</returns>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
33
SubProject/ChatServer/Properties/AssemblyInfo.cs
Normal file
33
SubProject/ChatServer/Properties/AssemblyInfo.cs
Normal file
@@ -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")]
|
||||
50
SubProject/ChatServer/Properties/Settings.Designer.cs
generated
Normal file
50
SubProject/ChatServer/Properties/Settings.Designer.cs
generated
Normal file
@@ -0,0 +1,50 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 이 코드는 도구를 사용하여 생성되었습니다.
|
||||
// 런타임 버전:4.0.30319.42000
|
||||
//
|
||||
// 파일 내용을 변경하면 잘못된 동작이 발생할 수 있으며, 코드를 다시 생성하면
|
||||
// 이러한 변경 내용이 손실됩니다.
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
12
SubProject/ChatServer/Properties/Settings.settings
Normal file
12
SubProject/ChatServer/Properties/Settings.settings
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="http://schemas.microsoft.com/VisualStudio/2004/01/settings" CurrentProfile="(Default)" GeneratedClassNamespace="ChatServer.Properties" GeneratedClassName="Settings">
|
||||
<Profiles />
|
||||
<Settings>
|
||||
<Setting Name="TcpPort" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">5000</Value>
|
||||
</Setting>
|
||||
<Setting Name="MaxClients" Type="System.Int32" Scope="User">
|
||||
<Value Profile="(Default)">1000</Value>
|
||||
</Setting>
|
||||
</Settings>
|
||||
</SettingsFile>
|
||||
421
SubProject/ChatServer/TcpChatServer.cs
Normal file
421
SubProject/ChatServer/TcpChatServer.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// TCP 채팅 서버
|
||||
/// </summary>
|
||||
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<ChatClient> clients = new List<ChatClient>();
|
||||
|
||||
public TcpChatServer(int tcpPort, int maxClients = 100)
|
||||
{
|
||||
this.tcpPort = tcpPort;
|
||||
this.maxClients = maxClients;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TCP 채팅 서버 시작
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트 연결 수락 루프
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트 메시지 수신 이벤트 핸들러
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 연결 처리
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 채팅 메시지 처리 (1:1 messaging)
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whisper message handler
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 사용자 목록 요청 처리
|
||||
/// </summary>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ping 처리
|
||||
/// </summary>
|
||||
private void HandlePing(ChatClient client)
|
||||
{
|
||||
var pongMsg = new ChatMessage
|
||||
{
|
||||
Type = MessageType.Pong,
|
||||
Timestamp = DateTime.Now
|
||||
};
|
||||
client.SendMessage(pongMsg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 클라이언트 연결 해제 이벤트 핸들러
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모든 클라이언트에게 메시지 전송
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 서버 상태 출력
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TCP 서버 중지
|
||||
/// </summary>
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
4
SubProject/ChatServer/packages.config
Normal file
4
SubProject/ChatServer/packages.config
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net48" />
|
||||
</packages>
|
||||
Reference in New Issue
Block a user