From e82f86191adb5893a793bb1bcdba83712de6a938 Mon Sep 17 00:00:00 2001 From: backuppc Date: Tue, 2 Dec 2025 15:03:51 +0900 Subject: [PATCH] Add Dashboard todo edit/delete/complete features and Note view count tracking --- Project/EETGW.csproj | 3 + .../Web/MachineBridge/MachineBridge.Board.cs | 158 ++++ .../MachineBridge/MachineBridge.MailData.cs | 75 ++ .../Web/MachineBridge/MachineBridge.Note.cs | 343 ++++++++ Project/Web/MachineBridge/WebSocketServer.cs | 93 +++ Project/frontend/src/App.tsx | 7 +- Project/frontend/src/communication.ts | 200 ++++- .../jobreport/JobReportDayDialog.tsx | 6 +- .../frontend/src/components/layout/Header.tsx | 9 + .../src/components/note/NoteEditModal.tsx | 283 +++++++ .../src/components/note/NoteViewModal.tsx | 74 ++ Project/frontend/src/pages/Dashboard.tsx | 786 +++++++++++++++++- Project/frontend/src/pages/Jobreport.tsx | 64 +- Project/frontend/src/pages/Kuntae.tsx | 5 +- Project/frontend/src/pages/MailList.tsx | 288 +++++++ Project/frontend/src/pages/Note.tsx | 452 ++++++++++ Project/frontend/src/pages/PatchList.tsx | 219 +++++ Project/frontend/src/pages/index.ts | 1 + Project/frontend/src/types.ts | 67 ++ SubProject/FPJ0000/DSNote.Designer.cs | 496 +++++++---- SubProject/FPJ0000/DSNote.xsd | 66 +- SubProject/FPJ0000/DSNote.xss | 2 +- SubProject/FPJ0000/Note/fNote.Designer.cs | 4 - SubProject/FPJ0000/Note/fNote.cs | 1 + SubProject/FPJ0000/Note/fNote.resx | 134 +-- 25 files changed, 3512 insertions(+), 324 deletions(-) create mode 100644 Project/Web/MachineBridge/MachineBridge.Board.cs create mode 100644 Project/Web/MachineBridge/MachineBridge.MailData.cs create mode 100644 Project/Web/MachineBridge/MachineBridge.Note.cs create mode 100644 Project/frontend/src/components/note/NoteEditModal.tsx create mode 100644 Project/frontend/src/components/note/NoteViewModal.tsx create mode 100644 Project/frontend/src/pages/MailList.tsx create mode 100644 Project/frontend/src/pages/Note.tsx create mode 100644 Project/frontend/src/pages/PatchList.tsx diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj index 1ee65db..8009d00 100644 --- a/Project/EETGW.csproj +++ b/Project/EETGW.csproj @@ -363,6 +363,9 @@ + + + diff --git a/Project/Web/MachineBridge/MachineBridge.Board.cs b/Project/Web/MachineBridge/MachineBridge.Board.cs new file mode 100644 index 0000000..86855ad --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.Board.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + /// + /// 게시판 목록 조회 (bidx로 구분: 5=패치내역, 기타=일반게시판) + /// + public string Board_GetList(int bidx, string searchKey) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Project.Properties.Settings.Default.CS;// Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + + var sql = @" + SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark, + dbo.getUserName(wuid) AS wuid_name + FROM Board WITH (nolock) + WHERE gcode = @gcode AND bidx = @bidx + AND (ISNULL(title,'') LIKE @search OR ISNULL(contents,'') LIKE @search OR ISNULL(wuid,'') LIKE @search) + ORDER BY wdate DESC"; + + if(bidx == 5) //패치내역은 모두가 다 확인할 수있도록 그룹코드를 제한하지 않는다 + { + sql = @" + SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark, + dbo.getUserName(wuid) AS wuid_name + FROM Board WITH (nolock) + WHERE bidx = @bidx + AND (ISNULL(title,'') LIKE @search OR ISNULL(contents,'') LIKE @search OR ISNULL(wuid,'') LIKE @search) + ORDER BY wdate DESC"; + } + + var cmd = new SqlCommand(sql, conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@bidx", SqlDbType.Int).Value = bidx; + cmd.Parameters.Add("@search", SqlDbType.NVarChar).Value = $"%{searchKey}%"; + + var list = new List(); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + list.Add(new + { + idx = reader.GetInt32(0), + bidx = reader.GetInt32(1), + header = reader.IsDBNull(2) ? "" : (reader.GetBoolean(2) ? "공지" : ""), + cate = reader.IsDBNull(3) ? "" : reader.GetString(3), + title = reader.IsDBNull(4) ? "" : reader.GetString(4), + contents = reader.IsDBNull(5) ? "" : reader.GetString(5), + file = reader.IsDBNull(6) ? "" : reader.GetString(6), + guid = reader.IsDBNull(7) ? "" : reader.GetString(7), + url = reader.IsDBNull(8) ? "" : reader.GetString(8), + wuid = reader.IsDBNull(9) ? "" : reader.GetString(9), + wdate = reader.IsDBNull(10) ? (DateTime?)null : reader.GetDateTime(10), + project = reader.IsDBNull(11) ? "" : reader.GetInt32(11).ToString(), + pidx = reader.IsDBNull(12) ? -1 : reader.GetInt32(12), + gcode = reader.IsDBNull(13) ? "" : reader.GetString(13), + close = reader.IsDBNull(14) ? false : reader.GetBoolean(14), + remark = reader.IsDBNull(15) ? "" : reader.GetString(15), + wuid_name = reader.IsDBNull(16) ? "" : reader.GetString(16) + }); + } + } + + return JsonConvert.SerializeObject(new { Success = true, Data = list }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 게시판 상세 조회 + /// + public string Board_GetDetail(int idx) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Project.Properties.Settings.Default.CS;//Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + + var cmd = new SqlCommand(@" + SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark, + dbo.getUserName(wuid) AS wuid_name + FROM Board WITH (nolock) + WHERE idx = @idx AND gcode = @gcode", conn); + + cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var data = new + { + idx = reader.GetInt32(0), + bidx = reader.GetInt32(1), + header = reader.IsDBNull(2) ? "" : (reader.GetBoolean(2) ? "공지" : ""), + cate = reader.IsDBNull(3) ? "" : reader.GetString(3), + title = reader.IsDBNull(4) ? "" : reader.GetString(4), + contents = reader.IsDBNull(5) ? "" : reader.GetString(5), + file = reader.IsDBNull(6) ? "" : reader.GetString(6), + guid = reader.IsDBNull(7) ? "" : reader.GetString(7), + url = reader.IsDBNull(8) ? "" : reader.GetString(8), + wuid = reader.IsDBNull(9) ? "" : reader.GetString(9), + wdate = reader.IsDBNull(10) ? (DateTime?)null : reader.GetDateTime(10), + project = reader.IsDBNull(11) ? "" : reader.GetInt32(11).ToString(), + pidx = reader.IsDBNull(12) ? -1 : reader.GetInt32(12), + gcode = reader.IsDBNull(13) ? "" : reader.GetString(13), + close = reader.IsDBNull(14) ? false : reader.GetBoolean(14), + remark = reader.IsDBNull(15) ? "" : reader.GetString(15), + wuid_name = reader.IsDBNull(16) ? "" : reader.GetString(16) + }; + + return JsonConvert.SerializeObject(new { Success = true, Data = data }); + } + else + { + return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." }); + } + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.MailData.cs b/Project/Web/MachineBridge/MachineBridge.MailData.cs new file mode 100644 index 0000000..a20750c --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.MailData.cs @@ -0,0 +1,75 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + /// + /// 메일 발신 내역 조회 + /// + public string Mail_GetList(string startDate, string endDate, string searchKey) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Project.Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + + var cmd = new SqlCommand(@" + SELECT idx, gcode, subject, body, fromlist, tolist, cc AS cclist, bcc AS bcclist, project, cate, pdate + FROM MailData WITH (nolock) + WHERE gcode = @gcode + AND (pdate BETWEEN @startDate AND @endDate) + AND (ISNULL(subject,'') LIKE @search OR ISNULL(fromlist,'') LIKE @search OR ISNULL(tolist,'') LIKE @search OR ISNULL(cate,'') LIKE @search) + ORDER BY pdate DESC", conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@startDate", SqlDbType.VarChar).Value = startDate; + cmd.Parameters.Add("@endDate", SqlDbType.VarChar).Value = endDate; + cmd.Parameters.Add("@search", SqlDbType.NVarChar).Value = $"%{searchKey}%"; + + var list = new List(); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + list.Add(new + { + idx = reader.GetInt32(0), + gcode = reader.IsDBNull(1) ? "" : reader.GetString(1), + uid = "", // uid 컬럼 없음 + subject = reader.IsDBNull(2) ? "" : reader.GetString(2), + htmlbody = reader.IsDBNull(3) ? "" : reader.GetString(3), // body를 htmlbody로 반환 + fromlist = reader.IsDBNull(4) ? "" : reader.GetString(4), + tolist = reader.IsDBNull(5) ? "" : reader.GetString(5), + cclist = reader.IsDBNull(6) ? "" : reader.GetString(6), + bcclist = reader.IsDBNull(7) ? "" : reader.GetString(7), + project = reader.IsDBNull(8) ? "" : reader.GetInt32(8).ToString(), + cate = reader.IsDBNull(9) ? "" : reader.GetString(9), + wdate = reader.IsDBNull(10) ? "" : reader.GetString(10) // pdate를 wdate로 반환 (프론트엔드 호환) + }); + } + } + + return JsonConvert.SerializeObject(new { Success = true, Data = list }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.Note.cs b/Project/Web/MachineBridge/MachineBridge.Note.cs new file mode 100644 index 0000000..d16b9b8 --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.Note.cs @@ -0,0 +1,343 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Runtime.InteropServices; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region Note API + + /// + /// 메모장 목록 조회 + /// + public string Note_GetList(string startDate, string endDate, string uid = "") + { + try + { + // 로그인 체크 + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + var cmd = new SqlCommand(); + cmd.Connection = conn; + + // 권한 체크: 레벨5 미만이면 자기 것만 보거나 공유된 것만 조회 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + + if (curLevel >= 5) + { + // 관리자: 모든 메모 조회 가능 + if (string.IsNullOrEmpty(uid)) + { + cmd.CommandText = @" + SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid, + ISNULL(viewcount, 0) as viewcount, viewdate, + '' as description, '' as description2 + FROM EETGW_Note WITH (nolock) + WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate + ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC"; + } + else + { + cmd.CommandText = @" + SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid, + ISNULL(viewcount, 0) as viewcount, viewdate, + '' as description, '' as description2 + FROM EETGW_Note WITH (nolock) + WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND uid = @uid + ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC"; + cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid; + } + } + else + { + // 일반 사용자: 자신이 작성했거나 공유된 메모만 조회 + cmd.CommandText = @" + SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid, + ISNULL(viewcount, 0) as viewcount, viewdate, + '' as description, '' as description2 + FROM EETGW_Note WITH (nolock) + WHERE (gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND uid = @currentUid) + OR (gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND ISNULL(share, 0) = 1) + ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC"; + cmd.Parameters.Add("@currentUid", SqlDbType.VarChar).Value = info.Login.no; + } + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@startDate", SqlDbType.VarChar).Value = startDate; + cmd.Parameters.Add("@endDate", SqlDbType.VarChar).Value = endDate; + + var list = new List(); + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + list.Add(new + { + idx = reader["idx"], + gcode = reader["gcode"], + pdate = reader["pdate"], + title = reader["title"], + uid = reader["uid"], + share = reader["share"], + wuid = reader["wuid"], + wdate = reader["wdate"], + guid = reader["guid"], + viewcount = reader["viewcount"], + viewdate = reader["viewdate"] != DBNull.Value ? reader["viewdate"] : null, + description = reader["description"], + description2 = reader["description2"] + }); + } + } + + return JsonConvert.SerializeObject(new { Success = true, Data = list }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메모장 상세 조회 + /// + public string Note_GetDetail(int idx) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var cs = Properties.Settings.Default.CS; + using (var conn = new SqlConnection(cs)) + { + conn.Open(); + + // 조회수 증가 및 조회일 업데이트 + var updateCmd = new SqlCommand(@" + UPDATE EETGW_Note + SET viewcount = ISNULL(viewcount, 0) + 1, viewdate = GETDATE() + WHERE gcode = @gcode AND idx = @idx", conn); + updateCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + updateCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + updateCmd.ExecuteNonQuery(); + + var cmd = new SqlCommand(@" + SELECT idx, gcode, pdate, title, uid, description, description2, share, wuid, wdate, guid, + ISNULL(viewcount, 0) as viewcount, viewdate + FROM EETGW_Note WITH (nolock) + WHERE gcode = @gcode AND idx = @idx", conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var item = new + { + idx = reader["idx"], + gcode = reader["gcode"], + pdate = reader["pdate"], + title = reader["title"], + uid = reader["uid"], + description = reader["description"], + description2 = reader["description2"], + share = reader["share"], + wuid = reader["wuid"], + wdate = reader["wdate"], + guid = reader["guid"], + viewcount = reader["viewcount"], + viewdate = reader["viewdate"] != DBNull.Value ? reader["viewdate"] : null + }; + return JsonConvert.SerializeObject(new { Success = true, Data = item }); + } + else + { + return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." }); + } + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메모장 추가 + /// + public string Note_Add(string pdate, string title, string uid, string description, string description2, bool share, string guid) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + // GUID가 비어있으면 생성 + if (string.IsNullOrEmpty(guid)) + { + guid = Guid.NewGuid().ToString(); + } + + var cs = Properties.Settings.Default.gwcs; + using (var conn = new SqlConnection(cs)) + { + conn.Open(); + var cmd = new SqlCommand(@" + INSERT INTO EETGW_Note (gcode, pdate, title, uid, description, description2, share, wuid, wdate, guid) + VALUES (@gcode, @pdate, @title, @uid, @description, @description2, @share, @wuid, @wdate, @guid); + SELECT CAST(SCOPE_IDENTITY() AS INT);", conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = pdate; + cmd.Parameters.Add("@title", SqlDbType.NVarChar).Value = title; + cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid; + cmd.Parameters.Add("@description", SqlDbType.NVarChar).Value = description ?? ""; + cmd.Parameters.Add("@description2", SqlDbType.NText).Value = description2 ?? ""; + cmd.Parameters.Add("@share", SqlDbType.Bit).Value = share; + cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no; + cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now; + cmd.Parameters.Add("@guid", SqlDbType.VarChar).Value = guid; + + var newIdx = cmd.ExecuteScalar(); + return JsonConvert.SerializeObject(new { Success = true, Idx = newIdx }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메모장 수정 + /// + public string Note_Edit(int idx, string pdate, string title, string uid, string description, string description2, bool share, string guid) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + + // 권한 체크: 자신의 메모이거나 관리자인 경우만 수정 가능 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + + var checkCmd = new SqlCommand(@" + SELECT uid FROM EETGW_Note WHERE gcode = @gcode AND idx = @idx", conn); + checkCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + checkCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + + var originalUid = checkCmd.ExecuteScalar()?.ToString(); + if (originalUid != info.Login.no && curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 수정할 수 없습니다." }); + } + + var cmd = new SqlCommand(@" + UPDATE EETGW_Note + SET pdate = @pdate, title = @title, uid = @uid, + description = @description, description2 = @description2, + share = @share, guid = @guid, wuid = @wuid, wdate = @wdate + WHERE gcode = @gcode AND idx = @idx", conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = pdate; + cmd.Parameters.Add("@title", SqlDbType.NVarChar).Value = title; + cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid; + cmd.Parameters.Add("@description", SqlDbType.NVarChar).Value = description ?? ""; + cmd.Parameters.Add("@description2", SqlDbType.NText).Value = description2 ?? ""; + cmd.Parameters.Add("@share", SqlDbType.Bit).Value = share; + cmd.Parameters.Add("@guid", SqlDbType.VarChar).Value = guid; + cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no; + cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now; + + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 멤모장 삭제 + /// + public string Note_Delete(int idx) + { + try + { + if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var connStr = Properties.Settings.Default.CS; + using (var conn = new SqlConnection(connStr)) + { + conn.Open(); + + // 권한 체크: 자신의 메모이거나 관리자인 경우만 삭제 가능 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + + var checkCmd = new SqlCommand(@" + SELECT uid FROM EETGW_Note WHERE gcode = @gcode AND idx = @idx", conn); + checkCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + checkCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + + var originalUid = checkCmd.ExecuteScalar()?.ToString(); + if (originalUid != info.Login.no && curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 삭제할 수 없습니다." }); + } + + var cmd = new SqlCommand(@" + DELETE FROM EETGW_Note + WHERE gcode = @gcode AND idx = @idx", conn); + + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode; + cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx; + + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + } +} diff --git a/Project/Web/MachineBridge/WebSocketServer.cs b/Project/Web/MachineBridge/WebSocketServer.cs index 25038a0..ecf4b4c 100644 --- a/Project/Web/MachineBridge/WebSocketServer.cs +++ b/Project/Web/MachineBridge/WebSocketServer.cs @@ -836,6 +836,99 @@ namespace Project.Web break; + // ===== Note API (메모장) ===== + case "NOTE_GET_LIST": + { + string startDate = json.startDate ?? ""; + string endDate = json.endDate ?? ""; + string uid = json.uid ?? ""; + string result = _bridge.Note_GetList(startDate, endDate, uid); + var response = new { type = "NOTE_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "NOTE_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.Note_GetDetail(idx); + var response = new { type = "NOTE_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "NOTE_ADD": + { + string pdate = json.pdate ?? ""; + string title = json.title ?? ""; + string uid = json.uid ?? ""; + string description = json.description ?? ""; + string description2 = json.description2 ?? ""; + bool share = json.share ?? false; + string guid = json.guid ?? ""; + string result = _bridge.Note_Add(pdate, title, uid, description, description2, share, guid); + var response = new { type = "NOTE_ADDED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "NOTE_EDIT": + { + int idx = json.idx ?? 0; + string pdate = json.pdate ?? ""; + string title = json.title ?? ""; + string uid = json.uid ?? ""; + string description = json.description ?? ""; + string description2 = json.description2 ?? ""; + bool share = json.share ?? false; + string guid = json.guid ?? ""; + string result = _bridge.Note_Edit(idx, pdate, title, uid, description, description2, share, guid); + var response = new { type = "NOTE_EDITED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "NOTE_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.Note_Delete(idx); + var response = new { type = "NOTE_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Board API (게시판 - 패치내역 등) ===== + case "BOARD_GET_LIST": + { + int bidx = json.bidx ?? 5; + string searchKey = json.searchKey ?? ""; + string result = _bridge.Board_GetList(bidx, searchKey); + var response = new { type = "BOARD_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "BOARD_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.Board_GetDetail(idx); + var response = new { type = "BOARD_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Mail API (메일 발신 내역) ===== + case "MAIL_GET_LIST": + { + string startDate = json.startDate ?? ""; + string endDate = json.endDate ?? ""; + string searchKey = json.searchKey ?? ""; + string result = _bridge.Mail_GetList(startDate, endDate, searchKey); + var response = new { type = "MAIL_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + // ===== Holiday API (월별근무표) ===== case "HOLIDAY_GET_LIST": { diff --git a/Project/frontend/src/App.tsx b/Project/frontend/src/App.tsx index 4f9e4f2..e77ab0a 100644 --- a/Project/frontend/src/App.tsx +++ b/Project/frontend/src/App.tsx @@ -1,7 +1,9 @@ import { useState, useEffect } from 'react'; import { HashRouter, Routes, Route } from 'react-router-dom'; import { Layout } from '@/components/layout'; -import { Dashboard, Todo, Kuntae, Jobreport, Project, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserAuthPage } from '@/pages'; +import { Dashboard, Todo, Kuntae, Jobreport, Project, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserAuthPage, Note } from '@/pages'; +import { PatchList } from '@/pages/PatchList'; +import { MailList } from '@/pages/MailList'; import { comms } from '@/communication'; import { UserInfo } from '@/types'; import { Loader2 } from 'lucide-react'; @@ -92,6 +94,9 @@ export default function App() { } /> } /> } /> + } /> + } /> + } /> {/* Tailwind Breakpoint Indicator - 개발용 */} diff --git a/Project/frontend/src/communication.ts b/Project/frontend/src/communication.ts index 905ed64..edc4e41 100644 --- a/Project/frontend/src/communication.ts +++ b/Project/frontend/src/communication.ts @@ -1,3 +1,48 @@ +import type { + ApiResponse, + LoginStatusResponse, + CheckAuthResponse, + TodoModel, + PurchaseItem, + PurchaseCount, + JobReportItem, + JobReportUser, + JobReportPermission, + JobReportTypeItem, + JobTypeItem, + MailFormItem, + UserGroupItem, + PermissionInfo, + AuthItem, + AuthFieldInfo, + AuthType, + MyAuthInfo, + ProjectSearchItem, + NoteItem, + KuntaeModel, + HolydayBalance, + HolyUser, + HolyRequestUser, + LoginResult, + UserGroup, + PreviousLoginInfo, + UserInfoDetail, + CommonCodeGroup, + CommonCode, + ItemInfo, + ItemDetail, + SupplierStaff, + PurchaseHistoryItem, + UserLevelInfo, + GroupUser, + UserFullData, + AppVersionInfo, + HolidayItem, + MachineBridgeInterface, + BoardItem, + MailItem, +} from '@/types'; + // WebView2 환경 감지 const isWebView = typeof window !== 'undefined' && window.chrome?.webview?.hostObjects !== undefined; @@ -799,7 +844,7 @@ class CommunicationLayer { public async getJobReportTypeList(sd: string, ed: string, uid: string = ''): Promise> { if (isWebView && machine) { - const result = await machine.Jobreport_GetTypeList(sd, ed, uid); + const result = await machine.Jobreport_GetList(sd, ed, uid, '', ''); return JSON.parse(result); } else { return this.wsRequest>('JOBREPORT_GET_TYPE_LIST', 'JOBREPORT_TYPE_LIST_DATA', { sd, ed, uid }); @@ -1080,6 +1125,159 @@ class CommunicationLayer { return this.wsRequest>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA'); } } + + // ===== Note API (메모장) ===== + + /** + * 메모장 목록 조회 + * @param startDate 시작일 (yyyy-MM-dd) + * @param endDate 종료일 (yyyy-MM-dd) + * @param uid 사용자 ID (빈 문자열이면 전체) + * @returns ApiResponse + */ + public async getNoteList(startDate: string, endDate: string, uid: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Note_GetList(startDate, endDate, uid); + return JSON.parse(result); + } else { + return this.wsRequest>('NOTE_GET_LIST', 'NOTE_LIST_DATA', { startDate, endDate, uid }); + } + } + + /** + * 메모장 상세 조회 + * @param idx 메모 인덱스 + * @returns ApiResponse + */ + public async getNoteDetail(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Note_GetDetail(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('NOTE_GET_DETAIL', 'NOTE_DETAIL_DATA', { idx }); + } + } + + /** + * 메모장 추가 + * @param pdate 날짜 (yyyy-MM-dd) + * @param title 제목 + * @param uid 작성자 ID + * @param description 내용 (plain text) + * @param description2 내용 (RTF - not used in web) + * @param share 공유 여부 + * @param guid 폴더 GUID (빈 문자열이면 자동 생성) + * @returns ApiResponse with new Idx + */ + public async addNote( + pdate: string, + title: string, + uid: string, + description: string, + description2: string = '', + share: boolean = false, + guid: string = '' + ): Promise> { + if (isWebView && machine) { + const result = await machine.Note_Add(pdate, title, uid, description, description2, share, guid); + return JSON.parse(result); + } else { + return this.wsRequest>('NOTE_ADD', 'NOTE_ADDED', { + pdate, title, uid, description, description2, share, guid + }); + } + } + + /** + * 메모장 수정 + * @param idx 메모 인덱스 + * @param pdate 날짜 (yyyy-MM-dd) + * @param title 제목 + * @param uid 작성자 ID + * @param description 내용 (plain text) + * @param description2 내용 (RTF - not used in web) + * @param share 공유 여부 + * @param guid 폴더 GUID + * @returns ApiResponse + */ + public async editNote( + idx: number, + pdate: string, + title: string, + uid: string, + description: string, + description2: string = '', + share: boolean = false, + guid: string = '' + ): Promise { + if (isWebView && machine) { + const result = await machine.Note_Edit(idx, pdate, title, uid, description, description2, share, guid); + return JSON.parse(result); + } else { + return this.wsRequest('NOTE_EDIT', 'NOTE_EDITED', { + idx, pdate, title, uid, description, description2, share, guid + }); + } + } + + /** + * 메모장 삭제 + * @param idx 메모 인덱스 + * @returns ApiResponse + */ + public async deleteNote(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.Note_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('NOTE_DELETE', 'NOTE_DELETED', { idx }); + } + } + + /** + * 게시판 목록 조회 + * @param bidx 게시판 인덱스 (5=패치내역) + * @param searchKey 검색어 + * @returns ApiResponse + */ + public async getBoardList(bidx: number, searchKey: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Board_GetList(bidx, searchKey); + return JSON.parse(result); + } else { + return this.wsRequest>('BOARD_GET_LIST', 'BOARD_LIST_DATA', { bidx, searchKey }); + } + } + + /** + * 게시판 상세 조회 + * @param idx 게시판 글 인덱스 + * @returns ApiResponse + */ + public async getBoardDetail(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Board_GetDetail(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('BOARD_GET_DETAIL', 'BOARD_DETAIL_DATA', { idx }); + } + } + + /** + * 메일 발신 내역 조회 + * @param startDate 시작일 (yyyy-MM-dd) + * @param endDate 종료일 (yyyy-MM-dd) + * @param searchKey 검색어 + * @returns ApiResponse + */ + public async getMailList(startDate: string, endDate: string, searchKey: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Mail_GetList(startDate, endDate, searchKey); + return JSON.parse(result); + } else { + return this.wsRequest>('MAIL_GET_LIST', 'MAIL_LIST_DATA', { startDate, endDate, searchKey }); + } + } } export const comms = new CommunicationLayer(); diff --git a/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx b/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx index 0b2fe07..b79b4f0 100644 --- a/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx +++ b/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; import { comms } from '@/communication'; -import { JobReportDayItem, HolidayItem } from '@/types'; +import { HolidayItem } from '@/types'; interface JobReportDayDialogProps { isOpen: boolean; @@ -32,8 +32,6 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD const [dayColumns, setDayColumns] = useState([]); const [userRows, setUserRows] = useState([]); const [currentUserId, setCurrentUserId] = useState(''); - const [currentUserLevel, setCurrentUserLevel] = useState(0); - const [authLevel, setAuthLevel] = useState(0); const [canViewAll, setCanViewAll] = useState(false); // 요일 배열 @@ -54,12 +52,10 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD const userId = loginStatus.User.Id; const userLevel = loginStatus.User.Level || 0; setCurrentUserId(userId); - setCurrentUserLevel(userLevel); // 업무일지(jobreport) 권한 가져오기 const authResponse = await comms.checkAuth('jobreport', 5); const jobReportAuthLevel = authResponse.EffectiveLevel || 0; - setAuthLevel(jobReportAuthLevel); // 유효 권한 레벨 = Max(사용자레벨, 권한레벨) const effectiveLevel = Math.max(userLevel, jobReportAuthLevel); diff --git a/Project/frontend/src/components/layout/Header.tsx b/Project/frontend/src/components/layout/Header.tsx index 868334c..0829356 100644 --- a/Project/frontend/src/components/layout/Header.tsx +++ b/Project/frontend/src/components/layout/Header.tsx @@ -115,6 +115,15 @@ const dropdownMenus: DropdownMenuConfig[] = [ { type: 'link', path: '/mail-form', icon: Mail, label: '메일양식' }, ], }, + { + label: '문서', + icon: FileText, + items: [ + { type: 'link', path: '/note', icon: FileText, label: '메모장' }, + { type: 'link', path: '/patch-list', icon: FileText, label: '패치 내역' }, + { type: 'link', path: '/mail-list', icon: Mail, label: '메일 내역' }, + ], + }, ]; function DropdownNavMenu({ diff --git a/Project/frontend/src/components/note/NoteEditModal.tsx b/Project/frontend/src/components/note/NoteEditModal.tsx new file mode 100644 index 0000000..0b01974 --- /dev/null +++ b/Project/frontend/src/components/note/NoteEditModal.tsx @@ -0,0 +1,283 @@ +import { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { X, Save, User, Calendar } from 'lucide-react'; +import { NoteItem } from '@/types'; +import { comms } from '@/communication'; + +interface NoteEditModalProps { + isOpen: boolean; + editingItem: NoteItem | null; + processing: boolean; + onClose: () => void; + onSave: (formData: { + pdate: string; + title: string; + uid: string; + description: string; + share: boolean; + guid: string; + }) => void; + initialEditMode?: boolean; +} + +export function NoteEditModal({ + isOpen, + editingItem, + processing, + onClose, + onSave, + initialEditMode = false, +}: NoteEditModalProps) { + const [pdate, setPdate] = useState(''); + const [title, setTitle] = useState(''); + const [uid, setUid] = useState(''); + const [description, setDescription] = useState(''); + const [share, setShare] = useState(false); + const [guid, setGuid] = useState(''); + const [isEditMode, setIsEditMode] = useState(false); + + // 현재 로그인 사용자 정보 로드 + const [currentUserId, setCurrentUserId] = useState(''); + const [currentUserLevel, setCurrentUserLevel] = useState(0); + + useEffect(() => { + const loadUserInfo = async () => { + try { + const loginStatus = await comms.checkLoginStatus(); + if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) { + setCurrentUserId(loginStatus.User.Id); + setCurrentUserLevel(loginStatus.User.Level); + } + } catch (error) { + console.error('로그인 정보 로드 오류:', error); + } + }; + loadUserInfo(); + }, []); + + // 모달이 열릴 때 폼 데이터 초기화 + useEffect(() => { + if (isOpen) { + if (editingItem) { + // 기존 메모 + setPdate(editingItem.pdate ? editingItem.pdate.split('T')[0] : ''); + setTitle(editingItem.title || ''); + setUid(editingItem.uid || currentUserId); + setDescription(editingItem.description || ''); + setShare(editingItem.share || false); + setGuid(editingItem.guid || ''); + setIsEditMode(initialEditMode); + } else { + // 신규 메모 - 편집 모드로 시작 + setPdate(new Date().toISOString().split('T')[0]); + setTitle(''); + setUid(currentUserId); + setDescription(''); + setShare(false); + setGuid(''); + setIsEditMode(true); + } + } + }, [isOpen, editingItem, currentUserId, initialEditMode]); + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + onSave({ pdate, title, uid, description, share, guid }); + }; + + if (!isOpen) return null; + + // 권한 체크: 자신의 메모이거나 관리자만 수정 가능 + const canEdit = !editingItem || editingItem.uid === currentUserId || currentUserLevel >= 5; + const canChangeUser = currentUserLevel >= 5; + const canChangeShare = !editingItem || editingItem.uid === currentUserId; + + return createPortal( +
+
+ {/* 헤더 */} +
+

+ {!editingItem ? '새 메모 작성' : '메모 편집'} +

+ +
+ + {/* 본문 - 좌우 레이아웃 */} +
+
+ {/* 좌측 - 정보 영역 */} +
+ {/* 날짜 */} +
+ + {isEditMode ? ( + setPdate(e.target.value)} + required + disabled={!canEdit} + className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed" + /> + ) : ( +
{pdate}
+ )} +
+ + {/* 작성자 */} +
+ + {isEditMode ? ( + <> + setUid(e.target.value)} + required + disabled={!canEdit || !canChangeUser} + className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed" + placeholder="사용자 ID" + /> + {!canChangeUser && ( +

+ 관리자만 변경 가능 +

+ )} + + ) : ( +
{uid}
+ )} +
+ + {/* 공유 여부 */} +
+ + {isEditMode ? ( + <> +
+ setShare(e.target.checked)} + disabled={!canEdit || !canChangeShare} + className="w-4 h-4 bg-white/20 border border-white/30 rounded focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed" + /> + +
+ {!canChangeShare && ( +

+ 본인 메모만 변경 가능 +

+ )} + + ) : ( +
{share ? '공유됨' : '비공유'}
+ )} +
+ + {!canEdit && isEditMode && ( +
+ 타인의 자료는 수정할 수 없습니다. +
+ )} +
+ + {/* 우측 - 제목 및 내용 영역 */} +
+ {/* 제목 */} +
+ + {isEditMode ? ( + setTitle(e.target.value)} + required + disabled={!canEdit} + className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed" + placeholder="메모 제목을 입력하세요" + /> + ) : ( +
{title}
+ )} +
+ + {/* 내용 */} +
+ + {isEditMode ? ( +