feat: React 프론트엔드 기능 대폭 확장

- 월별근무표: 휴일/근무일 관리, 자동 초기화
- 메일양식: 템플릿 CRUD, To/CC/BCC 설정
- 그룹정보: 부서 관리, 비트 연산 기반 권한 설정
- 업무일지: 수정 성공 메시지 제거, 오늘 근무시간 필터링 수정
- 웹소켓 메시지 type 충돌 버그 수정

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
backuppc
2025-11-27 17:25:31 +09:00
parent b57af6dad7
commit c9b5d756e1
65 changed files with 14028 additions and 467 deletions

View File

@@ -0,0 +1,705 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace Project.Web
{
/// <summary>
/// GroupWare WebSocket 서버
/// npm run dev 환경에서 핫 리로드 개발을 위한 WebSocket 통신 지원
/// </summary>
public class WebSocketServer
{
private HttpListener _httpListener;
private List<WebSocket> _clients = new List<WebSocket>();
private ConcurrentDictionary<WebSocket, SemaphoreSlim> _socketLocks = new ConcurrentDictionary<WebSocket, SemaphoreSlim>();
private MachineBridge _bridge;
private bool _isRunning = false;
public WebSocketServer(string url, MachineBridge bridge)
{
_bridge = bridge;
_httpListener = new HttpListener();
_httpListener.Prefixes.Add(url);
}
public void Start()
{
if (_isRunning) return;
try
{
_httpListener.Start();
_isRunning = true;
Console.WriteLine($"[WS] GroupWare WebSocket Server Started");
Task.Run(AcceptConnections);
}
catch (Exception ex)
{
Console.WriteLine($"[WS] Start Error: {ex.Message}");
}
}
public void Stop()
{
_isRunning = false;
try
{
_httpListener.Stop();
_httpListener.Close();
}
catch { }
}
private async Task AcceptConnections()
{
while (_httpListener.IsListening && _isRunning)
{
try
{
var context = await _httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
ProcessRequest(context);
}
else
{
context.Response.StatusCode = 400;
context.Response.Close();
}
}
catch (Exception ex)
{
if (_isRunning)
Console.WriteLine($"[WS] Accept Error: {ex.Message}");
}
}
}
private async void ProcessRequest(HttpListenerContext context)
{
WebSocketContext wsContext = null;
try
{
wsContext = await context.AcceptWebSocketAsync(subProtocol: null);
WebSocket socket = wsContext.WebSocket;
_socketLocks.TryAdd(socket, new SemaphoreSlim(1, 1));
lock (_clients) { _clients.Add(socket); }
Console.WriteLine("[WS] Client Connected");
await ReceiveLoop(socket);
}
catch (Exception ex)
{
Console.WriteLine($"[WS] Process Error: {ex.Message}");
}
finally
{
if (wsContext != null)
{
WebSocket socket = wsContext.WebSocket;
lock (_clients) { _clients.Remove(socket); }
if (_socketLocks.TryRemove(socket, out var semaphore))
{
semaphore.Dispose();
}
socket.Dispose();
}
}
}
private async Task ReceiveLoop(WebSocket socket)
{
var buffer = new byte[1024 * 4];
while (socket.State == WebSocketState.Open && _isRunning)
{
try
{
var result = await socket.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
if (result.MessageType == WebSocketMessageType.Close)
{
await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None);
}
else if (result.MessageType == WebSocketMessageType.Text)
{
string msg = Encoding.UTF8.GetString(buffer, 0, result.Count);
await HandleMessage(msg, socket);
}
}
catch
{
break;
}
}
}
private async Task HandleMessage(string msg, WebSocket socket)
{
try
{
dynamic json = JsonConvert.DeserializeObject(msg);
string type = json.type;
Console.WriteLine($"[WS] Message: {type}");
switch (type)
{
// ===== Todo API =====
case "GET_TODOS":
{
string result = _bridge.Todo_GetTodos();
var response = new { type = "TODOS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_TODO":
{
int id = json.id;
string result = _bridge.GetTodo(id);
var response = new { type = "TODO_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "CREATE_TODO":
{
string title = json.title ?? "";
string remark = json.remark ?? "";
string expire = json.expire;
int seqno = json.seqno ?? 0;
bool flag = json.flag ?? false;
string request = json.request;
string status = json.status ?? "0";
string result = _bridge.CreateTodo(title, remark, expire, seqno, flag, request, status);
var response = new { type = "TODO_CREATED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "UPDATE_TODO":
{
int idx = json.idx;
string title = json.title ?? "";
string remark = json.remark ?? "";
string expire = json.expire;
int seqno = json.seqno ?? 0;
bool flag = json.flag ?? false;
string request = json.request;
string status = json.status ?? "0";
string result = _bridge.Todo_UpdateTodo(idx, title, remark, expire, seqno, flag, request, status);
var response = new { type = "TODO_UPDATED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "DELETE_TODO":
{
int id = json.id;
string result = _bridge.Todo_DeleteTodo(id);
var response = new { type = "TODO_DELETED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_URGENT_TODOS":
{
string result = _bridge.GetUrgentTodos();
var response = new { type = "URGENT_TODOS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== Dashboard API =====
case "GET_PURCHASE_WAIT_COUNT":
{
string result = _bridge.GetPurchaseWaitCount();
var response = new { type = "PURCHASE_WAIT_COUNT_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_TODAY_COUNT_H":
{
string result = _bridge.TodayCountH();
var response = new { type = "TODAY_COUNT_H_DATA", count = int.Parse(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_HOLY_USER":
{
string result = _bridge.GetHolyUser();
var response = new { type = "HOLY_USER_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_HOLY_REQUEST_USER":
{
string result = _bridge.GetHolyRequestUser();
var response = new { type = "HOLY_REQUEST_USER_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_PURCHASE_NR_LIST":
{
string result = _bridge.GetPurchaseNRList();
var response = new { type = "PURCHASE_NR_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_PURCHASE_CR_LIST":
{
string result = _bridge.GetPurchaseCRList();
var response = new { type = "PURCHASE_CR_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_HOLYDAY_REQUEST_COUNT":
{
string result = _bridge.GetHolydayRequestCount();
var response = new { type = "HOLYDAY_REQUEST_COUNT_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_CURRENT_USER_COUNT":
{
string result = _bridge.GetCurrentUserCount();
var response = new { type = "CURRENT_USER_COUNT_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== Login API =====
case "CHECK_LOGIN_STATUS":
{
string result = _bridge.CheckLoginStatus();
var response = new { type = "LOGIN_STATUS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "LOGIN":
{
string gcode = json.gcode ?? "";
string id = json.id ?? "";
string password = json.password ?? "";
bool rememberMe = json.rememberMe ?? false;
string result = _bridge.Login(gcode, id, password, rememberMe);
var response = new { type = "LOGIN_RESULT", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "LOGOUT":
{
string result = _bridge.Logout();
var response = new { type = "LOGOUT_RESULT", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_USER_GROUPS":
{
string result = _bridge.GetUserGroups();
var response = new { type = "USER_GROUPS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_PREVIOUS_LOGIN_INFO":
{
string result = _bridge.GetPreviousLoginInfo();
var response = new { type = "PREVIOUS_LOGIN_INFO_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== User API =====
case "GET_CURRENT_USER_INFO":
{
string result = _bridge.GetCurrentUserInfo();
var response = new { type = "CURRENT_USER_INFO_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_USER_INFO_BY_ID":
{
string userId = json.userId ?? "";
string result = _bridge.GetUserInfoById(userId);
var response = new { type = "USER_INFO_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "SAVE_USER_INFO":
{
string userData = JsonConvert.SerializeObject(json.userData);
string result = _bridge.SaveUserInfo(userData);
var response = new { type = "USER_INFO_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "CHANGE_PASSWORD":
{
string oldPassword = json.oldPassword ?? "";
string newPassword = json.newPassword ?? "";
string result = _bridge.ChangePassword(oldPassword, newPassword);
var response = new { type = "PASSWORD_CHANGED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== Common Code API =====
case "COMMON_GET_GROUPS":
{
string result = _bridge.Common_GetGroups();
var response = new { type = "COMMON_GROUPS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "COMMON_GET_LIST":
{
string grp = json.grp ?? "99";
string result = _bridge.Common_GetList(grp);
var response = new { type = "COMMON_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "COMMON_SAVE":
{
int idx = json.idx ?? 0;
string grp = json.grp ?? "";
string code = json.code ?? "";
string svalue = json.svalue ?? "";
int ivalue = json.ivalue ?? 0;
float fvalue = json.fvalue ?? 0f;
string svalue2 = json.svalue2 ?? "";
string memo = json.memo ?? "";
string result = _bridge.Common_Save(idx, grp, code, svalue, ivalue, fvalue, svalue2, memo);
var response = new { type = "COMMON_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "COMMON_DELETE":
{
int idx = json.idx ?? 0;
string result = _bridge.Common_Delete(idx);
var response = new { type = "COMMON_DELETED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== Items API =====
case "ITEMS_GET_CATEGORIES":
{
string result = _bridge.Items_GetCategories();
var response = new { type = "ITEMS_CATEGORIES_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "ITEMS_GET_LIST":
{
string category = json.category ?? "";
string searchKey = json.searchKey ?? "";
string result = _bridge.Items_GetList(category, searchKey);
var response = new { type = "ITEMS_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "ITEMS_SAVE":
{
int idx = json.idx ?? 0;
string sid = json.sid ?? "";
string cate = json.cate ?? "";
string name = json.name ?? "";
string model = json.model ?? "";
string scale = json.scale ?? "";
string unit = json.unit ?? "";
decimal price = json.price ?? 0m;
string supply = json.supply ?? "";
string manu = json.manu ?? "";
string storage = json.storage ?? "";
bool disable = json.disable ?? false;
string memo = json.memo ?? "";
string result = _bridge.Items_Save(idx, sid, cate, name, model, scale, unit, price, supply, manu, storage, disable, memo);
var response = new { type = "ITEMS_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "ITEMS_DELETE":
{
int idx = json.idx ?? 0;
string result = _bridge.Items_Delete(idx);
var response = new { type = "ITEMS_DELETED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== UserList API =====
case "USERLIST_GET_CURRENT_LEVEL":
{
string result = _bridge.UserList_GetCurrentLevel();
var response = new { type = "USERLIST_CURRENT_LEVEL_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_GET_DEPTS":
{
string result = _bridge.UserList_GetDepts();
var response = new { type = "USERLIST_DEPTS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_GET_LIST":
{
string process = json.process ?? "%";
string result = _bridge.UserList_GetList(process);
var response = new { type = "USERLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_GET_USER":
{
string userId = json.userId ?? "";
string result = _bridge.UserList_GetUser(userId);
var response = new { type = "USERLIST_USER_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_SAVE_GROUP_USER":
{
string userId = json.userId ?? "";
string dept = json.dept ?? "";
int level = json.level ?? 1;
bool useUserState = json.useUserState ?? false;
bool useJobReport = json.useJobReport ?? false;
bool exceptHoly = json.exceptHoly ?? false;
string result = _bridge.UserList_SaveGroupUser(userId, dept, level, useUserState, useJobReport, exceptHoly);
var response = new { type = "USERLIST_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_SAVE_USER_FULL":
{
string userData = JsonConvert.SerializeObject(json.userData);
string result = _bridge.UserList_SaveUserFull(userData);
var response = new { type = "USERLIST_USER_FULL_SAVED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "USERLIST_DELETE_GROUP_USER":
{
string userId = json.userId ?? "";
string result = _bridge.UserList_DeleteGroupUser(userId);
var response = new { type = "USERLIST_DELETED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
// ===== JobReport API (JobReport 뷰/테이블) =====
case "JOBREPORT_GET_LIST":
{
string sd = json.sd ?? "";
string ed = json.ed ?? "";
string uid = json.uid ?? "";
string cate = json.cate ?? ""; // 사용안함 (호환성)
string searchKey = json.searchKey ?? "";
Console.WriteLine($"[WS] JOBREPORT_GET_LIST: sd={sd}, ed={ed}, uid={uid}, searchKey={searchKey}");
string result = _bridge.Jobreport_GetList(sd, ed, uid, cate, searchKey);
var response = new { type = "JOBREPORT_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_GET_USERS":
{
string result = _bridge.Jobreport_GetUsers();
var response = new { type = "JOBREPORT_USERS_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_GET_DETAIL":
{
int idx = json.idx ?? 0;
string result = _bridge.Jobreport_GetDetail(idx);
var response = new { type = "JOBREPORT_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_ADD":
{
string pdate = json.pdate ?? "";
string projectName = json.projectName ?? "";
string requestpart = json.requestpart ?? "";
string package = json.package ?? "";
string type1 = json.type ?? "";
string process = json.process ?? "";
string status = json.status ?? "진행 완료";
string description = json.description ?? "";
double hrs = json.hrs ?? 0.0;
double ot = json.ot ?? 0.0;
string jobgrp = json.jobgrp ?? "";
string tag = json.tag ?? "";
string result = _bridge.Jobreport_Add(pdate, projectName, requestpart, package, type1, process, status, description, hrs, ot, jobgrp, tag);
var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_EDIT":
{
int idx = json.idx ?? 0;
string pdate = json.pdate ?? "";
string projectName = json.projectName ?? "";
string requestpart = json.requestpart ?? "";
string package = json.package ?? "";
string type2 = json.type ?? "";
string process = json.process ?? "";
string status = json.status ?? "";
string description = json.description ?? "";
double hrs = json.hrs ?? 0.0;
double ot = json.ot ?? 0.0;
string jobgrp = json.jobgrp ?? "";
string tag = json.tag ?? "";
string result = _bridge.Jobreport_Edit(idx, pdate, projectName, requestpart, package, type2, process, status, description, hrs, ot, jobgrp, tag);
var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_DELETE":
{
int idx = json.idx ?? 0;
string result = _bridge.Jobreport_Delete(idx);
var response = new { type = "JOBREPORT_DELETED", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_GET_PERMISSION":
{
string targetUserId = json.targetUserId ?? "";
string result = _bridge.Jobreport_GetPermission(targetUserId);
var response = new { type = "JOBREPORT_PERMISSION", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "JOBREPORT_GET_JOBTYPES":
{
string process = json.process ?? "";
string result = _bridge.Jobreport_GetJobTypes(process);
var response = new { type = "JOBREPORT_JOBTYPES", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
case "GET_APP_VERSION":
{
string result = _bridge.GetAppVersion();
var response = new { type = "APP_VERSION", data = JsonConvert.DeserializeObject(result) };
await Send(socket, JsonConvert.SerializeObject(response));
}
break;
default:
Console.WriteLine($"[WS] Unknown message type: {type}");
break;
}
}
catch (Exception ex)
{
Console.WriteLine($"[WS] Handle Error: {ex.Message}");
}
}
private async Task Send(WebSocket socket, string message)
{
if (_socketLocks.TryGetValue(socket, out var semaphore))
{
await semaphore.WaitAsync();
try
{
if (socket.State == WebSocketState.Open)
{
byte[] buffer = Encoding.UTF8.GetBytes(message);
await socket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
finally
{
semaphore.Release();
}
}
}
public async void Broadcast(string message)
{
byte[] buffer = Encoding.UTF8.GetBytes(message);
WebSocket[] clientsCopy;
lock (_clients)
{
clientsCopy = _clients.ToArray();
}
foreach (var client in clientsCopy)
{
if (client.State == WebSocketState.Open && _socketLocks.TryGetValue(client, out var semaphore))
{
_ = Task.Run(async () =>
{
if (await semaphore.WaitAsync(0))
{
try
{
if (client.State == WebSocketState.Open)
{
await client.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
}
}
catch { }
finally
{
semaphore.Release();
}
}
});
}
}
}
}
}