From adcdc40169d76cee6d7167696f7b45d96c79339a Mon Sep 17 00:00:00 2001 From: backuppc Date: Fri, 28 Nov 2025 17:36:20 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=92=88=EB=AA=A9=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8C=A8=EB=84=90=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?=EB=B0=8F=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8/=EA=B7=BC?= =?UTF-8?q?=ED=83=9C/=EA=B6=8C=ED=95=9C=20=EA=B8=B0=EB=8A=A5=20=ED=99=95?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Items: 우측에 이미지, 담당자, 입고/발주내역 패널 추가 (fItems 윈폼 동일) - Project: 목록 및 상세 다이얼로그 구현 - Kuntae: 오류검사/수정 기능 추가 - UserAuth: 사용자 권한 관리 페이지 추가 - UserGroup: 그룹정보 다이얼로그로 전환 - Header: 사용자 메뉴 서브메뉴 방향 수정, 즐겨찾기 기능 - Backend API: Items 상세/담당자/구매내역, 근태 오류검사, 프로젝트 목록 등 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Project/EETGW.csproj | 1 + .../Web/MachineBridge/MachineBridge.Common.cs | 387 +++++++++++++ .../MachineBridge/MachineBridge.Jobreport.cs | 23 +- .../Web/MachineBridge/MachineBridge.Kuntae.cs | 372 ++++++++++++ .../MachineBridge/MachineBridge.Project.cs | 426 ++++++++++++++ .../MachineBridge/MachineBridge.UserAuth.cs | 339 +++++++++++ Project/Web/MachineBridge/WebSocketServer.cs | 430 +++++++++++++- Project/fMain.Designer.cs | 18 +- Project/fMain.resx | 29 +- Project/frontend/src/App.tsx | 15 +- Project/frontend/src/communication.ts | 284 +++++++++- .../components/favorite/FavoriteDialog.tsx | 188 +++++++ .../frontend/src/components/favorite/index.ts | 1 + .../src/components/items/ItemEditDialog.tsx | 484 ++++++++++++---- .../jobreport/JobTypeSelectModal.tsx | 38 +- .../jobreport/JobreportEditModal.tsx | 213 +++++-- .../jobreport/ProjectSearchDialog.tsx | 237 ++++++++ .../kuntae/KuntaeErrorCheckDialog.tsx | 422 ++++++++++++++ .../frontend/src/components/layout/Header.tsx | 206 ++++++- .../project/ProjectDetailDialog.tsx | 488 ++++++++++++++++ .../frontend/src/components/project/index.ts | 1 + .../src/components/user/UserGroupDialog.tsx | 530 ++++++++++++++++++ .../src/components/user/UserInfoDialog.tsx | 22 + .../src/components/user/UserSearchDialog.tsx | 212 +++++++ Project/frontend/src/components/user/index.ts | 1 + Project/frontend/src/pages/Items.tsx | 332 +++++++++-- Project/frontend/src/pages/Jobreport.tsx | 10 +- Project/frontend/src/pages/Project.tsx | 457 +++++++++++++++ Project/frontend/src/pages/UserAuth.tsx | 502 +++++++++++++++++ Project/frontend/src/pages/index.ts | 2 + Project/frontend/src/types.ts | 282 +++++++++- Project/frontend/vite.config.ts | 8 + 32 files changed, 6668 insertions(+), 292 deletions(-) create mode 100644 Project/Web/MachineBridge/MachineBridge.UserAuth.cs create mode 100644 Project/frontend/src/components/favorite/FavoriteDialog.tsx create mode 100644 Project/frontend/src/components/favorite/index.ts create mode 100644 Project/frontend/src/components/jobreport/ProjectSearchDialog.tsx create mode 100644 Project/frontend/src/components/kuntae/KuntaeErrorCheckDialog.tsx create mode 100644 Project/frontend/src/components/project/ProjectDetailDialog.tsx create mode 100644 Project/frontend/src/components/project/index.ts create mode 100644 Project/frontend/src/components/user/UserGroupDialog.tsx create mode 100644 Project/frontend/src/components/user/UserSearchDialog.tsx create mode 100644 Project/frontend/src/pages/Project.tsx create mode 100644 Project/frontend/src/pages/UserAuth.tsx diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj index bfc3b5b..5f97efa 100644 --- a/Project/EETGW.csproj +++ b/Project/EETGW.csproj @@ -363,6 +363,7 @@ + diff --git a/Project/Web/MachineBridge/MachineBridge.Common.cs b/Project/Web/MachineBridge/MachineBridge.Common.cs index d3e4347..6603e9f 100644 --- a/Project/Web/MachineBridge/MachineBridge.Common.cs +++ b/Project/Web/MachineBridge/MachineBridge.Common.cs @@ -228,6 +228,50 @@ namespace Project.Web } } + /// + /// 즐겨찾기 목록 조회 (grp=17) + /// memo가 표시명, svalue가 URL + /// + public string Favorite_GetList() + { + try + { + var sql = "select isnull(code,'') as code, isnull(memo,'') as name, isnull(svalue,'') as url " + + "from common WITH (nolock) " + + "where gcode = @gcode and grp = '17' and isnull(code,'') <> '' " + + "order by code"; + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var cmd = new SqlCommand(sql, cn); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + da.Dispose(); + cmd.Dispose(); + cn.Dispose(); + + var result = new System.Collections.Generic.List(); + foreach (DataRow dr in dt.Rows) + { + result.Add(new + { + name = dr["name"]?.ToString() ?? "", + url = dr["url"]?.ToString() ?? "" + }); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + catch (Exception ex) + { + Console.WriteLine($"Favorite_GetList 오류: {ex.Message}"); + return JsonConvert.SerializeObject(new { Success = false, Data = new object[] { }, Message = ex.Message }); + } + } + #endregion #region Items API @@ -421,6 +465,349 @@ namespace Project.Web } } + /// + /// 품목 이미지 조회 (Base64 반환) + /// + public string Items_GetImage(int idx) + { + try + { + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand("SELECT image FROM Items WHERE idx = @idx AND gcode = @gcode", cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + cn.Open(); + var data = cmd.ExecuteScalar() as byte[]; + + if (data != null && data.Length > 0) + { + var base64 = Convert.ToBase64String(data); + return JsonConvert.SerializeObject(new { Success = true, Data = base64 }); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = (string)null }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "이미지 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목 이미지 저장 (Base64 입력) + /// + public string Items_SaveImage(int idx, string base64Image) + { + try + { + byte[] imageData = null; + + if (!string.IsNullOrEmpty(base64Image)) + { + // data:image/png;base64, 형식 제거 + if (base64Image.Contains(",")) + { + base64Image = base64Image.Substring(base64Image.IndexOf(",") + 1); + } + imageData = Convert.FromBase64String(base64Image); + + // 이미지 크기 조정 (640x480 제한, WinForms과 동일) + using (var ms = new System.IO.MemoryStream(imageData)) + using (var img = System.Drawing.Image.FromStream(ms)) + { + System.Drawing.Image resized = img; + bool needResize = false; + + if (img.Width > 640) + { + var newRate = 640.0 / img.Width; + var newHeight = (int)(img.Height * newRate); + resized = new System.Drawing.Bitmap(640, newHeight); + using (var g = System.Drawing.Graphics.FromImage(resized)) + { + g.DrawImage(img, new System.Drawing.Rectangle(0, 0, 640, newHeight)); + } + needResize = true; + } + else if (img.Height > 480) + { + var newRate = 480.0 / img.Height; + var newWidth = (int)(img.Width * newRate); + resized = new System.Drawing.Bitmap(newWidth, 480); + using (var g = System.Drawing.Graphics.FromImage(resized)) + { + g.DrawImage(img, new System.Drawing.Rectangle(0, 0, newWidth, 480)); + } + needResize = true; + } + + using (var outMs = new System.IO.MemoryStream()) + { + resized.Save(outMs, System.Drawing.Imaging.ImageFormat.Jpeg); + imageData = outMs.ToArray(); + } + + if (needResize) + { + resized.Dispose(); + } + } + } + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand("UPDATE Items SET image = @image, wuid = @wuid, wdate = GETDATE() WHERE idx = @idx AND gcode = @gcode", cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + if (imageData != null) + { + cmd.Parameters.Add("@image", SqlDbType.VarBinary, -1).Value = imageData; + } + else + { + cmd.Parameters.Add("@image", SqlDbType.VarBinary, -1).Value = DBNull.Value; + } + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "이미지가 저장되었습니다." : "이미지 저장에 실패했습니다." }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "이미지 저장 실패: " + ex.Message }); + } + } + + /// + /// 품목 이미지 삭제 + /// + public string Items_DeleteImage(int idx) + { + return Items_SaveImage(idx, null); + } + + /// + /// 품목의 공급처 담당자 조회 + /// + public string Items_GetSupplierStaff(int supplyIdx) + { + try + { + if (supplyIdx <= 0) + { + return JsonConvert.SerializeObject(new { Success = true, Data = new object[] { } }); + } + + var sql = @"SELECT idx, name, grade, dept, tel, email, memo + FROM Staff WITH (NOLOCK) + WHERE gcode = @gcode AND cid = @cid"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@cid", supplyIdx); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + + var result = new System.Collections.Generic.List(); + foreach (DataRow dr in dt.Rows) + { + var name = dr["name"]?.ToString() ?? ""; + var grade = dr["grade"]?.ToString() ?? ""; + if (!string.IsNullOrEmpty(grade)) + { + name += $"({grade})"; + } + result.Add(new + { + idx = Convert.ToInt32(dr["idx"]), + name = name, + tel = dr["tel"]?.ToString() ?? "", + email = dr["email"]?.ToString() ?? "", + dept = dr["dept"]?.ToString() ?? "" + }); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "담당자 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목의 최근 입고내역 조회 (indate 기준) + /// + public string Items_GetIncomingHistory(int itemIdx) + { + try + { + var sql = @"SELECT TOP 10 idx, indate, request, pumqty, pumprice, state + FROM Purchase WITH (NOLOCK) + WHERE pumidx = @pumidx + AND ISNULL(indate, '') <> '' + AND ISNULL(isdel, 0) = 0 + ORDER BY indate DESC"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@pumidx", itemIdx); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + + var result = new System.Collections.Generic.List(); + foreach (DataRow dr in dt.Rows) + { + var date = dr["indate"]?.ToString() ?? ""; + // 년도 2자리로 표시 (2024-01-01 -> 24-01-01) + if (date.Length > 9) date = date.Substring(2); + + result.Add(new + { + idx = Convert.ToInt32(dr["idx"]), + date = date, + request = dr["request"]?.ToString() ?? "", + qty = dr["pumqty"] == DBNull.Value ? 0 : Convert.ToInt32(dr["pumqty"]), + price = dr["pumprice"] == DBNull.Value ? 0m : Convert.ToDecimal(dr["pumprice"]), + state = dr["state"]?.ToString() ?? "" + }); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "입고내역 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목의 발주내역 조회 (pdate 기준) + /// + public string Items_GetOrderHistory(int itemIdx) + { + try + { + var sql = @"SELECT TOP 10 idx, pdate, request, pumqty, pumprice, state + FROM Purchase WITH (NOLOCK) + WHERE pumidx = @pumidx + AND state <> 'Cancled' + AND ISNULL(isdel, 0) = 0 + ORDER BY pdate DESC"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@pumidx", itemIdx); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + + var result = new System.Collections.Generic.List(); + foreach (DataRow dr in dt.Rows) + { + var date = dr["pdate"]?.ToString() ?? ""; + // 년도 2자리로 표시 (2024-01-01 -> 24-01-01) + if (date.Length > 9) date = date.Substring(2); + + result.Add(new + { + idx = Convert.ToInt32(dr["idx"]), + date = date, + request = dr["request"]?.ToString() ?? "", + qty = dr["pumqty"] == DBNull.Value ? 0 : Convert.ToInt32(dr["pumqty"]), + price = dr["pumprice"] == DBNull.Value ? 0m : Convert.ToDecimal(dr["pumprice"]), + state = dr["state"]?.ToString() ?? "" + }); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "발주내역 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목 상세 정보 조회 (supplyidx 포함) + /// + public string Items_GetDetail(int idx) + { + try + { + var sql = @"SELECT idx, sid, cate, name, model, scale, unit, price, supply, supplyidx, manu, storage, disable, memo + FROM Items WITH (NOLOCK) + WHERE idx = @idx AND gcode = @gcode"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + cn.Open(); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var item = new + { + idx = reader.GetInt32(reader.GetOrdinal("idx")), + sid = reader["sid"]?.ToString() ?? "", + cate = reader["cate"]?.ToString() ?? "", + name = reader["name"]?.ToString() ?? "", + model = reader["model"]?.ToString() ?? "", + scale = reader["scale"]?.ToString() ?? "", + unit = reader["unit"]?.ToString() ?? "", + price = reader["price"] == DBNull.Value ? 0m : Convert.ToDecimal(reader["price"]), + supply = reader["supply"]?.ToString() ?? "", + supplyidx = reader["supplyidx"] == DBNull.Value ? -1 : Convert.ToInt32(reader["supplyidx"]), + manu = reader["manu"]?.ToString() ?? "", + storage = reader["storage"]?.ToString() ?? "", + disable = reader["disable"] != DBNull.Value && Convert.ToBoolean(reader["disable"]), + memo = reader["memo"]?.ToString() ?? "" + }; + + 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 }); + } + } + #endregion } } diff --git a/Project/Web/MachineBridge/MachineBridge.Jobreport.cs b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs index ede9341..7ec5e01 100644 --- a/Project/Web/MachineBridge/MachineBridge.Jobreport.cs +++ b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs @@ -93,16 +93,19 @@ namespace Project.Web } /// - /// 업무일지 상세 조회 (vJobReportForUser 뷰 사용) + /// 업무일지 상세 조회 (vJobReportForUser 뷰 + JobReport 테이블 조인) /// public string Jobreport_GetDetail(int id) { try { - var sql = @"SELECT idx, pidx, pdate, id, name, type, svalue, hrs, ot, requestpart, package, - userprocess, status, projectName, description, ww, otpms, process - FROM vJobReportForUser WITH (nolock) - WHERE idx = @idx AND gcode = @gcode"; + // 뷰에서 기본 정보 조회, 원본 테이블에서 jobgrp, tag 추가 조회 + var sql = @"SELECT v.idx, v.pidx, v.pdate, v.id, v.name, v.type, v.svalue, v.hrs, v.ot, + v.requestpart, v.package, v.userprocess, v.status, v.projectName, v.description, + v.ww, v.otpms, v.process, j.jobgrp, j.tag + FROM vJobReportForUser v WITH (nolock) + INNER JOIN JobReport j WITH (nolock) ON v.idx = j.idx + WHERE v.idx = @idx AND v.gcode = @gcode"; var cs = Properties.Settings.Default.gwcs; using (var cn = new SqlConnection(cs)) @@ -137,7 +140,7 @@ namespace Project.Web /// /// 업무일지 추가 (JobReport 테이블) /// - public string Jobreport_Add(string pdate, string projectName, string requestpart, string package, + public string Jobreport_Add(string pdate, string projectName, int pidx, string requestpart, string package, string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag) { try @@ -152,7 +155,7 @@ namespace Project.Web var sql = @"INSERT INTO JobReport (gcode, uid, pdate, projectName, requestpart, package, type, process, status, description, hrs, ot, jobgrp, tag, wuid, wdate, pidx) VALUES (@gcode, @uid, @pdate, @projectName, @requestpart, @package, - @type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), -1); + @type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), @pidx); SELECT SCOPE_IDENTITY();"; var cs = Properties.Settings.Default.gwcs; @@ -163,6 +166,7 @@ namespace Project.Web cmd.Parameters.AddWithValue("@uid", info.Login.no); cmd.Parameters.AddWithValue("@pdate", pdate); cmd.Parameters.AddWithValue("@projectName", projectName ?? ""); + cmd.Parameters.AddWithValue("@pidx", pidx); cmd.Parameters.AddWithValue("@requestpart", requestpart ?? ""); cmd.Parameters.AddWithValue("@package", package ?? ""); cmd.Parameters.AddWithValue("@type", type ?? ""); @@ -189,7 +193,7 @@ namespace Project.Web /// /// 업무일지 수정 (JobReport 테이블) /// - public string Jobreport_Edit(int idx, string pdate, string projectName, string requestpart, string package, + public string Jobreport_Edit(int idx, string pdate, string projectName, int pidx, string requestpart, string package, string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag) { try @@ -224,7 +228,7 @@ namespace Project.Web } var sql = @"UPDATE JobReport SET - pdate = @pdate, projectName = @projectName, requestpart = @requestpart, + pdate = @pdate, projectName = @projectName, pidx = @pidx, requestpart = @requestpart, package = @package, type = @type, process = @process, status = @status, description = @description, hrs = @hrs, ot = @ot, jobgrp = @jobgrp, tag = @tag, wuid = @wuid, wdate = GETDATE() @@ -238,6 +242,7 @@ namespace Project.Web cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); cmd.Parameters.AddWithValue("@pdate", pdate); cmd.Parameters.AddWithValue("@projectName", projectName ?? ""); + cmd.Parameters.AddWithValue("@pidx", pidx); cmd.Parameters.AddWithValue("@requestpart", requestpart ?? ""); cmd.Parameters.AddWithValue("@package", package ?? ""); cmd.Parameters.AddWithValue("@type", type ?? ""); diff --git a/Project/Web/MachineBridge/MachineBridge.Kuntae.cs b/Project/Web/MachineBridge/MachineBridge.Kuntae.cs index b59cb95..7677042 100644 --- a/Project/Web/MachineBridge/MachineBridge.Kuntae.cs +++ b/Project/Web/MachineBridge/MachineBridge.Kuntae.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Data; using System.Data.SqlClient; +using System.Text; using Newtonsoft.Json; using FCOMMON; @@ -11,6 +12,33 @@ namespace Project.Web { #region Kuntae API + #region 오류검사 관련 클래스 + public class KuntaeErrorCheckResult + { + public string Date { get; set; } + public string Gubun { get; set; } + public string OccurDay { get; set; } + public string OccurTime { get; set; } + public string UseDay { get; set; } + public string UseTime { get; set; } + public string CateError { get; set; } + public bool IsError { get; set; } + public bool IsMagam { get; set; } + } + + public class KuntaeErrorCheckProgress + { + public string CurrentDate { get; set; } + public double JobreportOT { get; set; } + public double HolidayRequestDay { get; set; } + public double HolidayRequestTime { get; set; } + public double KuntaeCRDay { get; set; } + public double KuntaeCRTime { get; set; } + public double KuntaeDRDay { get; set; } + public double KuntaeDRTime { get; set; } + } + #endregion + /// /// 근태 목록 조회 /// @@ -90,6 +118,350 @@ namespace Project.Web } } + /// + /// 근태 오류 검사 실행 + /// + public string Kuntae_ErrorCheck(string sd, string ed) + { + try + { + var startDate = DateTime.Parse(sd); + var endDate = DateTime.Parse(ed); + var gcode = info.Login.gcode; + var uid = info.Login.no; + + var okList = new List(); + var ngList = new List(); + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + using (var cmd = new SqlCommand("", cn)) + { + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = gcode; + cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid; + + var idx = 0; + while (true) + { + var currentDate = startDate.AddDays(idx++); + if (currentDate > endDate) break; + + var pdate = currentDate.ToString("yyyy-MM-dd"); + var result = CheckDateError(cmd, pdate, gcode); + + if (result.IsError) + ngList.Add(result); + else + okList.Add(result); + } + } + } + + return JsonConvert.SerializeObject(new { + Success = true, + OkList = okList, + NgList = ngList + }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 특정 날짜의 오류 검사 + /// + private KuntaeErrorCheckResult CheckDateError(SqlCommand cmd, string pdate, string gcode) + { + var result = new KuntaeErrorCheckResult + { + Date = pdate, + Gubun = "입력/생성", + IsError = false, + IsMagam = false + }; + + // 1. 업무일지 OT2 합계 (발생시간) + cmd.CommandText = $"SELECT SUM(ISNULL(ot2,0)) FROM jobreport WHERE gcode = @gcode AND pdate='{pdate}' AND ISNULL(ot,0) > 0 AND ISNULL(ot2,0) > 0"; + var jobreportOT = 0.0; + var objJobreport = cmd.ExecuteScalar(); + if (objJobreport != null && objJobreport != DBNull.Value) + jobreportOT = Convert.ToDouble(objJobreport); + + // 2. 휴가신청 확인 (승인된 것만) + cmd.CommandText = $"SELECT cate, SUM(HolyDays), SUM(HolyTimes) FROM EETGW_HolydayRequest WHERE gcode = @gcode AND sdate = '{pdate}' AND ISNULL(conf,0) = 1 GROUP BY cate"; + var holidayDay = 0.0; + var holidayTime = 0.0; + var cateListD = new Dictionary(); + var cateListT = new Dictionary(); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var cate = reader[0].ToString(); + var vDay = reader[1] != DBNull.Value ? Convert.ToDouble(reader[1]) : 0.0; + var vTime = reader[2] != DBNull.Value ? Convert.ToDouble(reader[2]) : 0.0; + + holidayDay += vDay; + holidayTime += vTime; + + if (vDay != 0.0) + { + if (cateListD.ContainsKey(cate)) + cateListD[cate] += vDay; + else + cateListD.Add(cate, vDay); + } + + if (vTime != 0.0) + { + if (cateListT.ContainsKey(cate)) + cateListT[cate] += vTime; + else + cateListT.Add(cate, vTime); + } + } + } + + // 3. 근태입력자료 확인 + cmd.CommandText = $@"SELECT cate, SUM(term), SUM(crtime), SUM(termdr), SUM(drtime), SUM(drtimepms) + FROM Holyday + WHERE gcode = @gcode AND sdate = '{pdate}' AND ISNULL(extidx,-1) <> -1 + GROUP BY cate"; + + var kuntaeCRDay = 0.0; + var kuntaeCRTime = 0.0; + var kuntaeDRDay = 0.0; + var kuntaeDRTime = 0.0; + var dCRD = new Dictionary(); + var dCRT = new Dictionary(); + var dDRD = new Dictionary(); + var dDRT = new Dictionary(); + + using (var reader = cmd.ExecuteReader()) + { + while (reader.Read()) + { + var cate = reader[0].ToString(); + var vCRD = reader[1] != DBNull.Value ? Convert.ToDouble(reader[1]) : 0.0; + var vCRT = reader[2] != DBNull.Value ? Convert.ToDouble(reader[2]) : 0.0; + var vDRD = reader[3] != DBNull.Value ? Convert.ToDouble(reader[3]) : 0.0; + var vDRT = reader[4] != DBNull.Value ? Convert.ToDouble(reader[4]) : 0.0; + + if (vCRD != 0.0) + { + if (dCRD.ContainsKey(cate)) dCRD[cate] += vCRD; + else dCRD.Add(cate, vCRD); + } + if (vCRT != 0.0) + { + if (dCRT.ContainsKey(cate)) dCRT[cate] += vCRT; + else dCRT.Add(cate, vCRT); + } + if (vDRD != 0.0) + { + if (dDRD.ContainsKey(cate)) dDRD[cate] += vDRD; + else dDRD.Add(cate, vDRD); + } + if (vDRT != 0.0) + { + if (dDRT.ContainsKey(cate)) dDRT[cate] += vDRT; + else dDRT.Add(cate, vDRT); + } + + kuntaeCRDay += vCRD; + kuntaeCRTime += vCRT; + kuntaeDRDay += vDRD; + kuntaeDRTime += vDRT; + } + } + + // 4. 카테고리별 데이터 확인 + var sbCate = new StringBuilder(); + var cateErr = false; + + // 휴가신청(일) vs 근태입력(CR일) + foreach (var item in cateListD) + { + if (!dCRD.ContainsKey(item.Key)) + { + sbCate.Append($"{item.Key}(X)"); + cateErr = true; + break; + } + else if (dCRD[item.Key] != item.Value) + { + sbCate.Append($"{item.Key}({dCRD[item.Key]}|{item.Value})"); + cateErr = true; + break; + } + } + + if (!cateErr) + { + foreach (var item in cateListT) + { + if (!dCRT.ContainsKey(item.Key)) + { + sbCate.Append($"{item.Key}(X)"); + cateErr = true; + break; + } + else if (dCRT[item.Key] != item.Value) + { + sbCate.Append($"{item.Key}({dCRT[item.Key]}|{item.Value})"); + cateErr = true; + break; + } + } + } + + if (!cateErr) + { + foreach (var item in dCRD) + { + if (item.Key.Equals("대체")) continue; + if (!cateListD.ContainsKey(item.Key)) + { + sbCate.Append($"{item.Key}(X)"); + cateErr = true; + break; + } + else if (cateListD[item.Key] != item.Value) + { + sbCate.Append($"{item.Key}({cateListD[item.Key]}|{item.Value})"); + cateErr = true; + break; + } + } + } + + if (!cateErr) + { + foreach (var item in dCRT) + { + if (item.Key.Equals("대체")) continue; + if (!cateListT.ContainsKey(item.Key)) + { + sbCate.Append($"{item.Key}(X)"); + cateErr = true; + break; + } + else if (cateListT[item.Key] != item.Value) + { + sbCate.Append($"{item.Key}({cateListT[item.Key]}|{item.Value})"); + cateErr = true; + break; + } + } + } + + // 5. 결과 생성 + result.OccurDay = $"--/{kuntaeDRDay}"; + result.OccurTime = $"{jobreportOT}/{kuntaeDRTime}"; + result.UseDay = $"{holidayDay}/{kuntaeCRDay}"; + result.UseTime = $"{holidayTime}/{kuntaeCRTime}"; + result.CateError = sbCate.ToString(); + + // 오류 여부 판단 + if (jobreportOT != kuntaeDRTime) result.IsError = true; + if (holidayDay != kuntaeCRDay) result.IsError = true; + if (holidayTime != kuntaeCRTime) result.IsError = true; + if (cateErr) result.IsError = true; + + // 마감 여부 확인 + if (result.IsError) + { + result.IsMagam = DBM.GetMagamStatus(pdate.Substring(0, 7)); + } + + return result; + } + + /// + /// 근태 오류 수정 (재생성) + /// + public string Kuntae_FixError(string pdate) + { + try + { + var gcode = info.Login.gcode; + var uid = info.Login.no; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + using (var cmd = new SqlCommand("", cn)) + { + cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = gcode; + cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid; + cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = pdate; + + // 1. 근태-업무일지자료 삭제 + cmd.CommandText = "DELETE FROM Holyday WHERE gcode = @gcode AND extcate = 'HO' AND sdate = @pdate AND ISNULL(extidx,-1) <> -1"; + var cnt1 = cmd.ExecuteNonQuery(); + + // 2. 근태-업무일지자료 생성 + cmd.CommandText = @"INSERT INTO Holyday(gcode, cate, sdate, edate, term, crtime, termdr, DrTime, contents, [uid], wdate, wuid, extcate, extidx) + SELECT gcode, '대체', pdate, pdate, 0, 0, 0, ISNULL(ot2,0), projectname, uid, GETDATE(), @uid + '-ERR', 'HO', idx + FROM jobreport + WHERE gcode = @gcode AND pdate = @pdate AND ISNULL(ot2,0) > 0 AND ISNULL(ot,0) > 0"; + var cnt2 = cmd.ExecuteNonQuery(); + + // 3. 근태-휴가신청자료 삭제 + cmd.CommandText = "DELETE FROM Holyday WHERE gcode = @gcode AND extcate = '휴가' AND sdate = @pdate AND ISNULL(extidx,-1) <> -1"; + var cnt3 = cmd.ExecuteNonQuery(); + + // 4. 근태-휴가신청자료 생성 (승인완료된 자료 대상) + cmd.CommandText = @"INSERT INTO Holyday(gcode, cate, sdate, edate, term, crtime, termdr, DrTime, contents, [uid], wdate, wuid, extcate, extidx) + SELECT gcode, cate, sdate, edate, ISNULL(holydays,0), ISNULL(holytimes,0), 0, 0, HolyReason, uid, GETDATE(), @uid + '-ERR', '휴가', idx + FROM EETGW_HolydayRequest + WHERE gcode = @gcode AND sdate = @pdate AND ISNULL(conf,0) = 1"; + var cnt4 = cmd.ExecuteNonQuery(); + + return JsonConvert.SerializeObject(new { + Success = true, + Message = $"{pdate} 재생성 완료 (삭제: {cnt1 + cnt3}건, 생성: {cnt2 + cnt4}건)" + }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 여러 날짜 오류 일괄 수정 + /// + public string Kuntae_FixErrors(string dates) + { + try + { + var dateList = JsonConvert.DeserializeObject>(dates); + var results = new List(); + + foreach (var date in dateList) + { + var resultJson = Kuntae_FixError(date); + var resultObj = JsonConvert.DeserializeObject(resultJson); + results.Add(new { Date = date, Success = (bool)resultObj.Success, Message = (string)resultObj.Message }); + } + + return JsonConvert.SerializeObject(new { Success = true, Results = results }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + #endregion } } diff --git a/Project/Web/MachineBridge/MachineBridge.Project.cs b/Project/Web/MachineBridge/MachineBridge.Project.cs index d066cb2..7752235 100644 --- a/Project/Web/MachineBridge/MachineBridge.Project.cs +++ b/Project/Web/MachineBridge/MachineBridge.Project.cs @@ -206,5 +206,431 @@ namespace Project.Web } #endregion + + #region Project Search API (업무일지용) + + /// + /// 업무일지용 프로젝트 검색 (Projects 테이블 + 과거 업무일지 항목명) + /// + public string Project_Search(string keyword) + { + try + { + var cs = Properties.Settings.Default.gwcs; + var result = new List(); + + // 1. Projects 테이블에서 검색 + var sqlProjects = @"SELECT idx, name, + ISNULL(userManager,'') as userManager, + ISNULL(userMain,'') as userMain, + ISNULL(status,'') as status + FROM Projects WITH (nolock) + WHERE gcode = @gcode + AND (name LIKE @keyword OR CAST(idx AS VARCHAR) LIKE @keyword) + AND status NOT IN ('보류', '취소', '완료(보고)') + ORDER BY status DESC, pdate DESC, name"; + + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sqlProjects, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@keyword", "%" + (keyword ?? "") + "%"); + + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + foreach (DataRow row in dt.Rows) + { + result.Add(new + { + idx = Convert.ToInt32(row["idx"]), + name = row["name"]?.ToString() ?? "", + userManager = row["userManager"]?.ToString() ?? "", + userMain = row["userMain"]?.ToString() ?? "", + status = row["status"]?.ToString() ?? "", + source = "project" + }); + } + } + } + + // 2. 과거 업무일지 항목명에서 검색 (프로젝트에 없는 항목들) + var sqlJobItems = @"SELECT TOP 50 + MAX(pdate) as lastDate, + projectName, + ISNULL(MAX(pidx), -1) as pidx + FROM JobReport WITH (nolock) + WHERE gcode = @gcode + AND projectName LIKE @keyword + AND projectName IS NOT NULL + AND projectName <> '' + GROUP BY projectName + ORDER BY MAX(pdate) DESC"; + + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sqlJobItems, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@keyword", "%" + (keyword ?? "") + "%"); + + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + foreach (DataRow row in dt.Rows) + { + var pidx = row["pidx"] != DBNull.Value ? Convert.ToInt32(row["pidx"]) : -1; + var projectName = row["projectName"]?.ToString() ?? ""; + + // 이미 Projects에서 가져온 항목과 중복 체크 + if (!result.Exists(r => ((dynamic)r).name == projectName)) + { + result.Add(new + { + idx = pidx, + name = projectName, + userManager = "", + userMain = "", + status = "", + source = "jobreport", + lastDate = row["lastDate"] != DBNull.Value + ? Convert.ToDateTime(row["lastDate"]).ToString("yyyy-MM-dd") + : "" + }); + } + } + } + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 현재 사용자의 프로젝트 목록 (업무일지 콤보박스용) + /// + public string Project_GetUserProjects() + { + try + { + var cs = Properties.Settings.Default.gwcs; + var userName = info.Login.no; + + var sql = @"SELECT idx, name, ISNULL(status,'') as status + FROM Projects WITH (nolock) + WHERE gcode = @gcode + AND (ISNULL(userManager,'') LIKE @userName + OR ISNULL(userMain,'') LIKE @userName + OR ISNULL(usersub,'') LIKE @userName) + AND status NOT IN ('보류', '취소', '완료(보고)') + ORDER BY status DESC, pdate DESC, name"; + + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@userName", "%" + userName + "%"); + + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + return JsonConvert.SerializeObject(new { Success = true, Data = dt }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + + #region Project List API (fProjectList 화면용) + + /// + /// 프로젝트 분류(category) 목록 조회 + /// + public string Project_GetCategories() + { + try + { + var sql = @"SELECT category FROM projects WITH (nolock) + WHERE gcode = @gcode AND ISNULL(category,'') <> '' + GROUP BY category ORDER BY category"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + var result = new List { "--전체--" }; + cn.Open(); + using (var rdr = cmd.ExecuteReader()) + { + while (rdr.Read()) + { + result.Add(rdr[0]?.ToString() ?? ""); + } + } + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 프로젝트 공정(userprocess) 목록 조회 + /// + public string Project_GetProcesses() + { + try + { + var sql = @"SELECT userprocess FROM projects WITH (nolock) + WHERE gcode = @gcode AND ISNULL(userprocess,'') <> '' + GROUP BY userprocess ORDER BY userprocess"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + var result = new List { "전체" }; + cn.Open(); + using (var rdr = cmd.ExecuteReader()) + { + while (rdr.Read()) + { + result.Add(rdr[0]?.ToString() ?? ""); + } + } + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 프로젝트 목록 조회 (fProjectList 화면용) + /// + public string Project_GetList(string statusFilter, string category, string process, string userFilter, string yearStart, string yearEnd, string dateType) + { + try + { + var sql = @"SELECT idx, pidx, status, asset, [level], rev, + [process], part, pdate, [name], userManager, usermain, usersub, userhw2, reqstaff, + costo, costn, cnt, remark_req, remark_ans, sdate, ddate, edate, odate, progress, + bmajoritem, memo, wuid, wdate, orderno, crdue, import, [path], userprocess, + bCost, bFanOut, bHighlight, div, model, serial, + championid, designid, epanelid, softwareid, + effect_tangible, effect_intangible, + dbo.getUserName2(championid, usermanager) as name_champion, + dbo.getUserName2(designid, usermain) as name_design, + dbo.getUserName2(epanelid, userhw2) as name_epanel, + dbo.getUserName2(softwareid, usersub) as name_software, + category, ReqLine, ReqSite, ReqPackage, ReqPlant, pno, kdate, jasmin, sfi, + (SELECT MAX(pdate) FROM ProjectsHistory WHERE pidx = Projects.idx) as lasthistory_date, + dbo.getLastProjectScheduleNo(gcode, idx) as lastSchNo, + cramount, panelimage, Priority, sfi_count, + dbo.getScheduleProgress(idx) as ProgressPrj, + dbo.getProjectFinishRate(gcode, idx) AS finishrate + FROM Projects WITH (nolock) + WHERE gcode = @gcode AND ISNULL(div,'') <> 'EB' AND ISNULL(isdel,0) = 0"; + + var parameters = new List(); + parameters.Add(new SqlParameter("@gcode", info.Login.gcode)); + + // 상태 필터 + if (!string.IsNullOrEmpty(statusFilter) && statusFilter != "all") + { + var statuses = statusFilter.Split(','); + var statusParams = new List(); + for (int i = 0; i < statuses.Length; i++) + { + var paramName = "@status" + i; + statusParams.Add(paramName); + parameters.Add(new SqlParameter(paramName, statuses[i].Trim())); + } + sql += " AND status IN (" + string.Join(",", statusParams) + ")"; + } + + // 분류 필터 + if (!string.IsNullOrEmpty(category) && category != "--전체--") + { + sql += " AND ISNULL(category,'') LIKE @category"; + parameters.Add(new SqlParameter("@category", category + "%")); + } + + // 공정 필터 + if (!string.IsNullOrEmpty(process) && process != "전체") + { + sql += " AND ISNULL(userprocess,'') = @process"; + parameters.Add(new SqlParameter("@process", process)); + } + + // 사용자 필터 + if (!string.IsNullOrEmpty(userFilter)) + { + sql += @" AND (ISNULL(userManager,'') LIKE @userFilter + OR ISNULL(usermain,'') LIKE @userFilter + OR ISNULL(reqstaff,'') LIKE @userFilter + OR ISNULL(usersub,'') LIKE @userFilter + OR dbo.getUserName(championid) LIKE @userFilter + OR dbo.getUserName(designid) LIKE @userFilter + OR dbo.getUserName(epanelid) LIKE @userFilter + OR dbo.getUserName(softwareid) LIKE @userFilter)"; + parameters.Add(new SqlParameter("@userFilter", "%" + userFilter + "%")); + } + + // 날짜 필터 + if (!string.IsNullOrEmpty(dateType) && dateType != "0" && !string.IsNullOrEmpty(yearStart)) + { + string dateField = "sdate"; + switch (dateType) + { + case "1": dateField = "sdate"; break; + case "2": dateField = "ddate"; break; + case "3": dateField = "edate"; break; + case "4": dateField = "odate"; break; + } + sql += $" AND {dateField} BETWEEN @dateStart AND @dateEnd"; + parameters.Add(new SqlParameter("@dateStart", yearStart + "-01-01")); + parameters.Add(new SqlParameter("@dateEnd", (string.IsNullOrEmpty(yearEnd) ? yearStart : yearEnd) + "-12-31")); + } + + // 정렬 + sql += @" ORDER BY (CASE + WHEN status = '진행' THEN '0' + WHEN status = '검토' THEN '1' + WHEN status = '대기' THEN '2' + WHEN status = '완료' THEN '3' + WHEN status = '완료(보고)' THEN '4' + WHEN status = '보류' THEN '5' + WHEN status = '취소' THEN '9' + ELSE '5' END), userManager, sdate"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddRange(parameters.ToArray()); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + + // 상태별 건수 집계 + var statusCounts = new Dictionary + { + { "검토", 0 }, { "진행", 0 }, { "대기", 0 }, + { "보류", 0 }, { "완료", 0 }, { "완료(보고)", 0 }, { "취소", 0 } + }; + decimal sumCosto = 0, sumCostn = 0; + + foreach (DataRow row in dt.Rows) + { + var st = row["status"]?.ToString() ?? ""; + if (statusCounts.ContainsKey(st)) + statusCounts[st]++; + + if (row["costo"] != DBNull.Value) sumCosto += Convert.ToDecimal(row["costo"]); + if (row["costn"] != DBNull.Value) sumCostn += Convert.ToDecimal(row["costn"]); + } + + return JsonConvert.SerializeObject(new + { + Success = true, + Data = dt, + StatusCounts = statusCounts, + TotalCosto = sumCosto, + TotalCostn = sumCostn, + CurrentUser = info.Login.nameK + }, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 프로젝트 히스토리 조회 + /// + public string Project_GetHistory(int projectIdx) + { + try + { + var sql = @"SELECT idx, pidx, pdate, progress, remark, wuid, wdate, + dbo.getUserName(wuid) as wname + FROM ProjectsHistory WITH (nolock) + WHERE pidx = @pidx + ORDER BY pdate DESC, idx DESC"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@pidx", projectIdx); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + return JsonConvert.SerializeObject(new { Success = true, Data = dt }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 프로젝트 일일 메모 조회 + /// + public string Project_GetDailyMemo(int projectIdx) + { + try + { + var sql = @"SELECT idx, pidx, pdate, remark, wuid, wdate, + dbo.getUserName(wuid) as wname + FROM EETGW_ProjecthistoryD WITH (nolock) + WHERE pidx = @pidx + ORDER BY pdate DESC, idx DESC"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@pidx", projectIdx); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + return JsonConvert.SerializeObject(new { Success = true, Data = dt }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion } } diff --git a/Project/Web/MachineBridge/MachineBridge.UserAuth.cs b/Project/Web/MachineBridge/MachineBridge.UserAuth.cs new file mode 100644 index 0000000..9101c4b --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.UserAuth.cs @@ -0,0 +1,339 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region UserAuth API (사용자 권한) + + /// + /// 사용자 권한 접근 가능 여부 확인 (Level 5 이상 또는 account 권한 5 이상) + /// + public string UserAuth_CanAccess() + { + try + { + int curLevel = Math.Max(info.Login.level, FCOMMON.DBM.getAuth(FCOMMON.DBM.eAuthType.account)); + bool canAccess = curLevel >= 5; + + return JsonConvert.SerializeObject(new + { + Success = true, + CanAccess = canAccess, + Level = curLevel, + Message = canAccess ? "" : "(관리자/계정담당자) 전용 메뉴 입니다" + }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 사용자 권한 목록 조회 + /// + public string UserAuth_GetList() + { + try + { + var sql = @"SELECT idx, [user], gcode, account, purchase, purchaseEB, holyday, + project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae + FROM Auth WITH (nolock) + WHERE gcode = @gcode + ORDER BY [user]"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + return JsonConvert.SerializeObject(new { Success = true, Data = dt }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 사용자 권한 저장 (추가/수정) + /// + public string UserAuth_Save(int idx, string user, int account, int purchase, int purchaseEB, + int holyday, int project, int jobreport, int scheapp, int equipment, int otconfirm, int holyreq, int kuntae) + { + try + { + var cs = Properties.Settings.Default.gwcs; + + if (idx == 0) + { + // 신규 추가 + // 먼저 중복 확인 + using (var cn = new SqlConnection(cs)) + { + var checkSql = "SELECT COUNT(*) FROM Auth WHERE gcode = @gcode AND [user] = @user"; + using (var cmd = new SqlCommand(checkSql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@user", user ?? ""); + cn.Open(); + var count = (int)cmd.ExecuteScalar(); + if (count > 0) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "이미 등록된 사용자입니다." }); + } + } + } + + var sql = @"INSERT INTO Auth (gcode, [user], account, purchase, purchaseEB, holyday, + project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae) + VALUES (@gcode, @user, @account, @purchase, @purchaseEB, @holyday, + @project, @jobreport, @scheapp, @equipment, @otconfirm, @holyreq, @kuntae); + SELECT SCOPE_IDENTITY();"; + + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@user", user ?? ""); + cmd.Parameters.AddWithValue("@account", account); + cmd.Parameters.AddWithValue("@purchase", purchase); + cmd.Parameters.AddWithValue("@purchaseEB", purchaseEB); + cmd.Parameters.AddWithValue("@holyday", holyday); + cmd.Parameters.AddWithValue("@project", project); + cmd.Parameters.AddWithValue("@jobreport", jobreport); + cmd.Parameters.AddWithValue("@scheapp", scheapp); + cmd.Parameters.AddWithValue("@equipment", equipment); + cmd.Parameters.AddWithValue("@otconfirm", otconfirm); + cmd.Parameters.AddWithValue("@holyreq", holyreq); + cmd.Parameters.AddWithValue("@kuntae", kuntae); + + cn.Open(); + var newId = Convert.ToInt32(cmd.ExecuteScalar()); + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } }); + } + } + else + { + // 수정 + var sql = @"UPDATE Auth SET + [user] = @user, account = @account, purchase = @purchase, purchaseEB = @purchaseEB, + holyday = @holyday, project = @project, jobreport = @jobreport, scheapp = @scheapp, + equipment = @equipment, otconfirm = @otconfirm, holyreq = @holyreq, kuntae = @kuntae + WHERE idx = @idx AND gcode = @gcode"; + + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@user", user ?? ""); + cmd.Parameters.AddWithValue("@account", account); + cmd.Parameters.AddWithValue("@purchase", purchase); + cmd.Parameters.AddWithValue("@purchaseEB", purchaseEB); + cmd.Parameters.AddWithValue("@holyday", holyday); + cmd.Parameters.AddWithValue("@project", project); + cmd.Parameters.AddWithValue("@jobreport", jobreport); + cmd.Parameters.AddWithValue("@scheapp", scheapp); + cmd.Parameters.AddWithValue("@equipment", equipment); + cmd.Parameters.AddWithValue("@otconfirm", otconfirm); + cmd.Parameters.AddWithValue("@holyreq", holyreq); + cmd.Parameters.AddWithValue("@kuntae", kuntae); + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 사용자 권한 삭제 + /// + public string UserAuth_Delete(int idx) + { + try + { + var sql = "DELETE FROM Auth WHERE idx = @idx AND gcode = @gcode"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다." }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 권한 항목 정보 반환 (프론트엔드 표시용) + /// + public string UserAuth_GetFields() + { + var fields = new[] + { + new { field = "user", label = "사용자 ID", description = "권한을 설정할 사용자 ID" }, + new { field = "account", label = "계정", description = "계정 관리 권한" }, + new { field = "purchase", label = "구매", description = "구매 관리 권한" }, + new { field = "purchaseEB", label = "구매(전자실)", description = "전자실 구매 권한" }, + new { field = "holyday", label = "출근부", description = "출근부 관리 권한" }, + new { field = "project", label = "프로젝트", description = "프로젝트 관리 권한" }, + new { field = "jobreport", label = "업무일지", description = "업무일지 관리 권한" }, + new { field = "scheapp", label = "스케쥴", description = "스케쥴 관리 권한" }, + new { field = "equipment", label = "장비목록", description = "장비 목록 관리 권한" }, + new { field = "otconfirm", label = "OT승인", description = "초과근무 승인 권한" }, + new { field = "holyreq", label = "휴가요청", description = "휴가 요청 관리 권한" }, + new { field = "kuntae", label = "근태", description = "근태 관리 권한" }, + }; + + return JsonConvert.SerializeObject(new { Success = true, Data = fields }); + } + + /// + /// 범용 권한 체크 API + /// authType: purchase, holyday, project, jobreport, savecost, equipment, otconfirm, kuntae, holyreq, account, purchaseEB + /// requiredLevel: 필요한 최소 레벨 (기본값 5) + /// + public string CheckAuth(string authType, int requiredLevel = 5) + { + try + { + // 사용자 기본 레벨 + int userLevel = info.Login.level; + + // authType에 해당하는 권한 레벨 조회 + int authLevel = 0; + if (!string.IsNullOrEmpty(authType)) + { + if (Enum.TryParse(authType, true, out var eType)) + { + authLevel = DBM.getAuth(eType); + } + } + + // 둘 중 높은 값 사용 + int effectiveLevel = Math.Max(userLevel, authLevel); + bool canAccess = effectiveLevel >= requiredLevel; + + return JsonConvert.SerializeObject(new + { + Success = true, + CanAccess = canAccess, + UserLevel = userLevel, + AuthLevel = authLevel, + EffectiveLevel = effectiveLevel, + RequiredLevel = requiredLevel, + AuthType = authType, + Message = canAccess ? "" : $"이 기능은 레벨 {requiredLevel} 이상 권한이 필요합니다." + }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 현재 로그인한 사용자의 전체 권한 정보 조회 + /// + public string GetMyAuth() + { + try + { + var sql = @"SELECT idx, [user], account, purchase, purchaseEB, holyday, + project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae + FROM Auth WITH (nolock) + WHERE gcode = @gcode AND [user] = @user"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@user", info.Login.no); + cn.Open(); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + return JsonConvert.SerializeObject(new + { + Success = true, + Data = new + { + UserLevel = info.Login.level, + account = reader["account"] != DBNull.Value ? (int)reader["account"] : 0, + purchase = reader["purchase"] != DBNull.Value ? (int)reader["purchase"] : 0, + purchaseEB = reader["purchaseEB"] != DBNull.Value ? (int)reader["purchaseEB"] : 0, + holyday = reader["holyday"] != DBNull.Value ? (int)reader["holyday"] : 0, + project = reader["project"] != DBNull.Value ? (int)reader["project"] : 0, + jobreport = reader["jobreport"] != DBNull.Value ? (int)reader["jobreport"] : 0, + scheapp = reader["scheapp"] != DBNull.Value ? (int)reader["scheapp"] : 0, + equipment = reader["equipment"] != DBNull.Value ? (int)reader["equipment"] : 0, + otconfirm = reader["otconfirm"] != DBNull.Value ? (int)reader["otconfirm"] : 0, + holyreq = reader["holyreq"] != DBNull.Value ? (int)reader["holyreq"] : 0, + kuntae = reader["kuntae"] != DBNull.Value ? (int)reader["kuntae"] : 0, + } + }); + } + else + { + // Auth 테이블에 없는 경우 기본값 반환 + return JsonConvert.SerializeObject(new + { + Success = true, + Data = new + { + UserLevel = info.Login.level, + account = 0, + purchase = 0, + purchaseEB = 0, + holyday = 0, + project = 0, + jobreport = 0, + scheapp = 0, + equipment = 0, + otconfirm = 0, + holyreq = 0, + kuntae = 0, + } + }); + } + } + } + } + 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 1197c12..c1b86bf 100644 --- a/Project/Web/MachineBridge/WebSocketServer.cs +++ b/Project/Web/MachineBridge/WebSocketServer.cs @@ -411,6 +411,14 @@ namespace Project.Web } break; + case "FAVORITE_GET_LIST": + { + string result = _bridge.Favorite_GetList(); + var response = new { type = "FAVORITE_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + // ===== Items API ===== case "ITEMS_GET_CATEGORIES": { @@ -460,6 +468,70 @@ namespace Project.Web } break; + case "ITEMS_GET_IMAGE": + { + int idx = json.idx ?? 0; + string result = _bridge.Items_GetImage(idx); + var response = new { type = "ITEMS_IMAGE_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_SAVE_IMAGE": + { + int idx = json.idx ?? 0; + string base64Image = json.base64Image ?? ""; + string result = _bridge.Items_SaveImage(idx, base64Image); + var response = new { type = "ITEMS_IMAGE_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_DELETE_IMAGE": + { + int idx = json.idx ?? 0; + string result = _bridge.Items_DeleteImage(idx); + var response = new { type = "ITEMS_IMAGE_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.Items_GetDetail(idx); + var response = new { type = "ITEMS_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_GET_SUPPLIER_STAFF": + { + int supplyIdx = json.supplyIdx ?? 0; + string result = _bridge.Items_GetSupplierStaff(supplyIdx); + var response = new { type = "ITEMS_SUPPLIER_STAFF_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_GET_INCOMING_HISTORY": + { + int itemIdx = json.itemIdx ?? 0; + string result = _bridge.Items_GetIncomingHistory(itemIdx); + var response = new { type = "ITEMS_INCOMING_HISTORY_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_GET_ORDER_HISTORY": + { + int itemIdx = json.itemIdx ?? 0; + string result = _bridge.Items_GetOrderHistory(itemIdx); + var response = new { type = "ITEMS_ORDER_HISTORY_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + // ===== UserList API ===== case "USERLIST_GET_CURRENT_LEVEL": { @@ -563,9 +635,10 @@ namespace Project.Web { string pdate = json.pdate ?? ""; string projectName = json.projectName ?? ""; + int pidx = json.pidx ?? -1; string requestpart = json.requestpart ?? ""; string package = json.package ?? ""; - string type1 = json.type ?? ""; + string jobType = json.jobType ?? ""; // type -> jobType (WebSocket type 필드 충돌 방지) string process = json.process ?? ""; string status = json.status ?? "진행 완료"; string description = json.description ?? ""; @@ -573,7 +646,7 @@ namespace Project.Web 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); + string result = _bridge.Jobreport_Add(pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag); var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) }; await Send(socket, JsonConvert.SerializeObject(response)); } @@ -584,9 +657,10 @@ namespace Project.Web int idx = json.idx ?? 0; string pdate = json.pdate ?? ""; string projectName = json.projectName ?? ""; + int pidx = json.pidx ?? -1; string requestpart = json.requestpart ?? ""; string package = json.package ?? ""; - string type2 = json.type ?? ""; + string jobType = json.jobType ?? ""; // type -> jobType (WebSocket type 필드 충돌 방지) string process = json.process ?? ""; string status = json.status ?? ""; string description = json.description ?? ""; @@ -594,7 +668,7 @@ namespace Project.Web 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); + string result = _bridge.Jobreport_Edit(idx, pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag); var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) }; await Send(socket, JsonConvert.SerializeObject(response)); } @@ -635,6 +709,354 @@ namespace Project.Web } break; + // ===== Kuntae API ===== + case "GET_KUNTAE_LIST": + { + string sd = json.sd ?? ""; + string ed = json.ed ?? ""; + string result = _bridge.Kuntae_GetList(sd, ed); + var response = new { type = "KUNTAE_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "DELETE_KUNTAE": + { + int id = json.id ?? 0; + string result = _bridge.Kuntae_Delete(id); + var response = new { type = "KUNTAE_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Holiday API (월별근무표) ===== + case "HOLIDAY_GET_LIST": + { + string month = json.month ?? ""; + string result = _bridge.Holiday_GetList(month); + var response = new { type = "HOLIDAY_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLIDAY_SAVE": + { + string month = json.month ?? ""; + string holidaysJson = JsonConvert.SerializeObject(json.holidays); + string result = _bridge.Holiday_Save(month, holidaysJson); + var response = new { type = "HOLIDAY_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLIDAY_INITIALIZE": + { + string month = json.month ?? ""; + string result = _bridge.Holiday_Initialize(month); + var response = new { type = "HOLIDAY_INITIALIZED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== MailForm API (메일양식) ===== + case "MAILFORM_GET_LIST": + { + string result = _bridge.MailForm_GetList(); + var response = new { type = "MAILFORM_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "MAILFORM_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.MailForm_GetDetail(idx); + var response = new { type = "MAILFORM_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "MAILFORM_ADD": + { + string cate = json.cate ?? ""; + string title = json.title ?? ""; + string tolist = json.tolist ?? ""; + string bcc = json.bcc ?? ""; + string cc = json.cc ?? ""; + string subject = json.subject ?? ""; + string tail = json.tail ?? ""; + string body = json.body ?? ""; + bool selfTo = json.selfTo ?? false; + bool selfCC = json.selfCC ?? false; + bool selfBCC = json.selfBCC ?? false; + string exceptmail = json.exceptmail ?? ""; + string exceptmailcc = json.exceptmailcc ?? ""; + string result = _bridge.MailForm_Add(cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc); + var response = new { type = "MAILFORM_ADDED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "MAILFORM_EDIT": + { + int idx = json.idx ?? 0; + string cate = json.cate ?? ""; + string title = json.title ?? ""; + string tolist = json.tolist ?? ""; + string bcc = json.bcc ?? ""; + string cc = json.cc ?? ""; + string subject = json.subject ?? ""; + string tail = json.tail ?? ""; + string body = json.body ?? ""; + bool selfTo = json.selfTo ?? false; + bool selfCC = json.selfCC ?? false; + bool selfBCC = json.selfBCC ?? false; + string exceptmail = json.exceptmail ?? ""; + string exceptmailcc = json.exceptmailcc ?? ""; + string result = _bridge.MailForm_Edit(idx, cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc); + var response = new { type = "MAILFORM_EDITED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "MAILFORM_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.MailForm_Delete(idx); + var response = new { type = "MAILFORM_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== UserGroup API (그룹정보/권한설정) ===== + case "USERGROUP_GET_LIST": + { + string result = _bridge.UserGroup_GetList(); + var response = new { type = "USERGROUP_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERGROUP_ADD": + { + string dept = json.dept ?? ""; + string path_kj = json.path_kj ?? ""; + int permission = json.permission ?? 1; + bool advpurchase = json.advpurchase ?? false; + bool advkisul = json.advkisul ?? false; + string managerinfo = json.managerinfo ?? ""; + string devinfo = json.devinfo ?? ""; + bool usemail = json.usemail ?? false; + string result = _bridge.UserGroup_Add(dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail); + var response = new { type = "USERGROUP_ADDED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERGROUP_EDIT": + { + string originalDept = json.originalDept ?? ""; + string dept = json.dept ?? ""; + string path_kj = json.path_kj ?? ""; + int permission = json.permission ?? 1; + bool advpurchase = json.advpurchase ?? false; + bool advkisul = json.advkisul ?? false; + string managerinfo = json.managerinfo ?? ""; + string devinfo = json.devinfo ?? ""; + bool usemail = json.usemail ?? false; + string result = _bridge.UserGroup_Edit(originalDept, dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail); + var response = new { type = "USERGROUP_EDITED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERGROUP_DELETE": + { + string dept = json.dept ?? ""; + string result = _bridge.UserGroup_Delete(dept); + var response = new { type = "USERGROUP_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERGROUP_GET_PERMISSION_INFO": + { + string result = _bridge.UserGroup_GetPermissionInfo(); + var response = new { type = "USERGROUP_PERMISSION_INFO", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== UserAuth API (사용자 권한) ===== + case "USERAUTH_CAN_ACCESS": + { + string result = _bridge.UserAuth_CanAccess(); + var response = new { type = "USERAUTH_CAN_ACCESS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERAUTH_GET_LIST": + { + string result = _bridge.UserAuth_GetList(); + var response = new { type = "USERAUTH_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERAUTH_SAVE": + { + int idx = json.idx ?? 0; + string user = json.user ?? ""; + int account = json.account ?? 0; + int purchase = json.purchase ?? 0; + int purchaseEB = json.purchaseEB ?? 0; + int holyday = json.holyday ?? 0; + int project = json.project ?? 0; + int jobreport = json.jobreport ?? 0; + int scheapp = json.scheapp ?? 0; + int equipment = json.equipment ?? 0; + int otconfirm = json.otconfirm ?? 0; + int holyreq = json.holyreq ?? 0; + int kuntae = json.kuntae ?? 0; + string result = _bridge.UserAuth_Save(idx, user, account, purchase, purchaseEB, holyday, project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae); + var response = new { type = "USERAUTH_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERAUTH_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.UserAuth_Delete(idx); + var response = new { type = "USERAUTH_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERAUTH_GET_FIELDS": + { + string result = _bridge.UserAuth_GetFields(); + var response = new { type = "USERAUTH_FIELDS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== 범용 권한 체크 API ===== + case "CHECK_AUTH": + { + string authType = json.authType ?? ""; + int requiredLevel = json.requiredLevel ?? 5; + string result = _bridge.CheckAuth(authType, requiredLevel); + var response = new { type = "CHECK_AUTH_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_MY_AUTH": + { + string result = _bridge.GetMyAuth(); + var response = new { type = "MY_AUTH_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== 프로젝트 검색 API (업무일지용) ===== + case "PROJECT_SEARCH": + { + string keyword = json.keyword ?? ""; + string result = _bridge.Project_Search(keyword); + var response = new { type = "PROJECT_SEARCH_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_USER_PROJECTS": + { + string result = _bridge.Project_GetUserProjects(); + var response = new { type = "PROJECT_USER_PROJECTS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_CATEGORIES": + { + string result = _bridge.Project_GetCategories(); + var response = new { type = "PROJECT_CATEGORIES_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_PROCESSES": + { + string result = _bridge.Project_GetProcesses(); + var response = new { type = "PROJECT_PROCESSES_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_LIST": + { + string statusFilter = json.statusFilter ?? ""; + string category = json.category ?? ""; + string process = json.process ?? ""; + string userFilter = json.userFilter ?? ""; + string yearStart = json.yearStart ?? ""; + string yearEnd = json.yearEnd ?? ""; + string dateType = json.dateType ?? "0"; + string result = _bridge.Project_GetList(statusFilter, category, process, userFilter, yearStart, yearEnd, dateType); + var response = new { type = "PROJECT_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_HISTORY": + { + int projectIdx = json.projectIdx ?? 0; + string result = _bridge.Project_GetHistory(projectIdx); + var response = new { type = "PROJECT_HISTORY_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "PROJECT_GET_DAILY_MEMO": + { + int projectIdx = json.projectIdx ?? 0; + string result = _bridge.Project_GetDailyMemo(projectIdx); + var response = new { type = "PROJECT_DAILY_MEMO_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== 근태 오류검사 API ===== + case "KUNTAE_ERROR_CHECK": + { + string sd = json.sd ?? ""; + string ed = json.ed ?? ""; + string result = _bridge.Kuntae_ErrorCheck(sd, ed); + var response = new { type = "KUNTAE_ERROR_CHECK_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "KUNTAE_FIX_ERROR": + { + string pdate = json.pdate ?? ""; + string result = _bridge.Kuntae_FixError(pdate); + var response = new { type = "KUNTAE_FIX_ERROR_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "KUNTAE_FIX_ERRORS": + { + string dates = JsonConvert.SerializeObject(json.dates); + string result = _bridge.Kuntae_FixErrors(dates); + var response = new { type = "KUNTAE_FIX_ERRORS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + default: Console.WriteLine($"[WS] Unknown message type: {type}"); break; diff --git a/Project/fMain.Designer.cs b/Project/fMain.Designer.cs index 156c7a5..baf04cf 100644 --- a/Project/fMain.Designer.cs +++ b/Project/fMain.Designer.cs @@ -292,6 +292,7 @@ // // itemsToolStripMenuItem // + this.itemsToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.itemsToolStripMenuItem.Name = "itemsToolStripMenuItem"; this.itemsToolStripMenuItem.Size = new System.Drawing.Size(153, 24); this.itemsToolStripMenuItem.Text = "품목정보"; @@ -305,40 +306,46 @@ this.권한설정ToolStripMenuItem, this.toolStripMenuItem12, this.그룹정보ToolStripMenuItem}); + this.userInfoToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.userInfoToolStripMenuItem.Name = "userInfoToolStripMenuItem"; this.userInfoToolStripMenuItem.Size = new System.Drawing.Size(153, 24); this.userInfoToolStripMenuItem.Text = "사용자"; // // userAccountToolStripMenuItem // + this.userAccountToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.userAccountToolStripMenuItem.Name = "userAccountToolStripMenuItem"; - this.userAccountToolStripMenuItem.Size = new System.Drawing.Size(134, 24); + this.userAccountToolStripMenuItem.Size = new System.Drawing.Size(152, 24); this.userAccountToolStripMenuItem.Text = "계정정보"; this.userAccountToolStripMenuItem.Click += new System.EventHandler(this.userAccountToolStripMenuItem_Click); // // myAccouserToolStripMenuItem // + this.myAccouserToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.myAccouserToolStripMenuItem.Name = "myAccouserToolStripMenuItem"; - this.myAccouserToolStripMenuItem.Size = new System.Drawing.Size(134, 24); + this.myAccouserToolStripMenuItem.Size = new System.Drawing.Size(152, 24); this.myAccouserToolStripMenuItem.Text = "계정목록"; this.myAccouserToolStripMenuItem.Click += new System.EventHandler(this.myAccouserToolStripMenuItem_Click); // // 권한설정ToolStripMenuItem // + this.권한설정ToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.권한설정ToolStripMenuItem.Name = "권한설정ToolStripMenuItem"; - this.권한설정ToolStripMenuItem.Size = new System.Drawing.Size(134, 24); + this.권한설정ToolStripMenuItem.Size = new System.Drawing.Size(152, 24); this.권한설정ToolStripMenuItem.Text = "권한설정"; this.권한설정ToolStripMenuItem.Click += new System.EventHandler(this.권한설정ToolStripMenuItem_Click); // // toolStripMenuItem12 // + this.toolStripMenuItem12.ForeColor = System.Drawing.Color.Blue; this.toolStripMenuItem12.Name = "toolStripMenuItem12"; - this.toolStripMenuItem12.Size = new System.Drawing.Size(131, 6); + this.toolStripMenuItem12.Size = new System.Drawing.Size(149, 6); // // 그룹정보ToolStripMenuItem // + this.그룹정보ToolStripMenuItem.ForeColor = System.Drawing.Color.Blue; this.그룹정보ToolStripMenuItem.Name = "그룹정보ToolStripMenuItem"; - this.그룹정보ToolStripMenuItem.Size = new System.Drawing.Size(134, 24); + this.그룹정보ToolStripMenuItem.Size = new System.Drawing.Size(152, 24); this.그룹정보ToolStripMenuItem.Text = "그룹정보"; this.그룹정보ToolStripMenuItem.Click += new System.EventHandler(this.그룹정보ToolStripMenuItem_Click); // @@ -351,6 +358,7 @@ // // mn_kuntae // + this.mn_kuntae.ForeColor = System.Drawing.Color.Blue; this.mn_kuntae.Name = "mn_kuntae"; this.mn_kuntae.Size = new System.Drawing.Size(153, 24); this.mn_kuntae.Text = "월별 근무표"; diff --git a/Project/fMain.resx b/Project/fMain.resx index 56712c8..803005d 100644 --- a/Project/fMain.resx +++ b/Project/fMain.resx @@ -141,15 +141,6 @@ 0IOABBs4KBjggQGBjQM9XoQwIYIHBB4yUpjo8AFLBS9fKjAgYSJCAQEyeJAgIUAEnj4RAgjgQUPPCgwQ 9Ey51APJABUIYKAwoADNBBlfRrCwtUOGBT49JEhAwIAFABQaFEDb0cMBAwg0DECbtOGFAoD7GlQomCHD gAA7 - - - - - R0lGODlhEAAQAIQXAM2VE9+3RfjopNSgIc+YFMmLEdyxVubEWt6xOuC6b+zau9SkF9mpL9uzbcySEuG2 - QeS8SsaHD+fBSv354vbii+3LYf///////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBF - Mi4wAwEBAAAh+QQBAAAXACwAAAAAEAAQAAAIggAvCBwo0IJBCwQTFqwAAQEDhAoXTpgoYQDEhBYqTKDA - kYKEBRclciRAoMEDCREuZtw40oKCCihVauxIIYEBmCkJruxYoWfMggYPsOyJU+WAABMqCJDgM+eFg0iV - Aigg4WfBo0kFADAYwWnBABSkQjSIcYDYiAMtBHCwFW3ag24HBgQAOw== @@ -452,16 +443,16 @@ iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8 - YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIGSURBVDhPlY9PaNNgGMbfi7iD+Oei4t3NphkeJogwETxI - E8GDKGyCXqZidUO7r+06EONhhS75vuq8DbQKWyJUBb30ItK4g1odztlpTRgM2hwmiCiDoWj6yheWugTm - nwceSJ73+T0kAL7Gu9aBHo2DLt4BQxyBic4OLy8KG8AQEmBEC2CIWTA6drSYgHTxHhgitjwhfoPJiARG - pBbIdcGBorA9BAvd/Lj5wT48UunH01UFByoZLNw8+eVy9mLzTPUq9r5O486SvDISzQcHDPH6pvt7n/XN - KcvEppi0KE6N92N99Cy+uxH/zjPfex4ffwGT4vvggN65hdjaFb+Um1KwlurBcvaAW0vIS7SUaA0QW33D - +8EBACCWdssvDb/NGTOJY0+rfYeWZ4+2Yylz8AmZzX3lt0sW/RxmPflfMGjRJH9fzPZsa6QlnDsn/HQu - dLcnLLab2OrSoE1nwqwnMk+7yAf1uf/eIJJUT8XQc1KOeR1LSxFbGwmAq0Xmta3+s5ORz/sDzvDhOM+U - BaVtwB7bGIDWUj0j5Z0hGbkbQzIL3/+qRlp61PqFVOxh+P5HYVlpW1RPvPpETyH3x9HelzwL9wJqmtci - rsnyaLLpBZP9QJPhaq9k016nTHf9Bktj612T3a6oSjMMrWXedU1a4Cy4ZcbChX81Z6Fp5ve7Jr2LJiv+ - jznD2V8tj6pV862slAAAAABJRU5ErkJggg== + YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIHSURBVDhPY2CAgZnGrAxLtTMZluosYFim08KwWFcdLL5K + i4dhmVYhwzLteQzLdFoZlqlLwfWggKU6qxmW6fyH48U6PxiWaHoyLNO8jiK+VOsJwyotCTTNWjYgSYG1 + lv/9Tub8T7lc/z/3ZMX/eXNiP9S05P9LvdzwP/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+B3vknOuv/oWvChUFq/x7omQfSy/B3f28vugJiMUgvw78DfbZ/D/Qs/3+gdxUp + GKQHpBcAJ5WqUADLneEAAAAASUVORK5CYII= diff --git a/Project/frontend/src/App.tsx b/Project/frontend/src/App.tsx index 68c7e9e..4f9e4f2 100644 --- a/Project/frontend/src/App.tsx +++ b/Project/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { HashRouter, Routes, Route } from 'react-router-dom'; import { Layout } from '@/components/layout'; -import { Dashboard, Todo, Kuntae, Jobreport, PlaceholderPage, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserGroupPage } from '@/pages'; +import { Dashboard, Todo, Kuntae, Jobreport, Project, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserAuthPage } from '@/pages'; import { comms } from '@/communication'; import { UserInfo } from '@/types'; import { Loader2 } from 'lucide-react'; @@ -85,15 +85,24 @@ export default function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> + } /> } /> } /> - } /> + {/* Tailwind Breakpoint Indicator - 개발용 */} +
+ XS + SM + MD + LG + XL + 2XL +
); } diff --git a/Project/frontend/src/communication.ts b/Project/frontend/src/communication.ts index 6b6aeb8..21c14e5 100644 --- a/Project/frontend/src/communication.ts +++ b/Project/frontend/src/communication.ts @@ -1,4 +1,4 @@ -import { MachineBridgeInterface, ApiResponse, TodoModel, PurchaseCount, HolyUser, HolyRequestUser, PurchaseItem, KuntaeModel, LoginStatusResponse, LoginResult, UserGroup, PreviousLoginInfo, UserInfoDetail, GroupUser, UserLevelInfo, UserFullData, JobReportItem, JobReportUser, CommonCodeGroup, CommonCode, ItemInfo, JobReportPermission, AppVersionInfo, JobTypeItem, HolidayItem, MailFormItem, UserGroupItem, PermissionInfo } from './types'; +import { MachineBridgeInterface, ApiResponse, TodoModel, PurchaseCount, HolyUser, HolyRequestUser, PurchaseItem, KuntaeModel, LoginStatusResponse, LoginResult, UserGroup, PreviousLoginInfo, UserInfoDetail, GroupUser, UserLevelInfo, UserFullData, JobReportItem, JobReportUser, CommonCodeGroup, CommonCode, ItemInfo, ItemDetail, SupplierStaff, PurchaseHistoryItem, JobReportPermission, AppVersionInfo, JobTypeItem, HolidayItem, MailFormItem, UserGroupItem, PermissionInfo, AuthItem, AuthFieldInfo, CheckAuthResponse, MyAuthInfo, AuthType, ProjectSearchItem } from './types'; // WebView2 환경인지 체크 const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview; @@ -281,6 +281,101 @@ class CommunicationLayer { } } + public async kuntaeErrorCheck(sd: string, ed: string): Promise { + if (isWebView && machine) { + const result = await machine.Kuntae_ErrorCheck(sd, ed); + return JSON.parse(result); + } else { + return this.wsRequest('KUNTAE_ERROR_CHECK', 'KUNTAE_ERROR_CHECK_DATA', { sd, ed }); + } + } + + public async kuntaeFixError(pdate: string): Promise { + if (isWebView && machine) { + const result = await machine.Kuntae_FixError(pdate); + return JSON.parse(result); + } else { + return this.wsRequest('KUNTAE_FIX_ERROR', 'KUNTAE_FIX_ERROR_DATA', { pdate }); + } + } + + public async kuntaeFixErrors(dates: string[]): Promise { + if (isWebView && machine) { + const result = await machine.Kuntae_FixErrors(JSON.stringify(dates)); + return JSON.parse(result); + } else { + return this.wsRequest('KUNTAE_FIX_ERRORS', 'KUNTAE_FIX_ERRORS_DATA', { dates }); + } + } + + // ===== Favorite API ===== + + public async getFavoriteList(): Promise { + if (isWebView && machine) { + const result = await machine.Favorite_GetList(); + return JSON.parse(result); + } else { + return this.wsRequest('FAVORITE_GET_LIST', 'FAVORITE_LIST_DATA'); + } + } + + // ===== Project List API ===== + + public async getProjectCategories(): Promise { + if (isWebView && machine) { + const result = await machine.Project_GetCategories(); + return JSON.parse(result); + } else { + return this.wsRequest('PROJECT_GET_CATEGORIES', 'PROJECT_CATEGORIES_DATA'); + } + } + + public async getProjectProcesses(): Promise { + if (isWebView && machine) { + const result = await machine.Project_GetProcesses(); + return JSON.parse(result); + } else { + return this.wsRequest('PROJECT_GET_PROCESSES', 'PROJECT_PROCESSES_DATA'); + } + } + + public async getProjectList( + statusFilter: string, + category: string, + process: string, + userFilter: string, + yearStart: string, + yearEnd: string, + dateType: string + ): Promise { + if (isWebView && machine) { + const result = await machine.Project_GetList(statusFilter, category, process, userFilter, yearStart, yearEnd, dateType); + return JSON.parse(result); + } else { + return this.wsRequest('PROJECT_GET_LIST', 'PROJECT_LIST_DATA', { + statusFilter, category, process, userFilter, yearStart, yearEnd, dateType + }); + } + } + + public async getProjectHistory(projectIdx: number): Promise { + if (isWebView && machine) { + const result = await machine.Project_GetHistory(projectIdx); + return JSON.parse(result); + } else { + return this.wsRequest('PROJECT_GET_HISTORY', 'PROJECT_HISTORY_DATA', { projectIdx }); + } + } + + public async getProjectDailyMemo(projectIdx: number): Promise { + if (isWebView && machine) { + const result = await machine.Project_GetDailyMemo(projectIdx); + return JSON.parse(result); + } else { + return this.wsRequest('PROJECT_GET_DAILY_MEMO', 'PROJECT_DAILY_MEMO_DATA', { projectIdx }); + } + } + // ===== Login API ===== @@ -450,6 +545,69 @@ class CommunicationLayer { } } + public async getItemImage(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetImage(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_IMAGE', 'ITEMS_IMAGE_DATA', { idx }); + } + } + + public async saveItemImage(idx: number, base64Image: string): Promise { + if (isWebView && machine) { + const result = await machine.Items_SaveImage(idx, base64Image); + return JSON.parse(result); + } else { + return this.wsRequest('ITEMS_SAVE_IMAGE', 'ITEMS_IMAGE_SAVED', { idx, base64Image }); + } + } + + public async deleteItemImage(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.Items_DeleteImage(idx); + return JSON.parse(result); + } else { + return this.wsRequest('ITEMS_DELETE_IMAGE', 'ITEMS_IMAGE_DELETED', { idx }); + } + } + + public async getItemDetail(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetDetail(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_DETAIL', 'ITEMS_DETAIL_DATA', { idx }); + } + } + + public async getSupplierStaff(supplyIdx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetSupplierStaff(supplyIdx); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_SUPPLIER_STAFF', 'ITEMS_SUPPLIER_STAFF_DATA', { supplyIdx }); + } + } + + public async getIncomingHistory(itemIdx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetIncomingHistory(itemIdx); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_INCOMING_HISTORY', 'ITEMS_INCOMING_HISTORY_DATA', { itemIdx }); + } + } + + public async getOrderHistory(itemIdx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetOrderHistory(itemIdx); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_ORDER_HISTORY', 'ITEMS_ORDER_HISTORY_DATA', { itemIdx }); + } + } + // ===== UserList API ===== public async getCurrentUserLevel(): Promise> { @@ -541,31 +699,31 @@ class CommunicationLayer { } public async addJobReport( - pdate: string, projectName: string, requestpart: string, package_: string, + pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string ): Promise { if (isWebView && machine) { - const result = await machine.Jobreport_Add(pdate, projectName, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag); + const result = await machine.Jobreport_Add(pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag); return JSON.parse(result); } else { return this.wsRequest('JOBREPORT_ADD', 'JOBREPORT_ADDED', { - pdate, projectName, requestpart, package: package_, type, process, status, description, hrs, ot, jobgrp, tag + pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag }); } } public async editJobReport( - idx: number, pdate: string, projectName: string, requestpart: string, package_: string, + idx: number, pdate: string, projectName: string, pidx: number | null, requestpart: string, package_: string, type: string, process: string, status: string, description: string, hrs: number, ot: number, jobgrp: string, tag: string ): Promise { if (isWebView && machine) { - const result = await machine.Jobreport_Edit(idx, pdate, projectName, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag); + const result = await machine.Jobreport_Edit(idx, pdate, projectName, pidx ?? -1, requestpart, package_, type, process, status, description, hrs, ot, jobgrp, tag); return JSON.parse(result); } else { return this.wsRequest('JOBREPORT_EDIT', 'JOBREPORT_EDITED', { - idx, pdate, projectName, requestpart, package: package_, type, process, status, description, hrs, ot, jobgrp, tag + idx, pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: type, process, status, description, hrs, ot, jobgrp, tag }); } } @@ -750,6 +908,118 @@ class CommunicationLayer { return this.wsRequest>('USERGROUP_GET_PERMISSION_INFO', 'USERGROUP_PERMISSION_INFO'); } } + + // ===== UserAuth API (사용자 권한) ===== + + public async userAuthCanAccess(): Promise<{ Success: boolean; CanAccess?: boolean; Level?: number; Message?: string }> { + if (isWebView && machine) { + const result = await machine.UserAuth_CanAccess(); + return JSON.parse(result); + } else { + return this.wsRequest<{ Success: boolean; CanAccess?: boolean; Level?: number; Message?: string }>('USERAUTH_CAN_ACCESS', 'USERAUTH_CAN_ACCESS_DATA'); + } + } + + public async getUserAuthList(): Promise> { + if (isWebView && machine) { + const result = await machine.UserAuth_GetList(); + return JSON.parse(result); + } else { + return this.wsRequest>('USERAUTH_GET_LIST', 'USERAUTH_LIST_DATA'); + } + } + + public async saveUserAuth( + idx: number, user: string, account: number, purchase: number, purchaseEB: number, + holyday: number, project: number, jobreport: number, scheapp: number, + equipment: number, otconfirm: number, holyreq: number, kuntae: number + ): Promise { + if (isWebView && machine) { + const result = await machine.UserAuth_Save(idx, user, account, purchase, purchaseEB, holyday, project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae); + return JSON.parse(result); + } else { + return this.wsRequest('USERAUTH_SAVE', 'USERAUTH_SAVED', { + idx, user, account, purchase, purchaseEB, holyday, project, jobreport, scheapp, equipment, otconfirm, holyreq, kuntae + }); + } + } + + public async deleteUserAuth(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.UserAuth_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('USERAUTH_DELETE', 'USERAUTH_DELETED', { idx }); + } + } + + public async getUserAuthFields(): Promise> { + if (isWebView && machine) { + const result = await machine.UserAuth_GetFields(); + return JSON.parse(result); + } else { + return this.wsRequest>('USERAUTH_GET_FIELDS', 'USERAUTH_FIELDS_DATA'); + } + } + + // ===== 범용 권한 체크 API ===== + + /** + * 범용 권한 체크 API + * @param authType 권한 타입 (purchase, holyday, project, jobreport, account 등) + * @param requiredLevel 필요한 최소 레벨 (기본값 5) + * @returns CheckAuthResponse - CanAccess, UserLevel, AuthLevel, EffectiveLevel 등 + */ + public async checkAuth(authType: AuthType | string, requiredLevel: number = 5): Promise { + if (isWebView && machine) { + const result = await machine.CheckAuth(authType, requiredLevel); + return JSON.parse(result); + } else { + return this.wsRequest('CHECK_AUTH', 'CHECK_AUTH_DATA', { authType, requiredLevel }); + } + } + + /** + * 현재 로그인한 사용자의 전체 권한 정보 조회 + * @returns ApiResponse - 사용자 레벨 및 각 항목별 권한 레벨 + */ + public async getMyAuth(): Promise> { + if (isWebView && machine) { + const result = await machine.GetMyAuth(); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_MY_AUTH', 'MY_AUTH_DATA'); + } + } + + // ===== 프로젝트 검색 API (업무일지용) ===== + + /** + * 프로젝트 검색 (Projects 테이블 + 과거 업무일지 항목명) + * @param keyword 검색어 + * @returns ApiResponse + */ + public async searchProjects(keyword: string): Promise> { + if (isWebView && machine) { + const result = await machine.Project_Search(keyword); + return JSON.parse(result); + } else { + return this.wsRequest>('PROJECT_SEARCH', 'PROJECT_SEARCH_DATA', { keyword }); + } + } + + /** + * 현재 사용자의 프로젝트 목록 조회 (업무일지 콤보박스용) + * @returns ApiResponse<{idx: number, name: string, status: string}[]> + */ + public async getUserProjects(): Promise> { + if (isWebView && machine) { + const result = await machine.Project_GetUserProjects(); + return JSON.parse(result); + } else { + return this.wsRequest>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA'); + } + } } export const comms = new CommunicationLayer(); diff --git a/Project/frontend/src/components/favorite/FavoriteDialog.tsx b/Project/frontend/src/components/favorite/FavoriteDialog.tsx new file mode 100644 index 0000000..1a3a031 --- /dev/null +++ b/Project/frontend/src/components/favorite/FavoriteDialog.tsx @@ -0,0 +1,188 @@ +import { useState, useEffect } from 'react'; +import { X, Star, Folder, Globe, FileText, Database, Server, Mail, Image, Film, Music, Archive, Terminal, Settings, HardDrive, Network, Cloud } from 'lucide-react'; +import { comms } from '@/communication'; +import { FavoriteItem } from '@/types'; + +interface FavoriteDialogProps { + isOpen: boolean; + onClose: () => void; +} + +// URL 유형에 따른 아이콘 및 색상 반환 +function getIconInfo(url: string): { icon: React.ElementType; color: string; bgColor: string } { + const lowerUrl = url.toLowerCase(); + + // 로컬 폴더/드라이브 경로 + if (lowerUrl.match(/^[a-z]:\\/i) || lowerUrl.startsWith('\\\\') || lowerUrl.startsWith('file:')) { + // 네트워크 경로 + if (lowerUrl.startsWith('\\\\')) { + return { icon: Network, color: 'text-purple-400', bgColor: 'from-purple-500/30 to-purple-600/30' }; + } + return { icon: Folder, color: 'text-yellow-400', bgColor: 'from-yellow-500/30 to-yellow-600/30' }; + } + + // 웹 URL + if (lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://')) { + // 특정 서비스 감지 + if (lowerUrl.includes('mail') || lowerUrl.includes('outlook') || lowerUrl.includes('gmail')) { + return { icon: Mail, color: 'text-red-400', bgColor: 'from-red-500/30 to-red-600/30' }; + } + if (lowerUrl.includes('cloud') || lowerUrl.includes('drive') || lowerUrl.includes('onedrive') || lowerUrl.includes('dropbox')) { + return { icon: Cloud, color: 'text-sky-400', bgColor: 'from-sky-500/30 to-sky-600/30' }; + } + if (lowerUrl.includes('server') || lowerUrl.includes('admin')) { + return { icon: Server, color: 'text-orange-400', bgColor: 'from-orange-500/30 to-orange-600/30' }; + } + if (lowerUrl.includes('database') || lowerUrl.includes('sql') || lowerUrl.includes('db')) { + return { icon: Database, color: 'text-emerald-400', bgColor: 'from-emerald-500/30 to-emerald-600/30' }; + } + return { icon: Globe, color: 'text-blue-400', bgColor: 'from-blue-500/30 to-blue-600/30' }; + } + + // 실행 파일 + if (lowerUrl.endsWith('.exe') || lowerUrl.endsWith('.bat') || lowerUrl.endsWith('.cmd') || lowerUrl.endsWith('.ps1')) { + return { icon: Terminal, color: 'text-green-400', bgColor: 'from-green-500/30 to-green-600/30' }; + } + + // 문서 파일 + if (lowerUrl.match(/\.(doc|docx|pdf|txt|xls|xlsx|ppt|pptx|hwp)$/i)) { + return { icon: FileText, color: 'text-blue-400', bgColor: 'from-blue-500/30 to-blue-600/30' }; + } + + // 이미지 파일 + if (lowerUrl.match(/\.(jpg|jpeg|png|gif|bmp|svg|webp)$/i)) { + return { icon: Image, color: 'text-pink-400', bgColor: 'from-pink-500/30 to-pink-600/30' }; + } + + // 비디오 파일 + if (lowerUrl.match(/\.(mp4|avi|mkv|mov|wmv)$/i)) { + return { icon: Film, color: 'text-violet-400', bgColor: 'from-violet-500/30 to-violet-600/30' }; + } + + // 오디오 파일 + if (lowerUrl.match(/\.(mp3|wav|flac|aac|ogg)$/i)) { + return { icon: Music, color: 'text-rose-400', bgColor: 'from-rose-500/30 to-rose-600/30' }; + } + + // 압축 파일 + if (lowerUrl.match(/\.(zip|rar|7z|tar|gz)$/i)) { + return { icon: Archive, color: 'text-amber-400', bgColor: 'from-amber-500/30 to-amber-600/30' }; + } + + // 설정 파일 + if (lowerUrl.match(/\.(ini|cfg|config|xml|json|yaml|yml)$/i)) { + return { icon: Settings, color: 'text-gray-400', bgColor: 'from-gray-500/30 to-gray-600/30' }; + } + + // 드라이브 루트 + if (lowerUrl.match(/^[a-z]:$/i)) { + return { icon: HardDrive, color: 'text-slate-400', bgColor: 'from-slate-500/30 to-slate-600/30' }; + } + + // 기본값: 폴더 + return { icon: Folder, color: 'text-yellow-400', bgColor: 'from-yellow-500/30 to-yellow-600/30' }; +} + +export function FavoriteDialog({ isOpen, onClose }: FavoriteDialogProps) { + const [favorites, setFavorites] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (isOpen) { + loadFavorites(); + } + }, [isOpen]); + + const loadFavorites = async () => { + setLoading(true); + try { + const response = await comms.getFavoriteList(); + if (response.Success && response.Data) { + // 이름으로 정렬 + const sorted = (response.Data as FavoriteItem[]).sort((a, b) => + a.name.localeCompare(b.name, 'ko') + ); + setFavorites(sorted); + } + } catch (error) { + console.error('즐겨찾기 로드 오류:', error); + } finally { + setLoading(false); + } + }; + + const handleItemClick = (url: string) => { + window.open(url, '_blank'); + onClose(); + }; + + if (!isOpen) return null; + + return ( +
+ {/* Backdrop */} +
+ + {/* Dialog */} +
+ {/* Header */} +
+
+ +

즐겨찾기

+ ({favorites.length}개) +
+ +
+ + {/* Content */} +
+ {loading ? ( +
+
+
+ ) : favorites.length === 0 ? ( +
+ 등록된 즐겨찾기가 없습니다. +
+ ) : ( +
+ {favorites.map((item, index) => { + const { icon: Icon, color, bgColor } = getIconInfo(item.url); + return ( + + ); + })} +
+ )} +
+ + {/* Footer */} +
+

+ 클릭하면 새 탭에서 열립니다 +

+
+
+
+ ); +} diff --git a/Project/frontend/src/components/favorite/index.ts b/Project/frontend/src/components/favorite/index.ts new file mode 100644 index 0000000..53e196a --- /dev/null +++ b/Project/frontend/src/components/favorite/index.ts @@ -0,0 +1 @@ +export { FavoriteDialog } from './FavoriteDialog'; diff --git a/Project/frontend/src/components/items/ItemEditDialog.tsx b/Project/frontend/src/components/items/ItemEditDialog.tsx index 22c6ff1..c430b3c 100644 --- a/Project/frontend/src/components/items/ItemEditDialog.tsx +++ b/Project/frontend/src/components/items/ItemEditDialog.tsx @@ -1,6 +1,7 @@ -import { useState, useEffect } from 'react'; -import { X, Save, Trash2 } from 'lucide-react'; +import { useState, useEffect, useRef, useCallback } from 'react'; +import { X, Save, Trash2, Upload, Clipboard, ImageIcon } from 'lucide-react'; import { ItemInfo } from '@/types'; +import { comms } from '@/communication'; interface ItemEditDialogProps { item: ItemInfo | null; @@ -13,13 +14,169 @@ interface ItemEditDialogProps { export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: ItemEditDialogProps) { const [editData, setEditData] = useState(null); const [saving, setSaving] = useState(false); + const [imageData, setImageData] = useState(null); + const [imageLoading, setImageLoading] = useState(false); + const [imageSaving, setImageSaving] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const fileInputRef = useRef(null); + const dropZoneRef = useRef(null); useEffect(() => { if (item) { setEditData({ ...item }); + setImageData(null); + // 기존 품목인 경우 이미지 로드 + if (item.idx > 0) { + loadImage(item.idx); + } } }, [item]); + // 이미지 로드 + const loadImage = async (idx: number) => { + setImageLoading(true); + try { + const result = await comms.getItemImage(idx); + if (result.Success && result.Data) { + setImageData(`data:image/jpeg;base64,${result.Data}`); + } else { + setImageData(null); + } + } catch (error) { + console.error('이미지 로드 실패:', error); + setImageData(null); + } finally { + setImageLoading(false); + } + }; + + // ESC 키로 닫기 + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape' && isOpen) { + onClose(); + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isOpen, onClose]); + + // 이미지를 Base64로 변환 + const convertToBase64 = (file: File): Promise => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result as string); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + }; + + // 이미지 파일 처리 + const handleImageFile = async (file: File) => { + if (!file.type.startsWith('image/')) { + alert('이미지 파일만 업로드 가능합니다.'); + return; + } + + try { + const base64 = await convertToBase64(file); + setImageData(base64); + + // 기존 품목인 경우 바로 저장 + if (editData && editData.idx > 0) { + setImageSaving(true); + const result = await comms.saveItemImage(editData.idx, base64); + if (!result.Success) { + alert(result.Message || '이미지 저장 실패'); + } + setImageSaving(false); + } + } catch (error) { + console.error('이미지 처리 실패:', error); + alert('이미지 처리에 실패했습니다.'); + } + }; + + // 파일 선택 + const handleFileSelect = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + handleImageFile(file); + } + // 같은 파일 다시 선택 가능하도록 + e.target.value = ''; + }; + + // 클립보드에서 붙여넣기 + const handlePaste = useCallback(async () => { + try { + const items = await navigator.clipboard.read(); + for (const item of items) { + const imageType = item.types.find(type => type.startsWith('image/')); + if (imageType) { + const blob = await item.getType(imageType); + const file = new File([blob], 'clipboard-image.png', { type: imageType }); + await handleImageFile(file); + return; + } + } + alert('클립보드에 이미지가 없습니다.'); + } catch (error) { + console.error('클립보드 읽기 실패:', error); + alert('클립보드에서 이미지를 가져올 수 없습니다.'); + } + }, [editData]); + + // 드래그 앤 드롭 + const handleDragEnter = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(true); + }; + + const handleDragLeave = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); + + const files = e.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + await handleImageFile(file); + } + }; + + // 이미지 삭제 + const handleDeleteImage = async () => { + if (!editData) return; + + if (!confirm('이미지를 삭제하시겠습니까?')) return; + + if (editData.idx > 0) { + setImageSaving(true); + const result = await comms.deleteItemImage(editData.idx); + if (result.Success) { + setImageData(null); + } else { + alert(result.Message || '이미지 삭제 실패'); + } + setImageSaving(false); + } else { + setImageData(null); + } + }; + if (!isOpen || !editData) return null; const isNew = editData.idx === 0; @@ -48,10 +205,13 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item return (
{/* 배경 오버레이 */} -
+
- {/* 다이얼로그 */} -
+ {/* 다이얼로그 - 이미지 영역 포함해서 더 넓게 */} +
e.stopPropagation()} + > {/* 헤더 */}

@@ -65,145 +225,245 @@ export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: Item

- {/* 내용 */} -
-
- {/* SID */} + {/* 내용 - 좌우 레이아웃 */} +
+ {/* 왼쪽: 폼 필드 */} +
+
+ {/* SID */} +
+ + setEditData({ ...editData, sid: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+ + {/* 분류 */} +
+ + setEditData({ ...editData, cate: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+
+ + {/* 품명 */}
- + setEditData({ ...editData, sid: e.target.value })} + value={editData.name} + onChange={(e) => setEditData({ ...editData, name: e.target.value })} className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" />
- {/* 분류 */} + {/* 모델 */}
- + setEditData({ ...editData, cate: e.target.value })} - className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" - /> -
-
- - {/* 품명 */} -
- - setEditData({ ...editData, name: e.target.value })} - className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" - /> -
- - {/* 모델 */} -
- - setEditData({ ...editData, model: e.target.value })} - className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" - /> -
- -
- {/* 규격 */} -
- - setEditData({ ...editData, scale: e.target.value })} + value={editData.model} + onChange={(e) => setEditData({ ...editData, model: e.target.value })} className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" />
- {/* 단위 */} +
+ {/* 규격 */} +
+ + setEditData({ ...editData, scale: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+ + {/* 단위 */} +
+ + setEditData({ ...editData, unit: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+ + {/* 단가 */} +
+ + setEditData({ ...editData, price: parseFloat(e.target.value) || 0 })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-right" + /> +
+
+ +
+ {/* 공급처 */} +
+ + setEditData({ ...editData, supply: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+ + {/* 제조사 */} +
+ + setEditData({ ...editData, manu: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+
+ + {/* 보관장소 */}
- + setEditData({ ...editData, unit: e.target.value })} + value={editData.storage} + onChange={(e) => setEditData({ ...editData, storage: e.target.value })} className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" />
- {/* 단가 */} + {/* 메모 */}
- - setEditData({ ...editData, price: parseFloat(e.target.value) || 0 })} - className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white text-right" - /> -
-
- -
- {/* 공급처 */} -
- - setEditData({ ...editData, supply: e.target.value })} - className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + +