diff --git a/Project/Dialog/fDashboardNew.Designer.cs b/Project/Dialog/fDashboard.Designer.cs similarity index 98% rename from Project/Dialog/fDashboardNew.Designer.cs rename to Project/Dialog/fDashboard.Designer.cs index 77bec37..8191e2d 100644 --- a/Project/Dialog/fDashboardNew.Designer.cs +++ b/Project/Dialog/fDashboard.Designer.cs @@ -1,6 +1,6 @@ namespace Project.Dialog { - partial class fDashboardNew + partial class fDashboard { /// /// Required designer variable. diff --git a/Project/Dialog/fDashboardNew.cs b/Project/Dialog/fDashboard.cs similarity index 77% rename from Project/Dialog/fDashboardNew.cs rename to Project/Dialog/fDashboard.cs index 031f011..67a62fa 100644 --- a/Project/Dialog/fDashboardNew.cs +++ b/Project/Dialog/fDashboard.cs @@ -15,26 +15,18 @@ using System.Windows.Forms; namespace Project.Dialog { - public partial class fDashboardNew : fBase + public partial class fDashboard : fBase { - private Web.WebSocketServer _wsServer; private WebView2 webView; - public fDashboardNew() + private Web.MachineBridge _machineBridge; + + public fDashboard() { InitializeComponent(); - - InitializeWebView2(); - - try - { - _wsServer = new Web.WebSocketServer("http://localhost:8081/", this); - } - catch (Exception ex) - { - MessageBox.Show("Failed to start WebSocket Server (Port 8081). Run as Admin or allow port.\n" + ex.Message); - } + _machineBridge = new Web.MachineBridge(this); } + bool loadok = false; public void RefreshView() { @@ -80,7 +72,7 @@ namespace Project.Dialog await this.webView.EnsureCoreWebView2Async(); } - var wwwroot = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot"); + var wwwroot = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "dist"); webView.CoreWebView2.SetVirtualHostNameToFolderMapping( "hmi.local", wwwroot, @@ -88,16 +80,13 @@ namespace Project.Dialog // 2. Inject Native Object - webView.CoreWebView2.AddHostObjectToScript("machine", new Web.MachineBridge(this)); + webView.CoreWebView2.AddHostObjectToScript("machine", _machineBridge); Pub.WebServiceURL = "http://hmi.local"; - // OWIN 서버의 DashBoard 페이지로 연결 - if (FCOMMON.info.Login.no.isEmpty()) - webView.Source = new Uri($"{Pub.WebServiceURL}/login.html"); - else - webView.Source = new Uri($"{Pub.WebServiceURL}/DashBoard"); + + RefreshPage(); label1.Visible = false; loadok = true; } @@ -106,6 +95,18 @@ namespace Project.Dialog MessageBox.Show($"WebView2 초기화 실패: {ex.Message}"); } } + + /// + /// 로그인 상태에 따라서 페이지를 전환한다 + /// + public void RefreshPage() + { + webView.Source = new Uri($"{Pub.WebServiceURL}/index.html"); + //if (FCOMMON.info.Login.no.isEmpty()) + //webView.Source = new Uri($"{Pub.WebServiceURL}/login.html"); + // else + // webView.Source = new Uri($"{Pub.WebServiceURL}/DashBoard/index.html"); + } protected override void OnLoad(EventArgs e) { base.OnLoad(e); diff --git a/Project/Dialog/fDashboardNew.resx b/Project/Dialog/fDashboard.resx similarity index 100% rename from Project/Dialog/fDashboardNew.resx rename to Project/Dialog/fDashboard.resx diff --git a/Project/Dialog/fLogin.cs b/Project/Dialog/fLogin.cs index 6bd1d41..c0d475d 100644 --- a/Project/Dialog/fLogin.cs +++ b/Project/Dialog/fLogin.cs @@ -192,24 +192,24 @@ namespace Project.Dialog } } - FCOMMON.info.Login.no = drUser.id; - FCOMMON.info.Login.nameK = drUser.name; - FCOMMON.info.Login.dept = cmbDept.Text;// userdr.dept;// cmbDept.Text; - FCOMMON.info.Login.level = drGrpUser.level; - FCOMMON.info.Login.email = drUser.email; - FCOMMON.info.Login.nameE = drUser.nameE; - FCOMMON.info.Login.hp = drUser.hp; - FCOMMON.info.Login.tel = drUser.tel; - FCOMMON.info.Login.title = drUser.dept + "(" + drUser.grade + ")"; - FCOMMON.info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView; + info.Login.no = drUser.id; + info.Login.nameK = drUser.name; + info.Login.dept = cmbDept.Text;// userdr.dept;// cmbDept.Text; + info.Login.level = drGrpUser.level; + info.Login.email = drUser.email; + info.Login.nameE = drUser.nameE; + info.Login.hp = drUser.hp; + info.Login.tel = drUser.tel; + info.Login.title = drUser.dept + "(" + drUser.grade + ")"; + info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView; //var gcode = FCOMMON.DBM.ExecuteScalar("select isnull(gcode,'NOGCODE') from UserGroup where dept ='" + cmbDept.Text + "'"); var gperm = FCOMMON.DBM.ExecuteScalar("select isnull(permission,0) from UserGroup where dept ='" + cmbDept.Text + "'"); - FCOMMON.info.Login.gcode = gCode;// gcode; - FCOMMON.info.Login.process = drUser.id == "dev" ? "개발자" : drGrpUser.Process; - FCOMMON.info.Login.permission = 0; - FCOMMON.info.Login.gpermission = int.Parse(gperm); - //FCOMMON.info.datapath = Pub.setting.SharedDataPath; - FCOMMON.info.ShowBuyerror = Pub.setting.Showbuyerror; //210625 + info.Login.gcode = gCode;// gcode; + info.Login.process = drUser.id == "dev" ? "개발자" : drGrpUser.Process; + info.Login.permission = 0; + info.Login.gpermission = int.Parse(gperm); + //info.datapath = Pub.setting.SharedDataPath; + info.ShowBuyerror = Pub.setting.Showbuyerror; //210625 @@ -220,34 +220,34 @@ namespace Project.Dialog { return; } - FCOMMON.info.Login.no = "dev"; - FCOMMON.info.Login.nameK = "개발자"; - FCOMMON.info.Login.dept = cmbDept.Text;// userdr.dept;// cmbDept.Text; - FCOMMON.info.Login.level = 10; - FCOMMON.info.Login.email = ""; - FCOMMON.info.Login.nameE = "DEVELOPER"; - FCOMMON.info.Login.hp = ""; - FCOMMON.info.Login.tel = ""; - FCOMMON.info.Login.title = "업무일지 개발자"; - FCOMMON.info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView; + info.Login.no = "dev"; + info.Login.nameK = "개발자"; + info.Login.dept = cmbDept.Text;// userdr.dept;// cmbDept.Text; + info.Login.level = 10; + info.Login.email = ""; + info.Login.nameE = "DEVELOPER"; + info.Login.hp = ""; + info.Login.tel = ""; + info.Login.title = "업무일지 개발자"; + info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView; //var gcode = FCOMMON.DBM.ExecuteScalar("select isnull(gcode,'NOGCODE') from UserGroup where dept ='" + cmbDept.Text + "'"); var gperm = FCOMMON.DBM.ExecuteScalar("select isnull(permission,0) from UserGroup where dept ='" + cmbDept.Text + "'"); - FCOMMON.info.Login.gcode = gCode; - FCOMMON.info.Login.process = "개발자"; - FCOMMON.info.Login.permission = 0; - FCOMMON.info.Login.gpermission = int.Parse(gperm); + info.Login.gcode = gCode; + info.Login.process = "개발자"; + info.Login.permission = 0; + info.Login.gpermission = int.Parse(gperm); //var datapath = FCOMMON.DBM.getCodeSavlue("55", "01"); - //FCOMMON.info.datapath = datapath;// Pub.setting.SharedDataPath; - FCOMMON.info.ShowBuyerror = Pub.setting.Showbuyerror; //210625 + //info.datapath = datapath;// Pub.setting.SharedDataPath; + info.ShowBuyerror = Pub.setting.Showbuyerror; //210625 } - //if (FCOMMON.info.datapath.isEmpty() && gCode == "EET1P") //210524 - // FCOMMON.info.datapath = @"\\k4fs3201n\k4bpartcenter$"; + //if (info.datapath.isEmpty() && gCode == "EET1P") //210524 + // info.datapath = @"\\k4fs3201n\k4bpartcenter$"; //using (var dbEnity = new EEEntitiesMain()) //{ // var drGrpUser = dbEnity.EETGW_GroupUser.Where(t => t.uid == userdr.id & t.gcode == gCode).FirstOrDefault(); - // if (drGrpUser == null) FCOMMON.info.Login.process = (userdr.id == "dev" ? "개발자" : ""); - // else FCOMMON.info.Login.process = drGrpUser.Process; + // if (drGrpUser == null) info.Login.process = (userdr.id == "dev" ? "개발자" : ""); + // else info.Login.process = drGrpUser.Process; //} //로그인정보 기록 @@ -260,7 +260,7 @@ namespace Project.Dialog Pub.MakeAutoJobReportByAuto(); DialogResult = DialogResult.OK; - FCOMMON.info.Login.loginusetime = (DateTime.Now - dt).TotalMilliseconds; + info.Login.loginusetime = (DateTime.Now - dt).TotalMilliseconds; } catch (Exception ex) { @@ -293,7 +293,7 @@ namespace Project.Dialog try { var ta = new dsMSSQLTableAdapters.EETGW_LoginInfoTableAdapter(); - ta.Insert(FCOMMON.info.Login.no, DateTime.Now, ip, fullname, info.Login.no, DateTime.Now); + ta.Insert(info.Login.no, DateTime.Now, ip, fullname, info.Login.no, DateTime.Now); } catch (Exception ex) { @@ -336,9 +336,9 @@ namespace Project.Dialog //} var gCode = this.cmbDept.SelectedValue.ToString();// as dsMSSQL.UserGroupRow; - FCOMMON.info.Login.gcode = gCode; - FCOMMON.info.Login.no = "new"; - FCOMMON.info.Login.dept = this.cmbDept.Text; + info.Login.gcode = gCode; + info.Login.no = "new"; + info.Login.dept = this.cmbDept.Text; var dlg = FCOMMON.Util.MsgQ($"현재 선택된 그룹[{this.cmbDept.Text}]의 사용자를 추가할까요?\n" + "추가된 사용자는 담당자로부터 승인 완료되어야 접속이 가능 합니다\n" + diff --git a/Project/EETGW.csproj b/Project/EETGW.csproj index 1ee4a2d..bfc3b5b 100644 --- a/Project/EETGW.csproj +++ b/Project/EETGW.csproj @@ -220,11 +220,11 @@ - + Form - - fDashboardNew.cs + + fDashboard.cs Form @@ -358,6 +358,12 @@ + + + + + + @@ -365,7 +371,6 @@ - Form @@ -426,8 +431,8 @@ fDisableItem.cs - - fDashboardNew.cs + + fDashboard.cs fDebug.cs @@ -732,6 +737,9 @@ + + xcopy /E /I /Y "$(ProjectDir)frontend\dist\*" "$(TargetDir)Web\Dist\" + 이 프로젝트는 이 컴퓨터에 없는 NuGet 패키지를 참조합니다. 해당 패키지를 다운로드하려면 NuGet 패키지 복원을 사용하십시오. 자세한 내용은 http://go.microsoft.com/fwlink/?LinkID=322105를 참조하십시오. 누락된 파일은 {0}입니다. diff --git a/Project/Web/MachineBridge/MachineBridge.Common.cs b/Project/Web/MachineBridge/MachineBridge.Common.cs index c6fbb17..d3e4347 100644 --- a/Project/Web/MachineBridge/MachineBridge.Common.cs +++ b/Project/Web/MachineBridge/MachineBridge.Common.cs @@ -229,5 +229,198 @@ namespace Project.Web } #endregion + + #region Items API + + /// + /// 품목 카테고리 목록 조회 + /// + public string Items_GetCategories() + { + try + { + var sql = "SELECT DISTINCT cate FROM Items WITH (NOLOCK) WHERE gcode = @gcode AND ISNULL(cate,'') <> '' ORDER BY cate"; + + 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(dr["cate"]?.ToString() ?? ""); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "카테고리 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목 목록 조회 + /// + public string Items_GetList(string category, string searchKey) + { + try + { + var cateSearch = string.IsNullOrEmpty(category) || category == "all" ? "%" : category; + var skey = string.IsNullOrEmpty(searchKey) || searchKey == "%" ? "%" : $"%{searchKey}%"; + + var sql = @"SELECT idx, sid, cate, name, model, scale, unit, price, supply, manu, storage, disable, memo + FROM Items WITH (NOLOCK) + WHERE gcode = @gcode + AND ISNULL(cate,'') LIKE @cate + AND (ISNULL(sid,'') LIKE @search OR ISNULL(name,'') LIKE @search OR ISNULL(model,'') LIKE @search) + ORDER BY sid, name"; + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var cmd = new SqlCommand(sql, cn); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@cate", cateSearch); + cmd.Parameters.AddWithValue("@search", skey); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + da.Dispose(); + cmd.Dispose(); + cn.Dispose(); + + return JsonConvert.SerializeObject(dt, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "품목 조회 실패: " + ex.Message }); + } + } + + /// + /// 품목 저장 + /// + public string Items_Save(int idx, string sid, string cate, string name, string model, + string scale, string unit, decimal price, string supply, string manu, string storage, bool disable, string memo) + { + try + { + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var sql = string.Empty; + var cmd = new SqlCommand(); + cmd.Connection = cn; + + // 신규 추가 시 SID 중복 체크 + if (idx == 0 && !string.IsNullOrEmpty(sid)) + { + var checkSql = "SELECT COUNT(*) FROM Items WITH (NOLOCK) WHERE gcode = @gcode AND sid = @sid"; + var checkCmd = new SqlCommand(checkSql, cn); + checkCmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + checkCmd.Parameters.AddWithValue("@sid", sid); + + cn.Open(); + var count = (int)checkCmd.ExecuteScalar(); + cn.Close(); + checkCmd.Dispose(); + + if (count > 0) + { + cn.Dispose(); + return JsonConvert.SerializeObject(new { Success = false, Message = $"이미 존재하는 SID입니다: {sid}" }); + } + } + + if (idx > 0) + { + sql = @"UPDATE Items SET + sid = @sid, cate = @cate, name = @name, model = @model, + scale = @scale, unit = @unit, price = @price, supply = @supply, + manu = @manu, storage = @storage, disable = @disable, memo = @memo, + wuid = @wuid, wdate = GETDATE() + WHERE idx = @idx AND gcode = @gcode"; + } + else + { + sql = @"INSERT INTO Items (gcode, sid, cate, name, model, scale, unit, price, supply, manu, storage, disable, memo, wuid, wdate) + VALUES (@gcode, @sid, @cate, @name, @model, @scale, @unit, @price, @supply, @manu, @storage, @disable, @memo, @wuid, GETDATE())"; + } + + cmd.CommandText = sql; + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@sid", sid ?? ""); + cmd.Parameters.AddWithValue("@cate", cate ?? ""); + cmd.Parameters.AddWithValue("@name", name ?? ""); + cmd.Parameters.AddWithValue("@model", model ?? ""); + cmd.Parameters.AddWithValue("@scale", scale ?? ""); + cmd.Parameters.AddWithValue("@unit", unit ?? ""); + cmd.Parameters.AddWithValue("@price", price); + cmd.Parameters.AddWithValue("@supply", supply ?? ""); + cmd.Parameters.AddWithValue("@manu", manu ?? ""); + cmd.Parameters.AddWithValue("@storage", storage ?? ""); + cmd.Parameters.AddWithValue("@disable", disable); + cmd.Parameters.AddWithValue("@memo", memo ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + + if (idx > 0) + { + cmd.Parameters.AddWithValue("@idx", idx); + } + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + cn.Close(); + + cmd.Dispose(); + cn.Dispose(); + + 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_Delete(int idx) + { + try + { + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var sql = "DELETE FROM Items WHERE idx = @idx AND gcode = @gcode"; + var cmd = new SqlCommand(sql, cn); + + cmd.Parameters.AddWithValue("@idx", idx); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + cn.Close(); + + cmd.Dispose(); + cn.Dispose(); + + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "삭제 실패: " + ex.Message }); + } + } + + #endregion } } diff --git a/Project/Web/MachineBridge/MachineBridge.Holiday.cs b/Project/Web/MachineBridge/MachineBridge.Holiday.cs new file mode 100644 index 0000000..b17be2f --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.Holiday.cs @@ -0,0 +1,177 @@ +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 Holiday API (월별근무표) + + /// + /// 월별근무표 목록 조회 + /// + public string Holiday_GetList(string month) + { + try + { + var monthPattern = month + "%"; + var sql = @"SELECT idx, pdate, free, memo, wuid, wdate + FROM HolidayList WITH (nolock) + WHERE pdate LIKE @month + ORDER BY pdate"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@month", monthPattern); + 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 Holiday_Save(string month, string holidaysJson) + { + try + { + var holidays = JsonConvert.DeserializeObject>(holidaysJson); + var cs = Properties.Settings.Default.gwcs; + + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + using (var tran = cn.BeginTransaction()) + { + try + { + foreach (var item in holidays) + { + var sql = @" + IF EXISTS (SELECT 1 FROM HolidayList WHERE pdate = @pdate) + UPDATE HolidayList SET free = @free, memo = @memo, wuid = @wuid, wdate = GETDATE() WHERE pdate = @pdate + ELSE + INSERT INTO HolidayList (pdate, free, memo, wuid, wdate) VALUES (@pdate, @free, @memo, @wuid, GETDATE())"; + + using (var cmd = new SqlCommand(sql, cn, tran)) + { + cmd.Parameters.AddWithValue("@pdate", item.pdate); + cmd.Parameters.AddWithValue("@free", item.free); + cmd.Parameters.AddWithValue("@memo", item.memo ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + cmd.ExecuteNonQuery(); + } + } + tran.Commit(); + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다." }); + } + catch + { + tran.Rollback(); + throw; + } + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 월별근무표 초기 데이터 생성 (해당 월에 데이터가 없을 때) + /// + public string Holiday_Initialize(string month) + { + try + { + // 해당 월 데이터 존재 여부 확인 + var monthPattern = month + "%"; + var checkSql = "SELECT COUNT(*) FROM HolidayList WITH (nolock) WHERE pdate LIKE @month"; + var cs = Properties.Settings.Default.gwcs; + + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + + using (var checkCmd = new SqlCommand(checkSql, cn)) + { + checkCmd.Parameters.AddWithValue("@month", monthPattern); + var count = (int)checkCmd.ExecuteScalar(); + + if (count > 0) + { + return JsonConvert.SerializeObject(new { Success = true, Message = "이미 데이터가 존재합니다.", Created = false }); + } + } + + // 해당 월의 모든 날짜 생성 + var startDate = DateTime.Parse(month + "-01"); + var endDate = startDate.AddMonths(1).AddDays(-1); + + using (var tran = cn.BeginTransaction()) + { + try + { + for (var date = startDate; date <= endDate; date = date.AddDays(1)) + { + var isFree = date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday; + var memo = date.DayOfWeek == DayOfWeek.Saturday ? "토요일" : + date.DayOfWeek == DayOfWeek.Sunday ? "일요일" : ""; + + var sql = @"INSERT INTO HolidayList (pdate, free, memo, wuid, wdate) + VALUES (@pdate, @free, @memo, @wuid, GETDATE())"; + + using (var cmd = new SqlCommand(sql, cn, tran)) + { + cmd.Parameters.AddWithValue("@pdate", date.ToString("yyyy-MM-dd")); + cmd.Parameters.AddWithValue("@free", isFree); + cmd.Parameters.AddWithValue("@memo", memo); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + cmd.ExecuteNonQuery(); + } + } + tran.Commit(); + return JsonConvert.SerializeObject(new { Success = true, Message = "초기 데이터가 생성되었습니다.", Created = true }); + } + catch + { + tran.Rollback(); + throw; + } + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + } + + public class HolidayItem + { + public int idx { get; set; } + public string pdate { get; set; } + public bool free { get; set; } + public string memo { get; set; } + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.Jobreport.cs b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs index 088843c..ede9341 100644 --- a/Project/Web/MachineBridge/MachineBridge.Jobreport.cs +++ b/Project/Web/MachineBridge/MachineBridge.Jobreport.cs @@ -12,62 +12,47 @@ namespace Project.Web #region Jobreport API /// - /// 업무일지 목록 조회 + /// 업무일지 목록 조회 (vJobReportForUser 뷰 사용) /// - public string Jobreport_GetList(string sd, string ed, string uid, string cate, string doit) + public string Jobreport_GetList(string sd, string ed, string uid, string cate, string searchKey) { try { - var sql = @"SELECT j.idx, j.jdate, j.uid, j.cate, j.title, j.doit, j.remark, j.jfrom, j.jto, - u.name as userName, j.wdate - FROM EETGW_Jobreport j WITH (nolock) - LEFT JOIN Users u ON j.uid = u.id - WHERE j.gcode = @gcode"; + 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 gcode = @gcode AND (pdate BETWEEN @sd AND @ed)"; var parameters = new List(); parameters.Add(new SqlParameter("@gcode", info.Login.gcode)); + parameters.Add(new SqlParameter("@sd", sd)); + parameters.Add(new SqlParameter("@ed", ed)); - if (!string.IsNullOrEmpty(sd)) - { - sql += " AND j.jdate >= @sd"; - parameters.Add(new SqlParameter("@sd", sd)); - } - if (!string.IsNullOrEmpty(ed)) - { - sql += " AND j.jdate <= @ed"; - parameters.Add(new SqlParameter("@ed", ed)); - } if (!string.IsNullOrEmpty(uid)) { - sql += " AND j.uid = @uid"; + sql += " AND id = @uid"; parameters.Add(new SqlParameter("@uid", uid)); } - if (!string.IsNullOrEmpty(cate)) + + if (!string.IsNullOrEmpty(searchKey)) { - sql += " AND j.cate = @cate"; - parameters.Add(new SqlParameter("@cate", cate)); - } - if (!string.IsNullOrEmpty(doit)) - { - sql += " AND j.doit = @doit"; - parameters.Add(new SqlParameter("@doit", doit)); + sql += " AND (requestpart LIKE @searchKey OR package LIKE @searchKey OR projectName LIKE @searchKey OR process LIKE @searchKey OR [type] LIKE @searchKey OR description LIKE @searchKey)"; + parameters.Add(new SqlParameter("@searchKey", "%" + searchKey + "%")); } - sql += " ORDER BY j.jdate DESC, j.idx DESC"; + sql += " ORDER BY pdate DESC, idx DESC"; var cs = Properties.Settings.Default.gwcs; - var cn = new SqlConnection(cs); - var cmd = new SqlCommand(sql, cn); - cmd.Parameters.AddRange(parameters.ToArray()); - - var da = new SqlDataAdapter(cmd); - var dt = new DataTable(); - da.Fill(dt); - da.Dispose(); - cmd.Dispose(); - cn.Dispose(); - - return JsonConvert.SerializeObject(new { Success = true, Data = dt }, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + 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) { @@ -88,18 +73,17 @@ namespace Project.Web ORDER BY u.name"; 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(); - - return JsonConvert.SerializeObject(dt, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + 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(dt); + } + } } catch (Exception ex) { @@ -109,36 +93,40 @@ namespace Project.Web } /// - /// 업무일지 상세 조회 + /// 업무일지 상세 조회 (vJobReportForUser 뷰 사용) /// public string Jobreport_GetDetail(int id) { try { - var sql = @"SELECT j.*, u.name as userName - FROM EETGW_Jobreport j WITH (nolock) - LEFT JOIN Users u ON j.uid = u.id - WHERE j.idx = @idx AND j.gcode = @gcode"; + 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"; var cs = Properties.Settings.Default.gwcs; - var cn = new SqlConnection(cs); - var cmd = new SqlCommand(sql, cn); - cmd.Parameters.AddWithValue("@idx", id); - 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(); - - if (dt.Rows.Count > 0) + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) { - return JsonConvert.SerializeObject(new { Success = true, Data = dt.Rows[0] }, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + cmd.Parameters.AddWithValue("@idx", id); + 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 = "데이터를 찾을 수 없습니다." }); + } } - - return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." }); } catch (Exception ex) { @@ -147,37 +135,50 @@ namespace Project.Web } /// - /// 업무일지 추가 + /// 업무일지 추가 (JobReport 테이블) /// - public string Jobreport_Add(string jdate, string cate, string title, string doit, string remark, string jfrom, string jto) + public string Jobreport_Add(string pdate, string projectName, string requestpart, string package, + string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag) { try { - var sql = @"INSERT INTO EETGW_Jobreport (gcode, uid, jdate, cate, title, doit, remark, jfrom, jto, wuid, wdate) - VALUES (@gcode, @uid, @jdate, @cate, @title, @doit, @remark, @jfrom, @jto, @wuid, GETDATE()); + // 마감 체크 + var smon = pdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + + 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); SELECT SCOPE_IDENTITY();"; var cs = Properties.Settings.Default.gwcs; - var cn = new SqlConnection(cs); - var cmd = new SqlCommand(sql, cn); - cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); - cmd.Parameters.AddWithValue("@uid", info.Login.no); - cmd.Parameters.AddWithValue("@jdate", jdate ?? DateTime.Now.ToString("yyyy-MM-dd")); - cmd.Parameters.AddWithValue("@cate", cate ?? ""); - cmd.Parameters.AddWithValue("@title", title ?? ""); - cmd.Parameters.AddWithValue("@doit", doit ?? ""); - cmd.Parameters.AddWithValue("@remark", remark ?? ""); - cmd.Parameters.AddWithValue("@jfrom", jfrom ?? ""); - cmd.Parameters.AddWithValue("@jto", jto ?? ""); - cmd.Parameters.AddWithValue("@wuid", info.Login.no); + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@uid", info.Login.no); + cmd.Parameters.AddWithValue("@pdate", pdate); + cmd.Parameters.AddWithValue("@projectName", projectName ?? ""); + cmd.Parameters.AddWithValue("@requestpart", requestpart ?? ""); + cmd.Parameters.AddWithValue("@package", package ?? ""); + cmd.Parameters.AddWithValue("@type", type ?? ""); + cmd.Parameters.AddWithValue("@process", process ?? ""); + cmd.Parameters.AddWithValue("@status", status ?? "진행 완료"); + cmd.Parameters.AddWithValue("@description", description ?? ""); + cmd.Parameters.AddWithValue("@hrs", hrs); + cmd.Parameters.AddWithValue("@ot", ot); + cmd.Parameters.AddWithValue("@jobgrp", jobgrp ?? ""); + cmd.Parameters.AddWithValue("@tag", tag ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); - cn.Open(); - var newId = Convert.ToInt32(cmd.ExecuteScalar()); - cn.Close(); - cmd.Dispose(); - cn.Dispose(); - - return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } }); + cn.Open(); + var newId = Convert.ToInt32(cmd.ExecuteScalar()); + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다.", Data = new { idx = newId } }); + } } catch (Exception ex) { @@ -186,39 +187,73 @@ namespace Project.Web } /// - /// 업무일지 수정 + /// 업무일지 수정 (JobReport 테이블) /// - public string Jobreport_Edit(int idx, string jdate, string cate, string title, string doit, string remark, string jfrom, string jto) + public string Jobreport_Edit(int idx, string pdate, string projectName, string requestpart, string package, + string type, string process, string status, string description, double hrs, double ot, string jobgrp, string tag) { try { - var sql = @"UPDATE EETGW_Jobreport SET - jdate = @jdate, cate = @cate, title = @title, doit = @doit, - remark = @remark, jfrom = @jfrom, jto = @jto, + // 권한 체크 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + + // 마감 체크 + var smon = pdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + + // 본인 자료인지 체크 (관리자가 아닌 경우) + if (curLevel < 5) + { + var checkSql = "SELECT uid FROM JobReport WHERE idx = @idx AND gcode = @gcode"; + var cs2 = Properties.Settings.Default.gwcs; + using (var cn2 = new SqlConnection(cs2)) + using (var cmd2 = new SqlCommand(checkSql, cn2)) + { + cmd2.Parameters.AddWithValue("@idx", idx); + cmd2.Parameters.AddWithValue("@gcode", info.Login.gcode); + cn2.Open(); + var ownerUid = cmd2.ExecuteScalar()?.ToString(); + if (ownerUid != info.Login.no) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 수정할 수 없습니다." }); + } + } + } + + var sql = @"UPDATE JobReport SET + pdate = @pdate, projectName = @projectName, requestpart = @requestpart, + package = @package, type = @type, process = @process, status = @status, + description = @description, hrs = @hrs, ot = @ot, jobgrp = @jobgrp, tag = @tag, wuid = @wuid, wdate = GETDATE() WHERE idx = @idx AND gcode = @gcode"; var cs = Properties.Settings.Default.gwcs; - var cn = new SqlConnection(cs); - var cmd = new SqlCommand(sql, cn); - cmd.Parameters.AddWithValue("@idx", idx); - cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); - cmd.Parameters.AddWithValue("@jdate", jdate ?? DateTime.Now.ToString("yyyy-MM-dd")); - cmd.Parameters.AddWithValue("@cate", cate ?? ""); - cmd.Parameters.AddWithValue("@title", title ?? ""); - cmd.Parameters.AddWithValue("@doit", doit ?? ""); - cmd.Parameters.AddWithValue("@remark", remark ?? ""); - cmd.Parameters.AddWithValue("@jfrom", jfrom ?? ""); - cmd.Parameters.AddWithValue("@jto", jto ?? ""); - cmd.Parameters.AddWithValue("@wuid", info.Login.no); + 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("@pdate", pdate); + cmd.Parameters.AddWithValue("@projectName", projectName ?? ""); + cmd.Parameters.AddWithValue("@requestpart", requestpart ?? ""); + cmd.Parameters.AddWithValue("@package", package ?? ""); + cmd.Parameters.AddWithValue("@type", type ?? ""); + cmd.Parameters.AddWithValue("@process", process ?? ""); + cmd.Parameters.AddWithValue("@status", status ?? ""); + cmd.Parameters.AddWithValue("@description", description ?? ""); + cmd.Parameters.AddWithValue("@hrs", hrs); + cmd.Parameters.AddWithValue("@ot", ot); + cmd.Parameters.AddWithValue("@jobgrp", jobgrp ?? ""); + cmd.Parameters.AddWithValue("@tag", tag ?? ""); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); - cn.Open(); - var result = cmd.ExecuteNonQuery(); - cn.Close(); - cmd.Dispose(); - cn.Dispose(); - - return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." }); + cn.Open(); + var result = cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "수정되었습니다." : "수정에 실패했습니다." }); + } } catch (Exception ex) { @@ -233,21 +268,128 @@ namespace Project.Web { try { - var sql = "DELETE FROM EETGW_Jobreport WHERE idx = @idx AND gcode = @gcode"; + // 권한 체크 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + + // 본인 자료인지 체크 (관리자가 아닌 경우) + if (curLevel < 5) + { + var checkSql = "SELECT uid, pdate FROM JobReport WHERE idx = @idx AND gcode = @gcode"; + var cs2 = Properties.Settings.Default.gwcs; + using (var cn2 = new SqlConnection(cs2)) + using (var cmd2 = new SqlCommand(checkSql, cn2)) + { + cmd2.Parameters.AddWithValue("@idx", idx); + cmd2.Parameters.AddWithValue("@gcode", info.Login.gcode); + cn2.Open(); + using (var reader = cmd2.ExecuteReader()) + { + if (reader.Read()) + { + var ownerUid = reader["uid"]?.ToString(); + var pdate = reader["pdate"]?.ToString(); + + if (ownerUid != info.Login.no) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 삭제할 수 없습니다." }); + } + + // 마감 체크 + if (!string.IsNullOrEmpty(pdate) && pdate.Length >= 7) + { + var smon = pdate.Substring(0, 7); + if (DBM.GetMagamStatus(smon)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"등록일이 속한 월({smon})이 마감되었습니다." }); + } + } + } + } + } + } + + var sql = "DELETE FROM JobReport WHERE idx = @idx AND gcode = @gcode"; var cs = Properties.Settings.Default.gwcs; - var cn = new SqlConnection(cs); - var cmd = new SqlCommand(sql, cn); - cmd.Parameters.AddWithValue("@idx", idx); - cmd.Parameters.AddWithValue("@gcode", info.Login.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); - cn.Open(); - var result = cmd.ExecuteNonQuery(); - cn.Close(); - cmd.Dispose(); - cn.Dispose(); + 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 }); + } + } - return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다." }); + /// + /// 업무형태 목록 조회 (Common 테이블, grp='15') + /// 트리뷰 형태: process > jobgrp > type + /// + public string Jobreport_GetJobTypes(string process = "") + { + try + { + var sql = @"SELECT idx, code, memo as [type], svalue as jobgrp, svalue2 as process + FROM Common WITH (nolock) + WHERE gcode = @gcode AND grp = '15'"; + + var parameters = new List(); + parameters.Add(new SqlParameter("@gcode", info.Login.gcode)); + + if (!string.IsNullOrEmpty(process)) + { + sql += " AND svalue2 = @process"; + parameters.Add(new SqlParameter("@process", process)); + } + + sql += " ORDER BY svalue2, svalue, memo"; + + 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 }); + } + } + + /// + /// 업무일지 권한 정보 조회 + /// 본인이거나 권한 레벨 5 이상이면 OT 열을 볼 수 있음 + /// + public string Jobreport_GetPermission(string targetUserId) + { + try + { + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport)); + bool canViewOT = string.IsNullOrEmpty(targetUserId) || + targetUserId == info.Login.no || + curLevel >= 5; + + return JsonConvert.SerializeObject(new + { + Success = true, + CurrentUserId = info.Login.no, + Level = curLevel, + CanViewOT = canViewOT + }); } catch (Exception ex) { diff --git a/Project/Web/MachineBridge/MachineBridge.Login.cs b/Project/Web/MachineBridge/MachineBridge.Login.cs index 7761442..29f5df5 100644 --- a/Project/Web/MachineBridge/MachineBridge.Login.cs +++ b/Project/Web/MachineBridge/MachineBridge.Login.cs @@ -191,8 +191,19 @@ namespace Project.Web { mainForm.OnLoginCompleted(); } - break; } + else if (form is Dialog.fDashboard dashForm) + { + if (dashForm.InvokeRequired) + { + dashForm.Invoke(new Action(() => dashForm.RefreshPage())); + } + else + { + dashForm.RefreshPage(); + } + } + } } catch (Exception ex) @@ -258,6 +269,30 @@ namespace Project.Web } } + /// + /// 현재 로그인 상태 확인 + /// + public string CheckLoginStatus() + { + var isLoggedIn = !string.IsNullOrEmpty(info.Login.no); + var result = new + { + Success = true, + IsLoggedIn = isLoggedIn, + User = isLoggedIn ? new + { + Id = info.Login.no, + Name = info.Login.nameK, + NameE = info.Login.nameE, + Dept = info.Login.dept, + Email = info.Login.email, + Level = info.Login.level, + Gcode = info.Login.gcode + } : null + }; + return JsonConvert.SerializeObject(result); + } + /// /// 그룹 목록 조회 /// @@ -288,6 +323,68 @@ namespace Project.Web return JsonConvert.SerializeObject(result); } + /// + /// 로그아웃 처리 + /// + public string Logout() + { + try + { + // 로그인 정보 초기화 + info.Login.no = ""; + info.Login.nameK = ""; + info.Login.nameE = ""; + info.Login.dept = ""; + info.Login.email = ""; + info.Login.level = 0; + info.Login.gcode = ""; + info.Login.hp = ""; + info.Login.tel = ""; + info.Login.title = ""; + info.Login.process = ""; + info.Login.permission = 0; + info.Login.gpermission = 0; + + // fMain의 CloseAllForm 호출 + CallMainFormCloseAllForm(); + + return JsonConvert.SerializeObject(new { Success = true, Message = "로그아웃 되었습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// fMain의 CloseAllForm() 호출 + /// + private void CallMainFormCloseAllForm() + { + try + { + foreach (Form form in Application.OpenForms) + { + if (form is fMain mainForm) + { + if (mainForm.InvokeRequired) + { + mainForm.Invoke(new Action(() => mainForm.CloseAllFormPublic())); + } + else + { + mainForm.CloseAllFormPublic(); + } + break; + } + } + } + catch (Exception ex) + { + Console.WriteLine($"CloseAllForm 호출 오류: {ex.Message}"); + } + } + #endregion } } diff --git a/Project/Web/MachineBridge/MachineBridge.MailForm.cs b/Project/Web/MachineBridge/MachineBridge.MailForm.cs new file mode 100644 index 0000000..91ba1e9 --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.MailForm.cs @@ -0,0 +1,200 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region MailForm API (메일양식) + + /// + /// 메일양식 목록 조회 + /// + public string MailForm_GetList() + { + try + { + var sql = @"SELECT idx, gcode, cate, title, tolist, bcc, cc, subject, tail, body, + selfTo, selfCC, selfBCC, wuid, wdate, exceptmail, exceptmailcc + FROM MailForm WITH (nolock) + WHERE gcode = @gcode + ORDER BY cate, title"; + + 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 MailForm_GetDetail(int idx) + { + try + { + var sql = @"SELECT idx, gcode, cate, title, tolist, bcc, cc, subject, tail, body, + selfTo, selfCC, selfBCC, wuid, wdate, exceptmail, exceptmailcc + FROM MailForm WITH (nolock) + WHERE idx = @idx"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + using (var da = new SqlDataAdapter(cmd)) + { + var dt = new DataTable(); + da.Fill(dt); + if (dt.Rows.Count > 0) + { + return JsonConvert.SerializeObject(new { Success = true, Data = dt.Rows[0] }); + } + return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메일양식 추가 + /// + public string MailForm_Add(string cate, string title, string tolist, string bcc, string cc, + string subject, string tail, string body, bool selfTo, bool selfCC, bool selfBCC, + string exceptmail, string exceptmailcc) + { + try + { + var sql = @"INSERT INTO MailForm (gcode, cate, title, tolist, bcc, cc, subject, tail, body, + selfTo, selfCC, selfBCC, wuid, wdate, exceptmail, exceptmailcc) + VALUES (@gcode, @cate, @title, @tolist, @bcc, @cc, @subject, @tail, @body, + @selfTo, @selfCC, @selfBCC, @wuid, GETDATE(), @exceptmail, @exceptmailcc); + 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("@title", title ?? ""); + cmd.Parameters.AddWithValue("@tolist", tolist ?? ""); + cmd.Parameters.AddWithValue("@bcc", bcc ?? ""); + cmd.Parameters.AddWithValue("@cc", cc ?? ""); + cmd.Parameters.AddWithValue("@subject", subject ?? ""); + cmd.Parameters.AddWithValue("@tail", tail ?? ""); + cmd.Parameters.AddWithValue("@body", body ?? ""); + cmd.Parameters.AddWithValue("@selfTo", selfTo); + cmd.Parameters.AddWithValue("@selfCC", selfCC); + cmd.Parameters.AddWithValue("@selfBCC", selfBCC); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + cmd.Parameters.AddWithValue("@exceptmail", exceptmail ?? ""); + cmd.Parameters.AddWithValue("@exceptmailcc", exceptmailcc ?? ""); + + cn.Open(); + var newIdx = cmd.ExecuteScalar(); + return JsonConvert.SerializeObject(new { Success = true, Message = "등록되었습니다.", idx = newIdx }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메일양식 수정 + /// + public string MailForm_Edit(int idx, string cate, string title, string tolist, string bcc, string cc, + string subject, string tail, string body, bool selfTo, bool selfCC, bool selfBCC, + string exceptmail, string exceptmailcc) + { + try + { + var sql = @"UPDATE MailForm SET + cate = @cate, title = @title, tolist = @tolist, bcc = @bcc, cc = @cc, + subject = @subject, tail = @tail, body = @body, + selfTo = @selfTo, selfCC = @selfCC, selfBCC = @selfBCC, + wuid = @wuid, wdate = GETDATE(), exceptmail = @exceptmail, exceptmailcc = @exceptmailcc + WHERE idx = @idx"; + + 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("@cate", cate ?? ""); + cmd.Parameters.AddWithValue("@title", title ?? ""); + cmd.Parameters.AddWithValue("@tolist", tolist ?? ""); + cmd.Parameters.AddWithValue("@bcc", bcc ?? ""); + cmd.Parameters.AddWithValue("@cc", cc ?? ""); + cmd.Parameters.AddWithValue("@subject", subject ?? ""); + cmd.Parameters.AddWithValue("@tail", tail ?? ""); + cmd.Parameters.AddWithValue("@body", body ?? ""); + cmd.Parameters.AddWithValue("@selfTo", selfTo); + cmd.Parameters.AddWithValue("@selfCC", selfCC); + cmd.Parameters.AddWithValue("@selfBCC", selfBCC); + cmd.Parameters.AddWithValue("@wuid", info.Login.no); + cmd.Parameters.AddWithValue("@exceptmail", exceptmail ?? ""); + cmd.Parameters.AddWithValue("@exceptmailcc", exceptmailcc ?? ""); + + cn.Open(); + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true, Message = "수정되었습니다." }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 메일양식 삭제 + /// + public string MailForm_Delete(int idx) + { + try + { + var sql = "DELETE FROM MailForm WHERE idx = @idx"; + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@idx", idx); + cn.Open(); + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true, Message = "삭제되었습니다." }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.Todo.cs b/Project/Web/MachineBridge/MachineBridge.Todo.cs index fecdc29..84aac74 100644 --- a/Project/Web/MachineBridge/MachineBridge.Todo.cs +++ b/Project/Web/MachineBridge/MachineBridge.Todo.cs @@ -79,6 +79,7 @@ namespace Project.Web } } + /// /// 할일 추가 /// diff --git a/Project/Web/MachineBridge/MachineBridge.User.cs b/Project/Web/MachineBridge/MachineBridge.User.cs new file mode 100644 index 0000000..860d6b6 --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.User.cs @@ -0,0 +1,290 @@ +using System; +using System.Linq; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region User API + + /// + /// 현재 로그인한 사용자 정보 조회 + /// + public string GetCurrentUserInfo() + { + try + { + if (string.IsNullOrEmpty(info.Login.no)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + var taUser = new dsMSSQLTableAdapters.UsersTableAdapter(); + var taGUser = new dsMSSQLTableAdapters.EETGW_GroupUserTableAdapter(); + + var drUser = taUser.GetID(info.Login.no).FirstOrDefault(); + var drGUser = taGUser.GetbyID(info.Login.gcode, info.Login.no).FirstOrDefault(); + + if (drUser == null) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 정보를 찾을 수 없습니다." }); + } + + var userInfo = new + { + Id = drUser.id, + NameK = drUser.name, + NameE = drUser.nameE, + Dept = drUser.dept, + Grade = drUser.grade, + Email = drUser.email, + Tel = drUser.tel, + Hp = drUser.hp, + DateIn = drUser.indate, + DateO = drUser.outdate, + Memo = drUser.memo, + Process = drGUser?.Process ?? "", + State = drGUser?.state ?? "", + UseJobReport = drGUser != null && !drGUser.IsuseJobReportNull() && drGUser.useJobReport, + UseUserState = drGUser != null && !drGUser.IsuseUserStateNull() && drGUser.useUserState, + ExceptHoly = drGUser != null && !drGUser.IsexceptHolyNull() && drGUser.exceptHoly, + Level = drGUser?.level ?? 0 + }; + + return JsonConvert.SerializeObject(new { Success = true, Data = userInfo }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 정보 조회 실패: " + ex.Message }); + } + } + + /// + /// 사용자 정보 조회 (ID로) + /// + public string GetUserInfoById(string userId) + { + try + { + if (string.IsNullOrEmpty(userId)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 ID를 입력하세요." }); + } + + var taUser = new dsMSSQLTableAdapters.UsersTableAdapter(); + var taGUser = new dsMSSQLTableAdapters.EETGW_GroupUserTableAdapter(); + + var drUser = taUser.GetID(userId).FirstOrDefault(); + var drGUser = taGUser.GetbyID(info.Login.gcode, userId).FirstOrDefault(); + + if (drUser == null) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "등록된 사용자가 없습니다." }); + } + + var userInfo = new + { + Id = drUser.id, + NameK = drUser.name, + NameE = drUser.nameE, + Dept = drUser.dept, + Grade = drUser.grade, + Email = drUser.email, + Tel = drUser.tel, + Hp = drUser.hp, + DateIn = drUser.indate, + DateO = drUser.outdate, + Memo = drUser.memo, + Process = drGUser?.Process ?? "", + State = drGUser?.state ?? "", + UseJobReport = drGUser != null && !drGUser.IsuseJobReportNull() && drGUser.useJobReport, + UseUserState = drGUser != null && !drGUser.IsuseUserStateNull() && drGUser.useUserState, + ExceptHoly = drGUser != null && !drGUser.IsexceptHolyNull() && drGUser.exceptHoly, + Level = drGUser?.level ?? 0 + }; + + return JsonConvert.SerializeObject(new { Success = true, Data = userInfo }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 정보 조회 실패: " + ex.Message }); + } + } + + /// + /// 사용자 정보 저장 + /// + public string SaveUserInfo(string jsonData) + { + try + { + var userData = JsonConvert.DeserializeObject(jsonData); + + if (userData == null) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "잘못된 데이터 형식입니다." }); + } + + var gcode = info.Login.gcode; + var uid = userData.Id; + + // 현재 사용자 권한 확인 + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.account)); + + // 그룹 사용자 정보 처리 + var taUserGrp = new dsMSSQLTableAdapters.EETGW_GroupUserTableAdapter(); + var dtUserGrp = taUserGrp.GetData(gcode); + var drGuser = dtUserGrp.Where(t => t.uid == uid).FirstOrDefault(); + + if (drGuser != null) + { + drGuser.Process = userData.Process ?? ""; + drGuser.state = userData.State ?? ""; + + if (curLevel > 4) + { + drGuser.useJobReport = userData.UseJobReport; + drGuser.useUserState = userData.UseUserState; + drGuser.exceptHoly = userData.ExceptHoly; + } + } + else + { + drGuser = dtUserGrp.NewEETGW_GroupUserRow(); + drGuser.wuid = info.Login.no; + drGuser.wdate = DateTime.Now; + drGuser.gcode = gcode; + drGuser.level = 1; + drGuser.uid = uid; + drGuser.state = userData.State ?? ""; + drGuser.Process = userData.Process ?? ""; + drGuser.useJobReport = userData.UseJobReport; + drGuser.useUserState = userData.UseUserState; + drGuser.exceptHoly = userData.ExceptHoly; + dtUserGrp.AddEETGW_GroupUserRow(drGuser); + } + + // 사용자 정보 처리 + var tauser = new dsMSSQLTableAdapters.UsersTableAdapter(); + var dtuser = tauser.GetID(uid); + var drUser = dtuser.FirstOrDefault(); + + if (drUser == null) + { + drUser = dtuser.NewUsersRow(); + drUser.wuid = info.Login.no; + drUser.wdate = DateTime.Now; + drUser.gcode = gcode; + drUser.level = 1; + drUser.id = uid; + drUser.password = "B6589FC6AB0DC82CF12099D1C2D40AB994E8410C"; // 기본값 0 + dtuser.AddUsersRow(drUser); + } + + drUser.name = userData.NameK ?? ""; + drUser.nameE = userData.NameE ?? ""; + drUser.dept = userData.Dept ?? ""; + drUser.email = userData.Email ?? ""; + drUser.tel = userData.Tel ?? ""; + drUser.hp = userData.Hp ?? ""; + drUser.indate = userData.DateIn ?? ""; + drUser.outdate = userData.DateO ?? ""; + drUser.memo = userData.Memo ?? ""; + drUser.processs = userData.Process ?? ""; + drUser.grade = userData.Grade ?? ""; + drUser.EndEdit(); + + var cnt1 = taUserGrp.Update(dtUserGrp); + var cnt2 = tauser.Update(dtuser); + + taUserGrp.Dispose(); + tauser.Dispose(); + + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "저장 실패: " + ex.Message }); + } + } + + /// + /// 비밀번호 변경 + /// + public string ChangePassword(string oldPassword, string newPassword) + { + try + { + if (string.IsNullOrEmpty(info.Login.no)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." }); + } + + if (string.IsNullOrEmpty(newPassword)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "새 비밀번호를 입력하세요." }); + } + + var uid = info.Login.no; + int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.account)); + + var taUser = new dsMSSQLTableAdapters.UsersTableAdapter(); + var dtUser = taUser.GetID(uid); + var drUser = dtUser.FirstOrDefault(); + + if (drUser == null) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 정보를 찾을 수 없습니다." }); + } + + // 관리자가 아니면 기존 암호 확인 + if (curLevel < 5) + { + var encOldPass = Pub.MakePasswordEnc(oldPassword); + if (!encOldPass.Equals(drUser.password)) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "기존 암호가 일치하지 않습니다." }); + } + } + + drUser.password = Pub.MakePasswordEnc(newPassword); + drUser.EndEdit(); + taUser.Update(dtUser); + taUser.Dispose(); + + return JsonConvert.SerializeObject(new { Success = true, Message = "비밀번호가 변경되었습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "비밀번호 변경 실패: " + ex.Message }); + } + } + + #endregion + } + + /// + /// 사용자 정보 데이터 클래스 + /// + public class UserInfoData + { + public string Id { get; set; } + public string NameK { get; set; } + public string NameE { get; set; } + public string Dept { get; set; } + public string Grade { get; set; } + public string Email { get; set; } + public string Tel { get; set; } + public string Hp { get; set; } + public string DateIn { get; set; } + public string DateO { get; set; } + public string Memo { get; set; } + public string Process { get; set; } + public string State { get; set; } + public bool UseJobReport { get; set; } + public bool UseUserState { get; set; } + public bool ExceptHoly { get; set; } + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.UserGroup.cs b/Project/Web/MachineBridge/MachineBridge.UserGroup.cs new file mode 100644 index 0000000..4a9771d --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.UserGroup.cs @@ -0,0 +1,228 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region UserGroup API (그룹정보/권한설정) + + /// + /// 그룹 목록 조회 + /// + public string UserGroup_GetList() + { + try + { + var sql = @"SELECT dept, gcode, path_kj, permission, advpurchase, advkisul, + managerinfo, devinfo, usemail + FROM UserGroup WITH (nolock) + WHERE gcode = @gcode + ORDER BY dept"; + + 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 UserGroup_Add(string dept, string path_kj, int permission, + bool advpurchase, bool advkisul, string managerinfo, string devinfo, bool usemail) + { + try + { + // 중복 체크 + var checkSql = "SELECT COUNT(*) FROM UserGroup WHERE gcode = @gcode AND dept = @dept"; + var cs = Properties.Settings.Default.gwcs; + + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + using (var checkCmd = new SqlCommand(checkSql, cn)) + { + checkCmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + checkCmd.Parameters.AddWithValue("@dept", dept); + var count = (int)checkCmd.ExecuteScalar(); + if (count > 0) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "이미 존재하는 부서명입니다." }); + } + } + + var sql = @"INSERT INTO UserGroup (dept, gcode, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail) + VALUES (@dept, @gcode, @path_kj, @permission, @advpurchase, @advkisul, @managerinfo, @devinfo, @usemail)"; + + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@dept", dept ?? ""); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@path_kj", path_kj ?? ""); + cmd.Parameters.AddWithValue("@permission", permission); + cmd.Parameters.AddWithValue("@advpurchase", advpurchase); + cmd.Parameters.AddWithValue("@advkisul", advkisul); + cmd.Parameters.AddWithValue("@managerinfo", managerinfo ?? ""); + cmd.Parameters.AddWithValue("@devinfo", devinfo ?? ""); + cmd.Parameters.AddWithValue("@usemail", usemail); + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true, Message = "등록되었습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 그룹 정보 수정 + /// + public string UserGroup_Edit(string originalDept, string dept, string path_kj, int permission, + bool advpurchase, bool advkisul, string managerinfo, string devinfo, bool usemail) + { + try + { + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + + // 부서명이 변경되었을 경우 중복 체크 + if (originalDept != dept) + { + var checkSql = "SELECT COUNT(*) FROM UserGroup WHERE gcode = @gcode AND dept = @dept"; + using (var checkCmd = new SqlCommand(checkSql, cn)) + { + checkCmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + checkCmd.Parameters.AddWithValue("@dept", dept); + var count = (int)checkCmd.ExecuteScalar(); + if (count > 0) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "이미 존재하는 부서명입니다." }); + } + } + } + + var sql = @"UPDATE UserGroup SET + dept = @dept, path_kj = @path_kj, permission = @permission, + advpurchase = @advpurchase, advkisul = @advkisul, + managerinfo = @managerinfo, devinfo = @devinfo, usemail = @usemail + WHERE gcode = @gcode AND dept = @originalDept"; + + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@originalDept", originalDept); + cmd.Parameters.AddWithValue("@dept", dept ?? ""); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@path_kj", path_kj ?? ""); + cmd.Parameters.AddWithValue("@permission", permission); + cmd.Parameters.AddWithValue("@advpurchase", advpurchase); + cmd.Parameters.AddWithValue("@advkisul", advkisul); + cmd.Parameters.AddWithValue("@managerinfo", managerinfo ?? ""); + cmd.Parameters.AddWithValue("@devinfo", devinfo ?? ""); + cmd.Parameters.AddWithValue("@usemail", usemail); + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true, Message = "수정되었습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 그룹 삭제 + /// + public string UserGroup_Delete(string dept) + { + try + { + // 해당 그룹에 소속된 사용자가 있는지 확인 + var checkSql = "SELECT COUNT(*) FROM GroupUser WHERE gcode = @gcode AND dept = @dept"; + var cs = Properties.Settings.Default.gwcs; + + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + + using (var checkCmd = new SqlCommand(checkSql, cn)) + { + checkCmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + checkCmd.Parameters.AddWithValue("@dept", dept); + var count = (int)checkCmd.ExecuteScalar(); + if (count > 0) + { + return JsonConvert.SerializeObject(new { Success = false, Message = $"해당 그룹에 {count}명의 사용자가 소속되어 있어 삭제할 수 없습니다." }); + } + } + + var sql = "DELETE FROM UserGroup WHERE gcode = @gcode AND dept = @dept"; + using (var cmd = new SqlCommand(sql, cn)) + { + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@dept", dept); + cmd.ExecuteNonQuery(); + return JsonConvert.SerializeObject(new { Success = true, Message = "삭제되었습니다." }); + } + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + /// + /// 권한 정보 목록 (프론트엔드용) + /// + public string UserGroup_GetPermissionInfo() + { + try + { + var permissions = new[] + { + new { index = 0, name = "menu_purchase", label = "구매신청", description = "구매신청 메뉴 표시" }, + new { index = 1, name = "menu_project", label = "프로젝트", description = "프로젝트 메뉴 표시" }, + new { index = 2, name = "menu_history", label = "업무일지", description = "업무일지 메뉴 표시" }, + new { index = 3, name = "menu_jago", label = "품목재고", description = "품목재고 메뉴 표시" }, + new { index = 4, name = "menu_equipment", label = "장비목록", description = "장비목록 메뉴 표시" }, + new { index = 5, name = "menu_workday", label = "근태관리", description = "근태관리 메뉴 표시" }, + new { index = 6, name = "purchase_adv", label = "(구매)상세입력", description = "구매신청 상세입력 권한" }, + new { index = 7, name = "menu_docu", label = "문서", description = "문서 메뉴 표시" }, + new { index = 8, name = "menu_logdata", label = "운영기록", description = "운영기록 메뉴 표시" }, + new { index = 9, name = "jobreport_kisul", label = "업무일지-기술료", description = "업무일지 기술료 보기 권한" }, + new { index = 10, name = "jobreport_editblock", label = "업무일지-편집제한", description = "업무일지 편집 제한" }, + }; + + return JsonConvert.SerializeObject(new { Success = true, Data = permissions }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message }); + } + } + + #endregion + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.UserList.cs b/Project/Web/MachineBridge/MachineBridge.UserList.cs new file mode 100644 index 0000000..27ff72e --- /dev/null +++ b/Project/Web/MachineBridge/MachineBridge.UserList.cs @@ -0,0 +1,378 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using Newtonsoft.Json; +using FCOMMON; + +namespace Project.Web +{ + public partial class MachineBridge + { + #region UserList API + + /// + /// 현재 사용자 권한 레벨 조회 (로그인 레벨 + account 권한 중 높은 값) + /// + public string UserList_GetCurrentLevel() + { + try + { + int curLevel = Math.Max(info.Login.level, FCOMMON.DBM.getAuth(FCOMMON.DBM.eAuthType.account)); + return JsonConvert.SerializeObject(new + { + Success = true, + Data = new + { + Level = curLevel, + CurrentUserId = info.Login.no, + CanEdit = curLevel >= 5 + } + }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "권한 조회 실패: " + ex.Message }); + } + } + + /// + /// 부서 목록 조회 + /// + public string UserList_GetDepts() + { + try + { + var sql = "SELECT DISTINCT dept FROM UserGroup WITH (NOLOCK) WHERE gcode = @gcode AND ISNULL(dept,'') <> '' ORDER BY dept"; + + 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(dr["dept"]?.ToString() ?? ""); + } + + return JsonConvert.SerializeObject(new { Success = true, Data = result }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "부서 조회 실패: " + ex.Message }); + } + } + + /// + /// 사용자 목록 조회 + /// + public string UserList_GetList(string process) + { + try + { + if (string.IsNullOrEmpty(process) || process == "%") process = "%"; + else process = "%" + process + "%"; + + var gcode = info.Login.gcode; + System.Diagnostics.Debug.WriteLine($"[UserList_GetList] gcode={gcode}, process={process}"); + + var sql = @"SELECT gcode, dept, level, name, nameE, grade, email, tel, indate, outdate, hp, + memo, processs, id, state, useJobReport, useUserState, exceptHoly + FROM vGroupUser WITH (NOLOCK) + WHERE gcode = @gcode + AND ISNULL(processs,'') LIKE @process + ORDER BY useUserState DESC, useJobReport DESC, name"; + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var cmd = new SqlCommand(sql, cn); + cmd.Parameters.AddWithValue("@gcode", gcode); + cmd.Parameters.AddWithValue("@process", process); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + + System.Diagnostics.Debug.WriteLine($"[UserList_GetList] 결과 행 수: {dt.Rows.Count}"); + + da.Dispose(); + cmd.Dispose(); + cn.Dispose(); + + return JsonConvert.SerializeObject(dt, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 목록 조회 실패: " + ex.Message }); + } + } + + /// + /// 사용자 상세 정보 조회 + /// + public string UserList_GetUser(string userId) + { + try + { + var sql = @"SELECT + u.id, + u.name, + u.nameE, + u.grade, + u.email, + u.tel, + u.indate, + u.outdate, + u.hp, + u.processs, + u.state, + u.memo, + gu.level, + gu.useUserState, + gu.useJobReport, + gu.exceptHoly, + gu.dept, + gu.gcode + FROM EETGW_GroupUser gu WITH (NOLOCK) + INNER JOIN Users u WITH (NOLOCK) ON gu.uid = u.id + WHERE gu.gcode = @gcode AND gu.uid = @uid"; + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var cmd = new SqlCommand(sql, cn); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@uid", userId); + + var da = new SqlDataAdapter(cmd); + var dt = new DataTable(); + da.Fill(dt); + da.Dispose(); + cmd.Dispose(); + cn.Dispose(); + + if (dt.Rows.Count > 0) + { + return JsonConvert.SerializeObject(new { Success = true, Data = dt.Rows[0] }, + new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }); + } + else + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자를 찾을 수 없습니다." }); + } + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 조회 실패: " + ex.Message }); + } + } + + /// + /// 사용자 전체 정보 저장 (Users + GroupUser) + /// + public string UserList_SaveUserFull(string jsonData) + { + try + { + var userData = JsonConvert.DeserializeObject(jsonData); + if (userData == null) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "잘못된 데이터 형식입니다." }); + } + + // 권한 체크 + int curLevel = Math.Max(info.Login.level, FCOMMON.DBM.getAuth(FCOMMON.DBM.eAuthType.account)); + bool isSelf = info.Login.no == userData.id; + + // 본인이 아니고 권한이 없으면 거부 + if (!isSelf && curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 계정은 편집할 수 없습니다." }); + } + + var cs = Properties.Settings.Default.gwcs; + using (var cn = new SqlConnection(cs)) + { + cn.Open(); + + // Users 테이블 업데이트 + var sqlUser = @"UPDATE Users SET + name = @name, + nameE = @nameE, + grade = @grade, + email = @email, + tel = @tel, + hp = @hp, + indate = @indate, + outdate = @outdate, + memo = @memo, + processs = @processs, + state = @state + WHERE id = @id"; + + using (var cmdUser = new SqlCommand(sqlUser, cn)) + { + cmdUser.Parameters.AddWithValue("@id", userData.id); + cmdUser.Parameters.AddWithValue("@name", userData.name ?? ""); + cmdUser.Parameters.AddWithValue("@nameE", userData.nameE ?? ""); + cmdUser.Parameters.AddWithValue("@grade", userData.grade ?? ""); + cmdUser.Parameters.AddWithValue("@email", userData.email ?? ""); + cmdUser.Parameters.AddWithValue("@tel", userData.tel ?? ""); + cmdUser.Parameters.AddWithValue("@hp", userData.hp ?? ""); + cmdUser.Parameters.AddWithValue("@indate", userData.indate ?? ""); + cmdUser.Parameters.AddWithValue("@outdate", userData.outdate ?? ""); + cmdUser.Parameters.AddWithValue("@memo", userData.memo ?? ""); + cmdUser.Parameters.AddWithValue("@processs", userData.processs ?? ""); + cmdUser.Parameters.AddWithValue("@state", userData.state ?? ""); + cmdUser.ExecuteNonQuery(); + } + + // EETGW_GroupUser 테이블 업데이트 (관리자만) + if (curLevel >= 5) + { + var sqlGroup = @"UPDATE EETGW_GroupUser SET + level = @level, + useUserState = @useUserState, + useJobReport = @useJobReport, + exceptHoly = @exceptHoly + WHERE gcode = @gcode AND uid = @uid"; + + using (var cmdGroup = new SqlCommand(sqlGroup, cn)) + { + cmdGroup.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmdGroup.Parameters.AddWithValue("@uid", userData.id); + cmdGroup.Parameters.AddWithValue("@level", userData.level); + cmdGroup.Parameters.AddWithValue("@useUserState", userData.useUserState); + cmdGroup.Parameters.AddWithValue("@useJobReport", userData.useJobReport); + cmdGroup.Parameters.AddWithValue("@exceptHoly", userData.exceptHoly); + cmdGroup.ExecuteNonQuery(); + } + } + } + + return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "저장 실패: " + ex.Message }); + } + } + + /// + /// 사용자 저장 (그룹 설정만) + /// + public string UserList_SaveGroupUser(string userId, string dept, int level, bool useUserState, bool useJobReport, bool exceptHoly) + { + try + { + // 권한 체크 + int curLevel = Math.Max(info.Login.level, FCOMMON.DBM.getAuth(FCOMMON.DBM.eAuthType.account)); + if (curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "사용자 관리 권한이 없습니다." }); + } + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + + var sql = @"UPDATE EETGW_GroupUser SET + dept = @dept, + level = @level, + useUserState = @useUserState, + useJobReport = @useJobReport, + exceptHoly = @exceptHoly + WHERE gcode = @gcode AND uid = @uid"; + + var cmd = new SqlCommand(sql, cn); + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@uid", userId); + cmd.Parameters.AddWithValue("@dept", dept ?? ""); + cmd.Parameters.AddWithValue("@level", level); + cmd.Parameters.AddWithValue("@useUserState", useUserState); + cmd.Parameters.AddWithValue("@useJobReport", useJobReport); + cmd.Parameters.AddWithValue("@exceptHoly", exceptHoly); + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + cn.Close(); + + cmd.Dispose(); + cn.Dispose(); + + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "저장되었습니다." : "저장에 실패했습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "저장 실패: " + ex.Message }); + } + } + + /// + /// 사용자 삭제 (그룹에서 제거) + /// + public string UserList_DeleteGroupUser(string userId) + { + try + { + // 권한 체크 + int curLevel = Math.Max(info.Login.level, FCOMMON.DBM.getAuth(FCOMMON.DBM.eAuthType.account)); + if (curLevel < 5) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "계정 관리자만 사용할 수 있습니다." }); + } + + var cs = Properties.Settings.Default.gwcs; + var cn = new SqlConnection(cs); + var sql = "DELETE FROM EETGW_GroupUser WHERE gcode = @gcode AND uid = @uid"; + var cmd = new SqlCommand(sql, cn); + + cmd.Parameters.AddWithValue("@gcode", info.Login.gcode); + cmd.Parameters.AddWithValue("@uid", userId); + + cn.Open(); + var result = cmd.ExecuteNonQuery(); + cn.Close(); + + cmd.Dispose(); + cn.Dispose(); + + return JsonConvert.SerializeObject(new { Success = result > 0, Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다." }); + } + catch (Exception ex) + { + return JsonConvert.SerializeObject(new { Success = false, Message = "삭제 실패: " + ex.Message }); + } + } + + #endregion + } + + /// + /// 사용자 전체 정보 데이터 클래스 + /// + public class UserListFullData + { + public string id { get; set; } + public string name { get; set; } + public string nameE { get; set; } + public string grade { get; set; } + public string email { get; set; } + public string tel { get; set; } + public string hp { get; set; } + public string indate { get; set; } + public string outdate { get; set; } + public string memo { get; set; } + public string processs { get; set; } + public string state { get; set; } + public int level { get; set; } + public bool useUserState { get; set; } + public bool useJobReport { get; set; } + public bool exceptHoly { get; set; } + } +} diff --git a/Project/Web/MachineBridge/MachineBridge.cs b/Project/Web/MachineBridge/MachineBridge.cs index 92152da..45975d8 100644 --- a/Project/Web/MachineBridge/MachineBridge.cs +++ b/Project/Web/MachineBridge/MachineBridge.cs @@ -20,12 +20,102 @@ namespace Project.Web public partial class MachineBridge { // Reference to the main form to update logic - private Dialog.fDashboardNew _host; + private Dialog.fDashboard _host; - public MachineBridge(Dialog.fDashboardNew host) + // WebSocket 서버 인스턴스 + private static Project.Web.WebSocketServer _wsServer; + private static readonly object _wsLock = new object(); + + private const int WS_PORT = 8082; + + public MachineBridge(Dialog.fDashboard host) { _host = host; + StartWebSocketServer(); } + + #region WebSocket Server Control + + /// + /// WebSocket 서버 시작 + /// + private void StartWebSocketServer() + { + lock (_wsLock) + { + if (_wsServer != null) + { + Console.WriteLine("[WS] WebSocket server already running"); + return; + } + + try + { + string url = $"http://localhost:{WS_PORT}/"; + _wsServer = new Project.Web.WebSocketServer(url, this); + _wsServer.Start(); + Console.WriteLine($"[WS] WebSocket server started on port {WS_PORT}"); + } + catch (Exception ex) + { + Console.WriteLine($"[WS] Failed to start WebSocket server: {ex.Message}"); + } + } + } + + /// + /// WebSocket 서버 중지 + /// + public static void StopWebSocketServer() + { + lock (_wsLock) + { + if (_wsServer != null) + { + _wsServer.Stop(); + _wsServer = null; + Console.WriteLine("[WS] WebSocket server stopped"); + } + } + } + + /// + /// WebSocket 서버 실행 여부 확인 + /// + public static bool IsWebSocketServerRunning() + { + lock (_wsLock) + { + return _wsServer != null; + } + } + + #endregion + + #region App Info + + /// + /// 애플리케이션 버전 정보 반환 + /// + public string GetAppVersion() + { + try + { + return JsonConvert.SerializeObject(new + { + Success = true, + ProductName = Application.ProductName, + ProductVersion = Application.ProductVersion, + DisplayVersion = $"{Application.ProductName} v{Application.ProductVersion}" + }); + } + 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 new file mode 100644 index 0000000..1197c12 --- /dev/null +++ b/Project/Web/MachineBridge/WebSocketServer.cs @@ -0,0 +1,705 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Project.Web +{ + /// + /// GroupWare WebSocket 서버 + /// npm run dev 환경에서 핫 리로드 개발을 위한 WebSocket 통신 지원 + /// + public class WebSocketServer + { + private HttpListener _httpListener; + private List _clients = new List(); + private ConcurrentDictionary _socketLocks = new ConcurrentDictionary(); + private MachineBridge _bridge; + private bool _isRunning = false; + + public WebSocketServer(string url, MachineBridge bridge) + { + _bridge = bridge; + _httpListener = new HttpListener(); + _httpListener.Prefixes.Add(url); + } + + public void Start() + { + if (_isRunning) return; + + try + { + _httpListener.Start(); + _isRunning = true; + Console.WriteLine($"[WS] GroupWare WebSocket Server Started"); + Task.Run(AcceptConnections); + } + catch (Exception ex) + { + Console.WriteLine($"[WS] Start Error: {ex.Message}"); + } + } + + public void Stop() + { + _isRunning = false; + try + { + _httpListener.Stop(); + _httpListener.Close(); + } + catch { } + } + + private async Task AcceptConnections() + { + while (_httpListener.IsListening && _isRunning) + { + try + { + var context = await _httpListener.GetContextAsync(); + if (context.Request.IsWebSocketRequest) + { + ProcessRequest(context); + } + else + { + context.Response.StatusCode = 400; + context.Response.Close(); + } + } + catch (Exception ex) + { + if (_isRunning) + Console.WriteLine($"[WS] Accept Error: {ex.Message}"); + } + } + } + + private async void ProcessRequest(HttpListenerContext context) + { + WebSocketContext wsContext = null; + try + { + wsContext = await context.AcceptWebSocketAsync(subProtocol: null); + WebSocket socket = wsContext.WebSocket; + _socketLocks.TryAdd(socket, new SemaphoreSlim(1, 1)); + + lock (_clients) { _clients.Add(socket); } + Console.WriteLine("[WS] Client Connected"); + + await ReceiveLoop(socket); + } + catch (Exception ex) + { + Console.WriteLine($"[WS] Process Error: {ex.Message}"); + } + finally + { + if (wsContext != null) + { + WebSocket socket = wsContext.WebSocket; + lock (_clients) { _clients.Remove(socket); } + + if (_socketLocks.TryRemove(socket, out var semaphore)) + { + semaphore.Dispose(); + } + socket.Dispose(); + } + } + } + + private async Task ReceiveLoop(WebSocket socket) + { + var buffer = new byte[1024 * 4]; + while (socket.State == WebSocketState.Open && _isRunning) + { + try + { + var result = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); + if (result.MessageType == WebSocketMessageType.Close) + { + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); + } + else if (result.MessageType == WebSocketMessageType.Text) + { + string msg = Encoding.UTF8.GetString(buffer, 0, result.Count); + await HandleMessage(msg, socket); + } + } + catch + { + break; + } + } + } + + private async Task HandleMessage(string msg, WebSocket socket) + { + try + { + dynamic json = JsonConvert.DeserializeObject(msg); + string type = json.type; + + Console.WriteLine($"[WS] Message: {type}"); + + switch (type) + { + // ===== Todo API ===== + case "GET_TODOS": + { + string result = _bridge.Todo_GetTodos(); + var response = new { type = "TODOS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_TODO": + { + int id = json.id; + string result = _bridge.GetTodo(id); + var response = new { type = "TODO_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "CREATE_TODO": + { + string title = json.title ?? ""; + string remark = json.remark ?? ""; + string expire = json.expire; + int seqno = json.seqno ?? 0; + bool flag = json.flag ?? false; + string request = json.request; + string status = json.status ?? "0"; + + string result = _bridge.CreateTodo(title, remark, expire, seqno, flag, request, status); + var response = new { type = "TODO_CREATED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "UPDATE_TODO": + { + int idx = json.idx; + string title = json.title ?? ""; + string remark = json.remark ?? ""; + string expire = json.expire; + int seqno = json.seqno ?? 0; + bool flag = json.flag ?? false; + string request = json.request; + string status = json.status ?? "0"; + + string result = _bridge.Todo_UpdateTodo(idx, title, remark, expire, seqno, flag, request, status); + var response = new { type = "TODO_UPDATED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "DELETE_TODO": + { + int id = json.id; + string result = _bridge.Todo_DeleteTodo(id); + var response = new { type = "TODO_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_URGENT_TODOS": + { + string result = _bridge.GetUrgentTodos(); + var response = new { type = "URGENT_TODOS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Dashboard API ===== + case "GET_PURCHASE_WAIT_COUNT": + { + string result = _bridge.GetPurchaseWaitCount(); + var response = new { type = "PURCHASE_WAIT_COUNT_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_TODAY_COUNT_H": + { + string result = _bridge.TodayCountH(); + var response = new { type = "TODAY_COUNT_H_DATA", count = int.Parse(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_HOLY_USER": + { + string result = _bridge.GetHolyUser(); + var response = new { type = "HOLY_USER_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_HOLY_REQUEST_USER": + { + string result = _bridge.GetHolyRequestUser(); + var response = new { type = "HOLY_REQUEST_USER_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_PURCHASE_NR_LIST": + { + string result = _bridge.GetPurchaseNRList(); + var response = new { type = "PURCHASE_NR_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_PURCHASE_CR_LIST": + { + string result = _bridge.GetPurchaseCRList(); + var response = new { type = "PURCHASE_CR_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_HOLYDAY_REQUEST_COUNT": + { + string result = _bridge.GetHolydayRequestCount(); + var response = new { type = "HOLYDAY_REQUEST_COUNT_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_CURRENT_USER_COUNT": + { + string result = _bridge.GetCurrentUserCount(); + var response = new { type = "CURRENT_USER_COUNT_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Login API ===== + case "CHECK_LOGIN_STATUS": + { + string result = _bridge.CheckLoginStatus(); + var response = new { type = "LOGIN_STATUS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "LOGIN": + { + string gcode = json.gcode ?? ""; + string id = json.id ?? ""; + string password = json.password ?? ""; + bool rememberMe = json.rememberMe ?? false; + string result = _bridge.Login(gcode, id, password, rememberMe); + var response = new { type = "LOGIN_RESULT", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "LOGOUT": + { + string result = _bridge.Logout(); + var response = new { type = "LOGOUT_RESULT", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_USER_GROUPS": + { + string result = _bridge.GetUserGroups(); + var response = new { type = "USER_GROUPS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_PREVIOUS_LOGIN_INFO": + { + string result = _bridge.GetPreviousLoginInfo(); + var response = new { type = "PREVIOUS_LOGIN_INFO_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== User API ===== + case "GET_CURRENT_USER_INFO": + { + string result = _bridge.GetCurrentUserInfo(); + var response = new { type = "CURRENT_USER_INFO_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_USER_INFO_BY_ID": + { + string userId = json.userId ?? ""; + string result = _bridge.GetUserInfoById(userId); + var response = new { type = "USER_INFO_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "SAVE_USER_INFO": + { + string userData = JsonConvert.SerializeObject(json.userData); + string result = _bridge.SaveUserInfo(userData); + var response = new { type = "USER_INFO_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "CHANGE_PASSWORD": + { + string oldPassword = json.oldPassword ?? ""; + string newPassword = json.newPassword ?? ""; + string result = _bridge.ChangePassword(oldPassword, newPassword); + var response = new { type = "PASSWORD_CHANGED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Common Code API ===== + case "COMMON_GET_GROUPS": + { + string result = _bridge.Common_GetGroups(); + var response = new { type = "COMMON_GROUPS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "COMMON_GET_LIST": + { + string grp = json.grp ?? "99"; + string result = _bridge.Common_GetList(grp); + var response = new { type = "COMMON_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "COMMON_SAVE": + { + int idx = json.idx ?? 0; + string grp = json.grp ?? ""; + string code = json.code ?? ""; + string svalue = json.svalue ?? ""; + int ivalue = json.ivalue ?? 0; + float fvalue = json.fvalue ?? 0f; + string svalue2 = json.svalue2 ?? ""; + string memo = json.memo ?? ""; + string result = _bridge.Common_Save(idx, grp, code, svalue, ivalue, fvalue, svalue2, memo); + var response = new { type = "COMMON_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "COMMON_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.Common_Delete(idx); + var response = new { type = "COMMON_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== Items API ===== + case "ITEMS_GET_CATEGORIES": + { + string result = _bridge.Items_GetCategories(); + var response = new { type = "ITEMS_CATEGORIES_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_GET_LIST": + { + string category = json.category ?? ""; + string searchKey = json.searchKey ?? ""; + string result = _bridge.Items_GetList(category, searchKey); + var response = new { type = "ITEMS_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_SAVE": + { + int idx = json.idx ?? 0; + string sid = json.sid ?? ""; + string cate = json.cate ?? ""; + string name = json.name ?? ""; + string model = json.model ?? ""; + string scale = json.scale ?? ""; + string unit = json.unit ?? ""; + decimal price = json.price ?? 0m; + string supply = json.supply ?? ""; + string manu = json.manu ?? ""; + string storage = json.storage ?? ""; + bool disable = json.disable ?? false; + string memo = json.memo ?? ""; + string result = _bridge.Items_Save(idx, sid, cate, name, model, scale, unit, price, supply, manu, storage, disable, memo); + var response = new { type = "ITEMS_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "ITEMS_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.Items_Delete(idx); + var response = new { type = "ITEMS_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== UserList API ===== + case "USERLIST_GET_CURRENT_LEVEL": + { + string result = _bridge.UserList_GetCurrentLevel(); + var response = new { type = "USERLIST_CURRENT_LEVEL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_GET_DEPTS": + { + string result = _bridge.UserList_GetDepts(); + var response = new { type = "USERLIST_DEPTS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_GET_LIST": + { + string process = json.process ?? "%"; + string result = _bridge.UserList_GetList(process); + var response = new { type = "USERLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_GET_USER": + { + string userId = json.userId ?? ""; + string result = _bridge.UserList_GetUser(userId); + var response = new { type = "USERLIST_USER_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_SAVE_GROUP_USER": + { + string userId = json.userId ?? ""; + string dept = json.dept ?? ""; + int level = json.level ?? 1; + bool useUserState = json.useUserState ?? false; + bool useJobReport = json.useJobReport ?? false; + bool exceptHoly = json.exceptHoly ?? false; + string result = _bridge.UserList_SaveGroupUser(userId, dept, level, useUserState, useJobReport, exceptHoly); + var response = new { type = "USERLIST_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_SAVE_USER_FULL": + { + string userData = JsonConvert.SerializeObject(json.userData); + string result = _bridge.UserList_SaveUserFull(userData); + var response = new { type = "USERLIST_USER_FULL_SAVED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "USERLIST_DELETE_GROUP_USER": + { + string userId = json.userId ?? ""; + string result = _bridge.UserList_DeleteGroupUser(userId); + var response = new { type = "USERLIST_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + // ===== JobReport API (JobReport 뷰/테이블) ===== + case "JOBREPORT_GET_LIST": + { + string sd = json.sd ?? ""; + string ed = json.ed ?? ""; + string uid = json.uid ?? ""; + string cate = json.cate ?? ""; // 사용안함 (호환성) + string searchKey = json.searchKey ?? ""; + Console.WriteLine($"[WS] JOBREPORT_GET_LIST: sd={sd}, ed={ed}, uid={uid}, searchKey={searchKey}"); + string result = _bridge.Jobreport_GetList(sd, ed, uid, cate, searchKey); + var response = new { type = "JOBREPORT_LIST_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_GET_USERS": + { + string result = _bridge.Jobreport_GetUsers(); + var response = new { type = "JOBREPORT_USERS_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_GET_DETAIL": + { + int idx = json.idx ?? 0; + string result = _bridge.Jobreport_GetDetail(idx); + var response = new { type = "JOBREPORT_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_ADD": + { + string pdate = json.pdate ?? ""; + string projectName = json.projectName ?? ""; + string requestpart = json.requestpart ?? ""; + string package = json.package ?? ""; + string type1 = json.type ?? ""; + string process = json.process ?? ""; + string status = json.status ?? "진행 완료"; + string description = json.description ?? ""; + double hrs = json.hrs ?? 0.0; + double ot = json.ot ?? 0.0; + string jobgrp = json.jobgrp ?? ""; + string tag = json.tag ?? ""; + string result = _bridge.Jobreport_Add(pdate, projectName, requestpart, package, type1, process, status, description, hrs, ot, jobgrp, tag); + var response = new { type = "JOBREPORT_ADDED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_EDIT": + { + int idx = json.idx ?? 0; + string pdate = json.pdate ?? ""; + string projectName = json.projectName ?? ""; + string requestpart = json.requestpart ?? ""; + string package = json.package ?? ""; + string type2 = json.type ?? ""; + string process = json.process ?? ""; + string status = json.status ?? ""; + string description = json.description ?? ""; + double hrs = json.hrs ?? 0.0; + double ot = json.ot ?? 0.0; + string jobgrp = json.jobgrp ?? ""; + string tag = json.tag ?? ""; + string result = _bridge.Jobreport_Edit(idx, pdate, projectName, requestpart, package, type2, process, status, description, hrs, ot, jobgrp, tag); + var response = new { type = "JOBREPORT_EDITED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_DELETE": + { + int idx = json.idx ?? 0; + string result = _bridge.Jobreport_Delete(idx); + var response = new { type = "JOBREPORT_DELETED", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_GET_PERMISSION": + { + string targetUserId = json.targetUserId ?? ""; + string result = _bridge.Jobreport_GetPermission(targetUserId); + var response = new { type = "JOBREPORT_PERMISSION", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "JOBREPORT_GET_JOBTYPES": + { + string process = json.process ?? ""; + string result = _bridge.Jobreport_GetJobTypes(process); + var response = new { type = "JOBREPORT_JOBTYPES", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + case "GET_APP_VERSION": + { + string result = _bridge.GetAppVersion(); + var response = new { type = "APP_VERSION", data = JsonConvert.DeserializeObject(result) }; + await Send(socket, JsonConvert.SerializeObject(response)); + } + break; + + default: + Console.WriteLine($"[WS] Unknown message type: {type}"); + break; + } + } + catch (Exception ex) + { + Console.WriteLine($"[WS] Handle Error: {ex.Message}"); + } + } + + private async Task Send(WebSocket socket, string message) + { + if (_socketLocks.TryGetValue(socket, out var semaphore)) + { + await semaphore.WaitAsync(); + try + { + if (socket.State == WebSocketState.Open) + { + byte[] buffer = Encoding.UTF8.GetBytes(message); + await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + finally + { + semaphore.Release(); + } + } + } + + public async void Broadcast(string message) + { + byte[] buffer = Encoding.UTF8.GetBytes(message); + WebSocket[] clientsCopy; + + lock (_clients) + { + clientsCopy = _clients.ToArray(); + } + + foreach (var client in clientsCopy) + { + if (client.State == WebSocketState.Open && _socketLocks.TryGetValue(client, out var semaphore)) + { + _ = Task.Run(async () => + { + if (await semaphore.WaitAsync(0)) + { + try + { + if (client.State == WebSocketState.Open) + { + await client.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); + } + } + catch { } + finally + { + semaphore.Release(); + } + } + }); + } + } + } + } +} diff --git a/Project/Web/WebSocketServer.cs b/Project/Web/WebSocketServer.cs deleted file mode 100644 index 7d700ca..0000000 --- a/Project/Web/WebSocketServer.cs +++ /dev/null @@ -1,221 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using System.Windows.Forms; - -namespace Project.Web -{ - public class WebSocketServer - { - private HttpListener _httpListener; - private List _clients = new List(); - private Dialog.fDashboardNew _mainForm; - - public WebSocketServer(string url, Dialog.fDashboardNew form) - { - _mainForm = form; - _httpListener = new HttpListener(); - _httpListener.Prefixes.Add(url); - _httpListener.Start(); - Console.WriteLine($"[WS] Listening on {url}"); - Task.Run(AcceptConnections); - } - - private async Task AcceptConnections() - { - while (_httpListener.IsListening) - { - try - { - var context = await _httpListener.GetContextAsync(); - if (context.Request.IsWebSocketRequest) - { - ProcessRequest(context); - } - else - { - context.Response.StatusCode = 400; - context.Response.Close(); - } - } - catch (Exception ex) - { - Console.WriteLine($"[WS] Error: {ex.Message}"); - } - } - } - - private System.Collections.Concurrent.ConcurrentDictionary _socketLocks = new System.Collections.Concurrent.ConcurrentDictionary(); - - private async void ProcessRequest(HttpListenerContext context) - { - WebSocketContext wsContext = null; - try - { - wsContext = await context.AcceptWebSocketAsync(subProtocol: null); - WebSocket socket = wsContext.WebSocket; - _socketLocks.TryAdd(socket, new SemaphoreSlim(1, 1)); - - lock (_clients) { _clients.Add(socket); } - Console.WriteLine("[WS] Client Connected"); - - await ReceiveLoop(socket); - } - catch (Exception ex) - { - Console.WriteLine($"[WS] Accept Error: {ex.Message}"); - } - finally - { - if (wsContext != null) - { - WebSocket socket = wsContext.WebSocket; - lock (_clients) { _clients.Remove(socket); } - - if (_socketLocks.TryRemove(socket, out var semaphore)) - { - semaphore.Dispose(); - } - socket.Dispose(); - } - } - } - - private async Task ReceiveLoop(WebSocket socket) - { - var buffer = new byte[1024 * 4]; - while (socket.State == WebSocketState.Open) - { - try - { - var result = await socket.ReceiveAsync(new ArraySegment(buffer), CancellationToken.None); - if (result.MessageType == WebSocketMessageType.Close) - { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", CancellationToken.None); - } - else if (result.MessageType == WebSocketMessageType.Text) - { - string msg = Encoding.UTF8.GetString(buffer, 0, result.Count); - HandleMessage(msg, socket); - } - } - catch - { - break; - } - } - } - - private async void HandleMessage(string msg, WebSocket socket) - { - // Simple JSON parsing (manual or Newtonsoft) - // Expected format: { "type": "...", "data": ... } - try - { - dynamic json = Newtonsoft.Json.JsonConvert.DeserializeObject(msg); - string type = json.type; - - Console.WriteLine($"HandleMessage:{type}"); - if (type == "GET_CONFIG") - { - //// Simulate Delay for Loading Screen Test - //await Task.Delay(1000); - - //// Send Config back - //var bridge = new MachineBridge(_mainForm); // Re-use logic - //string configJson = bridge.GetConfig(); - //var response = new { type = "CONFIG_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(configJson) }; - //await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); - } - else if (type == "GET_IO_LIST") - { - // var bridge = new MachineBridge(_mainForm); - // string ioJson = bridge.GetIOList(); - // var response = new { type = "IO_LIST_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(ioJson) }; - // await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); - //} - //else if (type == "GET_RECIPE_LIST") - //{ - // var bridge = new MachineBridge(_mainForm); - // string recipeJson = bridge.GetRecipeList(); - // var response = new { type = "RECIPE_LIST_DATA", data = Newtonsoft.Json.JsonConvert.DeserializeObject(recipeJson) }; - // await Send(socket, Newtonsoft.Json.JsonConvert.SerializeObject(response)); - } - else if (type == "SAVE_CONFIG") - { - //string configJson = Newtonsoft.Json.JsonConvert.SerializeObject(json.data); - //var bridge = new MachineBridge(_mainForm); - //bridge.SaveConfig(configJson); - } - - } - catch (Exception ex) - { - Console.WriteLine($"[WS] Msg Error: {ex.Message}"); - } - } - - private async Task Send(WebSocket socket, string message) - { - if (_socketLocks.TryGetValue(socket, out var semaphore)) - { - await semaphore.WaitAsync(); - try - { - if (socket.State == WebSocketState.Open) - { - byte[] buffer = Encoding.UTF8.GetBytes(message); - await socket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); - } - } - finally - { - semaphore.Release(); - } - } - } - - public async void Broadcast(string message) - { - byte[] buffer = Encoding.UTF8.GetBytes(message); - WebSocket[] clientsCopy; - - lock (_clients) - { - clientsCopy = _clients.ToArray(); - } - - foreach (var client in clientsCopy) - { - if (client.State == WebSocketState.Open && _socketLocks.TryGetValue(client, out var semaphore)) - { - // Fire and forget, but safely - _ = Task.Run(async () => - { - // Try to get lock immediately. If busy (sending previous frame), skip this frame to prevent lag. - if (await semaphore.WaitAsync(0)) - { - try - { - if (client.State == WebSocketState.Open) - { - await client.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); - } - } - catch { /* Ignore send errors */ } - finally - { - semaphore.Release(); - } - } - }); - } - } - } - } -} diff --git a/Project/Web/wwwroot/Todo/index.html b/Project/Web/wwwroot/Todo/index.html index 9b9363f..4896758 100644 --- a/Project/Web/wwwroot/Todo/index.html +++ b/Project/Web/wwwroot/Todo/index.html @@ -706,7 +706,7 @@ showLoading(); try { - const jsonStr = await machine.Todo_CreateTodo(title, remark, expire, seqno, flag, request, status); + const jsonStr = await machine.CreateTodo(title, remark, expire, seqno, flag, request, status); const data = JSON.parse(jsonStr); if (data.Success) { diff --git a/Project/fMain.Designer.cs b/Project/fMain.Designer.cs index 23317be..156c7a5 100644 --- a/Project/fMain.Designer.cs +++ b/Project/fMain.Designer.cs @@ -44,7 +44,6 @@ this.sbChat = new System.Windows.Forms.ToolStripStatusLabel(); this.menuStrip1 = new System.Windows.Forms.MenuStrip(); this.btSetting = new System.Windows.Forms.ToolStripMenuItem(); - this.로그인ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.commonToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.codesToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); this.itemsToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem(); @@ -245,7 +244,6 @@ this.menuStrip1.Font = new System.Drawing.Font("맑은 고딕", 10F); this.menuStrip1.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.btSetting, - this.로그인ToolStripMenuItem, this.commonToolStripMenuItem, this.managementToolStripMenuItem, this.mn_docu, @@ -269,14 +267,6 @@ this.btSetting.Text = "설정"; this.btSetting.Click += new System.EventHandler(this.settingToolStripMenuItem_Click); // - // 로그인ToolStripMenuItem - // - this.로그인ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("로그인ToolStripMenuItem.Image"))); - this.로그인ToolStripMenuItem.Name = "로그인ToolStripMenuItem"; - this.로그인ToolStripMenuItem.Size = new System.Drawing.Size(79, 23); - this.로그인ToolStripMenuItem.Text = "로그인"; - this.로그인ToolStripMenuItem.Click += new System.EventHandler(this.로그인ToolStripMenuItem_Click); - // // commonToolStripMenuItem // this.commonToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] { @@ -1269,7 +1259,6 @@ private System.Windows.Forms.ToolStripMenuItem mn_project; private System.Windows.Forms.ToolStripMenuItem projectImportCompleteToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem purchaseOrderImportToolStripMenuItem; - private System.Windows.Forms.ToolStripMenuItem 로그인ToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem 메일전송ToolStripMenuItem; private System.Windows.Forms.ToolStripMenuItem mn_dailyhistory; private System.Windows.Forms.ToolStripMenuItem 패치내역ToolStripMenuItem1; diff --git a/Project/fMain.cs b/Project/fMain.cs index c0f73a7..186ac5b 100644 --- a/Project/fMain.cs +++ b/Project/fMain.cs @@ -158,14 +158,15 @@ namespace Project Console.WriteLine($"WebView2 초기화 상태: {Pub.InitWebView}"); - Func_Login(); + // WebView2 로그인이 아닌 경우에만 여기서 후처리 실행 // WebView2 로그인의 경우 OnLoginCompleted()에서 호출됨 - if (Pub.InitWebView != 1) - { - OnLoginCompleted(); - } + //if (Pub.InitWebView != 1) + //{ + Menu_Dashboard(); + //OnLoginCompleted(); + //} } /// @@ -236,22 +237,22 @@ namespace Project Util.RunExplorer(cmd); } - void Func_Login() - { - this.sbWeb.Text = $"WebView:{Pub.InitWebView}"; - if (Pub.InitWebView == 1) - { - // WebView2 기반 대시보드 로그인 - Menu_Dashboard(); - } - else - { - // 기존 WinForms 로그인 - using (var f = new Dialog.fLogin()) - if (f.ShowDialog() != System.Windows.Forms.DialogResult.OK) - Application.ExitThread(); - } - } + //void Func_Login() + //{ + // this.sbWeb.Text = $"WebView:{Pub.InitWebView}"; + // if (Pub.InitWebView == 1) + // { + // // WebView2 기반 대시보드 로그인 + // Menu_Dashboard(); + // } + // else + // { + // // 기존 WinForms 로그인 + // using (var f = new Dialog.fLogin()) + // if (f.ShowDialog() != System.Windows.Forms.DialogResult.OK) + // Application.ExitThread(); + // } + //} void Func_RunStartForm() { var menu_purchaseVisible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_purchase); @@ -658,11 +659,11 @@ namespace Project f.Show(); } - private void 로그인ToolStripMenuItem_Click(object sender, EventArgs e) - { - CloseAllForm(); - Func_Login(); - } + //private void 로그인ToolStripMenuItem_Click(object sender, EventArgs e) + //{ + // CloseAllForm(); + // Func_Login(); + //} void CloseAllForm() { @@ -675,7 +676,25 @@ namespace Project tabControl1.TabPages.Remove(tab); this.tabControl1.Refresh(); } + } + /// + /// MachineBridge에서 호출 가능한 public 메서드 + /// + public void CloseAllFormPublic() + { + CloseAllForm(); + + // 상태바 정보 초기화 + sbLogin.Text = ""; + sbLoginUseTime.Text = ""; + + // 메뉴/툴바 비활성화 + menuStrip1.Enabled = false; + toolStrip1.Enabled = false; + btDev.Visible = false; + + Menu_Dashboard(); } private void 메일전송ToolStripMenuItem_Click(object sender, EventArgs e) @@ -1463,14 +1482,15 @@ namespace Project } - Dialog.fDashboardNew fdashboard = null; + //Dialog.fDashboard fdashboard = null; void Menu_Dashboard() { string formkey = "DASHBOARD"; if (!ShowForm(formkey)) { if (fdashboard == null || fdashboard.IsDisposed) - fdashboard = new Dialog.fDashboardNew(); + fdashboard = new Dialog.fDashboard(); + AddForm(formkey, fdashboard); } } @@ -1528,14 +1548,15 @@ namespace Project f.ShowDialog(); } + Dialog.fDashboard fdashboard = null; private void tabControl1_SelectedIndexChanged(object sender, EventArgs e) { - if(this.tabControl1.SelectedIndex == 0) + if (this.tabControl1.SelectedIndex == 0) { if (fdashboard != null) { fdashboard.RefreshView(); - Console.WriteLine( "view update"); + Console.WriteLine("view update"); } } diff --git a/Project/fSystemCheck.cs b/Project/fSystemCheck.cs index 2c4f2cd..fae4472 100644 --- a/Project/fSystemCheck.cs +++ b/Project/fSystemCheck.cs @@ -12,7 +12,6 @@ using System.Net; using System.IO.Compression; using System.IO; using FCOMMON; -using Microsoft.Owin.Hosting; using System.Diagnostics; namespace Project.Dialog diff --git a/Project/frontend/.gitignore b/Project/frontend/.gitignore new file mode 100644 index 0000000..e2b8924 --- /dev/null +++ b/Project/frontend/.gitignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Local env files +.env +.env.local +.env.*.local + +# Log files +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor directories and files +.idea/ +.vscode/ +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +# OS generated files +.DS_Store +Thumbs.db diff --git a/Project/frontend/README.md b/Project/frontend/README.md new file mode 100644 index 0000000..53f80e8 --- /dev/null +++ b/Project/frontend/README.md @@ -0,0 +1,148 @@ +# GroupWare React Frontend + +GroupWare 시스템의 React 기반 프론트엔드입니다. + +## 기술 스택 + +- **React 18** - UI 라이브러리 +- **TypeScript** - 타입 안정성 +- **Vite** - 빌드 도구 및 개발 서버 +- **Tailwind CSS** - 스타일링 +- **React Router v7** - 라우팅 +- **Lucide React** - 아이콘 + +## 프로젝트 구조 + +``` +frontend/ +├── src/ +│ ├── components/ +│ │ └── layout/ # 레이아웃 컴포넌트 +│ │ ├── Header.tsx +│ │ ├── Layout.tsx +│ │ └── Navigation.tsx +│ ├── pages/ # 페이지 컴포넌트 +│ │ ├── Dashboard.tsx +│ │ ├── Todo.tsx +│ │ └── Placeholder.tsx +│ ├── communication.ts # 통신 레이어 (WebView2 + WebSocket) +│ ├── types.ts # TypeScript 타입 정의 +│ ├── App.tsx # 메인 앱 컴포넌트 +│ ├── main.tsx # 엔트리 포인트 +│ └── index.css # 글로벌 스타일 +├── package.json +├── vite.config.ts +├── tailwind.config.js +├── tsconfig.json +├── build.bat # 프로덕션 빌드 스크립트 +└── run-dev.bat # 개발 서버 실행 스크립트 +``` + +## 개발 환경 설정 + +### 1. 의존성 설치 + +```bash +cd Project/frontend +npm install +``` + +### 2. 개발 서버 실행 + +```bash +npm run dev +# 또는 +run-dev.bat +``` + +개발 서버는 `http://localhost:5173`에서 실행됩니다. + +### 3. 핫 리로드 개발 + +개발 모드에서는 WebSocket(포트 8082)을 통해 C# 백엔드와 통신합니다. +GroupWare 애플리케이션에서 WebSocket 서버가 실행 중이어야 합니다. + +## 프로덕션 빌드 + +### 빌드 실행 + +```bash +npm run build +# 또는 +build.bat +``` + +빌드 결과물은 `dist/` 폴더에 생성되고, +`build.bat`을 사용하면 자동으로 `wwwroot/react-app/`으로 복사됩니다. + +### 접근 URL + +- **개발**: `http://localhost:5173` +- **프로덕션**: `http://localhost:7979/react-app/` + +## 통신 방식 + +### 듀얼 모드 통신 + +이 프로젝트는 두 가지 통신 방식을 지원합니다: + +| 환경 | 통신 방식 | 포트 | +|------|-----------|------| +| WebView2 (프로덕션) | HostObject 직접 호출 | - | +| Browser (개발) | WebSocket | 8082 | + +### 자동 감지 + +`communication.ts`에서 실행 환경을 자동으로 감지하여 적절한 통신 방식을 사용합니다: + +```typescript +const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview; + +if (isWebView) { + // WebView2 HostObject 사용 + const result = await machine.Todo_GetTodos(); +} else { + // WebSocket 사용 + ws.send(JSON.stringify({ type: 'GET_TODOS' })); +} +``` + +## 페이지 목록 + +| 경로 | 페이지 | 상태 | +|------|--------|------| +| `/` | 대시보드 | 완료 | +| `/todo` | 할일 관리 | 완료 | +| `/kuntae` | 근태 관리 | 개발 예정 | +| `/jobreport` | 업무 일지 | 개발 예정 | +| `/project` | 프로젝트 | 개발 예정 | +| `/common` | 공용 코드 | 개발 예정 | + +## 스타일 가이드 + +### 색상 팔레트 + +- **Primary**: Blue (`#3b82f6`) +- **Success**: Green (`#22c55e`) +- **Warning**: Amber (`#f59e0b`) +- **Danger**: Red (`#ef4444`) + +### Glass Effect + +```css +.glass-effect { + background: rgba(255, 255, 255, 0.25); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.18); +} +``` + +## C# WebSocket 서버 설정 + +`MachineBridge/WebSocketServer.cs`를 참고하여 WebSocket 서버를 시작하세요: + +```csharp +// 예시 코드 +var wsServer = new GroupWareWebSocketServer("http://localhost:8082/", bridge); +wsServer.Start(); +``` diff --git a/Project/frontend/build.bat b/Project/frontend/build.bat new file mode 100644 index 0000000..76ff707 --- /dev/null +++ b/Project/frontend/build.bat @@ -0,0 +1,50 @@ +@echo off +echo ======================================== +echo GroupWare Frontend Build Script +echo ======================================== + +REM 현재 디렉토리 저장 +set CURRENT_DIR=%CD% + +REM frontend 폴더로 이동 +cd /d "%~dp0" + +echo. +echo [1/3] Installing dependencies... +call npm install +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: npm install failed + pause + exit /b 1 +) + +echo. +echo [2/3] Building production... +call npm run build +if %ERRORLEVEL% NEQ 0 ( + echo ERROR: npm build failed + pause + exit /b 1 +) + +echo. +echo [3/3] Copying dist to wwwroot... +REM 기존 react 폴더 삭제 (있으면) +if exist "..\Web\wwwroot\react-app" rmdir /s /q "..\Web\wwwroot\react-app" + +REM dist 폴더를 wwwroot\react-app으로 복사 +xcopy /e /i /y "dist" "..\Web\wwwroot\react-app" + +echo. +echo ======================================== +echo Build completed successfully! +echo Output: Project\Web\wwwroot\react-app +echo ======================================== +echo. +echo Access via: http://localhost:7979/react-app/ +echo. + +REM 원래 디렉토리로 복귀 +cd /d "%CURRENT_DIR%" + +pause diff --git a/Project/frontend/index.html b/Project/frontend/index.html new file mode 100644 index 0000000..2578c1c --- /dev/null +++ b/Project/frontend/index.html @@ -0,0 +1,16 @@ + + + + + + + + + + GroupWare + + +
+ + + diff --git a/Project/frontend/package-lock.json b/Project/frontend/package-lock.json new file mode 100644 index 0000000..0a097fd --- /dev/null +++ b/Project/frontend/package-lock.json @@ -0,0 +1,2718 @@ +{ + "name": "groupware-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "groupware-frontend", + "version": "1.0.0", + "dependencies": { + "clsx": "^2.1.0", + "lucide-react": "^0.303.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^7.9.6", + "tailwind-merge": "^2.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "^5.0.10" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", + "integrity": "sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.27.0", + "caniuse-lite": "^1.0.30001754", + "fraction.js": "^5.3.4", + "normalize-range": "^0.1.2", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.31", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.31.tgz", + "integrity": "sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", + "integrity": "sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.25", + "caniuse-lite": "^1.0.30001754", + "electron-to-chromium": "^1.5.249", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.1.4" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001757", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001757.tgz", + "integrity": "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.0.tgz", + "integrity": "sha512-vXiThu1/rlos7EGu8TuNZQEg2e9TvhH9dmS4T4ZVzB7Ao1agEZ6EG3sn5n+hZRYUgduISd1HpngFzAZiDGm5vQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.260", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.260.tgz", + "integrity": "sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==", + "dev": true, + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.303.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.303.0.tgz", + "integrity": "sha512-B0B9T3dLEFBYPCUlnUS1mvAhW1craSbF9HO+JfBjAtpFUJ7gMIqmEwNSclikY3RiN2OnCkj/V1ReAQpaHae8Bg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", + "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.6.tgz", + "integrity": "sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.9.6", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.6.tgz", + "integrity": "sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==", + "license": "MIT", + "dependencies": { + "react-router": "7.9.6" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwind-merge": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz", + "integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.18.tgz", + "integrity": "sha512-6A2rnmW5xZMdw11LYjhcI5846rt9pbLSabY5XPxo+XWdxwZaFEn47Go4NzFiHu9sNNmr/kXivP1vStfvMaK1GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.6.0", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.7", + "lilconfig": "^3.1.3", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", + "postcss-nested": "^6.2.0", + "postcss-selector-parser": "^6.1.2", + "resolve": "^1.22.8", + "sucrase": "^3.35.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + } + } +} diff --git a/Project/frontend/package.json b/Project/frontend/package.json new file mode 100644 index 0000000..b6887b5 --- /dev/null +++ b/Project/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "groupware-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "clsx": "^2.1.0", + "lucide-react": "^0.303.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^7.9.6", + "tailwind-merge": "^2.2.0" + }, + "devDependencies": { + "@types/react": "^18.2.43", + "@types/react-dom": "^18.2.17", + "@vitejs/plugin-react": "^4.2.1", + "autoprefixer": "^10.4.16", + "postcss": "^8.4.32", + "tailwindcss": "^3.4.0", + "typescript": "^5.3.3", + "vite": "^5.0.10" + } +} diff --git a/Project/frontend/postcss.config.js b/Project/frontend/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/Project/frontend/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/Project/frontend/run-dev.bat b/Project/frontend/run-dev.bat new file mode 100644 index 0000000..a7151e5 --- /dev/null +++ b/Project/frontend/run-dev.bat @@ -0,0 +1,18 @@ +@echo off +echo ======================================== +echo GroupWare Frontend Development Server +echo ======================================== + +REM frontend 폴더로 이동 +cd /d "%~dp0" + +echo. +echo Starting Vite development server... +echo. +echo [INFO] Make sure GroupWare application is running +echo [INFO] WebSocket server should be on port 8082 +echo. +echo Press Ctrl+C to stop the server +echo. + +call npm run dev diff --git a/Project/frontend/src/App.tsx b/Project/frontend/src/App.tsx new file mode 100644 index 0000000..68c7e9e --- /dev/null +++ b/Project/frontend/src/App.tsx @@ -0,0 +1,99 @@ +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 { comms } from '@/communication'; +import { UserInfo } from '@/types'; +import { Loader2 } from 'lucide-react'; + +export default function App() { + const [isConnected, setIsConnected] = useState(false); + const [isLoggedIn, setIsLoggedIn] = useState(null); // null = 체크 중 + const [user, setUser] = useState(null); + + useEffect(() => { + // 통신 상태 구독 + const unsubscribe = comms.subscribe((msg: unknown) => { + const message = msg as { type?: string; connected?: boolean }; + if (message?.type === 'CONNECTION_STATE') { + setIsConnected(message.connected ?? false); + // 연결되면 로그인 상태 체크 + if (message.connected) { + checkLoginStatus(); + } + } + }); + + // 초기 연결 상태 설정 + setIsConnected(comms.getConnectionState()); + + // 연결되어 있으면 바로 로그인 상태 체크 + if (comms.getConnectionState()) { + checkLoginStatus(); + } + + return () => { + unsubscribe(); + }; + }, []); + + const checkLoginStatus = async () => { + try { + const result = await comms.checkLoginStatus(); + if (result.Success) { + setIsLoggedIn(result.IsLoggedIn); + setUser(result.User); + } else { + setIsLoggedIn(false); + setUser(null); + } + } catch (err) { + console.error('로그인 상태 체크 실패:', err); + setIsLoggedIn(false); + setUser(null); + } + }; + + const handleLoginSuccess = () => { + checkLoginStatus(); + }; + + // 로그인 상태 체크 중 + if (isLoggedIn === null) { + return ( +
+
+ +

로그인 상태 확인 중...

+
+
+ ); + } + + // 로그인 안됨 → 로그인 화면 표시 + if (!isLoggedIn) { + return ; + } + + // 로그인 됨 → 메인 앱 표시 + return ( + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); +} diff --git a/Project/frontend/src/communication.ts b/Project/frontend/src/communication.ts new file mode 100644 index 0000000..6b6aeb8 --- /dev/null +++ b/Project/frontend/src/communication.ts @@ -0,0 +1,755 @@ +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'; + +// WebView2 환경인지 체크 +const isWebView = typeof window !== 'undefined' && !!window.chrome?.webview; + +// 비동기 프록시 캐싱 (한 번만 초기화) +const machine: MachineBridgeInterface | null = isWebView + ? window.chrome!.webview!.hostObjects.machine + : null; + +type MessageCallback = (data: unknown) => void; + +class CommunicationLayer { + private listeners: MessageCallback[] = []; + private ws: WebSocket | null = null; + private isConnected = false; + private wsUrl = 'ws://localhost:8082'; // GroupWare WebSocket 포트 + + constructor() { + if (isWebView) { + console.log("[COMM] Running in WebView2 Mode (HostObject)"); + this.isConnected = true; + window.chrome!.webview!.addEventListener('message', (event: MessageEvent) => { + this.notifyListeners(event.data); + }); + // WebView2 환경에서도 연결 상태 알림 + setTimeout(() => { + this.notifyListeners({ type: 'CONNECTION_STATE', connected: true }); + }, 0); + } else { + console.log("[COMM] Running in Browser Mode (WebSocket)"); + this.connectWebSocket(); + } + } + + public isWebViewMode(): boolean { + return isWebView; + } + + private connectWebSocket() { + this.ws = new WebSocket(this.wsUrl); + + this.ws.onopen = () => { + console.log("[COMM] WebSocket Connected"); + this.isConnected = true; + this.notifyListeners({ type: 'CONNECTION_STATE', connected: true }); + }; + + this.ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + this.notifyListeners(data); + } catch (e) { + console.error("[COMM] JSON Parse Error", e); + } + }; + + this.ws.onclose = () => { + console.log("[COMM] WebSocket Closed. Reconnecting..."); + this.isConnected = false; + this.notifyListeners({ type: 'CONNECTION_STATE', connected: false }); + setTimeout(() => this.connectWebSocket(), 2000); + }; + + this.ws.onerror = (err) => { + console.error("[COMM] WebSocket Error", err); + }; + } + + private notifyListeners(data: unknown) { + this.listeners.forEach(cb => cb(data)); + } + + public subscribe(callback: MessageCallback) { + this.listeners.push(callback); + return () => { + this.listeners = this.listeners.filter(cb => cb !== callback); + }; + } + + public getConnectionState(): boolean { + return this.isConnected; + } + + // WebSocket 요청-응답 헬퍼 + private wsRequest(requestType: string, responseType: string, params?: Record): Promise { + return new Promise((resolve, reject) => { + if (!this.isConnected) { + setTimeout(() => { + if (!this.isConnected) reject(new Error("WebSocket connection timeout")); + }, 2000); + } + + const timeoutId = setTimeout(() => { + this.listeners = this.listeners.filter(cb => cb !== handler); + reject(new Error(`${requestType} timeout`)); + }, 10000); + + const handler = (data: unknown) => { + const msg = data as { type: string; data?: T; Success?: boolean; Message?: string }; + if (msg.type === responseType) { + clearTimeout(timeoutId); + this.listeners = this.listeners.filter(cb => cb !== handler); + resolve(msg.data as T); + } + }; + this.listeners.push(handler); + this.ws?.send(JSON.stringify({ ...params, type: requestType })); + }); + } + + // ===== Todo API ===== + + public async getTodos(): Promise> { + if (isWebView && machine) { + const result = await machine.Todo_GetTodos(); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_TODOS', 'TODOS_DATA'); + } + } + + public async getTodo(id: number): Promise> { + if (isWebView && machine) { + const result = await machine.Todo_GetTodo(id); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_TODO', 'TODO_DATA', { id }); + } + } + + public async createTodo( + title: string, + remark: string, + expire: string | null, + seqno: number, + flag: boolean, + request: string | null, + status: string + ): Promise> { + if (isWebView && machine) { + const result = await machine.CreateTodo(title, remark, expire, seqno, flag, request, status); + return JSON.parse(result); + } else { + return this.wsRequest>('CREATE_TODO', 'TODO_CREATED', { + title, remark, expire, seqno, flag, request, status + }); + } + } + + public async updateTodo( + idx: number, + title: string, + remark: string, + expire: string | null, + seqno: number, + flag: boolean, + request: string | null, + status: string + ): Promise { + if (isWebView && machine) { + const result = await machine.Todo_UpdateTodo(idx, title, remark, expire, seqno, flag, request, status); + return JSON.parse(result); + } else { + return this.wsRequest('UPDATE_TODO', 'TODO_UPDATED', { + idx, title, remark, expire, seqno, flag, request, status + }); + } + } + + public async deleteTodo(id: number): Promise { + if (isWebView && machine) { + const result = await machine.Todo_DeleteTodo(id); + return JSON.parse(result); + } else { + return this.wsRequest('DELETE_TODO', 'TODO_DELETED', { id }); + } + } + + public async getUrgentTodos(): Promise> { + if (isWebView && machine) { + const result = await machine.GetUrgentTodos(); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_URGENT_TODOS', 'URGENT_TODOS_DATA'); + } + } + + // ===== Dashboard API ===== + + public async getPurchaseWaitCount(): Promise { + if (isWebView && machine) { + const result = await machine.GetPurchaseWaitCount(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_PURCHASE_WAIT_COUNT', 'PURCHASE_WAIT_COUNT_DATA'); + } + } + + public async getTodayCountH(): Promise { + if (isWebView && machine) { + const result = await machine.TodayCountH(); + return parseInt(result, 10); + } else { + const response = await this.wsRequest<{ count: number }>('GET_TODAY_COUNT_H', 'TODAY_COUNT_H_DATA'); + return response.count; + } + } + + public async getHolyUser(): Promise { + if (isWebView && machine) { + const result = await machine.GetHolyUser(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_HOLY_USER', 'HOLY_USER_DATA'); + } + } + + public async getHolyRequestUser(): Promise { + if (isWebView && machine) { + const result = await machine.GetHolyRequestUser(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_HOLY_REQUEST_USER', 'HOLY_REQUEST_USER_DATA'); + } + } + + public async getPurchaseNRList(): Promise { + if (isWebView && machine) { + const result = await machine.GetPurchaseNRList(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_PURCHASE_NR_LIST', 'PURCHASE_NR_LIST_DATA'); + } + } + + public async getPurchaseCRList(): Promise { + if (isWebView && machine) { + const result = await machine.GetPurchaseCRList(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_PURCHASE_CR_LIST', 'PURCHASE_CR_LIST_DATA'); + } + } + + public async getHolydayRequestCount(): Promise<{ HOLY: number; Message?: string }> { + if (isWebView && machine) { + const result = await machine.GetHolydayRequestCount(); + return JSON.parse(result); + } else { + return this.wsRequest<{ HOLY: number; Message?: string }>('GET_HOLYDAY_REQUEST_COUNT', 'HOLYDAY_REQUEST_COUNT_DATA'); + } + } + + public async getCurrentUserCount(): Promise<{ Count: number; Message?: string }> { + if (isWebView && machine) { + const result = await machine.GetCurrentUserCount(); + return JSON.parse(result); + } else { + return this.wsRequest<{ Count: number; Message?: string }>('GET_CURRENT_USER_COUNT', 'CURRENT_USER_COUNT_DATA'); + } + } + + // ===== Kuntae API ===== + + public async getKuntaeList(sd: string, ed: string): Promise> { + if (isWebView && machine) { + const result = await machine.Kuntae_GetList(sd, ed); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_KUNTAE_LIST', 'KUNTAE_LIST_DATA', { sd, ed }); + } + } + + public async deleteKuntae(id: number): Promise { + if (isWebView && machine) { + const result = await machine.Kuntae_Delete(id); + return JSON.parse(result); + } else { + return this.wsRequest('DELETE_KUNTAE', 'KUNTAE_DELETED', { id }); + } + } + + + // ===== Login API ===== + + public async checkLoginStatus(): Promise { + if (isWebView && machine) { + const result = await machine.CheckLoginStatus(); + return JSON.parse(result); + } else { + return this.wsRequest('CHECK_LOGIN_STATUS', 'LOGIN_STATUS_DATA'); + } + } + + public async login(gcode: string, id: string, password: string, rememberMe: boolean): Promise { + if (isWebView && machine) { + const result = await machine.Login(gcode, id, password, rememberMe); + return JSON.parse(result); + } else { + return this.wsRequest('LOGIN', 'LOGIN_RESULT', { gcode, id, password, rememberMe }); + } + } + + public async logout(): Promise { + if (isWebView && machine) { + const result = await machine.Logout(); + return JSON.parse(result); + } else { + return this.wsRequest('LOGOUT', 'LOGOUT_RESULT'); + } + } + + public async getUserGroups(): Promise { + if (isWebView && machine) { + const result = await machine.GetUserGroups(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_USER_GROUPS', 'USER_GROUPS_DATA'); + } + } + + public async getPreviousLoginInfo(): Promise { + if (isWebView && machine) { + const result = await machine.GetPreviousLoginInfo(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_PREVIOUS_LOGIN_INFO', 'PREVIOUS_LOGIN_INFO_DATA'); + } + } + + // ===== User API ===== + + public async getCurrentUserInfo(): Promise> { + if (isWebView && machine) { + const result = await machine.GetCurrentUserInfo(); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_CURRENT_USER_INFO', 'CURRENT_USER_INFO_DATA'); + } + } + + public async getUserInfoById(userId: string): Promise> { + if (isWebView && machine) { + const result = await machine.GetUserInfoById(userId); + return JSON.parse(result); + } else { + return this.wsRequest>('GET_USER_INFO_BY_ID', 'USER_INFO_DATA', { userId }); + } + } + + public async saveUserInfo(userData: UserInfoDetail): Promise { + if (isWebView && machine) { + const result = await machine.SaveUserInfo(JSON.stringify(userData)); + return JSON.parse(result); + } else { + return this.wsRequest('SAVE_USER_INFO', 'USER_INFO_SAVED', { userData }); + } + } + + public async changePassword(oldPassword: string, newPassword: string): Promise { + if (isWebView && machine) { + const result = await machine.ChangePassword(oldPassword, newPassword); + return JSON.parse(result); + } else { + return this.wsRequest('CHANGE_PASSWORD', 'PASSWORD_CHANGED', { oldPassword, newPassword }); + } + } + + // ===== Common Code API ===== + + public async getCommonGroups(): Promise { + if (isWebView && machine) { + const result = await machine.Common_GetGroups(); + return JSON.parse(result); + } else { + return this.wsRequest('COMMON_GET_GROUPS', 'COMMON_GROUPS_DATA'); + } + } + + public async getCommonList(grp: string): Promise { + if (isWebView && machine) { + const result = await machine.Common_GetList(grp); + return JSON.parse(result); + } else { + return this.wsRequest('COMMON_GET_LIST', 'COMMON_LIST_DATA', { grp }); + } + } + + public async saveCommon(data: CommonCode): Promise { + if (isWebView && machine) { + const result = await machine.Common_Save( + data.idx, data.grp, data.code, data.svalue, + data.ivalue, data.fvalue, data.svalue2 || '', data.memo + ); + return JSON.parse(result); + } else { + return this.wsRequest('COMMON_SAVE', 'COMMON_SAVED', data as unknown as Record); + } + } + + public async deleteCommon(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.Common_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('COMMON_DELETE', 'COMMON_DELETED', { idx }); + } + } + + // ===== Items API ===== + + public async getItemCategories(): Promise> { + if (isWebView && machine) { + const result = await machine.Items_GetCategories(); + return JSON.parse(result); + } else { + return this.wsRequest>('ITEMS_GET_CATEGORIES', 'ITEMS_CATEGORIES_DATA'); + } + } + + public async getItemList(category: string, searchKey: string): Promise { + if (isWebView && machine) { + const result = await machine.Items_GetList(category, searchKey); + return JSON.parse(result); + } else { + return this.wsRequest('ITEMS_GET_LIST', 'ITEMS_LIST_DATA', { category, searchKey }); + } + } + + public async saveItem(data: ItemInfo): Promise { + if (isWebView && machine) { + const result = await machine.Items_Save( + data.idx, data.sid, data.cate, data.name, data.model, + data.scale, data.unit, data.price, data.supply, + data.manu, data.storage, data.disable, data.memo + ); + return JSON.parse(result); + } else { + return this.wsRequest('ITEMS_SAVE', 'ITEMS_SAVED', data as unknown as Record); + } + } + + public async deleteItem(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.Items_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('ITEMS_DELETE', 'ITEMS_DELETED', { idx }); + } + } + + // ===== UserList API ===== + + public async getCurrentUserLevel(): Promise> { + if (isWebView && machine) { + const result = await machine.UserList_GetCurrentLevel(); + return JSON.parse(result); + } else { + return this.wsRequest>('USERLIST_GET_CURRENT_LEVEL', 'USERLIST_CURRENT_LEVEL_DATA'); + } + } + + public async getUserList(process: string): Promise { + if (isWebView && machine) { + const result = await machine.UserList_GetList(process); + return JSON.parse(result); + } else { + return this.wsRequest('USERLIST_GET_LIST', 'USERLIST_LIST_DATA', { process }); + } + } + + public async getUserListUser(userId: string): Promise> { + if (isWebView && machine) { + const result = await machine.UserList_GetUser(userId); + return JSON.parse(result); + } else { + return this.wsRequest>('USERLIST_GET_USER', 'USERLIST_USER_DATA', { userId }); + } + } + + public async saveGroupUser( + userId: string, dept: string, level: number, + useUserState: boolean, useJobReport: boolean, exceptHoly: boolean + ): Promise { + if (isWebView && machine) { + const result = await machine.UserList_SaveGroupUser(userId, dept, level, useUserState, useJobReport, exceptHoly); + return JSON.parse(result); + } else { + return this.wsRequest('USERLIST_SAVE_GROUP_USER', 'USERLIST_SAVED', { + userId, dept, level, useUserState, useJobReport, exceptHoly + }); + } + } + + public async saveUserFull(userData: UserFullData): Promise { + if (isWebView && machine) { + const result = await machine.UserList_SaveUserFull(JSON.stringify(userData)); + return JSON.parse(result); + } else { + return this.wsRequest('USERLIST_SAVE_USER_FULL', 'USERLIST_USER_FULL_SAVED', { userData }); + } + } + + public async deleteGroupUser(userId: string): Promise { + if (isWebView && machine) { + const result = await machine.UserList_DeleteGroupUser(userId); + return JSON.parse(result); + } else { + return this.wsRequest('USERLIST_DELETE_GROUP_USER', 'USERLIST_DELETED', { userId }); + } + } + + // ===== JobReport API (JobReport 뷰/테이블) ===== + + public async getJobReportList(sd: string, ed: string, uid: string = '', searchKey: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Jobreport_GetList(sd, ed, uid, '', searchKey); + return JSON.parse(result); + } else { + return this.wsRequest>('JOBREPORT_GET_LIST', 'JOBREPORT_LIST_DATA', { sd, ed, uid, searchKey }); + } + } + + public async getJobReportUsers(): Promise { + if (isWebView && machine) { + const result = await machine.Jobreport_GetUsers(); + return JSON.parse(result); + } else { + return this.wsRequest('JOBREPORT_GET_USERS', 'JOBREPORT_USERS_DATA'); + } + } + + public async getJobReportDetail(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.Jobreport_GetDetail(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('JOBREPORT_GET_DETAIL', 'JOBREPORT_DETAIL_DATA', { idx }); + } + } + + public async addJobReport( + pdate: string, projectName: string, 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); + 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 + }); + } + } + + public async editJobReport( + idx: number, pdate: string, projectName: string, 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); + 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 + }); + } + } + + public async deleteJobReport(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.Jobreport_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('JOBREPORT_DELETE', 'JOBREPORT_DELETED', { idx }); + } + } + + public async getJobReportPermission(targetUserId: string): Promise { + if (isWebView && machine) { + const result = await machine.Jobreport_GetPermission(targetUserId); + return JSON.parse(result); + } else { + return this.wsRequest('JOBREPORT_GET_PERMISSION', 'JOBREPORT_PERMISSION', { targetUserId }); + } + } + + public async getJobTypes(process: string = ''): Promise> { + if (isWebView && machine) { + const result = await machine.Jobreport_GetJobTypes(process); + return JSON.parse(result); + } else { + return this.wsRequest>('JOBREPORT_GET_JOBTYPES', 'JOBREPORT_JOBTYPES', { process }); + } + } + + public async getAppVersion(): Promise { + if (isWebView && machine) { + const result = await machine.GetAppVersion(); + return JSON.parse(result); + } else { + return this.wsRequest('GET_APP_VERSION', 'APP_VERSION', {}); + } + } + + // ===== Holiday API (월별근무표) ===== + + public async getHolidayList(month: string): Promise> { + if (isWebView && machine) { + const result = await machine.Holiday_GetList(month); + return JSON.parse(result); + } else { + return this.wsRequest>('HOLIDAY_GET_LIST', 'HOLIDAY_LIST_DATA', { month }); + } + } + + public async saveHolidays(month: string, holidays: HolidayItem[]): Promise { + if (isWebView && machine) { + const result = await machine.Holiday_Save(month, JSON.stringify(holidays)); + return JSON.parse(result); + } else { + return this.wsRequest('HOLIDAY_SAVE', 'HOLIDAY_SAVED', { month, holidays }); + } + } + + public async initializeHoliday(month: string): Promise> { + if (isWebView && machine) { + const result = await machine.Holiday_Initialize(month); + return JSON.parse(result); + } else { + return this.wsRequest>('HOLIDAY_INITIALIZE', 'HOLIDAY_INITIALIZED', { month }); + } + } + + // ===== MailForm API (메일양식) ===== + + public async getMailFormList(): Promise> { + if (isWebView && machine) { + const result = await machine.MailForm_GetList(); + return JSON.parse(result); + } else { + return this.wsRequest>('MAILFORM_GET_LIST', 'MAILFORM_LIST_DATA'); + } + } + + public async getMailFormDetail(idx: number): Promise> { + if (isWebView && machine) { + const result = await machine.MailForm_GetDetail(idx); + return JSON.parse(result); + } else { + return this.wsRequest>('MAILFORM_GET_DETAIL', 'MAILFORM_DETAIL_DATA', { idx }); + } + } + + public async addMailForm( + cate: string, title: string, tolist: string, bcc: string, cc: string, + subject: string, tail: string, body: string, selfTo: boolean, selfCC: boolean, + selfBCC: boolean, exceptmail: string, exceptmailcc: string + ): Promise { + if (isWebView && machine) { + const result = await machine.MailForm_Add(cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc); + return JSON.parse(result); + } else { + return this.wsRequest('MAILFORM_ADD', 'MAILFORM_ADDED', { + cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc + }); + } + } + + public async editMailForm( + idx: number, cate: string, title: string, tolist: string, bcc: string, cc: string, + subject: string, tail: string, body: string, selfTo: boolean, selfCC: boolean, + selfBCC: boolean, exceptmail: string, exceptmailcc: string + ): Promise { + if (isWebView && machine) { + const result = await machine.MailForm_Edit(idx, cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc); + return JSON.parse(result); + } else { + return this.wsRequest('MAILFORM_EDIT', 'MAILFORM_EDITED', { + idx, cate, title, tolist, bcc, cc, subject, tail, body, selfTo, selfCC, selfBCC, exceptmail, exceptmailcc + }); + } + } + + public async deleteMailForm(idx: number): Promise { + if (isWebView && machine) { + const result = await machine.MailForm_Delete(idx); + return JSON.parse(result); + } else { + return this.wsRequest('MAILFORM_DELETE', 'MAILFORM_DELETED', { idx }); + } + } + + // ===== UserGroup API (그룹정보/권한설정) ===== + + public async getUserGroupList(): Promise> { + if (isWebView && machine) { + const result = await machine.UserGroup_GetList(); + return JSON.parse(result); + } else { + return this.wsRequest>('USERGROUP_GET_LIST', 'USERGROUP_LIST_DATA'); + } + } + + public async addUserGroup( + dept: string, path_kj: string, permission: number, advpurchase: boolean, + advkisul: boolean, managerinfo: string, devinfo: string, usemail: boolean + ): Promise { + if (isWebView && machine) { + const result = await machine.UserGroup_Add(dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail); + return JSON.parse(result); + } else { + return this.wsRequest('USERGROUP_ADD', 'USERGROUP_ADDED', { + dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail + }); + } + } + + public async editUserGroup( + originalDept: string, dept: string, path_kj: string, permission: number, + advpurchase: boolean, advkisul: boolean, managerinfo: string, devinfo: string, usemail: boolean + ): Promise { + if (isWebView && machine) { + const result = await machine.UserGroup_Edit(originalDept, dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail); + return JSON.parse(result); + } else { + return this.wsRequest('USERGROUP_EDIT', 'USERGROUP_EDITED', { + originalDept, dept, path_kj, permission, advpurchase, advkisul, managerinfo, devinfo, usemail + }); + } + } + + public async deleteUserGroup(dept: string): Promise { + if (isWebView && machine) { + const result = await machine.UserGroup_Delete(dept); + return JSON.parse(result); + } else { + return this.wsRequest('USERGROUP_DELETE', 'USERGROUP_DELETED', { dept }); + } + } + + public async getPermissionInfo(): Promise> { + if (isWebView && machine) { + const result = await machine.UserGroup_GetPermissionInfo(); + return JSON.parse(result); + } else { + return this.wsRequest>('USERGROUP_GET_PERMISSION_INFO', 'USERGROUP_PERMISSION_INFO'); + } + } +} + +export const comms = new CommunicationLayer(); diff --git a/Project/frontend/src/components/items/ItemEditDialog.tsx b/Project/frontend/src/components/items/ItemEditDialog.tsx new file mode 100644 index 0000000..22c6ff1 --- /dev/null +++ b/Project/frontend/src/components/items/ItemEditDialog.tsx @@ -0,0 +1,244 @@ +import { useState, useEffect } from 'react'; +import { X, Save, Trash2 } from 'lucide-react'; +import { ItemInfo } from '@/types'; + +interface ItemEditDialogProps { + item: ItemInfo | null; + isOpen: boolean; + onClose: () => void; + onSave: (item: ItemInfo) => Promise; + onDelete: (idx: number) => Promise; +} + +export function ItemEditDialog({ item, isOpen, onClose, onSave, onDelete }: ItemEditDialogProps) { + const [editData, setEditData] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (item) { + setEditData({ ...item }); + } + }, [item]); + + if (!isOpen || !editData) return null; + + const isNew = editData.idx === 0; + + const handleSave = async () => { + if (!editData) return; + setSaving(true); + try { + await onSave(editData); + } finally { + setSaving(false); + } + }; + + const handleDelete = async () => { + if (!editData || isNew) return; + if (!confirm('삭제하시겠습니까?')) return; + setSaving(true); + try { + await onDelete(editData.idx); + } finally { + setSaving(false); + } + }; + + return ( +
+ {/* 배경 오버레이 */} +
+ + {/* 다이얼로그 */} +
+ {/* 헤더 */} +
+

+ {isNew ? '품목 추가' : '품목 편집'} +

+ +
+ + {/* 내용 */} +
+
+ {/* 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, 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 })} + 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, storage: e.target.value })} + className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white" + /> +
+ + {/* 메모 */} +
+ +