diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj index 5f97efa..1ee65db 100644 --- a/Project/EETGW.csproj +++ b/Project/EETGW.csproj @@ -361,6 +361,7 @@ + diff --git a/Project/Web/MachineBridge/MachineBridge.Holyday.cs b/Project/Web/MachineBridge/MachineBridge.Holyday.cs new file mode 100644 index 0000000..ba4fc60 --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.Holyday.cs @@ -0,0 +1,447 @@ +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 Holyday (근태) API + + /// + /// 근태 목록 조회 (사용자 선택 가능) + /// + public string Holyday_GetList(string sd, string ed, string uid) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + + var sql = @"SELECT h.*, u.name as UserName + FROM Holyday h WITH (nolock) + LEFT JOIN Users u ON h.uid = u.id + WHERE h.gcode = @gcode"; + + var parameters = new List(); + parameters.Add(new SqlParameter("@gcode", info.Login.gcode)); + + // 권한에 따라 사용자 필터링 + if (curLevel < 5) + { + // 일반 사용자는 본인 것만 + sql += " AND h.uid = @uid"; + parameters.Add(new SqlParameter("@uid", info.Login.no)); + } + else if (!string.IsNullOrEmpty(uid) && uid != "%") + { + // 관리자가 특정 사용자 선택 + sql += " AND h.uid = @uid"; + parameters.Add(new SqlParameter("@uid", uid)); + } + + if (!string.IsNullOrEmpty(sd)) + { + sql += " AND h.sdate >= @sd"; + parameters.Add(new SqlParameter("@sd", sd)); + } + if (!string.IsNullOrEmpty(ed)) + { + sql += " AND h.sdate <= @ed"; + parameters.Add(new SqlParameter("@ed", ed)); + } + + sql += " ORDER BY h.sdate DESC, h.idx DESC"; + + 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); + return JsonConvert.SerializeObject(new { Success = true, Data = dt }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 근태 상세 조회 + /// + public string Holyday_GetDetail(int idx) + { + try + { + var sql = @"SELECT h.*, u.name as UserName + FROM Holyday h WITH (nolock) + LEFT JOIN Users u ON h.uid = u.id + WHERE h.idx = @idx AND h.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); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + if (dt.Rows.Count > 0) + { + var row = dt.Rows[0]; + var data = new Dictionary(); + foreach (DataColumn col in dt.Columns) + { + data[col.ColumnName] = row[col] == DBNull.Value ? null : row[col]; + } + return JsonConvert.SerializeObject(new { Success = true, Data = data }); + } + return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 근태 추가 + /// + public string Holyday_Add(string cate, string sdate, string edate, double term, double crtime, + double termDr, double drTime, string contents, string uid) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + if (curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." }); + } + + // 마감 체크 + var smon = sdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + + var sql = @"INSERT INTO Holyday (gcode, cate, sdate, edate, term, crtime, termDr, DrTime, contents, uid, wdate, wuid) + VALUES (@gcode, @cate, @sdate, @edate, @term, @crtime, @termDr, @drTime, @contents, @uid, GETDATE(), @wuid); + SELECT SCOPE_IDENTITY();"; + + 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("@cate", cate ?? ""); + cmd.Parameters.AddWithValue("@sdate", sdate); + cmd.Parameters.AddWithValue("@edate", edate); + cmd.Parameters.AddWithValue("@term", term); + cmd.Parameters.AddWithValue("@crtime", crtime); + cmd.Parameters.AddWithValue("@termDr", termDr); + cmd.Parameters.AddWithValue("@drTime", drTime); + cmd.Parameters.AddWithValue("@contents", contents ?? ""); + cmd.Parameters.AddWithValue("@uid", uid); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + cn.Open(); + var newId = Convert.ToInt32(cmd.ExecuteScalar()); + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 근태 수정 + /// + public string Holyday_Edit(int idx, string cate, string sdate, string edate, double term, double crtime, + double termDr, double drTime, string contents) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + if (curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." }); + } + + // 마감 체크 + var smon = sdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + + // 외부 연동 데이터 체크 + var checkSql = "SELECT extcate, extidx FROM Holyday WHERE idx = @idx AND gcode = @gcode"; + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + using (var cmd = new SqlCommand(checkSql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cn.Open(); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var extcate = reader["extcate"] != DBNull.Value ? reader["extcate"].ToString() : ""; + var extidx = reader["extidx"] != DBNull.Value ? Convert.ToInt32(reader["extidx"]) : -1; + + if (!string.IsNullOrEmpty(extcate) && extidx > 0) + { + return JsonConvert.SerializeObject(new { + Success = false, + Message = $"이 자료는 외부에서 자동생성된 자료입니다. (소스: {extcate}:{extidx})" + }); + } + } + } + } + + var sql = @"UPDATE Holyday SET + cate = @cate, sdate = @sdate, edate = @edate, term = @term, crtime = @crtime, + termDr = @termDr, DrTime = @drTime, contents = @contents, wuid = @wuid, wdate = GETDATE() + WHERE idx = @idx AND gcode = @gcode"; + + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@cate", cate ?? ""); + cmd.Parameters.AddWithValue("@sdate", sdate); + cmd.Parameters.AddWithValue("@edate", edate); + cmd.Parameters.AddWithValue("@term", term); + cmd.Parameters.AddWithValue("@crtime", crtime); + cmd.Parameters.AddWithValue("@termDr", termDr); + cmd.Parameters.AddWithValue("@drTime", drTime); + cmd.Parameters.AddWithValue("@contents", contents ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + 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 Holyday_Delete(int idx) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + if (curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." }); + } + + // 외부 연동 데이터 및 마감 체크 + var checkSql = "SELECT extcate, extidx, sdate FROM Holyday WHERE idx = @idx AND gcode = @gcode"; + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + using (var cmd = new SqlCommand(checkSql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cn.Open(); + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var extcate = reader["extcate"] != DBNull.Value ? reader["extcate"].ToString() : ""; + var extidx = reader["extidx"] != DBNull.Value ? Convert.ToInt32(reader["extidx"]) : -1; + var sdate = reader["sdate"] != DBNull.Value ? reader["sdate"].ToString() : ""; + + if (!string.IsNullOrEmpty(extcate) && extidx > 0) + { + return JsonConvert.SerializeObject(new { + Success = false, + Message = $"이 자료는 외부에서 자동생성된 자료입니다. (소스: {extcate}:{extidx})" + }); + } + + if (!string.IsNullOrEmpty(sdate) && sdate.Length >= 7) + { + var smon = sdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + } + } + } + } + + var sql = "DELETE FROM Holyday WHERE idx = @idx AND gcode = @gcode"; + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + 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 Holyday_GetUserList(string sd, string ed) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + + var sql = @"SELECT DISTINCT h.uid, u.name as UserName + FROM Holyday h WITH (nolock) + LEFT JOIN Users u ON h.uid = u.id + WHERE h.gcode = @gcode"; + + var parameters = new List(); + parameters.Add(new SqlParameter("@gcode", info.Login.gcode)); + + if (!string.IsNullOrEmpty(sd)) + { + sql += " AND h.sdate >= @sd"; + parameters.Add(new SqlParameter("@sd", sd)); + } + if (!string.IsNullOrEmpty(ed)) + { + sql += " AND h.sdate <= @ed"; + parameters.Add(new SqlParameter("@ed", ed)); + } + + sql += " ORDER BY u.name"; + + 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); + return JsonConvert.SerializeObject(dt); + } + } + } + catch (Exception ex) + { + Console.WriteLine($"Holyday_GetUserList 오류: {ex.Message}"); + return "[]"; + } + } + + /// + /// 근태 권한 정보 조회 + /// + public string Holyday_GetPermission() + { + try + { + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + bool canManage = curLevel >= 5; + + return JsonConvert.SerializeObject(new + { + Success = true, + CurrentUserId = info.Login.no, + Level = curLevel, + CanManage = canManage + }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 근태 잔량 조회 (연도별) + /// + public string Holyday_GetBalance(string year, string uid) + { + try + { + // 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.holyday)); + + // 본인 확인 또는 관리자 권한 확인 + if (uid != info.Login.no && curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "권한이 없습니다." }); + } + + var sql = @"SELECT + cate, + ISNULL(SUM(termDr), 0) as TotalGenDays, + ISNULL(SUM(DrTime), 0) as TotalGenHours, + ISNULL(SUM(term), 0) as TotalUseDays, + ISNULL(SUM(crtime), 0) as TotalUseHours + FROM Holyday WITH (nolock) + WHERE gcode = @gcode AND uid = @uid AND sdate LIKE @year + '%' + GROUP BY cate"; + + 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("@uid", uid); + cmd.Parameters.AddWithValue("@year", year); + + 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.Jobreport.cs b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs index 7ec5e01..8f7f1d7 100644 --- a/Project/Web/MachineBridge/MachineBridge.Jobreport.cs +++ b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs @@ -99,10 +99,10 @@ namespace Project.Web { try { - // 뷰에서 기본 정보 조회, 원본 테이블에서 jobgrp, tag 추가 조회 + // 뷰에서 기본 정보 조회, 원본 테이블에서 jobgrp, tag, otStart, otEnd 추가 조회 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 + v.ww, v.otpms, v.process, j.jobgrp, j.tag, j.otStart, j.otEnd FROM vJobReportForUser v WITH (nolock) INNER JOIN JobReport j WITH (nolock) ON v.idx = j.idx WHERE v.idx = @idx AND v.gcode = @gcode"; @@ -141,7 +141,8 @@ namespace Project.Web /// 업무일지 추가 (JobReport 테이블) /// 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) + string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag, + string otStart, string otEnd) { try { @@ -153,9 +154,9 @@ 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) + type, process, status, description, hrs, ot, jobgrp, tag, wuid, wdate, pidx, otStart, otEnd) VALUES (@gcode, @uid, @pdate, @projectName, @requestpart, @package, - @type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), @pidx); + @type, @process, @status, @description, @hrs, @ot, @jobgrp, @tag, @wuid, GETDATE(), @pidx, @otStart, @otEnd); SELECT SCOPE_IDENTITY();"; var cs = Properties.Settings.Default.gwcs; @@ -178,6 +179,20 @@ namespace Project.Web cmd.Parameters.AddWithValue("@jobgrp", jobgrp ?? ""); cmd.Parameters.AddWithValue("@tag", tag ?? ""); cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + // otStart, otEnd 처리 (HH:mm 형식을 datetime으로 변환) + if (!string.IsNullOrEmpty(otStart) && !string.IsNullOrEmpty(otEnd)) + { + var otStartDateTime = DateTime.Parse($"{pdate} {otStart}:00"); + var otEndDateTime = DateTime.Parse($"{pdate} {otEnd}:00"); + cmd.Parameters.AddWithValue("@otStart", otStartDateTime); + cmd.Parameters.AddWithValue("@otEnd", otEndDateTime); + } + else + { + cmd.Parameters.AddWithValue("@otStart", DBNull.Value); + cmd.Parameters.AddWithValue("@otEnd", DBNull.Value); + } cn.Open(); var newId = Convert.ToInt32(cmd.ExecuteScalar()); @@ -194,7 +209,8 @@ namespace Project.Web /// 업무일지 수정 (JobReport 테이블) /// 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) + string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag, + string otStart, string otEnd) { try { @@ -231,7 +247,7 @@ namespace Project.Web 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() + otStart = @otStart, otEnd = @otEnd, wuid = @wuid, wdate = GETDATE() WHERE idx = @idx AND gcode = @gcode"; var cs = Properties.Settings.Default.gwcs; @@ -254,6 +270,20 @@ namespace Project.Web cmd.Parameters.AddWithValue("@jobgrp", jobgrp ?? ""); cmd.Parameters.AddWithValue("@tag", tag ?? ""); cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + // otStart, otEnd 처리 (HH:mm 형식을 datetime으로 변환) + if (!string.IsNullOrEmpty(otStart) && !string.IsNullOrEmpty(otEnd)) + { + var otStartDateTime = DateTime.Parse($"{pdate} {otStart}:00"); + var otEndDateTime = DateTime.Parse($"{pdate} {otEnd}:00"); + cmd.Parameters.AddWithValue("@otStart", otStartDateTime); + cmd.Parameters.AddWithValue("@otEnd", otEndDateTime); + } + else + { + cmd.Parameters.AddWithValue("@otStart", DBNull.Value); + cmd.Parameters.AddWithValue("@otEnd", DBNull.Value); + } cn.Open(); var result = cmd.ExecuteNonQuery(); diff --git a/Project/Web/MachineBridge/WebSocketServer.cs b/Project/Web/MachineBridge/WebSocketServer.cs index c1b86bf..a423892 100644 --- a/Project/Web/MachineBridge/WebSocketServer.cs +++ b/Project/Web/MachineBridge/WebSocketServer.cs @@ -646,7 +646,9 @@ namespace Project.Web double ot = json.ot ?? 0.0; string jobgrp = json.jobgrp ?? ""; string tag = json.tag ?? ""; - string result = _bridge.Jobreport_Add(pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag); + string otStart = json.otStart ?? "00:00"; + string otEnd = json.otEnd ?? "00:00"; + string result = _bridge.Jobreport_Add(pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd); var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) }; await Send(socket, JsonConvert.SerializeObject(response)); } @@ -668,7 +670,9 @@ 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, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag); + string otStart = json.otStart ?? "00:00"; + string otEnd = json.otEnd ?? "00:00"; + string result = _bridge.Jobreport_Edit(idx, pdate, projectName, pidx, requestpart, package, jobType, process, status, description, hrs, ot, jobgrp, tag, otStart, otEnd); var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) }; await Send(socket, JsonConvert.SerializeObject(response)); } @@ -729,6 +733,99 @@ namespace Project.Web } break; + // ===== Holyday (근태) API ===== + case "HOLYDAY_GET_LIST": + { + string sd = json.sd ?? ""; + string ed = json.ed ?? ""; + string uid = json.uid ?? "%"; + string result = _bridge.Holyday_GetList(sd, ed, uid); + var response = new { type = "HOLYDAY_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.Holyday_GetDetail(idx); + var response = new { type = "HOLYDAY_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_ADD": + { + string cate = json.cate ?? ""; + string sdate = json.sdate ?? ""; + string edate = json.edate ?? ""; + double term = json.term ?? 0.0; + double crtime = json.crtime ?? 0.0; + double termDr = json.termDr ?? 0.0; + double drTime = json.drTime ?? 0.0; + string contents = json.contents ?? ""; + string uid = json.uid ?? ""; + string result = _bridge.Holyday_Add(cate, sdate, edate, term, crtime, termDr, drTime, contents, uid); + var response = new { type = "HOLYDAY_ADDED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_EDIT": + { + int idx = json.idx ?? 0; + string cate = json.cate ?? ""; + string sdate = json.sdate ?? ""; + string edate = json.edate ?? ""; + double term = json.term ?? 0.0; + double crtime = json.crtime ?? 0.0; + double termDr = json.termDr ?? 0.0; + double drTime = json.drTime ?? 0.0; + string contents = json.contents ?? ""; + string result = _bridge.Holyday_Edit(idx, cate, sdate, edate, term, crtime, termDr, drTime, contents); + var response = new { type = "HOLYDAY_EDITED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.Holyday_Delete(idx); + var response = new { type = "HOLYDAY_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_GET_USERLIST": + { + string sd = json.sd ?? ""; + string ed = json.ed ?? ""; + string result = _bridge.Holyday_GetUserList(sd, ed); + var response = new { type = "HOLYDAY_USERLIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_GET_PERMISSION": + { + string result = _bridge.Holyday_GetPermission(); + var response = new { type = "HOLYDAY_PERMISSION_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "HOLYDAY_GET_BALANCE": + { + string year = json.year ?? ""; + string uid = json.uid ?? ""; + string result = _bridge.Holyday_GetBalance(year, uid); + var response = new { type = "HOLYDAY_BALANCE_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Holiday API (월별근무표) ===== case "HOLIDAY_GET_LIST": { diff --git a/Project/frontend/src/communication.ts b/Project/frontend/src/communication.ts index 21c14e5..01b4ccd 100644 --- a/Project/frontend/src/communication.ts +++ b/Project/frontend/src/communication.ts @@ -1,9 +1,7 @@ -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?.hostObjects !== undefined; -// WebView2 환경인지 체크 -const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview; - -// 비동기 프록시 캐싱 (한 번만 초기화) const machine: MachineBridgeInterface | null = isWebView ? window.chrome!.webview!.hostObjects.machine : null; @@ -308,6 +306,50 @@ class CommunicationLayer { } } + // ===== Holyday (근태) API ===== + + public async getHolydayList(sd: string, ed: string, uid: string = '%'): Promise> { + return this.wsRequest>('HOLYDAY_GET_LIST', 'HOLYDAY_LIST_DATA', { sd, ed, uid }); + } + + public async getHolydayDetail(idx: number): Promise> { + return this.wsRequest>('HOLYDAY_GET_DETAIL', 'HOLYDAY_DETAIL_DATA', { idx }); + } + + public async addHolyday( + cate: string, sdate: string, edate: string, term: number, crtime: number, + termDr: number, drTime: number, contents: string, uid: string + ): Promise { + return this.wsRequest('HOLYDAY_ADD', 'HOLYDAY_ADDED', { + cate, sdate, edate, term, crtime, termDr, drTime, contents, uid + }); + } + + public async editHolyday( + idx: number, cate: string, sdate: string, edate: string, term: number, crtime: number, + termDr: number, drTime: number, contents: string + ): Promise { + return this.wsRequest('HOLYDAY_EDIT', 'HOLYDAY_EDITED', { + idx, cate, sdate, edate, term, crtime, termDr, drTime, contents + }); + } + + public async deleteHolyday(idx: number): Promise { + return this.wsRequest('HOLYDAY_DELETE', 'HOLYDAY_DELETED', { idx }); + } + + public async getHolydayUserList(sd: string, ed: string): Promise { + return this.wsRequest('HOLYDAY_GET_USERLIST', 'HOLYDAY_USERLIST_DATA', { sd, ed }); + } + + public async getHolydayPermission(): Promise { + return this.wsRequest('HOLYDAY_GET_PERMISSION', 'HOLYDAY_PERMISSION_DATA'); + } + + public async getHolydayBalance(year: string, uid: string): Promise> { + return this.wsRequest>('HOLYDAY_GET_BALANCE', 'HOLYDAY_BALANCE_DATA', { year, uid }); + } + // ===== Favorite API ===== public async getFavoriteList(): Promise { @@ -701,14 +743,14 @@ class CommunicationLayer { public async addJobReport( 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 + hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string ): Promise { if (isWebView && machine) { - const result = await machine.Jobreport_Add(pdate, projectName, pidx ?? -1, 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, otStart, otEnd); return JSON.parse(result); } else { return this.wsRequest('JOBREPORT_ADD', 'JOBREPORT_ADDED', { - pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: 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, otStart, otEnd }); } } @@ -716,14 +758,14 @@ class CommunicationLayer { public async editJobReport( 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 + hrs: number, ot: number, jobgrp: string, tag: string, otStart: string, otEnd: string ): Promise { if (isWebView && machine) { - const result = await machine.Jobreport_Edit(idx, pdate, projectName, pidx ?? -1, 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, otStart, otEnd); return JSON.parse(result); } else { return this.wsRequest('JOBREPORT_EDIT', 'JOBREPORT_EDITED', { - idx, pdate, projectName, pidx: pidx ?? -1, requestpart, package: package_, jobType: 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, otStart, otEnd }); } } @@ -746,6 +788,15 @@ class CommunicationLayer { } } + public async getJobReportTypeList(sd: string, ed: string, uid: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Jobreport_GetTypeList(sd, ed, uid); + return JSON.parse(result); + } else { + return this.wsRequest>('JOBREPORT_GET_TYPE_LIST', 'JOBREPORT_TYPE_LIST_DATA', { sd, ed, uid }); + } + } + public async getJobTypes(process: string = ''): Promise> { if (isWebView && machine) { const result = await machine.Jobreport_GetJobTypes(process); @@ -1012,12 +1063,12 @@ class CommunicationLayer { * 현재 사용자의 프로젝트 목록 조회 (업무일지 콤보박스용) * @returns ApiResponse<{idx: number, name: string, status: string}[]> */ - public async getUserProjects(): Promise> { + 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'); + return this.wsRequest>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA'); } } } diff --git a/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx b/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx new file mode 100644 index 0000000..0b2fe07 --- /dev/null +++ b/Project/frontend/src/components/jobreport/JobReportDayDialog.tsx @@ -0,0 +1,350 @@ +import { useState, useEffect } from 'react'; +import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react'; +import { comms } from '@/communication'; +import { JobReportDayItem, HolidayItem } from '@/types'; + +interface JobReportDayDialogProps { + isOpen: boolean; + onClose: () => void; + initialMonth?: string; // YYYY-MM format +} + +interface DayColumn { + day: number; + dayOfWeek: string; + isHoliday: boolean; + holidayMemo?: string; +} + +interface UserRow { + uid: string; + uname: string; + processs: string; + dailyData: Map; + totalHrs: number; + totalOt: number; + totalHolidayOt: number; +} + +export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportDayDialogProps) { + const [currentMonth, setCurrentMonth] = useState(initialMonth || new Date().toISOString().substring(0, 7)); + const [loading, setLoading] = useState(false); + const [dayColumns, setDayColumns] = useState([]); + const [userRows, setUserRows] = useState([]); + const [currentUserId, setCurrentUserId] = useState(''); + const [currentUserLevel, setCurrentUserLevel] = useState(0); + const [authLevel, setAuthLevel] = useState(0); + const [canViewAll, setCanViewAll] = useState(false); + + // 요일 배열 + const weekDays = ['일', '월', '화', '수', '목', '금', '토']; + + // 권한 및 사용자 정보 로드 + useEffect(() => { + if (isOpen) { + loadAuthAndUserInfo(); + } + }, [isOpen]); + + const loadAuthAndUserInfo = async () => { + try { + // 현재 로그인 사용자 정보 가져오기 + const loginStatus = await comms.checkLoginStatus(); + if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) { + const userId = loginStatus.User.Id; + const userLevel = loginStatus.User.Level || 0; + setCurrentUserId(userId); + setCurrentUserLevel(userLevel); + + // 업무일지(jobreport) 권한 가져오기 + const authResponse = await comms.checkAuth('jobreport', 5); + const jobReportAuthLevel = authResponse.EffectiveLevel || 0; + setAuthLevel(jobReportAuthLevel); + + // 유효 권한 레벨 = Max(사용자레벨, 권한레벨) + const effectiveLevel = Math.max(userLevel, jobReportAuthLevel); + setCanViewAll(effectiveLevel >= 5); + } + } catch (error) { + console.error('권한 정보 로드 오류:', error); + setCanViewAll(false); + } + }; + + // 데이터 로드 + useEffect(() => { + if (isOpen && currentUserId) { + loadData(); + } + }, [isOpen, currentMonth, currentUserId, canViewAll]); + + const loadData = async () => { + setLoading(true); + try { + // 현재는 기존 API를 사용하여 월별 데이터를 가져옴 + const startDate = `${currentMonth}-01`; + const year = parseInt(currentMonth.substring(0, 4)); + const month = parseInt(currentMonth.substring(5, 7)); + const lastDay = new Date(year, month, 0).getDate(); + const endDate = `${currentMonth}-${String(lastDay).padStart(2, '0')}`; + + const [jobReportResponse, holidayResponse] = await Promise.all([ + comms.getJobReportList(startDate, endDate, '', ''), + comms.getHolidayList(currentMonth) + ]); + + if (jobReportResponse.Success && jobReportResponse.Data) { + processData(jobReportResponse.Data, holidayResponse.Data || [], lastDay); + } + } catch (error) { + console.error('데이터 로드 오류:', error); + } finally { + setLoading(false); + } + }; + + const processData = (items: any[], holidays: HolidayItem[], lastDay: number) => { + // 날짜 컬럼 생성 + const columns: DayColumn[] = []; + const year = parseInt(currentMonth.substring(0, 4)); + const month = parseInt(currentMonth.substring(5, 7)); + + for (let day = 1; day <= lastDay; day++) { + const date = new Date(year, month - 1, day); + const dayOfWeek = weekDays[date.getDay()]; + const dateStr = `${currentMonth}-${String(day).padStart(2, '0')}`; + const holiday = holidays.find(h => h.pdate === dateStr); + + columns.push({ + day, + dayOfWeek, + isHoliday: holiday?.free || false, + holidayMemo: holiday?.memo + }); + } + setDayColumns(columns); + + // 사용자별 데이터 집계 + const userMap = new Map(); + + items.forEach(item => { + const uid = item.id || item.uid; + const uname = item.name || item.uname || uid; + const pdate = item.pdate?.substring(0, 10); + + if (!pdate || !pdate.startsWith(currentMonth)) return; + + // 권한 체크: 권한 레벨이 5 미만이고 본인이 아니면 스킵 + if (!canViewAll && uid !== currentUserId) return; + + const day = parseInt(pdate.substring(8, 10)); + + if (!userMap.has(uid)) { + userMap.set(uid, { + uid, + uname, + processs: item.userprocess || item.process || '', + dailyData: new Map(), + totalHrs: 0, + totalOt: 0, + totalHolidayOt: 0 + }); + } + + const userRow = userMap.get(uid)!; + const existing = userRow.dailyData.get(day); + const hrs = (existing?.hrs || 0) + (item.hrs || 0); + const ot = (existing?.ot || 0) + (item.ot || 0); + const jobtype = item.type || existing?.jobtype || ''; + + userRow.dailyData.set(day, { hrs, ot, jobtype }); + userRow.totalHrs += item.hrs || 0; + + // 휴일 OT 계산 + const column = columns[day - 1]; + if (column?.isHoliday) { + userRow.totalHolidayOt += item.ot || 0; + } else { + userRow.totalOt += item.ot || 0; + } + }); + + setUserRows(Array.from(userMap.values()).sort((a, b) => a.uname.localeCompare(b.uname))); + }; + + // 월 변경 + const changeMonth = (delta: number) => { + const [year, month] = currentMonth.split('-').map(Number); + const newDate = new Date(year, month - 1 + delta, 1); + setCurrentMonth(`${newDate.getFullYear()}-${String(newDate.getMonth() + 1).padStart(2, '0')}`); + }; + + // 셀 색상 결정 + const getCellStyle = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => { + if (!data) return 'text-gray-400'; + + if (data.jobtype === '휴가') return 'text-red-500 font-medium'; + if (isHoliday) return 'text-green-500 font-medium'; + + if (data.hrs > 8) return 'text-blue-500 font-medium'; + if (data.hrs < 8) return 'text-red-500'; + if (data.ot > 0) return 'text-purple-500 font-medium'; + + return 'text-white'; + }; + + // 셀 내용 포맷 + const formatCellContent = (data: { hrs: number; ot: number; jobtype: string } | undefined, isHoliday: boolean) => { + if (!data) return '--'; + + if (data.jobtype === '휴가' && data.hrs + data.ot === 8) return '휴가'; + + const prefix = isHoliday ? '*' : ''; + return `${prefix}${data.hrs}+${data.ot}`; + }; + + // 엑셀 내보내기 (간단한 CSV) + const exportToExcel = () => { + let csv = '사원명,' + dayColumns.map(c => `${c.day}(${c.dayOfWeek})`).join(',') + ',합계\n'; + + userRows.forEach(row => { + const cells = dayColumns.map(col => { + const data = row.dailyData.get(col.day); + return formatCellContent(data, col.isHoliday); + }); + const summary = `${row.totalHrs}+${row.totalOt}(*${row.totalHolidayOt})`; + csv += `${row.uname},${cells.join(',')},${summary}\n`; + }); + + const blob = new Blob(['\uFEFF' + csv], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + link.href = URL.createObjectURL(blob); + link.download = `업무일지_일별집계_${currentMonth}.csv`; + link.click(); + }; + + if (!isOpen) return null; + + return ( +
+
+ {/* 헤더 */} +
+
+

일별 근무시간 집계

+
+ + + {currentMonth} + + +
+
+
+ + +
+
+ + {/* 테이블 */} +
+ {loading ? ( +
+
데이터를 불러오는 중...
+
+ ) : ( + + + + + {dayColumns.map(col => ( + + ))} + + + + + {userRows.length === 0 ? ( + + + + ) : ( + userRows.map(row => ( + + + {dayColumns.map(col => { + const data = row.dailyData.get(col.day); + return ( + + ); + })} + + + )) + )} + +
+ 사원명 + + {col.day}
({col.dayOfWeek}) +
+ 합계 +
+ 조회된 데이터가 없습니다. +
+ {row.uname} + + {formatCellContent(data, col.isHoliday)} + + {row.totalHrs}+{row.totalOt}(*{row.totalHolidayOt}) +
+ )} +
+ + {/* 범례 */} +
+
+
-- : 자료없음
+
휴가 : 휴가
+
*8+2 : 휴일근무
+
9+0 : 8시간 초과
+
7+0 : 8시간 미만
+
8+2 : 8시간+OT
+
+
+
+
+ ); +} diff --git a/Project/frontend/src/components/jobreport/JobreportEditModal.tsx b/Project/frontend/src/components/jobreport/JobreportEditModal.tsx index 5372486..7ac1db7 100644 --- a/Project/frontend/src/components/jobreport/JobreportEditModal.tsx +++ b/Project/frontend/src/components/jobreport/JobreportEditModal.tsx @@ -18,6 +18,8 @@ export interface JobreportFormData { description: string; hrs: number; ot: number; + otStart: string; // 초과근무 시작시간 (HH:mm 형식) + otEnd: string; // 초과근무 종료시간 (HH:mm 형식) jobgrp: string; tag: string; } @@ -39,6 +41,8 @@ export const initialFormData: JobreportFormData = { description: '', hrs: 8, ot: 0, + otStart: '18:00', + otEnd: '20:00', jobgrp: '', tag: '', }; @@ -339,11 +343,10 @@ export function JobreportEditModal({ + + + {/* 컨텐츠 */} +
+
+ 기간: {startDate} ~ {endDate} +
+ +
+ + + + + + + + + + + {loading ? ( + + + + ) : items.length === 0 ? ( + + + + ) : ( + <> + {items.map((item, index) => ( + + + + + + + ))} + {/* 합계 */} + + + + + + + + )} + +
업무형태건수시간OT
+
+ + 데이터를 불러오는 중... +
+
+ 데이터가 없습니다. +
{item.type || '-'}{item.count}{item.hrs}h + {item.ot > 0 ? {item.ot}h : '-'} +
합계 + {items.reduce((acc, item) => acc + item.count, 0)} + {totalHrs}h + {totalOt > 0 ? {totalOt}h : '-'} +
+
+
+ + {/* 푸터 */} +
+ +
+ + + ); +} diff --git a/Project/frontend/src/components/kuntae/KuntaeEditModal.tsx b/Project/frontend/src/components/kuntae/KuntaeEditModal.tsx new file mode 100644 index 0000000..2b60e48 --- /dev/null +++ b/Project/frontend/src/components/kuntae/KuntaeEditModal.tsx @@ -0,0 +1,295 @@ +import React, { useState, useEffect } from 'react'; +import { X, Save, Calendar, Clock, FileText, User } from 'lucide-react'; +import { KuntaeModel } from '@/types'; + +interface KuntaeEditModalProps { + isOpen: boolean; + onClose: () => void; + onSave: (data: KuntaeFormData) => Promise; + initialData?: KuntaeModel | null; + mode: 'add' | 'edit' | 'copy'; +} + +export interface KuntaeFormData { + idx?: number; + cate: string; + sdate: string; + edate: string; + term: number; + crtime: number; + termDr: number; + drTime: number; + contents: string; + uid: string; +} + +const CATE_OPTIONS = ['연차', '대체', '공가', '경조', '병가', '오전반차', '오후반차', '조퇴', '외출', '지각', '결근', '휴직', '교육', '출장', '재택', '특근', '당직', '기타']; + +export function KuntaeEditModal({ isOpen, onClose, onSave, initialData, mode }: KuntaeEditModalProps) { + const [formData, setFormData] = useState({ + cate: '연차', + sdate: new Date().toISOString().split('T')[0], + edate: new Date().toISOString().split('T')[0], + term: 1, + crtime: 0, + termDr: 0, + drTime: 0, + contents: '', + uid: '', + }); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (isOpen) { + if (initialData) { + setFormData({ + idx: mode === 'edit' ? initialData.idx : undefined, + cate: initialData.cate || '연차', + sdate: initialData.sdate || new Date().toISOString().split('T')[0], + edate: initialData.edate || new Date().toISOString().split('T')[0], + term: initialData.term || 0, + crtime: initialData.crtime || 0, + termDr: initialData.termDr || 0, + drTime: initialData.DrTime || 0, + contents: initialData.contents || '', + uid: initialData.uid || '', + }); + } else { + // 초기화 + setFormData({ + cate: '연차', + sdate: new Date().toISOString().split('T')[0], + edate: new Date().toISOString().split('T')[0], + term: 1, + crtime: 0, + termDr: 0, + drTime: 0, + contents: '', + uid: '', // 상위 컴포넌트에서 현재 사용자 ID를 주입받거나 여기서 처리해야 함 + }); + } + } + }, [isOpen, initialData, mode]); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: name === 'term' || name === 'crtime' || name === 'termDr' || name === 'drTime' + ? parseFloat(value) || 0 + : value + })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setSaving(true); + try { + await onSave(formData); + onClose(); + } catch (error) { + console.error('Save error:', error); + alert('저장 중 오류가 발생했습니다.'); + } finally { + setSaving(false); + } + }; + + // 날짜 변경 시 기간 자동 계산 (단순 1일 차이) + useEffect(() => { + if (formData.sdate && formData.edate) { + const start = new Date(formData.sdate); + const end = new Date(formData.edate); + const diffTime = Math.abs(end.getTime() - start.getTime()); + const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; + + // 연차/휴가 등일 때만 자동 계산 + if (['연차', '공가', '경조', '병가', '교육', '출장'].includes(formData.cate)) { + setFormData(prev => ({ ...prev, term: diffDays })); + } + } + }, [formData.sdate, formData.edate, formData.cate]); + + if (!isOpen) return null; + + const title = mode === 'add' ? '근태 등록' : mode === 'edit' ? '근태 수정' : '근태 복사 등록'; + + return ( +
+
+ {/* 헤더 */} +
+

+ + {title} +

+ +
+ + {/* 폼 */} +
+ + {/* 구분 및 사용자 */} +
+
+ + +
+
+ +
+ + +
+
+
+ + {/* 기간 */} +
+
+ + +
+
+ + +
+
+ + {/* 사용량 (일/시간) */} +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + {/* 발생량 (일/시간) - 대체근무 등 발생 시 */} +
+
+ + +
+
+ + +
+
+ + {/* 내용 */} +
+ +
+ +