feat(service): Console_SendMail을 Windows 서비스로 변환
- MailService.cs 추가: ServiceBase 상속받는 Windows 서비스 클래스 - Program.cs 수정: 서비스/콘솔 모드 지원, 설치/제거 기능 추가 - 프로젝트 설정: System.ServiceProcess 참조 추가 - 배치 파일 추가: 서비스 설치/제거/콘솔실행 스크립트 주요 기능: - Windows 서비스로 백그라운드 실행 - 명령행 인수로 모드 선택 (-install, -uninstall, -console) - EventLog를 통한 서비스 로깅 - 안전한 서비스 시작/중지 처리 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
143
Project/Web/Controllers/APIController.cs
Normal file
143
Project/Web/Controllers/APIController.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class APIController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Getdata()
|
||||||
|
{
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
var sql = string.Empty;
|
||||||
|
var p_sql = getParams.Where(t => t.Key == "sql").FirstOrDefault();
|
||||||
|
if (p_sql.Key.isEmpty() == false) sql = p_sql.Value;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var p_table = getParams.Where(t => t.Key == "table").FirstOrDefault();
|
||||||
|
var p_gcode = getParams.Where(t => t.Key == "gcode").FirstOrDefault();
|
||||||
|
var p_where = getParams.Where(t => t.Key == "w").FirstOrDefault();
|
||||||
|
var p_order = getParams.Where(t => t.Key == "o").FirstOrDefault();
|
||||||
|
sql = "select * from {0} where gcode = '{gcode}'";
|
||||||
|
sql = string.Format(sql, p_table.Value, p_gcode.Value);
|
||||||
|
if (p_where.Key != null) sql += " and " + p_where.Value;
|
||||||
|
if (p_order.Key != null) sql += " order by " + p_order.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = sql.Replace("{gcode}", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs; // "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Gettable()
|
||||||
|
{
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
var sql = string.Empty;
|
||||||
|
var p_sql = getParams.Where(t => t.Key == "sql").FirstOrDefault();
|
||||||
|
if (p_sql.Key.isEmpty() == false) sql = p_sql.Value;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var p_table = getParams.Where(t => t.Key == "table").FirstOrDefault();
|
||||||
|
var p_gcode = getParams.Where(t => t.Key == "gcode").FirstOrDefault();
|
||||||
|
var p_where = getParams.Where(t => t.Key == "w").FirstOrDefault();
|
||||||
|
var p_order = getParams.Where(t => t.Key == "o").FirstOrDefault();
|
||||||
|
sql = "select * from {0} where gcode = '{gcode}'";
|
||||||
|
sql = string.Format(sql, p_table.Value, p_gcode.Value);
|
||||||
|
if (p_where.Key != null) sql += " and " + p_where.Value;
|
||||||
|
if (p_order.Key != null) sql += " order by " + p_order.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
sql = sql.Replace("{gcode}", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;// "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
267
Project/Web/Controllers/BaseController.cs
Normal file
267
Project/Web/Controllers/BaseController.cs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Project.Web.Model;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public struct MethodResult : IEquatable<MethodResult>
|
||||||
|
{
|
||||||
|
public string Content;
|
||||||
|
public byte[] Contentb;
|
||||||
|
public string Redirecturl;
|
||||||
|
|
||||||
|
public override bool Equals(object obj)
|
||||||
|
{
|
||||||
|
if (!(obj is MethodResult))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Equals((MethodResult)obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
|
if (Contentb == null)
|
||||||
|
return Content.GetHashCode() ^ Redirecturl.GetHashCode();
|
||||||
|
else
|
||||||
|
return Content.GetHashCode() ^ Redirecturl.GetHashCode() ^ Contentb.GetHexString().GetHashCode();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Equals(MethodResult other)
|
||||||
|
{
|
||||||
|
if (Content != other.Content)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Redirecturl != other.Redirecturl)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Contentb == other.Contentb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static bool operator ==(MethodResult point1, MethodResult point2)
|
||||||
|
{
|
||||||
|
return point1.Equals(point2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool operator !=(MethodResult point1, MethodResult point2)
|
||||||
|
{
|
||||||
|
return !point1.Equals(point2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class PostRequest : Attribute
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class BaseController : ApiController
|
||||||
|
{
|
||||||
|
public string QueryString { get; set; }
|
||||||
|
public string PostData { get; set; }
|
||||||
|
public string ParamData { get; set; }
|
||||||
|
|
||||||
|
protected string Trig_Ctrl { get; set; }
|
||||||
|
protected string Trig_func { get; set; }
|
||||||
|
|
||||||
|
public PageModel GetGlobalModel()
|
||||||
|
{
|
||||||
|
var config = RequestContext.Configuration;
|
||||||
|
var routeData = config.Routes.GetRouteData(Request).Values.ToList();
|
||||||
|
var name_ctrl = routeData[0].Value.ToString();
|
||||||
|
var name_action = routeData[1].Value.ToString();
|
||||||
|
|
||||||
|
|
||||||
|
return new PageModel
|
||||||
|
{
|
||||||
|
RouteData = routeData,
|
||||||
|
urlcontrol = name_ctrl,
|
||||||
|
urlaction = name_action
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public MethodResult View(bool nosubdir=false)
|
||||||
|
{
|
||||||
|
var config = RequestContext.Configuration;
|
||||||
|
if (config != null)
|
||||||
|
{
|
||||||
|
var routeData = config.Routes.GetRouteData(Request).Values.ToList();
|
||||||
|
var name_ctrl = routeData[0].Value.ToString();
|
||||||
|
if (nosubdir) name_ctrl = string.Empty;
|
||||||
|
var name_action = routeData[1].Value.ToString();
|
||||||
|
return View(name_ctrl, name_action);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return View(Trig_Ctrl + "/" + Trig_func);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static void ApplyCommonValue(ref string contents)
|
||||||
|
{
|
||||||
|
//메뉴 푸터 - 개발자 정보
|
||||||
|
if (contents.Contains("{title}"))
|
||||||
|
contents = contents.Replace("{title}", FCOMMON.info.Login.gcode + " Groupware");
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public MethodResult View(string Controller, string Action, Boolean applydefaultview = true)
|
||||||
|
{
|
||||||
|
var retval = new MethodResult();
|
||||||
|
|
||||||
|
if (Action.IndexOf(".") == -1)
|
||||||
|
Action += ".html"; //기본값 html 을 넣는다
|
||||||
|
|
||||||
|
var file = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", Controller, Action);
|
||||||
|
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(file) == false)
|
||||||
|
{
|
||||||
|
//error 폴더의 404.html 파일을 찾는다.
|
||||||
|
var file404 = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", "Error", "404.html");
|
||||||
|
if (System.IO.File.Exists(file404))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(file404, System.Text.Encoding.UTF8);
|
||||||
|
contents = contents.Replace("{errorfilename}", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
contents = "ERROR CODE - 404 (NOT FOUND) <br />" + file;
|
||||||
|
|
||||||
|
Console.WriteLine("view File not found : " + file);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//디폴트뷰의 내용을 가져온다 (있다면 적용한다)
|
||||||
|
if (applydefaultview)
|
||||||
|
{
|
||||||
|
//뷰파일이 있다면 그것을 적용한다
|
||||||
|
var laytoutfile = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", "Layout", "default.html");
|
||||||
|
if (System.IO.File.Exists(laytoutfile))
|
||||||
|
contents = System.IO.File.ReadAllText(laytoutfile, System.Text.Encoding.UTF8);
|
||||||
|
|
||||||
|
var fileContents = System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8);
|
||||||
|
if (String.IsNullOrEmpty(contents)) contents = fileContents;
|
||||||
|
else contents = contents.Replace("{contents}", fileContents);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//해당 뷰를 가져와서 반환한다
|
||||||
|
contents = System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//시스템변수 replace
|
||||||
|
contents = contents.Replace("{param_control}", Trig_Ctrl);
|
||||||
|
contents = contents.Replace("{param_function}", Trig_func);
|
||||||
|
|
||||||
|
retval.Content = contents;
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
public MethodResult View(string viewfilename, Boolean applydefaultview = true)
|
||||||
|
{
|
||||||
|
var retval = new MethodResult();
|
||||||
|
|
||||||
|
if (viewfilename.IndexOf(".") == -1)
|
||||||
|
viewfilename += ".html"; //기본값 html 을 넣는다
|
||||||
|
|
||||||
|
var file = AppDomain.CurrentDomain.BaseDirectory + "View" + viewfilename.Replace("/", "\\");
|
||||||
|
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(file) == false)
|
||||||
|
{
|
||||||
|
//error 폴더의 404.html 파일을 찾는다.
|
||||||
|
var file404 = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", "Error", "404.html");
|
||||||
|
if (System.IO.File.Exists(file404))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(file404, System.Text.Encoding.UTF8);
|
||||||
|
contents = contents.Replace("{errorfilename}", file);
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
contents = "ERROR CODE - 404 (NOT FOUND) <br />" + file;
|
||||||
|
|
||||||
|
Console.WriteLine("view File not found : " + file);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//디폴트뷰의 내용을 가져온다 (있다면 적용한다)
|
||||||
|
if (applydefaultview)
|
||||||
|
{
|
||||||
|
//뷰파일이 있다면 그것을 적용한다
|
||||||
|
var laytoutfile = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", "Layout", "default.html");
|
||||||
|
if (System.IO.File.Exists(laytoutfile))
|
||||||
|
contents = System.IO.File.ReadAllText(laytoutfile, System.Text.Encoding.UTF8);
|
||||||
|
|
||||||
|
var fileContents = System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8);
|
||||||
|
if (String.IsNullOrEmpty(contents)) contents = fileContents;
|
||||||
|
else contents = contents.Replace("{contents}", fileContents);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//해당 뷰를 가져와서 반환한다
|
||||||
|
contents = System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
//콘텐츠내의 file 을 찾아서 처리한다. ; 정규식의 처리속도가 느릴듯하여, 그냥 처리해본다
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var fileindexS = contents.IndexOf("{file:");
|
||||||
|
if (fileindexS == -1) break;
|
||||||
|
var fileindexE = contents.IndexOf("}", fileindexS);
|
||||||
|
if (fileindexE == -1) break;
|
||||||
|
if (fileindexE <= fileindexS + 5) break;
|
||||||
|
var inlinestr = contents.Substring(fileindexS, fileindexE - fileindexS + 1);
|
||||||
|
var filename = contents.Substring(fileindexS + 7, fileindexE - fileindexS - 8);
|
||||||
|
var load_file = String.Concat(AppDomain.CurrentDomain.BaseDirectory, "View", "\\", filename.Replace("/", "\\"));
|
||||||
|
load_file = load_file.Replace("\\\\", "\\");
|
||||||
|
String fileContents;// = String.Empty;
|
||||||
|
|
||||||
|
Console.WriteLine("file impot : " + load_file);
|
||||||
|
if (System.IO.File.Exists(load_file))
|
||||||
|
{
|
||||||
|
fileContents = System.IO.File.ReadAllText(load_file, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileContents = "{FileNotFound:" + filename + "}"; //파일이없다면 해당 부분은 오류 처리한다.
|
||||||
|
}
|
||||||
|
contents = contents.Replace(inlinestr, fileContents);
|
||||||
|
}
|
||||||
|
|
||||||
|
//시스템변수 replace
|
||||||
|
contents = contents.Replace("{param_control}", Trig_Ctrl);
|
||||||
|
contents = contents.Replace("{param_function}", Trig_func);
|
||||||
|
|
||||||
|
retval.Content = contents;
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
protected class Parameter
|
||||||
|
{
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public Parameter(string key_, string value_)
|
||||||
|
{
|
||||||
|
Key = key_;
|
||||||
|
Value = value_;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
454
Project/Web/Controllers/CommonController.cs
Normal file
454
Project/Web/Controllers/CommonController.cs
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
using FCM0000;
|
||||||
|
using Microsoft.Owin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class CommonController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetList(string grp=null)
|
||||||
|
{
|
||||||
|
var sql = string.Empty;
|
||||||
|
|
||||||
|
//코드그룹이 없다면 전체 목록을 조회할 수 있도록 99를 조회한다
|
||||||
|
if (string.IsNullOrEmpty(grp)) grp = "99";
|
||||||
|
|
||||||
|
// 특정 그룹의 데이터만 가져옴
|
||||||
|
sql = "select *" +
|
||||||
|
" from common" +
|
||||||
|
" where gcode = @gcode" +
|
||||||
|
" and grp = @grp" +
|
||||||
|
" order by code,svalue";
|
||||||
|
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(grp))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("grp", grp);
|
||||||
|
}
|
||||||
|
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
// 직접 파일을 읽어서 반환
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "Common.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 파일이 없으면 404 에러 페이지 또는 기본 메시지
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Save([FromBody] CommonModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var sql = string.Empty;
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand();
|
||||||
|
cmd.Connection = cn;
|
||||||
|
|
||||||
|
if (model.idx > 0)
|
||||||
|
{
|
||||||
|
// 업데이트
|
||||||
|
sql = @"UPDATE common SET
|
||||||
|
grp = @grp,
|
||||||
|
code = @code,
|
||||||
|
svalue = @svalue,
|
||||||
|
ivalue = @ivalue,
|
||||||
|
fvalue = @fvalue,
|
||||||
|
svalue2 = @svalue2,
|
||||||
|
memo = @memo,
|
||||||
|
wuid = @wuid,
|
||||||
|
wdate = GETDATE()
|
||||||
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 신규 추가
|
||||||
|
sql = @"INSERT INTO common (gcode, grp, code, svalue, ivalue, fvalue, svalue2, memo, wuid, wdate)
|
||||||
|
VALUES (@gcode, @grp, @code, @svalue, @ivalue, @fvalue, @svalue2, @memo, @wuid, GETDATE())";
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.CommandText = sql;
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("@grp", model.grp ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@code", model.code ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@svalue", model.svalue ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@ivalue", model.ivalue);
|
||||||
|
cmd.Parameters.AddWithValue("@fvalue", model.fvalue);
|
||||||
|
cmd.Parameters.AddWithValue("@svalue2", model.svalue2 ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@memo", model.memo ?? "");
|
||||||
|
cmd.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||||
|
|
||||||
|
if (model.idx > 0)
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddWithValue("@idx", model.idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
cn.Close();
|
||||||
|
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = result > 0,
|
||||||
|
Message = result > 0 ? "저장되었습니다." : "저장에 실패했습니다."
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "오류가 발생했습니다: " + ex.Message
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Delete([FromBody] DeleteModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var sql = "DELETE FROM common WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("@idx", model.idx);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
cn.Close();
|
||||||
|
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = result > 0,
|
||||||
|
Message = result > 0 ? "삭제되었습니다." : "삭제에 실패했습니다."
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "오류가 발생했습니다: " + ex.Message
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetGroups()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "select code, svalue, memo from common WITH (nolock) " +
|
||||||
|
"where gcode = @gcode and grp = '99' " +
|
||||||
|
"order by code";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage InitializeGroups()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
|
||||||
|
// 기본 그룹코드들 정의
|
||||||
|
var defaultGroups = new[]
|
||||||
|
{
|
||||||
|
new { code = "01", svalue = "부서코드" },
|
||||||
|
new { code = "02", svalue = "직급코드" },
|
||||||
|
new { code = "03", svalue = "공정코드" },
|
||||||
|
new { code = "04", svalue = "품목분류" },
|
||||||
|
new { code = "05", svalue = "업체분류" },
|
||||||
|
new { code = "06", svalue = "제조공정" },
|
||||||
|
new { code = "07", svalue = "장비제조" },
|
||||||
|
new { code = "08", svalue = "장비모델" },
|
||||||
|
new { code = "09", svalue = "장비기술" },
|
||||||
|
new { code = "99", svalue = "기타" }
|
||||||
|
};
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
|
||||||
|
int insertedCount = 0;
|
||||||
|
foreach (var group in defaultGroups)
|
||||||
|
{
|
||||||
|
// 이미 존재하는지 확인
|
||||||
|
var checkSql = "SELECT COUNT(*) FROM common WHERE gcode = @gcode AND grp = '99' AND code = @code";
|
||||||
|
var checkCmd = new System.Data.SqlClient.SqlCommand(checkSql, cn);
|
||||||
|
checkCmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
checkCmd.Parameters.AddWithValue("@code", group.code);
|
||||||
|
|
||||||
|
var exists = (int)checkCmd.ExecuteScalar() > 0;
|
||||||
|
checkCmd.Dispose();
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
// 새로 추가
|
||||||
|
var insertSql = @"INSERT INTO common (gcode, grp, code, svalue, ivalue, fvalue, svalue2, memo, wuid, wdate)
|
||||||
|
VALUES (@gcode, '99', @code, @svalue, 0, 0.0, '', '코드그룹 정의', @wuid, GETDATE())";
|
||||||
|
var insertCmd = new System.Data.SqlClient.SqlCommand(insertSql, cn);
|
||||||
|
insertCmd.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
insertCmd.Parameters.AddWithValue("@code", group.code);
|
||||||
|
insertCmd.Parameters.AddWithValue("@svalue", group.svalue);
|
||||||
|
insertCmd.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||||
|
|
||||||
|
insertCmd.ExecuteNonQuery();
|
||||||
|
insertCmd.Dispose();
|
||||||
|
insertedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cn.Close();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = $"그룹코드 초기화 완료. {insertedCount}개 추가됨."
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "오류가 발생했습니다: " + ex.Message
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetNavigationMenu()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 메뉴 정보를 하드코딩하거나 데이터베이스에서 가져올 수 있습니다.
|
||||||
|
// 향후 사용자 권한에 따른 메뉴 표시/숨김 기능도 추가 가능합니다.
|
||||||
|
var menuItems = new[]
|
||||||
|
{
|
||||||
|
new {
|
||||||
|
key = "dashboard",
|
||||||
|
title = "대시보드",
|
||||||
|
url = "/Dashboard/",
|
||||||
|
icon = "M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 1
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
key = "common",
|
||||||
|
title = "공용코드",
|
||||||
|
url = "/Common",
|
||||||
|
icon = "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 2
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
key = "jobreport",
|
||||||
|
title = "업무일지",
|
||||||
|
url = "/Jobreport/",
|
||||||
|
icon = "M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 3
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
key = "kuntae",
|
||||||
|
title = "근태관리",
|
||||||
|
url = "/Kuntae/",
|
||||||
|
icon = "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 4
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
key = "todo",
|
||||||
|
title = "할일관리",
|
||||||
|
url = "/Todo/",
|
||||||
|
icon = "M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 5
|
||||||
|
},
|
||||||
|
new {
|
||||||
|
key = "project",
|
||||||
|
title = "프로젝트",
|
||||||
|
url = "/Project/",
|
||||||
|
icon = "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10",
|
||||||
|
isVisible = true,
|
||||||
|
sortOrder = 6
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 사용자 권한에 따른 메뉴 필터링 로직을 여기에 추가할 수 있습니다.
|
||||||
|
// 예: var userLevel = FCOMMON.info.Login.level;
|
||||||
|
// if (userLevel < 5) { /* 특정 메뉴 숨김 */ }
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = menuItems,
|
||||||
|
Message = "메뉴 정보를 성공적으로 가져왔습니다."
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Data = (object)null,
|
||||||
|
Message = "메뉴 정보를 가져오는 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage CreateJsonResponse(object data)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
json,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DeleteModel
|
||||||
|
{
|
||||||
|
public int idx { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CommonModel
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
public int idx { get; set; } // 데이터고유번호
|
||||||
|
public string gcode { get; set; } // 그룹코드(데이터 그룹간 식별)
|
||||||
|
public string grp { get; set; } // 코드그룹
|
||||||
|
public string code { get; set; } // 코드
|
||||||
|
public string svalue { get; set; } // 값(문자열)
|
||||||
|
public int ivalue { get; set; } // 값(숫자)
|
||||||
|
public float fvalue { get; set; } // 값(실수)
|
||||||
|
public string memo { get; set; } // 비고
|
||||||
|
public string svalue2 { get; set; } // 값2(문자열)
|
||||||
|
public string wuid { get; set; } // 데이터기록자 사원번호
|
||||||
|
public string wdate { get; set; } // 데이터를기록한일시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
198
Project/Web/Controllers/CustomerController.cs
Normal file
198
Project/Web/Controllers/CustomerController.cs
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class CustomerController : BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody] string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string Test()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Find()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.CustomsTableAdapter();//.custrom EEEntitiesCommon();
|
||||||
|
var rows = db.GetData(FCOMMON.info.Login.gcode);// db.Customs.Where(t => t.gcode == FCOMMON.info.Login.gcode).OrderBy(t => t.name);
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.name}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.name2}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
//tbody.AppendLine($"<td>{item.model}</td>");
|
||||||
|
|
||||||
|
//if (item.price == null)
|
||||||
|
// tbody.AppendLine($"<td>--</td>");
|
||||||
|
//else
|
||||||
|
//{
|
||||||
|
// var price = (double)item.price / 1000.0;
|
||||||
|
|
||||||
|
// tbody.AppendLine($"<td>{price.ToString("N0")}</td>");
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
//tbody.AppendLine($"<td>{item.manu}</td>");
|
||||||
|
//tbody.AppendLine($"<td>{item.supply}</td>");
|
||||||
|
|
||||||
|
//if (item.remark.Length > 10)
|
||||||
|
// tbody.AppendLine($"<td>{item.remark.Substring(0, 10)}...</td>");
|
||||||
|
//else
|
||||||
|
// tbody.AppendLine($"<td>{item.remark}</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
//if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.CustomsTableAdapter();// EEEntitiesCommon();
|
||||||
|
var sd = DateTime.Now.ToString("yyyy-MM-01");
|
||||||
|
var rows = db.GetData(FCOMMON.info.Login.gcode);// .Customs.AsNoTracking().Where(t => t.gcode == FCOMMON.info.Login.gcode).OrderBy(t=>t.name);
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.grp}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name2}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.tel}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.fax}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.email}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.address}</td>");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty( item.memo)==false && item.memo.Length > 10) tbody.AppendLine($"<td>{item.memo.Substring(0, 10)}...</td>");
|
||||||
|
else tbody.AppendLine($"<td>{item.memo}</td>");
|
||||||
|
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
496
Project/Web/Controllers/DashBoardController.cs
Normal file
496
Project/Web/Controllers/DashBoardController.cs
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
using FCOMMON;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class DashBoardController : BaseController
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public void Index([FromBody] string value)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//// PUT api/values/5
|
||||||
|
//public void Put(int id, [FromBody] string value)
|
||||||
|
//{
|
||||||
|
//}
|
||||||
|
|
||||||
|
//// DELETE api/values/5
|
||||||
|
//public void Delete(int id)
|
||||||
|
//{
|
||||||
|
//}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string TodayCountH()
|
||||||
|
{
|
||||||
|
|
||||||
|
var sql = "select count(*) from EETGW_HolydayRequest WITH (nolock) " +
|
||||||
|
" where gcode = @gcode and isnull(conf,0) = 1 " +
|
||||||
|
" and sdate <= convert(varchar(10),GETDATE(),120) and edate >= convert(varchar(10),GETDATE(),120)";
|
||||||
|
|
||||||
|
var cn = DBM.getCn();
|
||||||
|
cn.Open();
|
||||||
|
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.Add("gcode", SqlDbType.VarChar).Value = FCOMMON.info.Login.gcode;
|
||||||
|
var cnt1 = (int)cmd.ExecuteScalar();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
return cnt1.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetHolydayRequestCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cn = DBM.getCn();
|
||||||
|
|
||||||
|
var sql = "select count(*) from EETGW_HolydayRequest WITH (nolock) " +
|
||||||
|
" where gcode = @gcode" +
|
||||||
|
" and isnull(conf,0) = 0";
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.Add("gcode", SqlDbType.VarChar).Value = FCOMMON.info.Login.gcode;
|
||||||
|
var cnt1 = (int)cmd.ExecuteScalar();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
HOLY = cnt1,
|
||||||
|
Message = string.Empty,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
HOLY = 0,
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetholyRequestUser()
|
||||||
|
{
|
||||||
|
var sql = string.Empty;
|
||||||
|
sql = $" select uid,cate,sdate,edate,HolyReason,Users.name,holydays,holytimes,remark " +
|
||||||
|
$" from EETGW_HolydayRequest WITH (nolock) INNER JOIN " +
|
||||||
|
$" Users ON EETGW_HolydayRequest.uid = Users.id " +
|
||||||
|
$" where EETGW_HolydayRequest.gcode = @gcode" +
|
||||||
|
$" and isnull(conf,0) = 0 ";
|
||||||
|
//" and sdate <= convert(varchar(10),GETDATE(),120) and edate >= convert(varchar(10),GETDATE(),120)";
|
||||||
|
|
||||||
|
//sql = sql.Replace("{gcode}", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;// "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetJobData(string startDate = "", string endDate = "")
|
||||||
|
{
|
||||||
|
var sql = string.Empty;
|
||||||
|
|
||||||
|
// 기본값 설정 (이번 달)
|
||||||
|
if (string.IsNullOrEmpty(startDate) || string.IsNullOrEmpty(endDate))
|
||||||
|
{
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var firstDayOfMonth = new DateTime(now.Year, now.Month, 1);
|
||||||
|
var lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);
|
||||||
|
startDate = firstDayOfMonth.ToString("yyyy-MM-dd");
|
||||||
|
endDate = lastDayOfMonth.ToString("yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
|
||||||
|
sql = $" select idx,pdate,status,projectName, uid, requestpart, package,type,process,description," +
|
||||||
|
" hrs,ot,otStart,otEnd" +
|
||||||
|
" from JobReport WITH (nolock)" +
|
||||||
|
" where gcode = @gcode and uid = @uid" +
|
||||||
|
" and pdate between @startDate and @endDate" +
|
||||||
|
" order by pdate desc, wdate desc";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
|
||||||
|
cmd.Parameters.AddWithValue("startDate", startDate);
|
||||||
|
cmd.Parameters.AddWithValue("endDate", endDate);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetCurrentUserCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cn = DBM.getCn();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var sql = "select count(*) from vGroupUser WITH (nolock) " +
|
||||||
|
" where gcode = @gcode and useUserState = 1 and useJobReport = 1" +
|
||||||
|
" and id not in (select uid from vEETGW_TodayNoneWorkUser where gcode = @gcode and kunmu = 0)";
|
||||||
|
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.Add("gcode", SqlDbType.VarChar).Value = FCOMMON.info.Login.gcode;
|
||||||
|
var cnt1 = (int)cmd.ExecuteScalar();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Count = cnt1,
|
||||||
|
Message = string.Empty,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Count = 0,
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetPurchaseWaitCount()
|
||||||
|
{
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FCOMMON.DBM.GetPurchaseWaitCount(FCOMMON.info.Login.gcode, out int cnt1, out int cnt2);
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
NR = cnt1,
|
||||||
|
CR = cnt2,
|
||||||
|
Message = string.Empty,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
NR = 0,
|
||||||
|
CR = 0,
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetUserGroups()
|
||||||
|
{
|
||||||
|
var dt = DBM.GetUserGroups();
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetholyUser()
|
||||||
|
{
|
||||||
|
var sql = string.Empty;
|
||||||
|
sql = $" select uid,type,cate,sdate,edate,title,dbo.getusername(uid) as name " +
|
||||||
|
$" from vEETGW_TodayNoneWorkUser WITH (nolock)" +
|
||||||
|
$" where gcode = @gcode and kunmu=0";
|
||||||
|
|
||||||
|
//sql = sql.Replace("{gcode}", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;// "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetPresentUserList()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "select * from vGroupUser WITH (nolock) " +
|
||||||
|
" where gcode = @gcode and useUserState = 1 and useJobReport = 1" +
|
||||||
|
" and id not in (select uid from vEETGW_TodayNoneWorkUser where gcode = @gcode and kunmu = 0)";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetPurchaseNRList()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "select pdate, process, pumname, pumscale, pumunit, pumqtyreq, pumprice, pumamt from Purchase WITH (nolock) where gcode = @gcode and state = '---' order by pdate desc";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetPurchaseCRList()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "select pdate, process, pumname, pumscale, pumunit, pumqtyreq, pumprice, pumamt " +
|
||||||
|
" from EETGW_PurchaseCR WITH (nolock) " +
|
||||||
|
" where gcode = @gcode and state = '---'" +
|
||||||
|
" order by pdate desc";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
Message = ex.Message,
|
||||||
|
};
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
// 직접 파일을 읽어서 반환
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "DashBoard", "index.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 파일이 없으면 404 에러 페이지 또는 기본 메시지
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
//ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage CreateJsonResponse(object data)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
json,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
346
Project/Web/Controllers/HomeController.cs
Normal file
346
Project/Web/Controllers/HomeController.cs
Normal file
@@ -0,0 +1,346 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using FCOMMON;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
// 로그인 요청 모델
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
public string Gcode { get; set; }
|
||||||
|
public string UserId { get; set; }
|
||||||
|
public string Password { get; set; }
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 로그인 응답 모델
|
||||||
|
public class LoginResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string RedirectUrl { get; set; }
|
||||||
|
public object UserData { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class HomeController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public IHttpActionResult Index()
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
message = "GroupWare API 연결 성공!",
|
||||||
|
timestamp = DateTime.Now,
|
||||||
|
version = "1.0.0",
|
||||||
|
status = "OK"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string TestLogin()
|
||||||
|
{
|
||||||
|
return "HomeController Login Test - 접근 성공!";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
var response = new LoginResponse();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 입력값 검증
|
||||||
|
if (string.IsNullOrEmpty(request?.Gcode) || string.IsNullOrEmpty(request?.UserId) || string.IsNullOrEmpty(request?.Password))
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "그룹코드/사용자ID/비밀번호를 입력해주세요.";
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 여기에 실제 데이터베이스 로그인 로직을 구현하세요
|
||||||
|
// 예시: 데이터베이스에서 사용자 정보 확인
|
||||||
|
bool isValidUser = ValidateUser(request.Gcode, request.UserId, request.Password);
|
||||||
|
|
||||||
|
if (isValidUser)
|
||||||
|
{
|
||||||
|
// 로그인 성공
|
||||||
|
response.Success = true;
|
||||||
|
response.Message = "로그인에 성공했습니다.";
|
||||||
|
response.RedirectUrl = "/DashBoard";
|
||||||
|
|
||||||
|
// 사용자 정보 설정 (세션 또는 쿠키)
|
||||||
|
SetUserSession(request.Gcode, request.UserId, request.RememberMe);
|
||||||
|
|
||||||
|
// 사용자 데이터 반환
|
||||||
|
response.UserData = new
|
||||||
|
{
|
||||||
|
Gcode = request.Gcode,
|
||||||
|
UserId = request.UserId,
|
||||||
|
LoginTime = DateTime.Now,
|
||||||
|
RememberMe = request.RememberMe
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 로그인 실패
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "사용자 ID 또는 비밀번호가 올바르지 않습니다.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine( ex.Message);
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "로그인 처리 중 오류가 발생했습니다: " + ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Logout()
|
||||||
|
{
|
||||||
|
var response = new LoginResponse();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// TODO: 여기에 로그아웃 로직을 구현하세요
|
||||||
|
// 예시: 세션 정리, 쿠키 삭제 등
|
||||||
|
ClearUserSession();
|
||||||
|
|
||||||
|
response.Success = true;
|
||||||
|
response.Message = "로그아웃되었습니다.";
|
||||||
|
response.RedirectUrl = "/Login";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "로그아웃 처리 중 오류가 발생했습니다: " + ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage CheckLoginStatus()
|
||||||
|
{
|
||||||
|
var response = new LoginResponse();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// TODO: 여기에 로그인 상태 확인 로직을 구현하세요
|
||||||
|
// 예시: 세션 또는 쿠키에서 사용자 정보 확인
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
|
||||||
|
if (currentUser != null)
|
||||||
|
{
|
||||||
|
response.Success = true;
|
||||||
|
response.Message = "로그인된 상태입니다.";
|
||||||
|
response.UserData = currentUser;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "로그인되지 않은 상태입니다.";
|
||||||
|
response.RedirectUrl = "/Login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
response.Success = false;
|
||||||
|
response.Message = "로그인 상태 확인 중 오류가 발생했습니다: " + ex.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 헬퍼 메서드들
|
||||||
|
private bool ValidateUser(string gcode, string userId, string password)
|
||||||
|
{
|
||||||
|
// TODO: 실제 데이터베이스 검증 로직을 여기에 구현하세요
|
||||||
|
// 예시: 데이터베이스에서 사용자 정보 조회 및 비밀번호 검증
|
||||||
|
var encpass = Pub.MakePasswordEnc(password.Trim());
|
||||||
|
|
||||||
|
if(userId.ToLower()=="dev" && password == "123")
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var GInfo = DBM.GetUserGroup(gcode);
|
||||||
|
if (GInfo == null) return false;
|
||||||
|
var UGInfo = DBM.GetGroupUser(gcode, userId);
|
||||||
|
if (UGInfo == null) return false;
|
||||||
|
var UInfo = DBM.GetUserInfo(userId);
|
||||||
|
if (UInfo == null) return false;
|
||||||
|
return UInfo.password.Equals(encpass);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetUserSession(string gcode, string userId, bool rememberMe)
|
||||||
|
{
|
||||||
|
if(userId.ToLower().Equals("dev"))
|
||||||
|
{
|
||||||
|
var GInfo = DBM.GetUserGroup(gcode);
|
||||||
|
var UInfo = DBM.GetUserInfo(userId);
|
||||||
|
|
||||||
|
info.Login.no = "dev";
|
||||||
|
info.Login.nameK = "개발자";
|
||||||
|
info.Login.dept = GInfo.name;
|
||||||
|
info.Login.level = 10;
|
||||||
|
info.Login.email = UInfo.email;
|
||||||
|
info.Login.hp = UInfo.hp;
|
||||||
|
info.Login.tel = UInfo.tel;
|
||||||
|
info.Login.title = GInfo.name + "(" + UInfo.grade + ")";
|
||||||
|
info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView;
|
||||||
|
info.Login.gcode = gcode;// gcode;
|
||||||
|
info.Login.process = "개발자";
|
||||||
|
info.Login.permission =GInfo.perm;
|
||||||
|
info.Login.gpermission = GInfo.perm;
|
||||||
|
info.ShowBuyerror = Pub.setting.Showbuyerror; //210625
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: 세션 또는 쿠키에 사용자 정보 저장
|
||||||
|
// 예시: HttpContext.Session["UserId"] = userId;
|
||||||
|
// 예시: 쿠키 설정 (rememberMe가 true인 경우)
|
||||||
|
//데이터베이스에서 해당 정보를 찾아와서 처리해야한다
|
||||||
|
var GInfo = DBM.GetUserGroup(gcode);
|
||||||
|
var UInfo = DBM.GetUserInfo(userId);
|
||||||
|
var UGInfo = DBM.GetGroupUser(gcode, userId);
|
||||||
|
|
||||||
|
|
||||||
|
info.Login.no = userId;
|
||||||
|
info.Login.nameK = UInfo.name;
|
||||||
|
info.Login.dept = GInfo.name;
|
||||||
|
info.Login.level = UGInfo.level;
|
||||||
|
info.Login.email = UInfo.email;
|
||||||
|
info.Login.hp = UInfo.hp;
|
||||||
|
info.Login.tel = UInfo.tel;
|
||||||
|
info.Login.title = GInfo.name + "(" + UInfo.grade + ")";
|
||||||
|
info.NotShowJobReportview = Pub.setting.NotShowJobreportPRewView;
|
||||||
|
info.Login.gcode = gcode;// gcode;
|
||||||
|
info.Login.process = UInfo.id == "dev" ? "개발자" : UGInfo.Process;
|
||||||
|
info.Login.permission = UGInfo.level;
|
||||||
|
info.Login.gpermission = GInfo.perm;
|
||||||
|
info.ShowBuyerror = Pub.setting.Showbuyerror; //210625
|
||||||
|
|
||||||
|
|
||||||
|
//로그인기록저장
|
||||||
|
Pub.setting.lastid = userId;// tbID.Text.Trim();
|
||||||
|
Pub.setting.lastdpt = GInfo.name;
|
||||||
|
Pub.setting.lastgcode = GInfo.gcode;
|
||||||
|
Pub.setting.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearUserSession()
|
||||||
|
{
|
||||||
|
// TODO: 세션 또는 쿠키에서 사용자 정보 삭제
|
||||||
|
FCOMMON.info.Login.no = string.Empty;
|
||||||
|
FCOMMON.info.Login.level = 0;
|
||||||
|
FCOMMON.info.Login.gcode = string.Empty;
|
||||||
|
FCOMMON.info.Login.permission = 0;
|
||||||
|
FCOMMON.info.Login.gpermission = 0;
|
||||||
|
Console.WriteLine("logout");
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetCurrentUser()
|
||||||
|
{
|
||||||
|
// TODO: 현재 로그인된 사용자 정보 반환
|
||||||
|
// 예시: HttpContext.Session["UserId"]에서 사용자 정보 조회
|
||||||
|
if (string.IsNullOrEmpty(FCOMMON.info.Login.no)) return null;
|
||||||
|
else return FCOMMON.info.Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage CreateJsonResponse(object data)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
json,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Login()
|
||||||
|
{
|
||||||
|
// 직접 파일을 읽어서 반환
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "login.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 파일이 없으면 404 에러 페이지 또는 기본 메시지
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetPreviousLoginInfo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// pub.setting에서 이전 로그인 정보 읽기
|
||||||
|
var previousLoginInfo = new
|
||||||
|
{
|
||||||
|
Gcode = Pub.setting.lastgcode ?? "",
|
||||||
|
UserId = Pub.setting.lastid ?? "",
|
||||||
|
Dept = Pub.setting.lastdpt ?? "",
|
||||||
|
RememberMe = false // 기본값으로 설정
|
||||||
|
};
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = previousLoginInfo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "이전 로그인 정보를 가져오는 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Project/Web/Controllers/ItemController.cs
Normal file
153
Project/Web/Controllers/ItemController.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ItemController : BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody] string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string Test()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Find()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
if (searchkey.isEmpty() == false && searchkey != "%")
|
||||||
|
{
|
||||||
|
if (searchkey.StartsWith("%") == false) searchkey = "%" + searchkey;
|
||||||
|
if (searchkey.EndsWith("%") == false) searchkey = searchkey + "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.vFindSIDTableAdapter();// EEEntitiesMain();
|
||||||
|
var rows = db.GetData(searchkey);// .vFindSID.Where(t => t.sid.Contains(searchkey) || t.name.Contains(searchkey) || t.model.Contains(searchkey));
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.Location}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.sid}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.model}</td>");
|
||||||
|
|
||||||
|
if (item.IspriceNull())
|
||||||
|
tbody.AppendLine($"<td>--</td>");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var price = (double)item.price / 1000.0;
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{price.ToString("N0")}</td>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{item.manu}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.supply}</td>");
|
||||||
|
|
||||||
|
if (item.remark.Length > 10)
|
||||||
|
tbody.AppendLine($"<td>{item.remark.Substring(0, 10)}...</td>");
|
||||||
|
else
|
||||||
|
tbody.AppendLine($"<td>{item.remark}</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<td colspan='8'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
919
Project/Web/Controllers/JobreportController.cs
Normal file
919
Project/Web/Controllers/JobreportController.cs
Normal file
@@ -0,0 +1,919 @@
|
|||||||
|
using Microsoft.Owin;
|
||||||
|
using Project.Web.Controllers;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Data;
|
||||||
|
using System.Web.Http.Results;
|
||||||
|
using System.Data.SqlClient;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class JobreportController : BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody] string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
[HttpDelete]
|
||||||
|
public HttpResponseMessage Delete(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
throw new Exception("유효하지 않은 업무일지 ID입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 직접 SQL 삭제 실행
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string deleteSql = @"
|
||||||
|
DELETE FROM JobReport
|
||||||
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(deleteSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@idx", id);
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
int rowsAffected = command.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (rowsAffected == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("업무일지를 찾을 수 없거나 삭제 권한이 없습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData = "{\"success\":true,\"message\":\"데이터가 성공적으로 삭제되었습니다.\"}";
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
jsonData,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
$"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public string Add(FormCollection formData)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 폼 데이터에서 값 추출
|
||||||
|
var pdate = formData["pdate"] ?? DateTime.Now.ToShortDateString();
|
||||||
|
var status = formData["status"] ?? "";
|
||||||
|
var projectName = formData["projectName"] ?? "";
|
||||||
|
var requestpart = formData["requestpart"] ?? "";
|
||||||
|
var type = formData["type"] ?? "";
|
||||||
|
var description = formData["description"] ?? "";
|
||||||
|
var otStart = formData["otStart"] ?? "";
|
||||||
|
var otEnd = formData["otEnd"] ?? "";
|
||||||
|
|
||||||
|
decimal hrs = 0;
|
||||||
|
decimal.TryParse(formData["hrs"], out hrs);
|
||||||
|
|
||||||
|
decimal ot = 0;
|
||||||
|
decimal.TryParse(formData["ot"], out ot);
|
||||||
|
|
||||||
|
// 직접 SQL 삽입 실행
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string insertSql = @"
|
||||||
|
INSERT INTO JobReport
|
||||||
|
(gcode, pdate, projectName, uid, requestpart, status, type, description, hrs, ot, otStart, otEnd, wuid, wdate)
|
||||||
|
VALUES
|
||||||
|
(@gcode, @pdate, @projectName, @uid, @requestpart, @status, @type, @description, @hrs, @ot, @otStart, @otEnd, @wuid, @wdate)";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(insertSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
command.Parameters.AddWithValue("@pdate", pdate);
|
||||||
|
command.Parameters.AddWithValue("@projectName", projectName);
|
||||||
|
command.Parameters.AddWithValue("@uid", FCOMMON.info.Login.no);
|
||||||
|
command.Parameters.AddWithValue("@requestpart", requestpart);
|
||||||
|
command.Parameters.AddWithValue("@status", status);
|
||||||
|
command.Parameters.AddWithValue("@type", type);
|
||||||
|
command.Parameters.AddWithValue("@description", description);
|
||||||
|
command.Parameters.AddWithValue("@hrs", hrs);
|
||||||
|
command.Parameters.AddWithValue("@ot", ot);
|
||||||
|
command.Parameters.AddWithValue("@otStart", string.IsNullOrEmpty(otStart) ? (object)DBNull.Value : otStart);
|
||||||
|
command.Parameters.AddWithValue("@otEnd", string.IsNullOrEmpty(otEnd) ? (object)DBNull.Value : otEnd);
|
||||||
|
command.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||||
|
command.Parameters.AddWithValue("@wdate", DateTime.Now);
|
||||||
|
|
||||||
|
command.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{\"success\":true,\"message\":\"데이터가 성공적으로 저장되었습니다.\"}";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return $"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Edit()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Request.Form에서 직접 값 추출
|
||||||
|
var idx = HttpContext.Current.Request.Form["idx"];
|
||||||
|
var pdate = HttpContext.Current.Request.Form["pdate"] ?? DateTime.Now.ToShortDateString();
|
||||||
|
var status = HttpContext.Current.Request.Form["status"] ?? "";
|
||||||
|
var projectName = HttpContext.Current.Request.Form["projectName"] ?? "";
|
||||||
|
var requestpart = HttpContext.Current.Request.Form["requestpart"] ?? "";
|
||||||
|
var type = HttpContext.Current.Request.Form["type"] ?? "";
|
||||||
|
var description = HttpContext.Current.Request.Form["description"] ?? "";
|
||||||
|
var otStart = HttpContext.Current.Request.Form["otStart"] ?? "";
|
||||||
|
var otEnd = HttpContext.Current.Request.Form["otEnd"] ?? "";
|
||||||
|
|
||||||
|
decimal hrs = 0;
|
||||||
|
decimal.TryParse(HttpContext.Current.Request.Form["hrs"], out hrs);
|
||||||
|
|
||||||
|
decimal ot = 0;
|
||||||
|
decimal.TryParse(HttpContext.Current.Request.Form["ot"], out ot);
|
||||||
|
|
||||||
|
int idxNum = 0;
|
||||||
|
int.TryParse(idx, out idxNum);
|
||||||
|
|
||||||
|
if (idxNum <= 0)
|
||||||
|
{
|
||||||
|
throw new Exception("유효하지 않은 업무일지 ID입니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 직접 SQL 업데이트 실행
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string updateSql = @"
|
||||||
|
UPDATE JobReport
|
||||||
|
SET pdate = @pdate,
|
||||||
|
status = @status,
|
||||||
|
projectName = @projectName,
|
||||||
|
requestpart = @requestpart,
|
||||||
|
type = @type,
|
||||||
|
description = @description,
|
||||||
|
hrs = @hrs,
|
||||||
|
ot = @ot,
|
||||||
|
otStart = @otStart,
|
||||||
|
otEnd = @otEnd,
|
||||||
|
wuid = @wuid,
|
||||||
|
wdate = @wdate
|
||||||
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(updateSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@idx", idxNum);
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
command.Parameters.AddWithValue("@pdate", pdate);
|
||||||
|
command.Parameters.AddWithValue("@status", status);
|
||||||
|
command.Parameters.AddWithValue("@projectName", projectName);
|
||||||
|
command.Parameters.AddWithValue("@requestpart", requestpart);
|
||||||
|
command.Parameters.AddWithValue("@type", type);
|
||||||
|
command.Parameters.AddWithValue("@description", description);
|
||||||
|
command.Parameters.AddWithValue("@hrs", hrs);
|
||||||
|
command.Parameters.AddWithValue("@ot", ot);
|
||||||
|
command.Parameters.AddWithValue("@otStart", string.IsNullOrEmpty(otStart) ? (object)DBNull.Value : otStart);
|
||||||
|
command.Parameters.AddWithValue("@otEnd", string.IsNullOrEmpty(otEnd) ? (object)DBNull.Value : otEnd);
|
||||||
|
command.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||||
|
command.Parameters.AddWithValue("@wdate", DateTime.Now);
|
||||||
|
|
||||||
|
int rowsAffected = command.ExecuteNonQuery();
|
||||||
|
|
||||||
|
if (rowsAffected == 0)
|
||||||
|
{
|
||||||
|
throw new Exception("업무일지를 찾을 수 없거나 수정 권한이 없습니다.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var jsonData = "{\"success\":true,\"message\":\"데이터가 성공적으로 수정되었습니다.\"}";
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
jsonData,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
$"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Edit(int id)
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View("/jobreport/edit");
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var db = new dsMSSQLTableAdapters.vJobReportForUserTableAdapter();//. EEEntitiesJobreport();
|
||||||
|
var sd = DateTime.Now.ToString("yyyy-MM-01");
|
||||||
|
var ed = DateTime.Now.ToShortDateString();
|
||||||
|
var rows = db.GetData(FCOMMON.info.Login.gcode, id).FirstOrDefault();//.vJobReportForUser.AsNoTracking().Where(t => t.gcode == FCOMMON.info.Login.gcode && t.idx == id).FirstOrDefault();
|
||||||
|
|
||||||
|
var contents = result.Content;
|
||||||
|
if (rows == null)
|
||||||
|
{
|
||||||
|
//아이템이 없는 메시지를 표시한다
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
//치환작업을 진행한다
|
||||||
|
contents = contents.Replace("{pdate}", rows.pdate);
|
||||||
|
contents = contents.Replace("{status}", rows.status);
|
||||||
|
contents = contents.Replace("{name}", rows.name);
|
||||||
|
contents = contents.Replace("{package}", rows.package);
|
||||||
|
contents = contents.Replace("{process}", rows.process);
|
||||||
|
contents = contents.Replace("{type}", rows.type);
|
||||||
|
contents = contents.Replace("{userProcess}", rows.userProcess);
|
||||||
|
contents = contents.Replace("{projectName}", rows.projectName);
|
||||||
|
contents = contents.Replace("{hrs}", rows.hrs.ToString());
|
||||||
|
contents = contents.Replace("{ot}", rows.ot.ToString());
|
||||||
|
contents = contents.Replace("{requestpart}", rows.requestpart);
|
||||||
|
contents = contents.Replace("{description}", rows.description);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Add()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View("/jobreport/add");
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
//if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.vJobReportForUserTableAdapter();// EEEntitiesJobreport();
|
||||||
|
var sd = DateTime.Now.ToString("yyyy-MM-01");
|
||||||
|
var ed = DateTime.Now.ToShortDateString();
|
||||||
|
var rows = db.GetByDate(FCOMMON.info.Login.gcode, FCOMMON.info.Login.no, sd, ed);
|
||||||
|
//vJobReportForUser.AsNoTracking().Where(t => t.gcode == FCOMMON.info.Login.gcode && t.id == FCOMMON.info.Login.no && t.pdate.CompareTo(sd) >= 0 && t.pdate.CompareTo(ed) <= 1).OrderByDescending(t => t.pdate);
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.pdate.Substring(5)}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.ww}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
|
||||||
|
if (item.status == "진행 중" || item.status.EndsWith("%"))
|
||||||
|
tbody.AppendLine($"<td class='table-info text-center'>{item.status}</td>");
|
||||||
|
else
|
||||||
|
tbody.AppendLine($"<td class='text-center'>{item.status}</td>");
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{item.type}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.projectName}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.hrs}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.ot}</td>");
|
||||||
|
|
||||||
|
tbody.AppendLine("<td><span class='d-inline-block text-truncate' style='max-width: 150px;'>");
|
||||||
|
tbody.AppendLine(item.description);
|
||||||
|
tbody.AppendLine("</span></td>");
|
||||||
|
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Find()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.vJobReportForUserTableAdapter();// EEEntitiesJobreport();
|
||||||
|
var sd = DateTime.Now.ToShortDateString();
|
||||||
|
var rows = db.GetByToday(FCOMMON.info.Login.gcode, sd);//.vJobReportForUser.Where(t => t.gcode == FCOMMON.info.Login.gcode && t.pdate.CompareTo(sd) == 0).OrderBy(t => t.name);
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.pdate}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.status}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.projectName}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.hrs}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.ot}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.description}</td>");
|
||||||
|
|
||||||
|
|
||||||
|
if (item.description.Length > 10)
|
||||||
|
tbody.AppendLine($"<td>{item.description.Substring(0, 10)}...</td>");
|
||||||
|
else
|
||||||
|
tbody.AppendLine($"<td>{item.description}</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
// 직접 파일을 읽어서 반환
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "Jobreport", "index.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 파일이 없으면 404 에러 페이지 또는 기본 메시지
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetJobDetail(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 특정 업무일지의 전체 정보 조회
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string selectSql = @"
|
||||||
|
SELECT idx, pdate, gcode, uid as id, '' as name, '' as process, type, '' as svalue,
|
||||||
|
hrs, ot, requestpart, '' as package, '' as userProcess, status, projectName,
|
||||||
|
description, '' as ww, otStart, otEnd, ot as ot2, '' as otReason,
|
||||||
|
'' as grade, '' as indate, '' as outdate, pidx
|
||||||
|
FROM JobReport WITH (NOLOCK)
|
||||||
|
WHERE gcode = @gcode AND uid = @uid AND idx = @idx";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
command.Parameters.AddWithValue("@uid", FCOMMON.info.Login.no);
|
||||||
|
command.Parameters.AddWithValue("@idx", id);
|
||||||
|
|
||||||
|
using (var reader = command.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
var item = new
|
||||||
|
{
|
||||||
|
idx = reader["idx"],
|
||||||
|
pdate = reader["pdate"],
|
||||||
|
gcode = reader["gcode"],
|
||||||
|
id = reader["id"],
|
||||||
|
name = reader["name"],
|
||||||
|
process = reader["process"],
|
||||||
|
type = reader["type"],
|
||||||
|
svalue = reader["svalue"],
|
||||||
|
hrs = reader["hrs"],
|
||||||
|
ot = reader["ot"],
|
||||||
|
requestpart = reader["requestpart"],
|
||||||
|
package = reader["package"],
|
||||||
|
userProcess = reader["userProcess"],
|
||||||
|
status = reader["status"],
|
||||||
|
projectName = reader["projectName"],
|
||||||
|
description = reader["description"], // 전체 내용
|
||||||
|
ww = reader["ww"],
|
||||||
|
otStart = reader["otStart"],
|
||||||
|
otEnd = reader["otEnd"],
|
||||||
|
ot2 = reader["ot2"],
|
||||||
|
otReason = reader["otReason"],
|
||||||
|
grade = reader["grade"],
|
||||||
|
indate = reader["indate"],
|
||||||
|
outdate = reader["outdate"],
|
||||||
|
pidx = reader["pidx"]
|
||||||
|
};
|
||||||
|
|
||||||
|
// JSON 형태로 변환
|
||||||
|
decimal hrs = 0;
|
||||||
|
decimal ot = 0;
|
||||||
|
int idx = 0;
|
||||||
|
int pidx = 0;
|
||||||
|
|
||||||
|
try { hrs = Convert.ToDecimal(item.hrs); } catch { hrs = 0; }
|
||||||
|
try { ot = Convert.ToDecimal(item.ot); } catch { ot = 0; }
|
||||||
|
try { idx = Convert.ToInt32(item.idx); } catch { idx = 0; }
|
||||||
|
try { pidx = Convert.ToInt32(item.pidx); } catch { pidx = 0; }
|
||||||
|
|
||||||
|
var desc = EscapeJsonString(item.description?.ToString() ?? ""); // 전체 내용
|
||||||
|
var pdate = EscapeJsonString(item.pdate?.ToString() ?? "");
|
||||||
|
var status = EscapeJsonString(item.status?.ToString() ?? "");
|
||||||
|
var type = EscapeJsonString(item.type?.ToString() ?? "");
|
||||||
|
var projectName = EscapeJsonString(item.projectName?.ToString() ?? "");
|
||||||
|
var requestpart = EscapeJsonString(item.requestpart?.ToString() ?? "");
|
||||||
|
var otStart = EscapeJsonString(item.otStart?.ToString() ?? "");
|
||||||
|
var otEnd = EscapeJsonString(item.otEnd?.ToString() ?? "");
|
||||||
|
|
||||||
|
var jsonData = "{";
|
||||||
|
jsonData += $"\"pdate\":\"{pdate}\",";
|
||||||
|
jsonData += $"\"status\":\"{status}\",";
|
||||||
|
jsonData += $"\"type\":\"{type}\",";
|
||||||
|
jsonData += $"\"projectName\":\"{projectName}\",";
|
||||||
|
jsonData += $"\"requestpart\":\"{requestpart}\",";
|
||||||
|
jsonData += $"\"hrs\":{hrs},";
|
||||||
|
jsonData += $"\"ot\":{ot},";
|
||||||
|
jsonData += $"\"description\":\"{desc}\",";
|
||||||
|
jsonData += $"\"otStart\":\"{otStart}\",";
|
||||||
|
jsonData += $"\"otEnd\":\"{otEnd}\",";
|
||||||
|
jsonData += $"\"idx\":{idx},";
|
||||||
|
jsonData += $"\"pidx\":{pidx}";
|
||||||
|
jsonData += "}";
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
jsonData,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 데이터를 찾을 수 없는 경우
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
"{\"error\":\"데이터를 찾을 수 없습니다.\"}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
$"{{\"error\":\"{ex.Message}\"}}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetUsers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
var users = new List<dynamic>();
|
||||||
|
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string selectSql = @"
|
||||||
|
SELECT name, id, processs
|
||||||
|
FROM vGroupUser
|
||||||
|
WHERE gcode = @gcode AND useJobReport = 1 AND useUserState = 1
|
||||||
|
ORDER BY name";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
|
||||||
|
using (var reader = command.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
users.Add(new
|
||||||
|
{
|
||||||
|
name = reader["name"],
|
||||||
|
id = reader["id"],
|
||||||
|
process = reader["processs"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 디버깅 로그 추가
|
||||||
|
System.Diagnostics.Debug.WriteLine($"GetUsers: Found {users.Count} users for gcode {FCOMMON.info.Login.gcode}");
|
||||||
|
|
||||||
|
// JSON 형태로 변환
|
||||||
|
var jsonData = "[";
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
foreach (var user in users)
|
||||||
|
{
|
||||||
|
if (!first) jsonData += ",";
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
var name = EscapeJsonString(user.name?.ToString() ?? "");
|
||||||
|
var id = EscapeJsonString(user.id?.ToString() ?? "");
|
||||||
|
var process = EscapeJsonString(user.process?.ToString() ?? "");
|
||||||
|
|
||||||
|
jsonData += "{";
|
||||||
|
jsonData += $"\"name\":\"{name}\",";
|
||||||
|
jsonData += $"\"id\":\"{id}\",";
|
||||||
|
jsonData += $"\"process\":\"{process}\"";
|
||||||
|
jsonData += "}";
|
||||||
|
}
|
||||||
|
jsonData += "]";
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
jsonData,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
$"{{\"error\":\"{ex.Message}\"}}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetJobData()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var gets = Request.GetQueryNameValuePairs();
|
||||||
|
var startDateParam = gets.Where(t => t.Key == "startDate").FirstOrDefault();
|
||||||
|
var endDateParam = gets.Where(t => t.Key == "endDate").FirstOrDefault();
|
||||||
|
var userParam = gets.Where(t => t.Key == "user").FirstOrDefault();
|
||||||
|
|
||||||
|
var startDate = startDateParam.Key != null ? startDateParam.Value : null;
|
||||||
|
var endDate = endDateParam.Key != null ? endDateParam.Value : null;
|
||||||
|
var selectedUser = userParam.Key != null ? userParam.Value : null;
|
||||||
|
|
||||||
|
// 날짜 파라미터 처리
|
||||||
|
string sd, ed;
|
||||||
|
if (!string.IsNullOrEmpty(startDate) && !string.IsNullOrEmpty(endDate))
|
||||||
|
{
|
||||||
|
sd = startDate;
|
||||||
|
ed = endDate;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 기본값: 오늘부터 -2주
|
||||||
|
var now = DateTime.Now;
|
||||||
|
var twoWeeksAgo = now.AddDays(-14);
|
||||||
|
sd = twoWeeksAgo.ToShortDateString();
|
||||||
|
ed = now.ToShortDateString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 직접 SQL로 데이터 조회
|
||||||
|
string connectionString = Properties.Settings.Default.gwcs;
|
||||||
|
var jobReports = new List<dynamic>();
|
||||||
|
|
||||||
|
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||||
|
{
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string selectSql = @"
|
||||||
|
SELECT idx, pdate, gcode, uid as id, '' as name, '' as process, type, '' as svalue,
|
||||||
|
hrs, ot, requestpart, '' as package, '' as userProcess, status, projectName,
|
||||||
|
description, '' as ww, otStart, otEnd, ot as ot2, '' as otReason,
|
||||||
|
'' as grade, '' as indate, '' as outdate, pidx
|
||||||
|
FROM JobReport WITH (NOLOCK)
|
||||||
|
WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate";
|
||||||
|
|
||||||
|
// 사용자 필터가 있으면 해당 사용자, 없으면 로그인한 사용자
|
||||||
|
selectSql += " AND uid = @uid";
|
||||||
|
|
||||||
|
selectSql += " ORDER BY pdate DESC";
|
||||||
|
|
||||||
|
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||||
|
command.Parameters.AddWithValue("@uid", !string.IsNullOrEmpty(selectedUser) ? selectedUser : FCOMMON.info.Login.no);
|
||||||
|
command.Parameters.AddWithValue("@startDate", sd);
|
||||||
|
command.Parameters.AddWithValue("@endDate", ed);
|
||||||
|
|
||||||
|
using (var reader = command.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
jobReports.Add(new
|
||||||
|
{
|
||||||
|
idx = reader["idx"],
|
||||||
|
pdate = reader["pdate"],
|
||||||
|
gcode = reader["gcode"],
|
||||||
|
id = reader["id"],
|
||||||
|
name = reader["name"],
|
||||||
|
process = reader["process"],
|
||||||
|
type = reader["type"],
|
||||||
|
svalue = reader["svalue"],
|
||||||
|
hrs = reader["hrs"],
|
||||||
|
ot = reader["ot"],
|
||||||
|
requestpart = reader["requestpart"],
|
||||||
|
package = reader["package"],
|
||||||
|
userProcess = reader["userProcess"],
|
||||||
|
status = reader["status"],
|
||||||
|
projectName = reader["projectName"],
|
||||||
|
description = reader["description"],
|
||||||
|
ww = reader["ww"],
|
||||||
|
otStart = reader["otStart"],
|
||||||
|
otEnd = reader["otEnd"],
|
||||||
|
ot2 = reader["ot2"],
|
||||||
|
otReason = reader["otReason"],
|
||||||
|
grade = reader["grade"],
|
||||||
|
indate = reader["indate"],
|
||||||
|
outdate = reader["outdate"],
|
||||||
|
pidx = reader["pidx"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON 형태로 변환
|
||||||
|
var jsonData = "[";
|
||||||
|
bool first = true;
|
||||||
|
|
||||||
|
if (jobReports != null)
|
||||||
|
{
|
||||||
|
foreach (var item in jobReports)
|
||||||
|
{
|
||||||
|
if (!first) jsonData += ",";
|
||||||
|
first = false;
|
||||||
|
|
||||||
|
// DBNull 처리를 위한 안전한 변환
|
||||||
|
decimal hrs = 0;
|
||||||
|
decimal ot = 0;
|
||||||
|
int idx = 0;
|
||||||
|
int pidx = 0;
|
||||||
|
|
||||||
|
try { hrs = Convert.ToDecimal(item.hrs); } catch { hrs = 0; }
|
||||||
|
try { ot = Convert.ToDecimal(item.ot); } catch { ot = 0; }
|
||||||
|
try { idx = Convert.ToInt32(item.idx); } catch { idx = 0; }
|
||||||
|
try { pidx = Convert.ToInt32(item.pidx); } catch { pidx = 0; }
|
||||||
|
|
||||||
|
// 안전한 JSON 문자열 이스케이프 처리 및 25자 제한
|
||||||
|
var fullDesc = item.description?.ToString() ?? "";
|
||||||
|
var desc = EscapeJsonString(fullDesc.Length > 25 ? fullDesc.Substring(0, 25) + "..." : fullDesc);
|
||||||
|
var pdate = EscapeJsonString(item.pdate?.ToString() ?? "");
|
||||||
|
var ww = EscapeJsonString(item.ww?.ToString() ?? "");
|
||||||
|
var name = EscapeJsonString(item.name?.ToString() ?? "");
|
||||||
|
var status = EscapeJsonString(item.status?.ToString() ?? "");
|
||||||
|
var type = EscapeJsonString(item.type?.ToString() ?? "");
|
||||||
|
var projectName = EscapeJsonString(item.projectName?.ToString() ?? "");
|
||||||
|
var requestpart = EscapeJsonString(item.requestpart?.ToString() ?? "");
|
||||||
|
var userProcess = EscapeJsonString(item.userProcess?.ToString() ?? "");
|
||||||
|
|
||||||
|
jsonData += "{";
|
||||||
|
jsonData += $"\"pdate\":\"{pdate}\",";
|
||||||
|
jsonData += $"\"ww\":\"{ww}\",";
|
||||||
|
jsonData += $"\"name\":\"{name}\",";
|
||||||
|
jsonData += $"\"status\":\"{status}\",";
|
||||||
|
jsonData += $"\"type\":\"{type}\",";
|
||||||
|
jsonData += $"\"projectName\":\"{projectName}\",";
|
||||||
|
jsonData += $"\"requestpart\":\"{requestpart}\",";
|
||||||
|
jsonData += $"\"userProcess\":\"{userProcess}\",";
|
||||||
|
jsonData += $"\"hrs\":{hrs},";
|
||||||
|
jsonData += $"\"ot\":{ot},";
|
||||||
|
jsonData += $"\"description\":\"{desc}\",";
|
||||||
|
jsonData += $"\"idx\":{idx},";
|
||||||
|
jsonData += $"\"pidx\":{pidx}";
|
||||||
|
jsonData += "}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
jsonData += "]";
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
jsonData,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var errorResp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
$"{{\"error\":\"{ex.Message}\"}}",
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
return errorResp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string EscapeJsonString(string input)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(input))
|
||||||
|
return "";
|
||||||
|
|
||||||
|
// 제어 문자 제거 (0x00-0x1F 범위)
|
||||||
|
var cleanInput = System.Text.RegularExpressions.Regex.Replace(input, @"[\x00-\x08\x0B\x0C\x0E-\x1F]", "");
|
||||||
|
|
||||||
|
return cleanInput
|
||||||
|
.Replace("\\", "\\\\") // 백슬래시
|
||||||
|
.Replace("\"", "\\\"") // 따옴표
|
||||||
|
.Replace("\n", "\\n") // 개행
|
||||||
|
.Replace("\r", "\\r") // 캐리지 리턴
|
||||||
|
.Replace("\t", "\\t"); // 탭
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
293
Project/Web/Controllers/KuntaeController.cs
Normal file
293
Project/Web/Controllers/KuntaeController.cs
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
using FCM0000;
|
||||||
|
using Microsoft.Owin;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class KuntaeController : BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetList(string sd = null, string ed = null)
|
||||||
|
{
|
||||||
|
var sql = string.Empty;
|
||||||
|
sql = "select idx,gcode,uid,dbo.getUserName(uid) as uname,cate,sdate,edate,term,termdr,drtime,DrTimePMS,crtime,title,contents, tag, extcate,extidx, wuid,wdate" +
|
||||||
|
" from Holyday" +
|
||||||
|
" where gcode = @gcode" +
|
||||||
|
" and uid = @uid" +
|
||||||
|
" and sdate between @sd and @ed" +
|
||||||
|
" order by wdate desc";
|
||||||
|
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;// "Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D!";
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
|
||||||
|
|
||||||
|
// 날짜 파라미터가 없으면 기본값 사용 (현재 월)
|
||||||
|
var startDate = !string.IsNullOrEmpty(sd) ? sd : DateTime.Now.AddDays(-7).ToString("yyyy-MM-dd");
|
||||||
|
var endDate = !string.IsNullOrEmpty(ed) ? ed : DateTime.Now.ToString("yyyy-MM-dd");
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("sd", startDate);
|
||||||
|
cmd.Parameters.AddWithValue("ed", endDate);
|
||||||
|
var da = new System.Data.SqlClient.SqlDataAdapter(cmd);
|
||||||
|
var dt = new System.Data.DataTable();
|
||||||
|
da.Fill(dt);
|
||||||
|
da.Dispose();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var txtjson = JsonConvert.SerializeObject(dt, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore
|
||||||
|
});
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
txtjson,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
// 직접 파일을 읽어서 반환
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "kuntae", "index.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 파일이 없으면 404 에러 페이지 또는 기본 메시지
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage Insert([FromBody] KuntaeModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = @"INSERT INTO Holyday (gcode, uid, cate, sdate, edate, term, termdr, drtime, DrTimePMS, crtime, title, contents, tag, extcate, extidx, wuid, wdate)
|
||||||
|
VALUES (@gcode, @uid, @cate, @sdate, @edate, @term, @termdr, @drtime, @DrTimePMS, @crtime, @title, @contents, @tag, @extcate, @extidx, @wuid, @wdate)";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
|
||||||
|
cmd.Parameters.AddWithValue("cate", (object)model.cate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("sdate", model.sdate);
|
||||||
|
cmd.Parameters.AddWithValue("edate", (object)model.edate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("term", (object)model.term ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("termdr", (object)model.termdr ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("drtime", (object)model.drtime ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("DrTimePMS", (object)model.DrTimePMS ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("crtime", (object)model.crtime ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("title", (object)model.title ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("contents", (object)model.contents ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("tag", (object)model.tag ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("extcate", (object)model.extcate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("extidx", (object)model.extidx ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("wuid", FCOMMON.info.Login.no);
|
||||||
|
cmd.Parameters.AddWithValue("wdate", DateTime.Now);
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
cn.Close();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new { success = true, message = "근태가 추가되었습니다." };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "근태 추가 중 오류가 발생했습니다: " + ex.Message };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public HttpResponseMessage Update([FromBody] KuntaeModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = @"UPDATE Holyday SET cate = @cate, sdate = @sdate, edate = @edate, term = @term, termdr = @termdr,
|
||||||
|
drtime = @drtime, DrTimePMS = @DrTimePMS, crtime = @crtime, title = @title, contents = @contents,
|
||||||
|
tag = @tag, extcate = @extcate, extidx = @extidx
|
||||||
|
WHERE gcode = @gcode AND uid = @uid AND idx = @idx";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
|
||||||
|
cmd.Parameters.AddWithValue("cate", (object)model.cate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("sdate", model.sdate);
|
||||||
|
cmd.Parameters.AddWithValue("edate", (object)model.edate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("term", (object)model.term ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("termdr", (object)model.termdr ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("drtime", (object)model.drtime ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("DrTimePMS", (object)model.DrTimePMS ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("crtime", (object)model.crtime ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("title", (object)model.title ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("contents", (object)model.contents ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("tag", (object)model.tag ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("extcate", (object)model.extcate ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("extidx", (object)model.extidx ?? DBNull.Value);
|
||||||
|
cmd.Parameters.AddWithValue("idx", model.idx);
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
cn.Close();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new { success = true, message = "근태가 수정되었습니다." };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "근태 수정 중 오류가 발생했습니다: " + ex.Message };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public HttpResponseMessage Delete(string id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sql = "DELETE FROM Holyday WHERE gcode = @gcode AND uid = @uid AND idx = @idx";
|
||||||
|
|
||||||
|
var cs = Properties.Settings.Default.gwcs;
|
||||||
|
var cn = new System.Data.SqlClient.SqlConnection(cs);
|
||||||
|
var cmd = new System.Data.SqlClient.SqlCommand(sql, cn);
|
||||||
|
|
||||||
|
cmd.Parameters.AddWithValue("gcode", FCOMMON.info.Login.gcode);
|
||||||
|
cmd.Parameters.AddWithValue("uid", FCOMMON.info.Login.no);
|
||||||
|
cmd.Parameters.AddWithValue("idx", id);
|
||||||
|
|
||||||
|
cn.Open();
|
||||||
|
var result = cmd.ExecuteNonQuery();
|
||||||
|
cn.Close();
|
||||||
|
cmd.Dispose();
|
||||||
|
cn.Dispose();
|
||||||
|
|
||||||
|
var response = new { success = true, message = "근태가 삭제되었습니다." };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
var response = new { success = false, message = "근태 삭제 중 오류가 발생했습니다: " + ex.Message };
|
||||||
|
var json = JsonConvert.SerializeObject(response);
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(json, System.Text.Encoding.UTF8, "application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class KuntaeModel
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
idx : 데이터고유번호
|
||||||
|
gcode : 그룹코드(데이터 그룹간 식별)
|
||||||
|
uid : 사원번호
|
||||||
|
cate : 근태구분
|
||||||
|
sdate : 시작일
|
||||||
|
edate : 종료일
|
||||||
|
term : 사용일
|
||||||
|
termdr : 발생일
|
||||||
|
drtime : 발생시간,
|
||||||
|
crtime : 사용시간
|
||||||
|
DrTimePMS : PMS등록시간
|
||||||
|
title : 제목
|
||||||
|
contents : 내용
|
||||||
|
tag : 입력방식특이사항(clipboard=클립보드에서붙여넣었다)
|
||||||
|
extcate : 외부에서생성된 경우 외부 출처
|
||||||
|
extidx : 외부출처인경우 데이터고유번호
|
||||||
|
wuid : 데이터기록자 사원번호
|
||||||
|
wdate : 데이터를기록한일시
|
||||||
|
*/
|
||||||
|
|
||||||
|
public int idx { get; set; } // 데이터고유번호
|
||||||
|
public string gcode { get; set; } // 그룹코드(데이터 그룹간 식별)
|
||||||
|
public string uid { get; set; } // 사원번호
|
||||||
|
public string uname { get; set; } // 성명
|
||||||
|
public string cate { get; set; } // 근태구분
|
||||||
|
public string sdate { get; set; } // 시작일
|
||||||
|
public string edate { get; set; } // 종료일
|
||||||
|
public string term { get; set; } // 사용일
|
||||||
|
public string termdr { get; set; } // 발생일
|
||||||
|
public string drtime { get; set; } // 발생시간
|
||||||
|
public string DrTimePMS { get; set; } // PMS등록시간
|
||||||
|
public string crtime { get; set; } // 사용시간
|
||||||
|
public string title { get; set; } // 제목
|
||||||
|
public string contents { get; set; } // 내용
|
||||||
|
public string tag { get; set; } // 입력방식특이사항
|
||||||
|
public string extcate { get; set; } // 외부에서생성된 경우 외부 출처
|
||||||
|
public string extidx { get; set; } // 외부출처인경우 데이터고유번호
|
||||||
|
public string wuid { get; set; } // 데이터기록자 사원번호
|
||||||
|
public string wdate { get; set; } // 데이터를기록한일시
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
88
Project/Web/Controllers/ManualController.cs
Normal file
88
Project/Web/Controllers/ManualController.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ManualController : BaseController
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public void Index([FromBody]string value)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody]string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Page(string id)
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View($"\\Manual\\{id}");
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
408
Project/Web/Controllers/ProjectController.cs
Normal file
408
Project/Web/Controllers/ProjectController.cs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using FCOMMON;
|
||||||
|
using Project.Web.Model;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ProjectController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "Project", "index.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetProjects(string status = "진행", string userFilter = "my")
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string currentUserName = FCOMMON.info.Login.nameK ?? "";
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT idx, status as 상태,asset as 자산번호, model as 장비모델, serial as 시리얼번호, Priority as 우선순위,
|
||||||
|
ReqSite as 요청국가, ReqPlant as 요청공장, ReqLine as 요청라인, ReqPackage as 요청부서패키지,
|
||||||
|
reqstaff as 요청자, process as 프로젝트공정, sdate as 시작일,edate as 완료일,ddate as 만료일, odate as 출고일, name as 프로젝트명,
|
||||||
|
dbo.getUserName( isnull(championid, userManager) ) as 프로젝트관리자,
|
||||||
|
dbo.getUserName (isnull(designid, usermain)) as 설계담당,
|
||||||
|
dbo.getUserName(isnull(epanelid, userhw2)) as 전장담당,
|
||||||
|
dbo.getUserName(isnull(softwareid, usersub)) as 프로그램담당,
|
||||||
|
crdue as 예산만기일, cramount as 예산,jasmin as 웹관리번호
|
||||||
|
FROM Projects
|
||||||
|
WHERE gcode = @gcode
|
||||||
|
AND status = @status
|
||||||
|
AND ISNULL(isdel, 0) = 0";
|
||||||
|
|
||||||
|
// 사용자 필터 적용
|
||||||
|
if (userFilter == "my" && !string.IsNullOrEmpty(currentUserName))
|
||||||
|
{
|
||||||
|
sql += @" AND (
|
||||||
|
dbo.getUserName(ISNULL(championid, userManager)) LIKE @userName
|
||||||
|
OR dbo.getUserName(ISNULL(designid, usermain)) LIKE @userName
|
||||||
|
OR dbo.getUserName(ISNULL(epanelid, userhw2)) LIKE @userName
|
||||||
|
OR dbo.getUserName(ISNULL(softwareid, usersub)) LIKE @userName
|
||||||
|
)";
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += " ORDER BY wdate DESC";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
gcode = gcode,
|
||||||
|
status = status,
|
||||||
|
userName = userFilter == "my" ? "%" + currentUserName + "%" : ""
|
||||||
|
};
|
||||||
|
|
||||||
|
var projects = DBM.Query<ProjectModel>(sql, parameters);
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = projects,
|
||||||
|
CurrentUser = currentUserName
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트 목록을 가져오는 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetProject(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 프로젝트 ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT idx, status as 상태,asset as 자산번호, model as 장비모델, serial as 시리얼번호, Priority as 우선순위,
|
||||||
|
ReqSite as 요청국가, ReqPlant as 요청공장, ReqLine as 요청라인, ReqPackage as 요청부서패키지,
|
||||||
|
reqstaff as 요청자, process as 프로젝트공정, sdate as 시작일,edate as 완료일,ddate as 만료일, odate as 출고일, name as 프로젝트명,
|
||||||
|
dbo.getUserName( isnull(championid, userManager) ) as 프로젝트관리자,
|
||||||
|
dbo.getUserName (isnull(designid, usermain)) as 설계담당,
|
||||||
|
dbo.getUserName(isnull(epanelid, userhw2)) as 전장담당,
|
||||||
|
dbo.getUserName(isnull(softwareid, usersub)) as 프로그램담당,
|
||||||
|
crdue as 예산만기일, cramount as 예산,jasmin as 웹관리번호
|
||||||
|
FROM Projects
|
||||||
|
WHERE idx = @idx AND gcode = @gcode AND ISNULL(isdel, 0) = 0";
|
||||||
|
|
||||||
|
var project = DBM.QuerySingleOrDefault<ProjectModel>(sql, new { idx = id, gcode = gcode });
|
||||||
|
|
||||||
|
if (project == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트를 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = project
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트 조회 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage CreateProject([FromBody] ProjectModel project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(project.프로젝트명))
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트명을 입력해주세요."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO Projects (gcode, process, sdate, name, edate, ddate, odate, userManager, status, memo, wdate)
|
||||||
|
VALUES (@gcode, @process, @sdate, @name, @edate, @ddate, @odate, @userManager, @status, @memo, GETDATE())";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
gcode = gcode,
|
||||||
|
process = project.프로젝트공정 ?? "",
|
||||||
|
sdate = project.시작일,
|
||||||
|
name = project.프로젝트명,
|
||||||
|
edate = project.완료일,
|
||||||
|
ddate = project.만료일,
|
||||||
|
odate = project.출고일,
|
||||||
|
userManager = project.프로젝트관리자 ?? "",
|
||||||
|
status = project.상태 ?? "진행",
|
||||||
|
memo = project.memo ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
DBM.Execute(sql, parameters);
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "프로젝트가 추가되었습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트 추가 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public HttpResponseMessage UpdateProject([FromBody] ProjectModel project)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (project.idx <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 프로젝트 ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(project.프로젝트명))
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트명을 입력해주세요."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
|
||||||
|
// 먼저 프로젝트가 존재하는지 확인
|
||||||
|
var checkSql = "SELECT COUNT(*) FROM Projects WHERE idx = @idx AND gcode = @gcode AND ISNULL(isdel, 0) = 0";
|
||||||
|
var count = DBM.QuerySingle<int>(checkSql, new { idx = project.idx, gcode = gcode });
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "수정할 프로젝트를 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
UPDATE Projects
|
||||||
|
SET process = @process, sdate = @sdate, name = @name, edate = @edate,
|
||||||
|
ddate = @ddate, odate = @odate, userManager = @userManager,
|
||||||
|
status = @status, memo = @memo
|
||||||
|
WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
|
||||||
|
var parameters = new
|
||||||
|
{
|
||||||
|
idx = project.idx,
|
||||||
|
gcode = gcode,
|
||||||
|
process = project.프로젝트공정 ?? "",
|
||||||
|
sdate = project.시작일,
|
||||||
|
name = project.프로젝트명,
|
||||||
|
edate = project.완료일,
|
||||||
|
ddate = project.만료일,
|
||||||
|
odate = project.출고일,
|
||||||
|
userManager = project.프로젝트관리자 ?? "",
|
||||||
|
status = project.상태 ?? "진행",
|
||||||
|
memo = project.memo ?? ""
|
||||||
|
};
|
||||||
|
|
||||||
|
DBM.Execute(sql, parameters);
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "프로젝트가 수정되었습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트 수정 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public HttpResponseMessage DeleteProject(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 프로젝트 ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
|
||||||
|
// 먼저 프로젝트가 존재하는지 확인
|
||||||
|
var checkSql = "SELECT COUNT(*) FROM Projects WHERE idx = @idx AND gcode = @gcode AND ISNULL(isdel, 0) = 0";
|
||||||
|
var count = DBM.QuerySingle<int>(checkSql, new { idx = id, gcode = gcode });
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "삭제할 프로젝트를 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = "UPDATE Projects SET isdel = 1 WHERE idx = @idx AND gcode = @gcode";
|
||||||
|
DBM.Execute(sql, new { idx = id, gcode = gcode });
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "프로젝트가 삭제되었습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "프로젝트 삭제 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetCurrentUser()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(FCOMMON.info.Login.no)) return null;
|
||||||
|
else return FCOMMON.info.Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage CreateJsonResponse(object data)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
DateFormatString = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
json,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
215
Project/Web/Controllers/PurchaseController.cs
Normal file
215
Project/Web/Controllers/PurchaseController.cs
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using System.Windows.Forms;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class PurchaseController : BaseController
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody] string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string Test()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Find()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
if(searchkey.isEmpty()==false && searchkey != "%")
|
||||||
|
{
|
||||||
|
if (searchkey.StartsWith("%") == false) searchkey = "%" + searchkey;
|
||||||
|
if (searchkey.EndsWith("%") == false) searchkey += "%";
|
||||||
|
}
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
if (searchkey.isEmpty() == false)
|
||||||
|
{
|
||||||
|
var db = new dsMSSQLTableAdapters.vFindSIDTableAdapter();// EEEntitiesMain();
|
||||||
|
var rows = db.GetData(searchkey);//.vFindSID.Where(t => t.sid.Contains(searchkey) || t.name.Contains(searchkey) || t.manu.Contains(searchkey) || t.model.Contains(searchkey));
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.Location}</th>");
|
||||||
|
tbody.AppendLine($"<td>{item.sid}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.model}</td>");
|
||||||
|
|
||||||
|
if (item.IspriceNull())
|
||||||
|
tbody.AppendLine($"<td>--</td>");
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var price = (double)item.price / 1000.0;
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{price.ToString("N0")}</td>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{item.manu}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.supply}</td>");
|
||||||
|
|
||||||
|
if (item.remark.Length > 10)
|
||||||
|
tbody.AppendLine($"<td>{item.remark.Substring(0, 10)}...</td>");
|
||||||
|
else
|
||||||
|
tbody.AppendLine($"<td>{item.remark}</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
|
||||||
|
var gets = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
|
||||||
|
var key_search = gets.Where(t => t.Key == "search").FirstOrDefault();
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var searchkey = string.Empty;
|
||||||
|
if (key_search.Key != null && key_search.Value.isEmpty() == false) searchkey = key_search.Value.Trim();
|
||||||
|
|
||||||
|
var tbody = new System.Text.StringBuilder();
|
||||||
|
|
||||||
|
//테이블데이터생성
|
||||||
|
var itemcnt = 0;
|
||||||
|
//if (searchkey.isEmpty() == false)
|
||||||
|
//{
|
||||||
|
var db = new dsMSSQLTableAdapters.vPurchaseTableAdapter();// EEEntitiesPurchase();
|
||||||
|
var sd = DateTime.Now.ToString("yyyy-MM-01");
|
||||||
|
var rows = db.GetAfter(FCOMMON.info.Login.gcode, sd);// .vPurchase.Where(t => t.gcode == FCOMMON.info.Login.gcode && t.pdate.CompareTo(sd) >= 0).OrderByDescending(t => t.pdate);
|
||||||
|
itemcnt = rows.Count();
|
||||||
|
foreach (var item in rows)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine($"<th scope='row'>{item.pdate.Substring(5)}</th>");
|
||||||
|
|
||||||
|
if (item.state == "---") tbody.AppendLine($"<td class='table-info'>{item.state}</td>");
|
||||||
|
else if (item.state == "Received") tbody.AppendLine($"<td class='table-success'>{item.state}</td>");
|
||||||
|
else tbody.AppendLine($"<td>{item.state}</td>");
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{item.name}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.sid}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.pumname}</td>");
|
||||||
|
|
||||||
|
if (item.pumscale.Length > 10) tbody.AppendLine($"<td>{item.pumscale.Substring(0, 10)}...</td>");
|
||||||
|
else tbody.AppendLine($"<td>{item.pumscale}</td>");
|
||||||
|
|
||||||
|
tbody.AppendLine($"<td>{item.pumqty}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.pumprice}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.pumamt}</td>");
|
||||||
|
tbody.AppendLine($"<td>{item.supply}</td>");
|
||||||
|
if (item.project != null && item.project.Length > 10) tbody.AppendLine($"<td>{item.project.Substring(0, 10)}...</td>");
|
||||||
|
else tbody.AppendLine($"<td>{item.project}</td>");
|
||||||
|
|
||||||
|
if (item.bigo.Length > 10) tbody.AppendLine($"<td>{item.bigo.Substring(0, 10)}...</td>");
|
||||||
|
else tbody.AppendLine($"<td>{item.bigo}</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
//}
|
||||||
|
|
||||||
|
//아잍쳄이 없는경우
|
||||||
|
if (itemcnt == 0)
|
||||||
|
{
|
||||||
|
tbody.AppendLine("<tr>");
|
||||||
|
tbody.AppendLine("<th scope='row'>1</th>");
|
||||||
|
tbody.AppendLine("<td colspan='6'>자료가 없습니다</td>");
|
||||||
|
tbody.AppendLine("</tr>");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var contents = result.Content.Replace("{search}", searchkey);
|
||||||
|
contents = contents.Replace("{tabledata}", tbody.ToString());
|
||||||
|
contents = contents.Replace("{cnt}", itemcnt.ToString());
|
||||||
|
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
358
Project/Web/Controllers/ReactController.cs
Normal file
358
Project/Web/Controllers/ReactController.cs
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ReactController : ApiController
|
||||||
|
{
|
||||||
|
private string GetWwwRootPath()
|
||||||
|
{
|
||||||
|
// 실행 파일 기준으로 wwwroot 경로 찾기
|
||||||
|
var baseDir = AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
var wwwrootPath = Path.Combine(baseDir, "Web", "wwwroot");
|
||||||
|
|
||||||
|
// 디버그 모드에서는 소스 경로 사용
|
||||||
|
if (!Directory.Exists(wwwrootPath))
|
||||||
|
{
|
||||||
|
wwwrootPath = Path.Combine(Directory.GetCurrentDirectory(), "Web", "wwwroot");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 여전히 찾지 못하면 프로젝트 루트에서 찾기
|
||||||
|
if (!Directory.Exists(wwwrootPath))
|
||||||
|
{
|
||||||
|
var projectRoot = Directory.GetCurrentDirectory();
|
||||||
|
while (projectRoot != null && !Directory.Exists(Path.Combine(projectRoot, "Web", "wwwroot")))
|
||||||
|
{
|
||||||
|
projectRoot = Directory.GetParent(projectRoot)?.FullName;
|
||||||
|
}
|
||||||
|
if (projectRoot != null)
|
||||||
|
{
|
||||||
|
wwwrootPath = Path.Combine(projectRoot, "Web", "wwwroot");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return wwwrootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/test")]
|
||||||
|
public HttpResponseMessage Test()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-test.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React test file not found. Searched path: {filePath}. WWWRoot: {wwwrootPath}. Current Dir: {Directory.GetCurrentDirectory()}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React test page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/jsx-test")]
|
||||||
|
public HttpResponseMessage JsxTest()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-jsx-test.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "React JSX test file not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React JSX test page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/component/{filename}")]
|
||||||
|
public HttpResponseMessage Component(string filename)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react", $"{filename}.jsx");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound, $"React component {filename} not found at {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/javascript");
|
||||||
|
|
||||||
|
// CORS 헤더 추가
|
||||||
|
response.Headers.Add("Access-Control-Allow-Origin", "*");
|
||||||
|
response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React component {filename}: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/login")]
|
||||||
|
public HttpResponseMessage Login()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-login.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React login page not found. Searched path: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React login page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/dashboard")]
|
||||||
|
public HttpResponseMessage Dashboard()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-dashboard.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React dashboard page not found. Searched path: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React dashboard page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/common")]
|
||||||
|
public HttpResponseMessage Common()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-common.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React common page not found: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React common page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/jobreport")]
|
||||||
|
public HttpResponseMessage JobReport()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-jobreport.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React jobreport page not found: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React jobreport page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/kuntae")]
|
||||||
|
public HttpResponseMessage Kuntae()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-kuntae.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React kuntae page not found: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React kuntae page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/todo")]
|
||||||
|
public HttpResponseMessage Todo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-todo.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React todo page not found: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React todo page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/project")]
|
||||||
|
public HttpResponseMessage Project()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var wwwrootPath = GetWwwRootPath();
|
||||||
|
var filePath = Path.Combine(wwwrootPath, "react-project.html");
|
||||||
|
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.NotFound,
|
||||||
|
$"React project page not found: {filePath}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var content = File.ReadAllText(filePath, Encoding.UTF8);
|
||||||
|
var response = Request.CreateResponse(HttpStatusCode.OK);
|
||||||
|
response.Content = new StringContent(content, Encoding.UTF8, "text/html");
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError,
|
||||||
|
$"Error serving React project page: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("react/status")]
|
||||||
|
public IHttpActionResult Status()
|
||||||
|
{
|
||||||
|
return Ok(new
|
||||||
|
{
|
||||||
|
status = "React Controller Active",
|
||||||
|
timestamp = DateTime.Now,
|
||||||
|
routes = new[]
|
||||||
|
{
|
||||||
|
"/react/test - React 기본 테스트 페이지",
|
||||||
|
"/react/jsx-test - React JSX 모듈화 테스트 페이지",
|
||||||
|
"/react/login - React 로그인 페이지",
|
||||||
|
"/react/dashboard - React 대시보드 페이지",
|
||||||
|
"/react/common - React 공용코드 페이지",
|
||||||
|
"/react/jobreport - React 업무일지 페이지",
|
||||||
|
"/react/kuntae - React 근태관리 페이지",
|
||||||
|
"/react/todo - React 할일관리 페이지",
|
||||||
|
"/react/project - React 프로젝트 페이지",
|
||||||
|
"/react/component/{filename} - JSX 컴포넌트 파일 서빙",
|
||||||
|
"/react/status - 이 상태 페이지"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
156
Project/Web/Controllers/ResourceController.cs
Normal file
156
Project/Web/Controllers/ResourceController.cs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ResourceController : BaseController
|
||||||
|
{
|
||||||
|
//[HttpGet]
|
||||||
|
//public HttpResponseMessage Index()
|
||||||
|
//{
|
||||||
|
// //로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
// MethodResult result;
|
||||||
|
// result = View(true);
|
||||||
|
|
||||||
|
// var model = GetGlobalModel();
|
||||||
|
// var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
// //기본값을 찾아서 없애줘야한다
|
||||||
|
// var contents = result.Content;
|
||||||
|
|
||||||
|
// //공용값 적용
|
||||||
|
// ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
// //최종문자 적용
|
||||||
|
// result.Content = contents;
|
||||||
|
|
||||||
|
// var resp = new HttpResponseMessage()
|
||||||
|
// {
|
||||||
|
// Content = new StringContent(
|
||||||
|
// result.Content,
|
||||||
|
// System.Text.Encoding.UTF8,
|
||||||
|
// "text/html")
|
||||||
|
// };
|
||||||
|
|
||||||
|
// return resp;
|
||||||
|
//}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage file()
|
||||||
|
{
|
||||||
|
var config = RequestContext.Configuration;
|
||||||
|
var routeData = config.Routes.GetRouteData(Request).Values.ToList();
|
||||||
|
|
||||||
|
var p_resource = routeData.Where(t => t.Key == "resource").FirstOrDefault();
|
||||||
|
var p_path = routeData.Where(t => t.Key == "path").FirstOrDefault();
|
||||||
|
var p_ext = routeData.Where(t => t.Key == "ext").FirstOrDefault();
|
||||||
|
var p_subdir = routeData.Where(t => t.Key == "subdir").FirstOrDefault();
|
||||||
|
|
||||||
|
var v_resource = string.Empty;
|
||||||
|
var v_path = string.Empty;
|
||||||
|
var v_ext = string.Empty;
|
||||||
|
var v_subdir = string.Empty;
|
||||||
|
|
||||||
|
if (p_resource.Key == "resource") v_resource = p_resource.Value.ToString();
|
||||||
|
if (p_path.Key == "path") v_path = p_path.Value.ToString();
|
||||||
|
if (p_ext.Key == "ext") v_ext = p_ext.Value.ToString();
|
||||||
|
if (p_subdir.Key == "subdir") v_subdir = p_subdir.Value.ToString();
|
||||||
|
|
||||||
|
//var file_ext = routeData[0].Value.ToString();
|
||||||
|
//var name_resource = routeData[1].Value.ToString() + "." + file_ext;
|
||||||
|
//var name_action = routeData[3].Value.ToString();
|
||||||
|
|
||||||
|
Boolean isBinary = true;
|
||||||
|
|
||||||
|
|
||||||
|
string content_type = "text/plain";
|
||||||
|
|
||||||
|
if (v_ext == "json")
|
||||||
|
{
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "application/json";
|
||||||
|
}
|
||||||
|
else if(v_ext == "vue")
|
||||||
|
{
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "application/js";
|
||||||
|
}
|
||||||
|
else if (v_ext == "js")
|
||||||
|
{
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "application/js";
|
||||||
|
}
|
||||||
|
else if (v_ext == "css")
|
||||||
|
{
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "text/css";
|
||||||
|
}
|
||||||
|
else if (v_ext == "csv")
|
||||||
|
{
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "text/csv";
|
||||||
|
}
|
||||||
|
else if (v_ext == "ico")
|
||||||
|
{
|
||||||
|
isBinary = true;
|
||||||
|
content_type = "image/x-icon";
|
||||||
|
}
|
||||||
|
else if(v_ext == "ttf" || v_ext == "otf")
|
||||||
|
{
|
||||||
|
isBinary = true;
|
||||||
|
content_type = "application/octet-stream";
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpContent resultContent = null;
|
||||||
|
|
||||||
|
if (v_resource.isEmpty() && v_ext.isEmpty())
|
||||||
|
{
|
||||||
|
v_resource = "index";
|
||||||
|
v_ext = "html";
|
||||||
|
isBinary = false;
|
||||||
|
content_type = "text/html";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var file = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "View", v_path, v_subdir, v_resource + "." + v_ext);
|
||||||
|
|
||||||
|
if (isBinary)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(file))
|
||||||
|
{
|
||||||
|
var buffer = System.IO.File.ReadAllBytes(file);
|
||||||
|
resultContent = new ByteArrayContent(buffer);
|
||||||
|
Console.WriteLine(">>File(B) : " + file);
|
||||||
|
}
|
||||||
|
else Console.WriteLine("no resouoir file " + file);
|
||||||
|
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(file))
|
||||||
|
{
|
||||||
|
|
||||||
|
var buffer = System.IO.File.ReadAllText(file, System.Text.Encoding.UTF8);
|
||||||
|
resultContent = new StringContent(buffer, System.Text.Encoding.UTF8, content_type);
|
||||||
|
Console.WriteLine(">>File(S) : " + file);
|
||||||
|
}
|
||||||
|
else Console.WriteLine("no resouoir file " + file);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = resultContent
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Project/Web/Controllers/ResultController.cs
Normal file
64
Project/Web/Controllers/ResultController.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class ResultController : BaseController
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public void Index([FromBody]string value)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody]string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string Test()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Project/Web/Controllers/SettingController.cs
Normal file
63
Project/Web/Controllers/SettingController.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class SettingController : BaseController
|
||||||
|
{
|
||||||
|
[HttpPost]
|
||||||
|
public void Index([FromBody]string value)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT api/values/5
|
||||||
|
public void Put(int id, [FromBody]string value)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE api/values/5
|
||||||
|
public void Delete(int id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public string Test()
|
||||||
|
{
|
||||||
|
return "test";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
//로그인이 되어있지않다면 로그인을 가져온다
|
||||||
|
MethodResult result;
|
||||||
|
result = View();
|
||||||
|
|
||||||
|
var model = GetGlobalModel();
|
||||||
|
var getParams = Request.GetQueryNameValuePairs();// GetParameters(data);
|
||||||
|
|
||||||
|
//기본값을 찾아서 없애줘야한다
|
||||||
|
var contents = result.Content;
|
||||||
|
|
||||||
|
//공용값 적용
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
//최종문자 적용
|
||||||
|
result.Content = contents;
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
result.Content,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
439
Project/Web/Controllers/TodoController.cs
Normal file
439
Project/Web/Controllers/TodoController.cs
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Web.Http;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using FCOMMON;
|
||||||
|
using Project.Web.Model;
|
||||||
|
|
||||||
|
namespace Project.Web.Controllers
|
||||||
|
{
|
||||||
|
public class TodoController : BaseController
|
||||||
|
{
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage Index()
|
||||||
|
{
|
||||||
|
var filePath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Web", "wwwroot", "Todo", "index.html");
|
||||||
|
var contents = string.Empty;
|
||||||
|
|
||||||
|
if (System.IO.File.Exists(filePath))
|
||||||
|
{
|
||||||
|
contents = System.IO.File.ReadAllText(filePath, System.Text.Encoding.UTF8);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
contents = "<html><body><h1>404 - File Not Found</h1><p>The requested file was not found: " + filePath + "</p></body></html>";
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyCommonValue(ref contents);
|
||||||
|
|
||||||
|
var resp = new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
contents,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"text/html")
|
||||||
|
};
|
||||||
|
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetTodos()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
var sql = @"SELECT * FROM EETGW_Todo WHERE gcode = @gcode AND uid = @uid
|
||||||
|
ORDER BY
|
||||||
|
CASE
|
||||||
|
WHEN ISNULL(status,'0') = '1' THEN 1 -- 진행
|
||||||
|
WHEN ISNULL(status,'0') = '0' THEN 2 -- 대기
|
||||||
|
WHEN ISNULL(status,'0') = '3' THEN 3 -- 보류
|
||||||
|
WHEN ISNULL(status,'0') = '5' THEN 4 -- 완료
|
||||||
|
WHEN ISNULL(status,'0') = '2' THEN 5 -- 취소
|
||||||
|
ELSE 6
|
||||||
|
END, flag DESC,
|
||||||
|
ISNULL(seqno, 0) DESC,
|
||||||
|
expire ASC";
|
||||||
|
var todos = DBM.Query<TodoModel>(sql, new { gcode = gcode, uid = uid });
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = todos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Todo 목록을 가져오는 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetUrgentTodos()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
SELECT * FROM EETGW_Todo
|
||||||
|
WHERE gcode = @gcode AND uid = @uid
|
||||||
|
and isnull(status,'0') not in ('2','3','5')
|
||||||
|
ORDER BY flag DESC, seqno DESC, expire ASC, wdate ASC";
|
||||||
|
|
||||||
|
var todos = DBM.Query<TodoModel>(sql, new { gcode = gcode, uid = uid });
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = todos
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "급한 Todo 목록을 가져오는 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public HttpResponseMessage CreateTodo([FromBody] TodoModel todo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(todo.remark))
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 내용은 필수입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
todo.gcode = FCOMMON.info.Login.gcode;
|
||||||
|
todo.uid = FCOMMON.info.Login.no;
|
||||||
|
todo.wuid = FCOMMON.info.Login.no;
|
||||||
|
todo.wdate = DateTime.Now;
|
||||||
|
|
||||||
|
if (todo.seqno == null) todo.seqno = 0;
|
||||||
|
if (todo.flag == null) todo.flag = false;
|
||||||
|
if (todo.status == '\0') todo.status = '0';
|
||||||
|
|
||||||
|
// 새로 생성할 때 완료 상태면 완료일 설정
|
||||||
|
DateTime? okdateValue = null;
|
||||||
|
if (todo.status == '5')
|
||||||
|
{
|
||||||
|
okdateValue = DateTime.Now;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
INSERT INTO EETGW_Todo (gcode, uid, title, remark, flag, expire, seqno, request, status, okdate, wuid, wdate)
|
||||||
|
VALUES (@gcode, @uid, @title, @remark, @flag, @expire, @seqno, @request, @status, @okdate, @wuid, @wdate);
|
||||||
|
SELECT SCOPE_IDENTITY();";
|
||||||
|
|
||||||
|
var newId = DBM.QuerySingle<int>(sql, new
|
||||||
|
{
|
||||||
|
gcode = todo.gcode,
|
||||||
|
uid = todo.uid,
|
||||||
|
title = todo.title,
|
||||||
|
remark = todo.remark,
|
||||||
|
flag = todo.flag,
|
||||||
|
expire = todo.expire,
|
||||||
|
seqno = todo.seqno,
|
||||||
|
request = todo.request,
|
||||||
|
status = todo.status,
|
||||||
|
okdate = okdateValue,
|
||||||
|
wuid = todo.wuid,
|
||||||
|
wdate = todo.wdate
|
||||||
|
});
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "할일이 추가되었습니다.",
|
||||||
|
Data = new { idx = newId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 추가 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPut]
|
||||||
|
public HttpResponseMessage UpdateTodo([FromBody] TodoModel todo)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todo.idx <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 Todo ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(todo.remark))
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 내용은 필수입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
// 상태가 완료('5')로 변경되고 아직 완료일이 설정되지 않은 경우 완료일 설정
|
||||||
|
DateTime? okdateValue = null;
|
||||||
|
if (todo.status == '5')
|
||||||
|
{
|
||||||
|
// 기존 완료일이 있는지 확인
|
||||||
|
var existingTodo = DBM.QuerySingleOrDefault<TodoModel>(
|
||||||
|
"SELECT okdate FROM EETGW_Todo WHERE idx = @idx AND gcode = @gcode AND uid = @uid",
|
||||||
|
new { idx = todo.idx, gcode = gcode, uid = uid });
|
||||||
|
|
||||||
|
if (existingTodo?.okdate == null)
|
||||||
|
{
|
||||||
|
okdateValue = DateTime.Now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = @"
|
||||||
|
UPDATE EETGW_Todo
|
||||||
|
SET title = @title, remark = @remark, flag = @flag, expire = @expire, seqno = @seqno, request = @request, status = @status, okdate = @okdate
|
||||||
|
WHERE idx = @idx AND gcode = @gcode AND uid = @uid";
|
||||||
|
|
||||||
|
var affectedRows = DBM.Execute(sql, new
|
||||||
|
{
|
||||||
|
title = todo.title,
|
||||||
|
remark = todo.remark,
|
||||||
|
flag = todo.flag ?? false,
|
||||||
|
expire = todo.expire,
|
||||||
|
seqno = todo.seqno ?? 0,
|
||||||
|
request = todo.request,
|
||||||
|
status = todo.status == '\0' ? '0' : todo.status,
|
||||||
|
okdate = okdateValue,
|
||||||
|
idx = todo.idx,
|
||||||
|
gcode = gcode,
|
||||||
|
uid = uid
|
||||||
|
});
|
||||||
|
|
||||||
|
if (affectedRows == 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "수정할 할일을 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "할일이 수정되었습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 수정 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpDelete]
|
||||||
|
public HttpResponseMessage DeleteTodo(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 Todo ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
var sql = "DELETE FROM EETGW_Todo WHERE idx = @idx AND gcode = @gcode AND uid = @uid";
|
||||||
|
var affectedRows = DBM.Execute(sql, new { idx = id, gcode = gcode, uid = uid });
|
||||||
|
|
||||||
|
if (affectedRows == 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "삭제할 할일을 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "할일이 삭제되었습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 삭제 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public HttpResponseMessage GetTodo(int id)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var currentUser = GetCurrentUser();
|
||||||
|
if (currentUser == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "로그인되지 않은 상태입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id <= 0)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "유효하지 않은 Todo ID입니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
string gcode = FCOMMON.info.Login.gcode;
|
||||||
|
string uid = FCOMMON.info.Login.no;
|
||||||
|
|
||||||
|
var sql = "SELECT * FROM EETGW_Todo WHERE idx = @idx AND gcode = @gcode AND uid = @uid";
|
||||||
|
var todo = DBM.QuerySingleOrDefault<TodoModel>(sql, new { idx = id, gcode = gcode, uid = uid });
|
||||||
|
|
||||||
|
if (todo == null)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일을 찾을 수 없습니다."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Data = todo
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return CreateJsonResponse(new
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "할일 조회 중 오류가 발생했습니다: " + ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private object GetCurrentUser()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(FCOMMON.info.Login.no)) return null;
|
||||||
|
else return FCOMMON.info.Login;
|
||||||
|
}
|
||||||
|
|
||||||
|
private HttpResponseMessage CreateJsonResponse(object data)
|
||||||
|
{
|
||||||
|
var json = JsonConvert.SerializeObject(data, new JsonSerializerSettings
|
||||||
|
{
|
||||||
|
NullValueHandling = NullValueHandling.Ignore,
|
||||||
|
DateFormatString = "yyyy-MM-dd HH:mm:ss"
|
||||||
|
});
|
||||||
|
|
||||||
|
return new HttpResponseMessage()
|
||||||
|
{
|
||||||
|
Content = new StringContent(
|
||||||
|
json,
|
||||||
|
System.Text.Encoding.UTF8,
|
||||||
|
"application/json")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
158
Project/Web/wwwroot/common/navigation.html
Normal file
158
Project/Web/wwwroot/common/navigation.html
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
<!-- 공통 네비게이션 -->
|
||||||
|
<nav id="main-navigation" class="glass-effect border-b border-white/10 relative z-40">
|
||||||
|
<div class="container mx-auto px-4">
|
||||||
|
<div class="flex items-center justify-between h-16">
|
||||||
|
<!-- 로고 및 브랜드 -->
|
||||||
|
<div class="flex items-center space-x-8">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="text-xl font-bold text-white">GroupWare</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 데스크톱 메뉴 -->
|
||||||
|
<nav class="hidden md:flex space-x-1">
|
||||||
|
<a href="/DashBoard/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="dashboard">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"></path>
|
||||||
|
</svg>
|
||||||
|
대시보드
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Common/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="common">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
공용코드
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Jobreport/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="jobreport">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||||
|
</svg>
|
||||||
|
업무일지
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Kuntae/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="kuntae">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
근태관리
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Todo/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="todo">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4"></path>
|
||||||
|
</svg>
|
||||||
|
할일관리
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Project/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="project">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||||
|
</svg>
|
||||||
|
프로젝트
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Purchase/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="purchase">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
||||||
|
</svg>
|
||||||
|
구매관리
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="/Customer/"
|
||||||
|
class="nav-item px-3 py-2 rounded-md text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10"
|
||||||
|
data-page="customer">
|
||||||
|
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||||
|
</svg>
|
||||||
|
고객관리
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우측 메뉴 -->
|
||||||
|
<div class="flex items-center space-x-4">
|
||||||
|
<div class="text-sm text-white/60">
|
||||||
|
<span>사용자</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 모바일 메뉴 버튼 -->
|
||||||
|
<button id="mobile-menu-button"
|
||||||
|
class="md:hidden text-white/60 hover:text-white focus:outline-none focus:text-white">
|
||||||
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 모바일 메뉴 -->
|
||||||
|
<div id="mobile-menu" class="md:hidden border-t border-white/10 py-2 hidden">
|
||||||
|
<a href="/DashBoard/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="dashboard">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"></path>
|
||||||
|
</svg>
|
||||||
|
대시보드
|
||||||
|
</a>
|
||||||
|
<a href="/Common/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="common">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
공용코드
|
||||||
|
</a>
|
||||||
|
<a href="/Jobreport/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="jobreport">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||||
|
</svg>
|
||||||
|
업무일지
|
||||||
|
</a>
|
||||||
|
<a href="/Kuntae/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="kuntae">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
근태관리
|
||||||
|
</a>
|
||||||
|
<a href="/Todo/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="todo">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4"></path>
|
||||||
|
</svg>
|
||||||
|
할일관리
|
||||||
|
</a>
|
||||||
|
<a href="/Project/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="project">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||||
|
</svg>
|
||||||
|
프로젝트
|
||||||
|
</a>
|
||||||
|
<a href="/Purchase/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="purchase">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
||||||
|
</svg>
|
||||||
|
구매관리
|
||||||
|
</a>
|
||||||
|
<a href="/Customer/" class="nav-item-mobile block px-3 py-2 text-sm font-medium transition-colors text-white/60 hover:text-white hover:bg-white/10" data-page="customer">
|
||||||
|
<svg class="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||||
|
</svg>
|
||||||
|
고객관리
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
490
Project/Web/wwwroot/lib/css/tailwind.min.css
vendored
Normal file
490
Project/Web/wwwroot/lib/css/tailwind.min.css
vendored
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
/* Tailwind CSS v3.3.0 - Custom Build for GroupWare React Components */
|
||||||
|
|
||||||
|
/*! tailwindcss v3.3.0 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
|
*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}::after,::before{--tw-content:''}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace, SFMono-Regular, "Roboto Mono", "Courier New", monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}
|
||||||
|
|
||||||
|
/* Base Variables */
|
||||||
|
:root {
|
||||||
|
--color-white: 255 255 255;
|
||||||
|
--color-black: 0 0 0;
|
||||||
|
--color-gray-50: 249 250 251;
|
||||||
|
--color-gray-100: 243 244 246;
|
||||||
|
--color-gray-200: 229 231 235;
|
||||||
|
--color-gray-300: 209 213 219;
|
||||||
|
--color-gray-400: 156 163 175;
|
||||||
|
--color-gray-500: 107 114 128;
|
||||||
|
--color-gray-600: 75 85 99;
|
||||||
|
--color-gray-700: 55 65 81;
|
||||||
|
--color-gray-800: 31 41 55;
|
||||||
|
--color-gray-900: 17 24 39;
|
||||||
|
--color-blue-50: 239 246 255;
|
||||||
|
--color-blue-100: 219 234 254;
|
||||||
|
--color-blue-200: 191 219 254;
|
||||||
|
--color-blue-300: 147 197 253;
|
||||||
|
--color-blue-400: 96 165 250;
|
||||||
|
--color-blue-500: 59 130 246;
|
||||||
|
--color-blue-600: 37 99 235;
|
||||||
|
--color-blue-700: 29 78 216;
|
||||||
|
--color-blue-800: 30 64 175;
|
||||||
|
--color-blue-900: 30 58 138;
|
||||||
|
--color-indigo-50: 238 242 255;
|
||||||
|
--color-indigo-100: 224 231 255;
|
||||||
|
--color-indigo-200: 199 210 254;
|
||||||
|
--color-indigo-300: 165 180 252;
|
||||||
|
--color-indigo-400: 129 140 248;
|
||||||
|
--color-indigo-500: 99 102 241;
|
||||||
|
--color-indigo-600: 79 70 229;
|
||||||
|
--color-indigo-700: 67 56 202;
|
||||||
|
--color-indigo-800: 55 48 163;
|
||||||
|
--color-indigo-900: 49 46 129;
|
||||||
|
--color-purple-50: 245 243 255;
|
||||||
|
--color-purple-100: 237 233 254;
|
||||||
|
--color-purple-200: 221 214 254;
|
||||||
|
--color-purple-300: 196 181 253;
|
||||||
|
--color-purple-400: 167 139 250;
|
||||||
|
--color-purple-500: 139 92 246;
|
||||||
|
--color-purple-600: 124 58 237;
|
||||||
|
--color-purple-700: 109 40 217;
|
||||||
|
--color-purple-800: 91 33 182;
|
||||||
|
--color-purple-900: 76 29 149;
|
||||||
|
--color-green-50: 240 253 244;
|
||||||
|
--color-green-100: 220 252 231;
|
||||||
|
--color-green-200: 187 247 208;
|
||||||
|
--color-green-300: 134 239 172;
|
||||||
|
--color-green-400: 74 222 128;
|
||||||
|
--color-green-500: 34 197 94;
|
||||||
|
--color-green-600: 22 163 74;
|
||||||
|
--color-green-700: 21 128 61;
|
||||||
|
--color-green-800: 22 101 52;
|
||||||
|
--color-green-900: 20 83 45;
|
||||||
|
--color-yellow-50: 255 251 235;
|
||||||
|
--color-yellow-100: 254 243 199;
|
||||||
|
--color-yellow-200: 253 230 138;
|
||||||
|
--color-yellow-300: 252 211 77;
|
||||||
|
--color-yellow-400: 251 191 36;
|
||||||
|
--color-yellow-500: 245 158 11;
|
||||||
|
--color-yellow-600: 217 119 6;
|
||||||
|
--color-yellow-700: 180 83 9;
|
||||||
|
--color-yellow-800: 146 64 14;
|
||||||
|
--color-yellow-900: 120 53 15;
|
||||||
|
--color-red-50: 254 242 242;
|
||||||
|
--color-red-100: 254 226 226;
|
||||||
|
--color-red-200: 252 165 165;
|
||||||
|
--color-red-300: 248 113 113;
|
||||||
|
--color-red-400: 239 68 68;
|
||||||
|
--color-red-500: 239 68 68;
|
||||||
|
--color-red-600: 220 38 38;
|
||||||
|
--color-red-700: 185 28 28;
|
||||||
|
--color-red-800: 153 27 27;
|
||||||
|
--color-red-900: 127 29 29;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility Classes - Core */
|
||||||
|
.container { width: 100%; }
|
||||||
|
@media (min-width: 640px) { .container { max-width: 640px; } }
|
||||||
|
@media (min-width: 768px) { .container { max-width: 768px; } }
|
||||||
|
@media (min-width: 1024px) { .container { max-width: 1024px; } }
|
||||||
|
@media (min-width: 1280px) { .container { max-width: 1280px; } }
|
||||||
|
@media (min-width: 1536px) { .container { max-width: 1536px; } }
|
||||||
|
|
||||||
|
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||||
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.py-8 { padding-top: 2rem; padding-bottom: 2rem; }
|
||||||
|
|
||||||
|
/* Positioning */
|
||||||
|
.fixed { position: fixed; }
|
||||||
|
.relative { position: relative; }
|
||||||
|
.absolute { position: absolute; }
|
||||||
|
.inset-0 { inset: 0px; }
|
||||||
|
.top-4 { top: 1rem; }
|
||||||
|
.right-4 { right: 1rem; }
|
||||||
|
.top-full { top: 100%; }
|
||||||
|
.z-10 { z-index: 10; }
|
||||||
|
.z-40 { z-index: 40; }
|
||||||
|
.z-50 { z-index: 50; }
|
||||||
|
|
||||||
|
/* Display */
|
||||||
|
.block { display: block; }
|
||||||
|
.inline-block { display: inline-block; }
|
||||||
|
.inline { display: inline; }
|
||||||
|
.flex { display: flex; }
|
||||||
|
.inline-flex { display: inline-flex; }
|
||||||
|
.table { display: table; }
|
||||||
|
.table-cell { display: table-cell; }
|
||||||
|
.grid { display: grid; }
|
||||||
|
.hidden { display: none; }
|
||||||
|
|
||||||
|
/* Flexbox */
|
||||||
|
.flex-1 { flex: 1 1 0%; }
|
||||||
|
.flex-wrap { flex-wrap: wrap; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.items-end { align-items: flex-end; }
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.justify-end { justify-content: flex-end; }
|
||||||
|
|
||||||
|
/* Grid */
|
||||||
|
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||||
|
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:col-span-2 { grid-column: span 2 / span 2; }
|
||||||
|
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
.gap-2 { gap: 0.5rem; }
|
||||||
|
.gap-3 { gap: 0.75rem; }
|
||||||
|
.gap-4 { gap: 1rem; }
|
||||||
|
.gap-6 { gap: 1.5rem; }
|
||||||
|
.space-x-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.25rem * var(--tw-space-x-reverse)); margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse))); }
|
||||||
|
.space-x-2 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.5rem * var(--tw-space-x-reverse)); margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse))); }
|
||||||
|
.space-x-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(0.75rem * var(--tw-space-x-reverse)); margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse))); }
|
||||||
|
.space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); }
|
||||||
|
.space-x-8 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(2rem * var(--tw-space-x-reverse)); margin-left: calc(2rem * calc(1 - var(--tw-space-x-reverse))); }
|
||||||
|
.space-y-1 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.25rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0.25rem * var(--tw-space-y-reverse)); }
|
||||||
|
.space-y-3 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); }
|
||||||
|
.space-y-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); margin-bottom: calc(1rem * var(--tw-space-y-reverse)); }
|
||||||
|
|
||||||
|
/* Margins */
|
||||||
|
.m-2 { margin: 0.5rem; }
|
||||||
|
.mb-1 { margin-bottom: 0.25rem; }
|
||||||
|
.mb-2 { margin-bottom: 0.5rem; }
|
||||||
|
.mb-4 { margin-bottom: 1rem; }
|
||||||
|
.mb-6 { margin-bottom: 1.5rem; }
|
||||||
|
.mr-1 { margin-right: 0.25rem; }
|
||||||
|
.mr-2 { margin-right: 0.5rem; }
|
||||||
|
.mr-3 { margin-right: 0.75rem; }
|
||||||
|
.ml-2 { margin-left: 0.5rem; }
|
||||||
|
.ml-4 { margin-left: 1rem; }
|
||||||
|
.ml-auto { margin-left: auto; }
|
||||||
|
.mt-6 { margin-top: 1.5rem; }
|
||||||
|
|
||||||
|
/* Padding */
|
||||||
|
.p-1 { padding: 0.25rem; }
|
||||||
|
.p-3 { padding: 0.75rem; }
|
||||||
|
.p-4 { padding: 1rem; }
|
||||||
|
.p-6 { padding: 1.5rem; }
|
||||||
|
.p-8 { padding: 2rem; }
|
||||||
|
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
||||||
|
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
||||||
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||||
|
.px-6 { padding-left: 1.5rem; padding-right: 1.5rem; }
|
||||||
|
.py-0 { padding-top: 0px; padding-bottom: 0px; }
|
||||||
|
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
||||||
|
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
||||||
|
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
||||||
|
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||||||
|
.pt-0 { padding-top: 0px; }
|
||||||
|
.pb-0 { padding-bottom: 0px; }
|
||||||
|
|
||||||
|
/* Size */
|
||||||
|
.w-4 { width: 1rem; }
|
||||||
|
.w-5 { width: 1.25rem; }
|
||||||
|
.w-6 { width: 1.5rem; }
|
||||||
|
.w-8 { width: 2rem; }
|
||||||
|
.w-12 { width: 3rem; }
|
||||||
|
.w-16 { width: 4rem; }
|
||||||
|
.w-20 { width: 5rem; }
|
||||||
|
.w-24 { width: 6rem; }
|
||||||
|
.w-28 { width: 7rem; }
|
||||||
|
.w-32 { width: 8rem; }
|
||||||
|
.w-40 { width: 10rem; }
|
||||||
|
.w-48 { width: 12rem; }
|
||||||
|
.w-full { width: 100%; }
|
||||||
|
.min-w-32 { min-width: 8rem; }
|
||||||
|
.min-w-48 { min-width: 12rem; }
|
||||||
|
.max-w-xs { max-width: 20rem; }
|
||||||
|
.max-w-md { max-width: 28rem; }
|
||||||
|
.max-w-2xl { max-width: 42rem; }
|
||||||
|
.h-4 { height: 1rem; }
|
||||||
|
.h-5 { height: 1.25rem; }
|
||||||
|
.h-6 { height: 1.5rem; }
|
||||||
|
.h-8 { height: 2rem; }
|
||||||
|
.h-10 { height: 2.5rem; }
|
||||||
|
.h-16 { height: 4rem; }
|
||||||
|
.min-h-screen { min-height: 100vh; }
|
||||||
|
.max-h-40 { max-height: 10rem; }
|
||||||
|
|
||||||
|
/* Background */
|
||||||
|
.bg-white { background-color: rgb(255 255 255); }
|
||||||
|
.bg-black { background-color: rgb(0 0 0); }
|
||||||
|
.bg-gray-500 { background-color: rgb(107 114 128); }
|
||||||
|
.bg-gray-800 { background-color: rgb(31 41 55); }
|
||||||
|
.bg-blue-300 { background-color: rgb(147 197 253); }
|
||||||
|
.bg-blue-500 { background-color: rgb(59 130 246); }
|
||||||
|
.bg-green-300 { background-color: rgb(134 239 172); }
|
||||||
|
.bg-green-500 { background-color: rgb(34 197 94); }
|
||||||
|
.bg-yellow-300 { background-color: rgb(252 211 77); }
|
||||||
|
.bg-yellow-500 { background-color: rgb(245 158 11); }
|
||||||
|
.bg-red-300 { background-color: rgb(248 113 113); }
|
||||||
|
.bg-red-400 { background-color: rgb(239 68 68); }
|
||||||
|
.bg-red-500 { background-color: rgb(239 68 68); }
|
||||||
|
.bg-purple-500 { background-color: rgb(139 92 246); }
|
||||||
|
.bg-orange-500 { background-color: rgb(249 115 22); }
|
||||||
|
|
||||||
|
/* Background with opacity */
|
||||||
|
.bg-white\/5 { background-color: rgb(255 255 255 / 0.05); }
|
||||||
|
.bg-white\/10 { background-color: rgb(255 255 255 / 0.1); }
|
||||||
|
.bg-white\/20 { background-color: rgb(255 255 255 / 0.2); }
|
||||||
|
.bg-white\/30 { background-color: rgb(255 255 255 / 0.3); }
|
||||||
|
.bg-gray-500\/20 { background-color: rgb(107 114 128 / 0.2); }
|
||||||
|
.bg-blue-500\/20 { background-color: rgb(59 130 246 / 0.2); }
|
||||||
|
.bg-green-500\/20 { background-color: rgb(34 197 94 / 0.2); }
|
||||||
|
.bg-yellow-500\/20 { background-color: rgb(245 158 11 / 0.2); }
|
||||||
|
.bg-red-500\/20 { background-color: rgb(239 68 68 / 0.2); }
|
||||||
|
.bg-purple-500\/20 { background-color: rgb(139 92 246 / 0.2); }
|
||||||
|
.bg-orange-500\/90 { background-color: rgb(249 115 22 / 0.9); }
|
||||||
|
.bg-blue-500\/90 { background-color: rgb(59 130 246 / 0.9); }
|
||||||
|
.bg-green-500\/90 { background-color: rgb(34 197 94 / 0.9); }
|
||||||
|
.bg-red-500\/90 { background-color: rgb(239 68 68 / 0.9); }
|
||||||
|
|
||||||
|
/* Gradient backgrounds */
|
||||||
|
.bg-gradient-to-br { background-image: linear-gradient(to bottom right, var(--tw-gradient-stops)); }
|
||||||
|
.from-blue-900 { --tw-gradient-from: rgb(30 58 138); --tw-gradient-to: rgb(30 58 138 / 0); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); }
|
||||||
|
.via-purple-900 { --tw-gradient-to: rgb(76 29 149 / 0); --tw-gradient-stops: var(--tw-gradient-from), rgb(76 29 149), var(--tw-gradient-to); }
|
||||||
|
.to-indigo-900 { --tw-gradient-to: rgb(49 46 129); }
|
||||||
|
|
||||||
|
/* Text Colors */
|
||||||
|
.text-white { color: rgb(255 255 255); }
|
||||||
|
.text-gray-300 { color: rgb(209 213 219); }
|
||||||
|
.text-gray-500 { color: rgb(107 114 128); }
|
||||||
|
.text-blue-300 { color: rgb(147 197 253); }
|
||||||
|
.text-green-300 { color: rgb(134 239 172); }
|
||||||
|
.text-yellow-300 { color: rgb(252 211 77); }
|
||||||
|
.text-red-300 { color: rgb(248 113 113); }
|
||||||
|
.text-red-400 { color: rgb(239 68 68); }
|
||||||
|
.text-orange-100 { color: rgb(255 237 213); }
|
||||||
|
.text-orange-900 { color: rgb(154 52 18); }
|
||||||
|
.text-primary-200 { color: rgb(147 197 253); }
|
||||||
|
.text-primary-300 { color: rgb(147 197 253); }
|
||||||
|
.text-primary-400 { color: rgb(96 165 250); }
|
||||||
|
.text-success-200 { color: rgb(187 247 208); }
|
||||||
|
.text-success-300 { color: rgb(134 239 172); }
|
||||||
|
.text-success-400 { color: rgb(74 222 128); }
|
||||||
|
.text-warning-300 { color: rgb(252 211 77); }
|
||||||
|
.text-danger-300 { color: rgb(248 113 113); }
|
||||||
|
.text-danger-400 { color: rgb(239 68 68); }
|
||||||
|
|
||||||
|
/* Text with opacity */
|
||||||
|
.text-white\/50 { color: rgb(255 255 255 / 0.5); }
|
||||||
|
.text-white\/60 { color: rgb(255 255 255 / 0.6); }
|
||||||
|
.text-white\/70 { color: rgb(255 255 255 / 0.7); }
|
||||||
|
.text-white\/80 { color: rgb(255 255 255 / 0.8); }
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
.text-xs { font-size: 0.75rem; line-height: 1rem; }
|
||||||
|
.text-sm { font-size: 0.875rem; line-height: 1.25rem; }
|
||||||
|
.text-base { font-size: 1rem; line-height: 1.5rem; }
|
||||||
|
.text-lg { font-size: 1.125rem; line-height: 1.75rem; }
|
||||||
|
.text-xl { font-size: 1.25rem; line-height: 1.75rem; }
|
||||||
|
.text-2xl { font-size: 1.5rem; line-height: 2rem; }
|
||||||
|
.font-medium { font-weight: 500; }
|
||||||
|
.font-semibold { font-weight: 600; }
|
||||||
|
.font-bold { font-weight: 700; }
|
||||||
|
.uppercase { text-transform: uppercase; }
|
||||||
|
.tracking-wider { letter-spacing: 0.05em; }
|
||||||
|
.text-left { text-align: left; }
|
||||||
|
.text-center { text-align: center; }
|
||||||
|
.truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.underline { text-decoration-line: underline; }
|
||||||
|
.whitespace-nowrap { white-space: nowrap; }
|
||||||
|
|
||||||
|
/* Borders */
|
||||||
|
.border { border-width: 1px; }
|
||||||
|
.border-b { border-bottom-width: 1px; }
|
||||||
|
.border-l-4 { border-left-width: 4px; }
|
||||||
|
.border-r { border-right-width: 1px; }
|
||||||
|
.border-t { border-top-width: 1px; }
|
||||||
|
.border-white\/10 { border-color: rgb(255 255 255 / 0.1); }
|
||||||
|
.border-white\/20 { border-color: rgb(255 255 255 / 0.2); }
|
||||||
|
.border-white\/30 { border-color: rgb(255 255 255 / 0.3); }
|
||||||
|
.border-gray-500\/30 { border-color: rgb(107 114 128 / 0.3); }
|
||||||
|
.border-orange-700 { border-color: rgb(194 65 12); }
|
||||||
|
.divide-y > :not([hidden]) ~ :not([hidden]) { --tw-divide-y-reverse: 0; border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse))); border-bottom-width: calc(1px * var(--tw-divide-y-reverse)); }
|
||||||
|
.divide-white\/10 > :not([hidden]) ~ :not([hidden]) { border-color: rgb(255 255 255 / 0.1); }
|
||||||
|
.divide-white\/20 > :not([hidden]) ~ :not([hidden]) { border-color: rgb(255 255 255 / 0.2); }
|
||||||
|
|
||||||
|
/* Border Radius */
|
||||||
|
.rounded { border-radius: 0.25rem; }
|
||||||
|
.rounded-md { border-radius: 0.375rem; }
|
||||||
|
.rounded-lg { border-radius: 0.5rem; }
|
||||||
|
.rounded-2xl { border-radius: 1rem; }
|
||||||
|
.rounded-full { border-radius: 9999px; }
|
||||||
|
|
||||||
|
/* Shadows */
|
||||||
|
.shadow-sm { box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); }
|
||||||
|
.shadow-lg { box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -2px rgb(0 0 0 / 0.05); }
|
||||||
|
|
||||||
|
/* Overflow */
|
||||||
|
.overflow-hidden { overflow: hidden; }
|
||||||
|
.overflow-x-auto { overflow-x: auto; }
|
||||||
|
.overflow-y-auto { overflow-y: auto; }
|
||||||
|
|
||||||
|
/* Interactivity */
|
||||||
|
.cursor-pointer { cursor: pointer; }
|
||||||
|
.cursor-not-allowed { cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* Focus */
|
||||||
|
.focus\:outline-none:focus { outline: 2px solid transparent; outline-offset: 2px; }
|
||||||
|
.focus\:ring-1:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
|
||||||
|
.focus\:ring-2:focus { --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color); --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color); box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000); }
|
||||||
|
.focus\:ring-white\/50:focus { --tw-ring-color: rgb(255 255 255 / 0.5); }
|
||||||
|
.focus\:ring-primary-400:focus { --tw-ring-color: rgb(96 165 250); }
|
||||||
|
.focus\:border-transparent:focus { border-color: transparent; }
|
||||||
|
.focus\:ring-offset-0:focus { --tw-ring-offset-width: 0px; }
|
||||||
|
|
||||||
|
/* Hover Effects */
|
||||||
|
.hover\:bg-white\/10:hover { background-color: rgb(255 255 255 / 0.1); }
|
||||||
|
.hover\:bg-white\/20:hover { background-color: rgb(255 255 255 / 0.2); }
|
||||||
|
.hover\:bg-white\/30:hover { background-color: rgb(255 255 255 / 0.3); }
|
||||||
|
.hover\:bg-primary-600:hover { background-color: rgb(37 99 235); }
|
||||||
|
.hover\:bg-green-600:hover { background-color: rgb(22 163 74); }
|
||||||
|
.hover\:bg-red-600:hover { background-color: rgb(220 38 38); }
|
||||||
|
.hover\:bg-purple-600:hover { background-color: rgb(124 58 237); }
|
||||||
|
.hover\:text-white:hover { color: rgb(255 255 255); }
|
||||||
|
.hover\:text-blue-200:hover { color: rgb(191 219 254); }
|
||||||
|
.hover\:text-blue-300:hover { color: rgb(147 197 253); }
|
||||||
|
.hover\:text-primary-300:hover { color: rgb(147 197 253); }
|
||||||
|
|
||||||
|
/* Disabled */
|
||||||
|
.disabled\:opacity-50:disabled { opacity: 0.5; }
|
||||||
|
.disabled\:cursor-not-allowed:disabled { cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* Transitions */
|
||||||
|
.transition-colors { transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||||
|
.transition-all { transition-property: all; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||||
|
.transition-transform { transition-property: transform; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; }
|
||||||
|
.duration-200 { transition-duration: 200ms; }
|
||||||
|
.duration-300 { transition-duration: 300ms; }
|
||||||
|
|
||||||
|
/* Transforms */
|
||||||
|
.transform { transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
|
||||||
|
.translate-x-0 { --tw-translate-x: 0px; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
|
||||||
|
.rotate-180 { --tw-rotate: 180deg; transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); }
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
.animate-spin { animation: spin 1s linear infinite; }
|
||||||
|
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
||||||
|
.animate-slide-up { animation: slideUp 0.3s ease-out; }
|
||||||
|
.animate-fade-in { animation: fadeIn 0.5s ease-in-out; }
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
50% { opacity: .5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { transform: translateY(10px); opacity: 0; }
|
||||||
|
to { transform: translateY(0); opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Backdrop Filter */
|
||||||
|
.backdrop-blur-sm { backdrop-filter: blur(4px); }
|
||||||
|
|
||||||
|
/* Opacity */
|
||||||
|
.opacity-0 { opacity: 0; }
|
||||||
|
.opacity-50 { opacity: 0.5; }
|
||||||
|
.opacity-100 { opacity: 1; }
|
||||||
|
|
||||||
|
/* Visibility */
|
||||||
|
.invisible { visibility: hidden; }
|
||||||
|
.visible { visibility: visible; }
|
||||||
|
|
||||||
|
/* Responsive Design Utilities */
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.sm\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.md\:flex { display: flex; }
|
||||||
|
.md\:hidden { display: none; }
|
||||||
|
.md\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
|
.md\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
.md\:grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.lg\:col-span-2 { grid-column: span 2 / span 2; }
|
||||||
|
.lg\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Tailwind Config Extensions */
|
||||||
|
.bg-primary-500 { background-color: rgb(59 130 246); }
|
||||||
|
.bg-primary-600 { background-color: rgb(37 99 235); }
|
||||||
|
.bg-success-500 { background-color: rgb(34 197 94); }
|
||||||
|
.bg-success-600 { background-color: rgb(22 163 74); }
|
||||||
|
.bg-warning-500 { background-color: rgb(245 158 11); }
|
||||||
|
.bg-warning-600 { background-color: rgb(217 119 6); }
|
||||||
|
.bg-danger-500 { background-color: rgb(239 68 68); }
|
||||||
|
.bg-danger-600 { background-color: rgb(220 38 38); }
|
||||||
|
.bg-primary-500\/20 { background-color: rgb(59 130 246 / 0.2); }
|
||||||
|
.bg-primary-500\/30 { background-color: rgb(59 130 246 / 0.3); }
|
||||||
|
.bg-success-500\/20 { background-color: rgb(34 197 94 / 0.2); }
|
||||||
|
.bg-success-500\/30 { background-color: rgb(34 197 94 / 0.3); }
|
||||||
|
.bg-warning-500\/20 { background-color: rgb(245 158 11 / 0.2); }
|
||||||
|
.bg-warning-500\/30 { background-color: rgb(245 158 11 / 0.3); }
|
||||||
|
.bg-danger-500\/20 { background-color: rgb(239 68 68 / 0.2); }
|
||||||
|
.bg-danger-500\/30 { background-color: rgb(239 68 68 / 0.3); }
|
||||||
|
|
||||||
|
/* Custom Utilities for the project */
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid #fff;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
4
Project/Web/wwwroot/lib/js/babel.min.js
vendored
Normal file
4
Project/Web/wwwroot/lib/js/babel.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
29924
Project/Web/wwwroot/lib/js/react-dom.development.js
Normal file
29924
Project/Web/wwwroot/lib/js/react-dom.development.js
Normal file
File diff suppressed because it is too large
Load Diff
3343
Project/Web/wwwroot/lib/js/react.development.js
Normal file
3343
Project/Web/wwwroot/lib/js/react.development.js
Normal file
File diff suppressed because it is too large
Load Diff
81
Project/Web/wwwroot/lib/js/tailwind-config.js
Normal file
81
Project/Web/wwwroot/lib/js/tailwind-config.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
// Tailwind CSS Configuration for GroupWare React Components
|
||||||
|
// This configuration includes all custom colors and extensions used in the React components
|
||||||
|
|
||||||
|
window.tailwind = {
|
||||||
|
config: {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
50: '#fffbeb',
|
||||||
|
100: '#fef3c7',
|
||||||
|
200: '#fde68a',
|
||||||
|
300: '#fcd34d',
|
||||||
|
400: '#fbbf24',
|
||||||
|
500: '#f59e0b',
|
||||||
|
600: '#d97706',
|
||||||
|
700: '#b45309',
|
||||||
|
800: '#92400e',
|
||||||
|
900: '#78350f',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#ef4444',
|
||||||
|
600: '#dc2626',
|
||||||
|
700: '#b91c1c',
|
||||||
|
800: '#991b1b',
|
||||||
|
900: '#7f1d1d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply configuration if tailwindcss is available
|
||||||
|
if (typeof tailwindcss !== 'undefined') {
|
||||||
|
tailwindcss.config = window.tailwind.config;
|
||||||
|
}
|
||||||
559
Project/Web/wwwroot/react/CommonApp.jsx
Normal file
559
Project/Web/wwwroot/react/CommonApp.jsx
Normal file
@@ -0,0 +1,559 @@
|
|||||||
|
// CommonApp.jsx - React Common Code Management Component for GroupWare
|
||||||
|
const { useState, useEffect, useRef } = React;
|
||||||
|
|
||||||
|
const CommonApp = () => {
|
||||||
|
// 상태 관리
|
||||||
|
const [groupData, setGroupData] = useState([]);
|
||||||
|
const [currentData, setCurrentData] = useState([]);
|
||||||
|
const [selectedGroupCode, setSelectedGroupCode] = useState(null);
|
||||||
|
const [selectedGroupName, setSelectedGroupName] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [deleteTargetIdx, setDeleteTargetIdx] = useState(null);
|
||||||
|
const [editMode, setEditMode] = useState('add');
|
||||||
|
|
||||||
|
// 편집 폼 데이터
|
||||||
|
const [editData, setEditData] = useState({
|
||||||
|
idx: '',
|
||||||
|
grp: '',
|
||||||
|
code: '',
|
||||||
|
svalue: '',
|
||||||
|
ivalue: '',
|
||||||
|
fvalue: '',
|
||||||
|
svalue2: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 페이지 로드시 초기 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
|
loadGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// API 호출 함수들
|
||||||
|
const loadGroups = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/GetGroups');
|
||||||
|
const data = await response.json();
|
||||||
|
setGroupData(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('그룹 데이터 로드 중 오류 발생:', error);
|
||||||
|
showNotification('그룹 데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadDataByGroup = async (grp) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
let url = 'http://127.0.0.1:7979/Common/GetList';
|
||||||
|
if (grp) {
|
||||||
|
url += '?grp=' + encodeURIComponent(grp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
setCurrentData(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('데이터 로드 중 오류 발생:', error);
|
||||||
|
showNotification('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveData = async () => {
|
||||||
|
try {
|
||||||
|
const data = {
|
||||||
|
idx: parseInt(editData.idx) || 0,
|
||||||
|
grp: editData.grp,
|
||||||
|
code: editData.code,
|
||||||
|
svalue: editData.svalue,
|
||||||
|
ivalue: parseInt(editData.ivalue) || 0,
|
||||||
|
fvalue: parseFloat(editData.fvalue) || 0.0,
|
||||||
|
svalue2: editData.svalue2,
|
||||||
|
memo: editData.memo
|
||||||
|
};
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/Save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.Success) {
|
||||||
|
showNotification(result.Message, 'success');
|
||||||
|
setShowEditModal(false);
|
||||||
|
if (selectedGroupCode) {
|
||||||
|
loadDataByGroup(selectedGroupCode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(result.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('저장 중 오류 발생:', error);
|
||||||
|
showNotification('저장 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteItem = async () => {
|
||||||
|
if (!deleteTargetIdx) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/Delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ idx: deleteTargetIdx })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
showNotification(data.Message, 'success');
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
if (selectedGroupCode) {
|
||||||
|
loadDataByGroup(selectedGroupCode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('삭제 중 오류 발생:', error);
|
||||||
|
showNotification('삭제 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
setDeleteTargetIdx(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이벤트 핸들러들
|
||||||
|
const selectGroup = (code, name) => {
|
||||||
|
setSelectedGroupCode(code);
|
||||||
|
setSelectedGroupName(name);
|
||||||
|
loadDataByGroup(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openAddModal = () => {
|
||||||
|
if (!selectedGroupCode) {
|
||||||
|
showNotification('먼저 코드그룹을 선택하세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setEditMode('add');
|
||||||
|
setEditData({
|
||||||
|
idx: '',
|
||||||
|
grp: selectedGroupCode,
|
||||||
|
code: '',
|
||||||
|
svalue: '',
|
||||||
|
ivalue: '',
|
||||||
|
fvalue: '',
|
||||||
|
svalue2: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openEditModal = (item) => {
|
||||||
|
setEditMode('edit');
|
||||||
|
setEditData({
|
||||||
|
idx: item.idx,
|
||||||
|
grp: item.grp || '',
|
||||||
|
code: item.code || '',
|
||||||
|
svalue: item.svalue || '',
|
||||||
|
ivalue: item.ivalue || '',
|
||||||
|
fvalue: item.fvalue || '',
|
||||||
|
svalue2: item.svalue2 || '',
|
||||||
|
memo: item.memo || ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openDeleteModal = (idx) => {
|
||||||
|
setDeleteTargetIdx(idx);
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
setShowEditModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModals = () => {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setDeleteTargetIdx(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field, value) => {
|
||||||
|
setEditData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
// 기존 알림 제거
|
||||||
|
const existing = document.querySelectorAll('.notification-toast');
|
||||||
|
existing.forEach(el => el.remove());
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90',
|
||||||
|
success: 'bg-green-500/90',
|
||||||
|
warning: 'bg-yellow-500/90',
|
||||||
|
error: 'bg-red-500/90'
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
info: '🔵',
|
||||||
|
success: '✅',
|
||||||
|
warning: '⚠️',
|
||||||
|
error: '❌'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification-toast fixed top-4 right-4 ${colors[type]} backdrop-blur-sm text-white px-4 py-3 rounded-lg z-50 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `<div class="flex items-center"><span class="mr-2">${icons[type]}</span>${message}</div>`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 키보드 이벤트
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
closeModals();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
{/* Navigation Component */}
|
||||||
|
<CommonNavigation currentPage="common" />
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 2열 구조 메인 컨테이너 */}
|
||||||
|
<div className="flex gap-6 h-[calc(100vh-200px)]">
|
||||||
|
{/* 좌측: 코드그룹 리스트 */}
|
||||||
|
<div className="w-80">
|
||||||
|
<div className="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
||||||
|
<div className="p-4 border-b border-white/10">
|
||||||
|
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14-7l-7 7-7-7M19 21l-7-7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
코드그룹 목록
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{groupData.length === 0 ? (
|
||||||
|
<div className="text-white/70 text-center py-4">그룹 데이터가 없습니다.</div>
|
||||||
|
) : (
|
||||||
|
groupData.map(group => (
|
||||||
|
<div
|
||||||
|
key={group.code}
|
||||||
|
className={`cursor-pointer p-3 rounded-lg border border-white/20 hover:bg-white/10 transition-all ${
|
||||||
|
selectedGroupCode === group.code ? 'bg-white/20' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => selectGroup(group.code, group.memo)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<span className="text-white text-sm font-medium">{group.code}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-white font-medium">{group.memo}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 상세 데이터 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
||||||
|
{/* 상단 헤더 */}
|
||||||
|
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>
|
||||||
|
{selectedGroupCode ? `${selectedGroupCode} - ${selectedGroupName}` : '코드그룹을 선택하세요'}
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/70 text-sm mt-1">
|
||||||
|
총 <span className="text-white font-medium">{currentData.length}</span>건
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={openAddModal}
|
||||||
|
className="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg transition-all border border-white/30 flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 테이블 */}
|
||||||
|
<div className="flex-1 overflow-x-auto overflow-y-auto custom-scrollbar">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
|
||||||
|
<th className="w-32 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
|
||||||
|
<th className="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
|
||||||
|
<th className="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
|
||||||
|
<th className="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{currentData.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="6" className="px-4 py-8 text-center text-white/70">
|
||||||
|
<svg className="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
{selectedGroupCode ? '데이터가 없습니다.' : '좌측에서 코드그룹을 선택하세요'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
currentData.map(item => (
|
||||||
|
<tr
|
||||||
|
key={item.idx}
|
||||||
|
className="hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
|
onClick={() => openEditModal(item)}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.code || '-'}</td>
|
||||||
|
<td className="px-4 py-4 text-sm text-white">{item.memo || '-'}</td>
|
||||||
|
<td className="px-4 py-4 text-sm text-white max-w-32 truncate" title={item.svalue || '-'}>
|
||||||
|
{item.svalue || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.ivalue || '0'}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.fvalue || '0.0'}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.svalue2 || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로딩 인디케이터 */}
|
||||||
|
{loading && (
|
||||||
|
<div className="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm z-40">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
데이터 로딩 중...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 추가/편집 모달 */}
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
{editMode === 'add' ? '공용코드 추가' : '공용코드 편집'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={closeModals}
|
||||||
|
className="text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">코드그룹 *</label>
|
||||||
|
<select
|
||||||
|
value={editData.grp}
|
||||||
|
onChange={(e) => handleInputChange('grp', e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="" className="bg-gray-800 text-white">선택하세요</option>
|
||||||
|
{groupData.map(group => (
|
||||||
|
<option key={group.code} value={group.code} className="bg-gray-800 text-white">
|
||||||
|
{group.code}-{group.memo}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">코드 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.code}
|
||||||
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="코드를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(문자열)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.svalue}
|
||||||
|
onChange={(e) => handleInputChange('svalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="문자열 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(숫자)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editData.ivalue}
|
||||||
|
onChange={(e) => handleInputChange('ivalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="숫자 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(실수)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={editData.fvalue}
|
||||||
|
onChange={(e) => handleInputChange('fvalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="실수 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값2</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.svalue2}
|
||||||
|
onChange={(e) => handleInputChange('svalue2', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="추가 문자열 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">비고</label>
|
||||||
|
<textarea
|
||||||
|
value={editData.memo}
|
||||||
|
onChange={(e) => handleInputChange('memo', e.target.value)}
|
||||||
|
rows="3"
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="비고사항을 입력하세요"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-between">
|
||||||
|
{editMode === 'edit' && (
|
||||||
|
<button
|
||||||
|
onClick={() => openDeleteModal(editData.idx)}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-2 ml-auto">
|
||||||
|
<button
|
||||||
|
onClick={closeModals}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveData}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 삭제 확인 모달 */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-md animate-slide-up">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className="w-12 h-12 bg-red-100/20 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg className="w-6 h-6 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-white">삭제 확인</h3>
|
||||||
|
<p className="text-sm text-white/70">이 작업은 되돌릴 수 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-white/80 mb-6">
|
||||||
|
선택한 공용코드를 삭제하시겠습니까?<br/>
|
||||||
|
<span className="text-sm text-white/60">이 작업은 되돌릴 수 없습니다.</span>
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={closeModals}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={deleteItem}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
590
Project/Web/wwwroot/react/CommonCode.jsx
Normal file
590
Project/Web/wwwroot/react/CommonCode.jsx
Normal file
@@ -0,0 +1,590 @@
|
|||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function CommonCode() {
|
||||||
|
// 상태 관리
|
||||||
|
const [currentData, setCurrentData] = useState([]);
|
||||||
|
const [groupData, setGroupData] = useState([]);
|
||||||
|
const [selectedGroupCode, setSelectedGroupCode] = useState(null);
|
||||||
|
const [selectedGroupName, setSelectedGroupName] = useState('');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [deleteTargetIdx, setDeleteTargetIdx] = useState(null);
|
||||||
|
const [editData, setEditData] = useState({
|
||||||
|
idx: '',
|
||||||
|
grp: '',
|
||||||
|
code: '',
|
||||||
|
svalue: '',
|
||||||
|
ivalue: '',
|
||||||
|
fvalue: '',
|
||||||
|
svalue2: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 초기 데이터 로드
|
||||||
|
useEffect(() => {
|
||||||
|
loadGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 코드그룹 목록 로드
|
||||||
|
const loadGroups = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/GetGroups');
|
||||||
|
const data = await response.json();
|
||||||
|
setGroupData(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('그룹 데이터 로드 중 오류 발생:', error);
|
||||||
|
showNotification('그룹 데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 그룹 선택 처리
|
||||||
|
const selectGroup = async (code, name) => {
|
||||||
|
setSelectedGroupCode(code);
|
||||||
|
setSelectedGroupName(name);
|
||||||
|
await loadDataByGroup(code);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 특정 그룹의 데이터 로드
|
||||||
|
const loadDataByGroup = async (grp) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
let url = 'http://127.0.0.1:7979/Common/GetList';
|
||||||
|
if (grp) {
|
||||||
|
url += '?grp=' + encodeURIComponent(grp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.json();
|
||||||
|
setCurrentData(data || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('데이터 로드 중 오류 발생:', error);
|
||||||
|
showNotification('데이터 로드 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 추가 모달 표시
|
||||||
|
const showAddModal = () => {
|
||||||
|
if (!selectedGroupCode) {
|
||||||
|
showNotification('먼저 코드그룹을 선택하세요.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditMode(false);
|
||||||
|
setEditData({
|
||||||
|
idx: '',
|
||||||
|
grp: selectedGroupCode,
|
||||||
|
code: '',
|
||||||
|
svalue: '',
|
||||||
|
ivalue: '',
|
||||||
|
fvalue: '',
|
||||||
|
svalue2: '',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 편집 모달 표시
|
||||||
|
const editItem = (idx) => {
|
||||||
|
const item = currentData.find(x => x.idx === idx);
|
||||||
|
if (!item) return;
|
||||||
|
|
||||||
|
setIsEditMode(true);
|
||||||
|
setEditData({
|
||||||
|
idx: item.idx,
|
||||||
|
grp: item.grp || '',
|
||||||
|
code: item.code || '',
|
||||||
|
svalue: item.svalue || '',
|
||||||
|
ivalue: item.ivalue || '',
|
||||||
|
fvalue: item.fvalue || '',
|
||||||
|
svalue2: item.svalue2 || '',
|
||||||
|
memo: item.memo || ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 데이터 저장
|
||||||
|
const saveData = async () => {
|
||||||
|
const form = document.getElementById('editForm');
|
||||||
|
if (!form.checkValidity()) {
|
||||||
|
form.reportValidity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
idx: parseInt(editData.idx) || 0,
|
||||||
|
grp: editData.grp,
|
||||||
|
code: editData.code,
|
||||||
|
svalue: editData.svalue,
|
||||||
|
ivalue: parseInt(editData.ivalue) || 0,
|
||||||
|
fvalue: parseFloat(editData.fvalue) || 0.0,
|
||||||
|
svalue2: editData.svalue2,
|
||||||
|
memo: editData.memo
|
||||||
|
};
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/Save', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
if (result.Success) {
|
||||||
|
showNotification(result.Message, 'success');
|
||||||
|
setShowEditModal(false);
|
||||||
|
if (selectedGroupCode) {
|
||||||
|
await loadDataByGroup(selectedGroupCode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(result.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('저장 중 오류 발생:', error);
|
||||||
|
showNotification('저장 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 삭제 확인
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!deleteTargetIdx) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Common/Delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ idx: deleteTargetIdx })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.Success) {
|
||||||
|
showNotification(data.Message, 'success');
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setDeleteTargetIdx(null);
|
||||||
|
if (selectedGroupCode) {
|
||||||
|
await loadDataByGroup(selectedGroupCode);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('삭제 중 오류 발생:', error);
|
||||||
|
showNotification('삭제 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 삭제 요청
|
||||||
|
const deleteCurrentItem = () => {
|
||||||
|
if (!editData.idx) {
|
||||||
|
showNotification('삭제할 항목이 없습니다.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setDeleteTargetIdx(parseInt(editData.idx));
|
||||||
|
setShowEditModal(false);
|
||||||
|
setShowDeleteModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 알림 표시 함수
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||||
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||||
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||||
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
info: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
success: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
warning: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
),
|
||||||
|
error: (
|
||||||
|
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// React에서는 실제 DOM 조작 대신 Toast 라이브러리나 상태로 관리하는 것이 좋습니다
|
||||||
|
// 여기서는 기존 방식을 유지하되 React 컴포넌트 스타일로 구현
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
${type === 'info' ? '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>' : ''}
|
||||||
|
${type === 'success' ? '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>' : ''}
|
||||||
|
${type === 'warning' ? '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path></svg>' : ''}
|
||||||
|
${type === 'error' ? '<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>' : ''}
|
||||||
|
<span class="ml-2">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 키보드 이벤트 핸들러
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 입력 필드 변경 핸들러
|
||||||
|
const handleInputChange = (field, value) => {
|
||||||
|
setEditData(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 2열 구조 메인 컨테이너 */}
|
||||||
|
<div className="flex gap-6 h-[calc(100vh-200px)]">
|
||||||
|
{/* 좌측: 코드그룹 리스트 */}
|
||||||
|
<div className="w-80">
|
||||||
|
<div className="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
||||||
|
<div className="p-4 border-b border-white/10">
|
||||||
|
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14-7l-7 7-7-7M19 21l-7-7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
코드그룹 목록
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto custom-scrollbar p-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
{groupData.length === 0 ? (
|
||||||
|
<div className="text-white/70 text-center py-4">그룹 데이터가 없습니다.</div>
|
||||||
|
) : (
|
||||||
|
groupData.map(group => (
|
||||||
|
<div
|
||||||
|
key={group.code}
|
||||||
|
className={`group-item cursor-pointer p-3 rounded-lg border border-white/20 hover:bg-white/10 transition-all ${
|
||||||
|
selectedGroupCode === group.code ? 'bg-white/20' : ''
|
||||||
|
}`}
|
||||||
|
onClick={() => selectGroup(group.code, group.memo)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center mr-3">
|
||||||
|
<span className="text-white text-sm font-medium">{group.code}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="text-white font-medium">{group.memo}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 상세 데이터 */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
|
||||||
|
{/* 상단 헤더 */}
|
||||||
|
<div className="p-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>{selectedGroupCode ? `${selectedGroupCode} - ${selectedGroupName}` : '코드그룹을 선택하세요'}</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-white/70 text-sm mt-1">총 <span className="text-white font-medium">{currentData.length}</span>건</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={showAddModal}
|
||||||
|
className="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg transition-all border border-white/30 flex items-center text-sm"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 테이블 */}
|
||||||
|
<div className="flex-1 overflow-x-auto overflow-y-auto custom-scrollbar">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
|
||||||
|
<th className="w-32 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
|
||||||
|
<th className="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
|
||||||
|
<th className="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
|
||||||
|
<th className="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{currentData.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="6" className="px-4 py-8 text-center text-white/70">
|
||||||
|
<svg className="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
{selectedGroupCode ? '데이터가 없습니다.' : '좌측에서 코드그룹을 선택하세요'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
currentData.map(item => (
|
||||||
|
<tr
|
||||||
|
key={item.idx}
|
||||||
|
className="hover:bg-white/5 transition-colors cursor-pointer"
|
||||||
|
onClick={() => editItem(item.idx)}
|
||||||
|
>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.code || '-'}</td>
|
||||||
|
<td className="px-4 py-4 text-sm text-white">{item.memo || '-'}</td>
|
||||||
|
<td className="px-4 py-4 text-sm text-white svalue-cell" title={item.svalue || '-'}>{item.svalue || '-'}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.ivalue || '0'}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.fvalue || '0.0'}</td>
|
||||||
|
<td className="px-4 py-4 whitespace-nowrap text-sm text-white">{item.svalue2 || '-'}</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로딩 인디케이터 */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
데이터 로딩 중...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 추가/편집 모달 */}
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white">
|
||||||
|
{isEditMode ? '공용코드 편집' : '공용코드 추가'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEditModal(false)}
|
||||||
|
className="text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<form id="editForm" className="p-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">코드그룹 *</label>
|
||||||
|
<select
|
||||||
|
value={editData.grp}
|
||||||
|
onChange={(e) => handleInputChange('grp', e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="" className="bg-gray-800 text-white">선택하세요</option>
|
||||||
|
{groupData.map(group => (
|
||||||
|
<option key={group.code} value={group.code} className="bg-gray-800 text-white">
|
||||||
|
{group.code}-{group.memo}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">코드 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.code}
|
||||||
|
onChange={(e) => handleInputChange('code', e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="코드를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(문자열)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.svalue}
|
||||||
|
onChange={(e) => handleInputChange('svalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="문자열 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(숫자)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={editData.ivalue}
|
||||||
|
onChange={(e) => handleInputChange('ivalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="숫자 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값(실수)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
value={editData.fvalue}
|
||||||
|
onChange={(e) => handleInputChange('fvalue', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="실수 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">값2</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.svalue2}
|
||||||
|
onChange={(e) => handleInputChange('svalue2', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="추가 문자열 값"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4">
|
||||||
|
<label className="block text-sm font-medium text-white/70 mb-2">비고</label>
|
||||||
|
<textarea
|
||||||
|
value={editData.memo}
|
||||||
|
onChange={(e) => handleInputChange('memo', e.target.value)}
|
||||||
|
rows="3"
|
||||||
|
className="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="비고사항을 입력하세요"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-between">
|
||||||
|
<button
|
||||||
|
onClick={deleteCurrentItem}
|
||||||
|
disabled={!isEditMode}
|
||||||
|
className={`bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center ${
|
||||||
|
!isEditMode ? 'opacity-50 cursor-not-allowed' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
||||||
|
</svg>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEditModal(false)}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={saveData}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 삭제 확인 모달 */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-md animate-slide-up">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<div className="w-12 h-12 bg-red-100 rounded-full flex items-center justify-center mr-4">
|
||||||
|
<svg className="w-6 h-6 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.732-.833-2.5 0L4.268 18.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">삭제 확인</h3>
|
||||||
|
<p className="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 mb-6">
|
||||||
|
선택한 공용코드를 삭제하시겠습니까?<br />
|
||||||
|
<span className="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</span>
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => { setShowDeleteModal(false); setDeleteTargetIdx(null); }}
|
||||||
|
className="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={confirmDelete}
|
||||||
|
className="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
166
Project/Web/wwwroot/react/CommonNavigation.jsx
Normal file
166
Project/Web/wwwroot/react/CommonNavigation.jsx
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// CommonNavigation.jsx - React Navigation Component for GroupWare
|
||||||
|
const CommonNavigation = ({ currentPage = 'dashboard' }) => {
|
||||||
|
const [menuItems, setMenuItems] = useState([]);
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState('사용자');
|
||||||
|
|
||||||
|
// 기본 메뉴 아이템 - React 경로 사용
|
||||||
|
const defaultMenuItems = [
|
||||||
|
{
|
||||||
|
key: 'dashboard',
|
||||||
|
title: '대시보드',
|
||||||
|
url: '/react/dashboard',
|
||||||
|
icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'common',
|
||||||
|
title: '공용코드',
|
||||||
|
url: '/react/common',
|
||||||
|
icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'jobreport',
|
||||||
|
title: '업무일지',
|
||||||
|
url: '/react/jobreport',
|
||||||
|
icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'kuntae',
|
||||||
|
title: '근태관리',
|
||||||
|
url: '/react/kuntae',
|
||||||
|
icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'todo',
|
||||||
|
title: '할일관리',
|
||||||
|
url: '/react/todo',
|
||||||
|
icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'project',
|
||||||
|
title: '프로젝트',
|
||||||
|
url: '/react/project',
|
||||||
|
icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'purchase',
|
||||||
|
title: '구매관리',
|
||||||
|
url: '/Purchase/',
|
||||||
|
icon: 'M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'customer',
|
||||||
|
title: '고객관리',
|
||||||
|
url: '/Customer/',
|
||||||
|
icon: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z',
|
||||||
|
isVisible: true,
|
||||||
|
sortOrder: 8
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 메뉴 아이템 로드 - defaultMenuItems 사용 (React 경로)
|
||||||
|
useEffect(() => {
|
||||||
|
setMenuItems(defaultMenuItems);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 보이는 메뉴 아이템만 정렬해서 반환
|
||||||
|
const visibleItems = menuItems
|
||||||
|
.filter(item => item.isVisible)
|
||||||
|
.sort((a, b) => a.sortOrder - b.sortOrder);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className="glass-effect border-b border-white/10 relative z-40">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex items-center justify-between h-16">
|
||||||
|
{/* 로고 및 브랜드 */}
|
||||||
|
<div className="flex items-center space-x-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="text-xl font-bold text-white">GroupWare</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 데스크톱 메뉴 */}
|
||||||
|
<nav className="hidden md:flex space-x-1">
|
||||||
|
{visibleItems.map(item => (
|
||||||
|
<a
|
||||||
|
key={item.key}
|
||||||
|
href={item.url}
|
||||||
|
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||||
|
currentPage === item.key
|
||||||
|
? 'bg-white/20 text-white'
|
||||||
|
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={item.icon}></path>
|
||||||
|
</svg>
|
||||||
|
{item.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측 메뉴 */}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<div className="text-sm text-white/60">
|
||||||
|
<span>{currentUser}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모바일 메뉴 버튼 */}
|
||||||
|
<button
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
className="md:hidden text-white/60 hover:text-white focus:outline-none focus:text-white"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
) : (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모바일 메뉴 */}
|
||||||
|
{mobileMenuOpen && (
|
||||||
|
<div className="md:hidden border-t border-white/10 py-2">
|
||||||
|
{visibleItems.map(item => (
|
||||||
|
<a
|
||||||
|
key={item.key}
|
||||||
|
href={item.url}
|
||||||
|
className={`block px-3 py-2 text-sm font-medium transition-colors ${
|
||||||
|
currentPage === item.key
|
||||||
|
? 'bg-white/20 text-white'
|
||||||
|
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||||
|
}`}
|
||||||
|
onClick={() => setMobileMenuOpen(false)}
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 inline mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d={item.icon}></path>
|
||||||
|
</svg>
|
||||||
|
{item.title}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
669
Project/Web/wwwroot/react/DashboardApp.jsx
Normal file
669
Project/Web/wwwroot/react/DashboardApp.jsx
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
// DashboardApp.jsx - React Dashboard Component for GroupWare
|
||||||
|
const { useState, useEffect, useRef } = React;
|
||||||
|
|
||||||
|
function DashboardApp() {
|
||||||
|
// 상태 관리
|
||||||
|
const [dashboardData, setDashboardData] = useState({
|
||||||
|
presentCount: 0,
|
||||||
|
leaveCount: 0,
|
||||||
|
leaveRequestCount: 0,
|
||||||
|
purchaseCountNR: 0,
|
||||||
|
purchaseCountCR: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [lastUpdated, setLastUpdated] = useState('');
|
||||||
|
const [modals, setModals] = useState({
|
||||||
|
presentUsers: false,
|
||||||
|
holidayUsers: false,
|
||||||
|
holidayRequest: false,
|
||||||
|
purchaseNR: false,
|
||||||
|
purchaseCR: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [modalData, setModalData] = useState({
|
||||||
|
presentUsers: [],
|
||||||
|
holidayUsers: [],
|
||||||
|
holidayRequests: [],
|
||||||
|
purchaseNR: [],
|
||||||
|
purchaseCR: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const [todoList, setTodoList] = useState([]);
|
||||||
|
|
||||||
|
// 모달 제어 함수
|
||||||
|
const showModal = (modalName) => {
|
||||||
|
setModals(prev => ({ ...prev, [modalName]: true }));
|
||||||
|
loadModalData(modalName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideModal = (modalName) => {
|
||||||
|
setModals(prev => ({ ...prev, [modalName]: false }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Dashboard 데이터 로드
|
||||||
|
const loadDashboardData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// 실제 DashBoardController API 호출
|
||||||
|
const [
|
||||||
|
currentUserResponse,
|
||||||
|
leaveCountResponse,
|
||||||
|
holyRequestResponse,
|
||||||
|
purchaseWaitResponse
|
||||||
|
] = await Promise.all([
|
||||||
|
fetch('http://127.0.0.1:7979/DashBoard/GetCurrentUserCount'),
|
||||||
|
fetch('http://127.0.0.1:7979/DashBoard/TodayCountH'),
|
||||||
|
fetch('http://127.0.0.1:7979/DashBoard/GetHolydayRequestCount'),
|
||||||
|
fetch('http://127.0.0.1:7979/DashBoard/GetPurchaseWaitCount')
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 현재 근무자 수 (JSON 응답)
|
||||||
|
const currentUserData = await currentUserResponse.json();
|
||||||
|
const presentCount = currentUserData.Count || 0;
|
||||||
|
|
||||||
|
// 휴가자 수 (텍스트 응답)
|
||||||
|
const leaveCountText = await leaveCountResponse.text();
|
||||||
|
const leaveCount = parseInt(leaveCountText.replace(/"/g, ''), 10) || 0;
|
||||||
|
|
||||||
|
// 휴가 요청 수 (JSON 응답)
|
||||||
|
const holyRequestData = await holyRequestResponse.json();
|
||||||
|
const leaveRequestCount = holyRequestData.HOLY || 0;
|
||||||
|
|
||||||
|
// 구매 대기 수 (JSON 응답)
|
||||||
|
const purchaseWaitData = await purchaseWaitResponse.json();
|
||||||
|
const purchaseCountNR = purchaseWaitData.NR || 0;
|
||||||
|
const purchaseCountCR = purchaseWaitData.CR || 0;
|
||||||
|
|
||||||
|
setDashboardData({
|
||||||
|
presentCount,
|
||||||
|
leaveCount,
|
||||||
|
leaveRequestCount,
|
||||||
|
purchaseCountNR,
|
||||||
|
purchaseCountCR
|
||||||
|
});
|
||||||
|
|
||||||
|
setLastUpdated(new Date().toLocaleString('ko-KR'));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('대시보드 데이터 로드 실패:', error);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 모달 데이터 로드
|
||||||
|
const loadModalData = async (modalName) => {
|
||||||
|
try {
|
||||||
|
let endpoint = '';
|
||||||
|
switch (modalName) {
|
||||||
|
case 'presentUsers':
|
||||||
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPresentUserList';
|
||||||
|
break;
|
||||||
|
case 'holidayUsers':
|
||||||
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetholyUser';
|
||||||
|
break;
|
||||||
|
case 'holidayRequest':
|
||||||
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetholyRequestUser';
|
||||||
|
break;
|
||||||
|
case 'purchaseNR':
|
||||||
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPurchaseNRList';
|
||||||
|
break;
|
||||||
|
case 'purchaseCR':
|
||||||
|
endpoint = 'http://127.0.0.1:7979/DashBoard/GetPurchaseCRList';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(endpoint);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
setModalData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[modalName]: Array.isArray(data) ? data : []
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`모달 데이터 로드 실패 (${modalName}):`, error);
|
||||||
|
setModalData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[modalName]: []
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Todo 목록 로드
|
||||||
|
const loadTodoList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://127.0.0.1:7979/Todo/GetUrgentTodos');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success && data.Data) {
|
||||||
|
setTodoList(data.Data);
|
||||||
|
} else {
|
||||||
|
setTodoList([]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Todo 목록 로드 실패:', error);
|
||||||
|
setTodoList([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 자동 새로고침 (30초마다)
|
||||||
|
useEffect(() => {
|
||||||
|
loadDashboardData();
|
||||||
|
loadModalData('holidayUsers'); // 휴가자 목록 자동 로드
|
||||||
|
loadTodoList(); // Todo 목록 자동 로드
|
||||||
|
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
loadDashboardData();
|
||||||
|
loadModalData('holidayUsers'); // 30초마다 휴가자 목록도 새로고침
|
||||||
|
loadTodoList(); // 30초마다 Todo 목록도 새로고침
|
||||||
|
}, 30000); // 30초
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 통계 카드 컴포넌트
|
||||||
|
const StatCard = ({ title, count, icon, color, onClick, isClickable = false }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`glass-effect rounded-2xl p-6 card-hover animate-slide-up ${isClickable ? 'cursor-pointer' : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-white/70 text-sm font-medium">{title}</p>
|
||||||
|
<p className="text-3xl font-bold text-white">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-pulse bg-white/20 h-8 w-16 rounded"></div>
|
||||||
|
) : count}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className={`w-12 h-12 ${color}/20 rounded-full flex items-center justify-center`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 테이블 모달 컴포넌트
|
||||||
|
const TableModal = ({ isOpen, onClose, title, headers, data, renderRow, maxWidth = 'max-w-4xl' }) => {
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className={`glass-effect rounded-2xl w-full ${maxWidth} max-h-[80vh] overflow-hidden animate-slide-up`}>
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<div className="overflow-x-auto max-h-[60vh] custom-scrollbar">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
{headers.map((header, index) => (
|
||||||
|
<th key={index} className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">
|
||||||
|
{header}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{data.length > 0 ? (
|
||||||
|
data.map((item, index) => renderRow(item, index))
|
||||||
|
) : (
|
||||||
|
<tr>
|
||||||
|
<td colSpan={headers.length} className="px-6 py-8 text-center text-white/50">
|
||||||
|
데이터가 없습니다.
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-between items-center">
|
||||||
|
<p className="text-white/70 text-sm">
|
||||||
|
총 <span className="font-medium">{data.length}</span>건
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
닫기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 헤더 */}
|
||||||
|
<div className="text-center mb-8 animate-fade-in">
|
||||||
|
<h1 className="text-4xl font-bold mb-4">근태현황 대시보드</h1>
|
||||||
|
<p className="text-white/70">실시간 근태 및 업무 현황을 확인하세요</p>
|
||||||
|
{lastUpdated && (
|
||||||
|
<p className="text-white/50 text-sm mt-2">마지막 업데이트: {lastUpdated}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 새로고침 버튼 */}
|
||||||
|
<div className="flex justify-end mb-6">
|
||||||
|
<button
|
||||||
|
onClick={loadDashboardData}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="glass-effect rounded-lg px-4 py-2 text-white hover:bg-white/10 transition-all duration-300 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2 inline-block"></div>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4 mr-2 inline-block" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
새로고침
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 카드 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||||||
|
<StatCard
|
||||||
|
title="출근(대상)"
|
||||||
|
count={dashboardData.presentCount}
|
||||||
|
isClickable={true}
|
||||||
|
onClick={() => showModal('presentUsers')}
|
||||||
|
color="bg-success-500"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-success-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="휴가"
|
||||||
|
count={dashboardData.leaveCount}
|
||||||
|
isClickable={true}
|
||||||
|
onClick={() => showModal('holidayUsers')}
|
||||||
|
color="bg-warning-500"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-warning-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="휴가요청"
|
||||||
|
count={dashboardData.leaveRequestCount}
|
||||||
|
isClickable={true}
|
||||||
|
onClick={() => showModal('holidayRequest')}
|
||||||
|
color="bg-primary-500"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="구매요청(NR)"
|
||||||
|
count={dashboardData.purchaseCountNR}
|
||||||
|
isClickable={true}
|
||||||
|
onClick={() => showModal('purchaseNR')}
|
||||||
|
color="bg-danger-500"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-danger-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatCard
|
||||||
|
title="구매요청(CR)"
|
||||||
|
count={dashboardData.purchaseCountCR}
|
||||||
|
isClickable={true}
|
||||||
|
onClick={() => showModal('purchaseCR')}
|
||||||
|
color="bg-purple-500"
|
||||||
|
icon={
|
||||||
|
<svg className="w-6 h-6 text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z"></path>
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 2칸 레이아웃: 좌측 휴가현황, 우측 할일 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 animate-slide-up">
|
||||||
|
{/* 좌측: 휴가/기타 현황 */}
|
||||||
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-white/10">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>
|
||||||
|
</svg>
|
||||||
|
휴가/기타 현황
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto max-h-[460px] custom-scrollbar">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">이름</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">형태</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">종류</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">기간</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">사유</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{modalData.holidayUsers.map((user, index) => {
|
||||||
|
// 형태에 따른 색상 결정
|
||||||
|
const typeColorClass = (user.type === '휴가') ? 'bg-green-500/20 text-green-300' : 'bg-warning-500/20 text-warning-300';
|
||||||
|
|
||||||
|
// 종류에 따른 색상 결정
|
||||||
|
let cateColorClass = 'bg-warning-500/20 text-warning-300'; // 기본값
|
||||||
|
if (user.cate === '휴가') {
|
||||||
|
cateColorClass = 'bg-warning-500/20 text-warning-300'; // 노란색 계열
|
||||||
|
} else if (user.cate === '파견') {
|
||||||
|
cateColorClass = 'bg-purple-500/20 text-purple-300'; // 보라색 계열
|
||||||
|
} else {
|
||||||
|
cateColorClass = 'bg-warning-500/20 text-warning-300'; // 기타는 주황색 계열
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기간 표시 형식 개선
|
||||||
|
let periodText = '';
|
||||||
|
if (user.sdate && user.edate) {
|
||||||
|
if (user.sdate === user.edate) {
|
||||||
|
periodText = user.sdate;
|
||||||
|
} else {
|
||||||
|
periodText = `${user.sdate}~${user.edate}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
periodText = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={index} className="hover:bg-white/5">
|
||||||
|
<td className="px-4 py-3 text-white text-sm font-medium">{user.name || '이름 없음'}</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${typeColorClass}`}>
|
||||||
|
{user.type || 'N/A'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${cateColorClass}`}>
|
||||||
|
{user.cate || '종류 없음'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-white text-sm">{periodText}</td>
|
||||||
|
<td className="px-4 py-3 text-white/70 text-sm">{user.title || '사유 없음'}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{modalData.holidayUsers.length === 0 && (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="5" className="px-4 py-8 text-center text-white/50">
|
||||||
|
현재 휴가자가 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 할일 */}
|
||||||
|
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||||
|
<div className="px-6 py-4 border-b border-white/10">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center justify-between">
|
||||||
|
<span className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||||
|
</svg>
|
||||||
|
할일
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
className="text-xs bg-primary-500/20 hover:bg-primary-500/30 text-primary-300 hover:text-primary-200 px-3 py-1 rounded-full transition-colors flex items-center"
|
||||||
|
onClick={() => alert('할일 추가 기능은 준비 중입니다.')}
|
||||||
|
>
|
||||||
|
<svg className="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
할일추가
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="text-xs bg-white/20 hover:bg-white/30 px-3 py-1 rounded-full transition-colors"
|
||||||
|
onClick={() => window.location.href = '/react/todo'}
|
||||||
|
>
|
||||||
|
전체보기
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="space-y-3 max-h-[384px] overflow-y-auto custom-scrollbar">
|
||||||
|
{todoList.length > 0 ? (
|
||||||
|
todoList.map((todo, index) => {
|
||||||
|
const flagIcon = todo.flag ? '📌 ' : '';
|
||||||
|
|
||||||
|
// 상태별 클래스
|
||||||
|
const getTodoStatusClass = (status) => {
|
||||||
|
switch(status) {
|
||||||
|
case '0': return 'bg-gray-500/20 text-gray-300';
|
||||||
|
case '1': return 'bg-primary-500/20 text-primary-300';
|
||||||
|
case '2': return 'bg-danger-500/20 text-danger-300';
|
||||||
|
case '3': return 'bg-warning-500/20 text-warning-300';
|
||||||
|
case '5': return 'bg-success-500/20 text-success-300';
|
||||||
|
default: return 'bg-white/10 text-white/50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTodoStatusText = (status) => {
|
||||||
|
switch(status) {
|
||||||
|
case '0': return '대기';
|
||||||
|
case '1': return '진행';
|
||||||
|
case '2': return '취소';
|
||||||
|
case '3': return '보류';
|
||||||
|
case '5': return '완료';
|
||||||
|
default: return '대기';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTodoSeqnoClass = (seqno) => {
|
||||||
|
switch(seqno) {
|
||||||
|
case '0': return 'bg-gray-500/20 text-gray-300';
|
||||||
|
case '1': return 'bg-success-500/20 text-success-300';
|
||||||
|
case '2': return 'bg-warning-500/20 text-warning-300';
|
||||||
|
case '3': return 'bg-danger-500/20 text-danger-300';
|
||||||
|
default: return 'bg-white/10 text-white/50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTodoSeqnoText = (seqno) => {
|
||||||
|
switch(seqno) {
|
||||||
|
case '0': return '낮음';
|
||||||
|
case '1': return '보통';
|
||||||
|
case '2': return '높음';
|
||||||
|
case '3': return '긴급';
|
||||||
|
default: return '보통';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const statusClass = getTodoStatusClass(todo.status);
|
||||||
|
const statusText = getTodoStatusText(todo.status);
|
||||||
|
const seqnoClass = getTodoSeqnoClass(todo.seqno);
|
||||||
|
const seqnoText = getTodoSeqnoText(todo.seqno);
|
||||||
|
|
||||||
|
const expireText = todo.expire ? new Date(todo.expire).toLocaleDateString('ko-KR') : '';
|
||||||
|
const isExpired = todo.expire && new Date(todo.expire) < new Date();
|
||||||
|
const expireClass = isExpired ? 'text-danger-400' : 'text-white/60';
|
||||||
|
|
||||||
|
// 만료일이 지난 경우 배경을 적색계통으로 강조
|
||||||
|
const expiredBgClass = isExpired ? 'bg-danger-600/30 border-danger-400/40 hover:bg-danger-600/40' : 'bg-white/10 hover:bg-white/15 border-white/20';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className={`${expiredBgClass} backdrop-blur-sm rounded-lg p-3 transition-colors cursor-pointer border`}
|
||||||
|
onClick={() => alert('Todo 상세보기 기능은 준비 중입니다.')}>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${seqnoClass}`}>
|
||||||
|
{seqnoText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{expireText && (
|
||||||
|
<span className={`text-xs ${expireClass}`}>
|
||||||
|
{expireText}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-white text-sm font-medium mb-1">
|
||||||
|
{flagIcon}{todo.title || '제목 없음'}
|
||||||
|
</p>
|
||||||
|
{todo.description && (
|
||||||
|
<p className="text-white/70 text-xs mb-2 line-clamp-2">
|
||||||
|
{todo.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{todo.request && (
|
||||||
|
<p className="text-white/50 text-xs mt-2">요청자: {todo.request}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-white/50 py-8">
|
||||||
|
<svg className="w-8 h-8 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||||
|
</svg>
|
||||||
|
급한 할일이 없습니다
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 출근 대상자 모달 */}
|
||||||
|
<TableModal
|
||||||
|
isOpen={modals.presentUsers}
|
||||||
|
onClose={() => hideModal('presentUsers')}
|
||||||
|
title="금일 출근 대상자 목록"
|
||||||
|
headers={['사번', '이름', '공정', '직급', '상태', '이메일']}
|
||||||
|
data={modalData.presentUsers}
|
||||||
|
renderRow={(user, index) => (
|
||||||
|
<tr key={index} className="hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{user.id || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{user.name || '이름 없음'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{user.gname || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{user.level || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-success-400 text-sm">출근</td>
|
||||||
|
<td className="px-6 py-4 text-white/70 text-sm">{user.email || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 휴가 요청 모달 */}
|
||||||
|
<TableModal
|
||||||
|
isOpen={modals.holidayRequest}
|
||||||
|
onClose={() => hideModal('holidayRequest')}
|
||||||
|
title="휴가 신청 목록"
|
||||||
|
maxWidth="max-w-6xl"
|
||||||
|
headers={['사번', '이름', '항목', '일자', '요청일', '요청시간', '비고']}
|
||||||
|
data={modalData.holidayRequests}
|
||||||
|
renderRow={(request, index) => (
|
||||||
|
<tr key={index} className="hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{request.uid || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{request.name || '이름 없음'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{request.cate || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">
|
||||||
|
{request.sdate && request.edate ? `${request.sdate} ~ ${request.edate}` : 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{request.holydays || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{request.holytimes || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white/70 text-sm">{request.HolyReason || request.remark || 'N/A'}</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 구매요청 NR 모달 */}
|
||||||
|
<TableModal
|
||||||
|
isOpen={modals.purchaseNR}
|
||||||
|
onClose={() => hideModal('purchaseNR')}
|
||||||
|
title="구매요청(NR) 목록"
|
||||||
|
maxWidth="max-w-7xl"
|
||||||
|
headers={['요청일', '공정', '품목', '규격', '단위', '수량', '단가', '금액']}
|
||||||
|
data={modalData.purchaseNR}
|
||||||
|
renderRow={(item, index) => (
|
||||||
|
<tr key={index} className="hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pdate || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.process || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumname || '품목 없음'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumscale || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumunit || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumqtyreq || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">
|
||||||
|
{item.pumprice ? `₩${Number(item.pumprice).toLocaleString()}` : 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-danger-400 text-sm font-medium">
|
||||||
|
{item.pumamt ? `₩${Number(item.pumamt).toLocaleString()}` : 'N/A'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 구매요청 CR 모달 */}
|
||||||
|
<TableModal
|
||||||
|
isOpen={modals.purchaseCR}
|
||||||
|
onClose={() => hideModal('purchaseCR')}
|
||||||
|
title="구매요청(CR) 목록"
|
||||||
|
maxWidth="max-w-7xl"
|
||||||
|
headers={['요청일', '공정', '품목', '규격', '단위', '수량', '단가', '금액']}
|
||||||
|
data={modalData.purchaseCR}
|
||||||
|
renderRow={(item, index) => (
|
||||||
|
<tr key={index} className="hover:bg-white/5">
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pdate || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.process || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumname || '품목 없음'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumscale || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumunit || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">{item.pumqtyreq || 'N/A'}</td>
|
||||||
|
<td className="px-6 py-4 text-white text-sm">
|
||||||
|
{item.pumprice ? `₩${Number(item.pumprice).toLocaleString()}` : 'N/A'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-purple-400 text-sm font-medium">
|
||||||
|
{item.pumamt ? `₩${Number(item.pumamt).toLocaleString()}` : 'N/A'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
Project/Web/wwwroot/react/DevWarning.jsx
Normal file
21
Project/Web/wwwroot/react/DevWarning.jsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// 개발중 경고 메시지 컴포넌트
|
||||||
|
function DevWarning({ show = false }) {
|
||||||
|
// show props가 false이거나 없으면 아무것도 렌더링하지 않음
|
||||||
|
if (!show) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dev-warning animate-slide-up">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="title">🚧 개발중인 기능입니다</p>
|
||||||
|
<p className="description">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
988
Project/Web/wwwroot/react/JobReport.jsx
Normal file
988
Project/Web/wwwroot/react/JobReport.jsx
Normal file
@@ -0,0 +1,988 @@
|
|||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function JobReport() {
|
||||||
|
// 상태 관리
|
||||||
|
const [jobData, setJobData] = useState([]);
|
||||||
|
const [filteredData, setFilteredData] = useState([]);
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(25);
|
||||||
|
const [sortColumn, setSortColumn] = useState('pdate');
|
||||||
|
const [sortDirection, setSortDirection] = useState('desc');
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
const [editData, setEditData] = useState({});
|
||||||
|
const [isEditMode, setIsEditMode] = useState(false);
|
||||||
|
|
||||||
|
// 필터 상태
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
status: '',
|
||||||
|
type: '',
|
||||||
|
user: '',
|
||||||
|
project: '',
|
||||||
|
search: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 통계 데이터
|
||||||
|
const [statistics, setStatistics] = useState({
|
||||||
|
totalDays: 0,
|
||||||
|
todayHours: 0,
|
||||||
|
todayProgress: 0,
|
||||||
|
totalOT: 0,
|
||||||
|
activeProjects: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트 마운트시 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
initializeFilters();
|
||||||
|
loadJobData();
|
||||||
|
loadUserList();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 필터 변경 시 데이터 필터링
|
||||||
|
useEffect(() => {
|
||||||
|
filterData();
|
||||||
|
}, [jobData, filters]);
|
||||||
|
|
||||||
|
// 초기 필터 설정 (오늘부터 -2주)
|
||||||
|
const initializeFilters = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toISOString().split('T')[0];
|
||||||
|
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
||||||
|
|
||||||
|
setFilters(prev => ({
|
||||||
|
...prev,
|
||||||
|
startDate: twoWeeksAgo,
|
||||||
|
endDate: today
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 업무일지 데이터 로드
|
||||||
|
const loadJobData = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
let url = '/Jobreport/GetJobData';
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (filters.startDate) params.append('startDate', filters.startDate);
|
||||||
|
if (filters.endDate) params.append('endDate', filters.endDate);
|
||||||
|
if (filters.user) params.append('user', filters.user);
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
url += '?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: 데이터를 불러오는데 실패했습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseText = await response.text();
|
||||||
|
const responseData = JSON.parse(responseText);
|
||||||
|
|
||||||
|
if (Array.isArray(responseData)) {
|
||||||
|
setJobData(responseData);
|
||||||
|
} else if (responseData.error) {
|
||||||
|
throw new Error(responseData.error);
|
||||||
|
} else {
|
||||||
|
setJobData([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading job data:', error);
|
||||||
|
setJobData([]);
|
||||||
|
showNotification('데이터를 불러오는데 실패했습니다: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 사용자 목록 로드
|
||||||
|
const loadUserList = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Jobreport/GetUsers');
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
// 사용자 데이터를 상태로 저장 (필요시)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('사용자 목록 로드 중 오류:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 통계 업데이트
|
||||||
|
useEffect(() => {
|
||||||
|
updateStatistics();
|
||||||
|
}, [jobData]);
|
||||||
|
|
||||||
|
const updateStatistics = () => {
|
||||||
|
const totalDays = new Set(jobData.map(item => item.pdate)).size;
|
||||||
|
const totalOT = jobData.reduce((sum, item) => sum + (parseFloat(item.ot) || 0), 0);
|
||||||
|
const activeProjects = new Set(jobData.filter(item => item.status === '진행중').map(item => item.projectName)).size;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const todayData = jobData.filter(item => {
|
||||||
|
if (!item.pdate) return false;
|
||||||
|
const itemDate = item.pdate.toString();
|
||||||
|
if (itemDate.length >= 10) {
|
||||||
|
return itemDate.substring(0, 10) === today;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
let todayHours = 0;
|
||||||
|
if (todayData.length > 0) {
|
||||||
|
todayHours = todayData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0);
|
||||||
|
}
|
||||||
|
const todayProgress = (todayHours / 8) * 100;
|
||||||
|
|
||||||
|
setStatistics({
|
||||||
|
totalDays,
|
||||||
|
todayHours,
|
||||||
|
todayProgress,
|
||||||
|
totalOT,
|
||||||
|
activeProjects
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 데이터 필터링
|
||||||
|
const filterData = () => {
|
||||||
|
let filtered = jobData.filter(item => {
|
||||||
|
const statusMatch = !filters.status || item.status === filters.status;
|
||||||
|
const typeMatch = !filters.type || item.type === filters.type;
|
||||||
|
const projectMatch = !filters.project || item.projectName === filters.project;
|
||||||
|
const searchMatch = !filters.search ||
|
||||||
|
(item.description && item.description.toLowerCase().includes(filters.search.toLowerCase())) ||
|
||||||
|
(item.projectName && item.projectName.toLowerCase().includes(filters.search.toLowerCase())) ||
|
||||||
|
(item.requestpart && item.requestpart.toLowerCase().includes(filters.search.toLowerCase()));
|
||||||
|
|
||||||
|
return statusMatch && typeMatch && projectMatch && searchMatch;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 정렬
|
||||||
|
filtered.sort((a, b) => {
|
||||||
|
let aVal = a[sortColumn];
|
||||||
|
let bVal = b[sortColumn];
|
||||||
|
|
||||||
|
if (sortColumn === 'pdate') {
|
||||||
|
aVal = new Date(aVal);
|
||||||
|
bVal = new Date(bVal);
|
||||||
|
} else if (['hrs', 'ot'].includes(sortColumn)) {
|
||||||
|
aVal = parseFloat(aVal) || 0;
|
||||||
|
bVal = parseFloat(bVal) || 0;
|
||||||
|
} else {
|
||||||
|
aVal = (aVal || '').toString().toLowerCase();
|
||||||
|
bVal = (bVal || '').toString().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1;
|
||||||
|
if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredData(filtered);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터 변경 핸들러
|
||||||
|
const handleFilterChange = (field, value) => {
|
||||||
|
setFilters(prev => ({ ...prev, [field]: value }));
|
||||||
|
|
||||||
|
// 날짜 필터 변경시 데이터 다시 로드
|
||||||
|
if (field === 'startDate' || field === 'endDate' || field === 'user') {
|
||||||
|
setTimeout(() => loadJobData(), 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터 초기화
|
||||||
|
const clearFilters = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toISOString().split('T')[0];
|
||||||
|
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
||||||
|
|
||||||
|
setFilters({
|
||||||
|
startDate: twoWeeksAgo,
|
||||||
|
endDate: today,
|
||||||
|
status: '',
|
||||||
|
type: '',
|
||||||
|
user: '',
|
||||||
|
project: '',
|
||||||
|
search: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => loadJobData(), 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 정렬 처리
|
||||||
|
const handleSort = (column) => {
|
||||||
|
if (sortColumn === column) {
|
||||||
|
setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc');
|
||||||
|
} else {
|
||||||
|
setSortColumn(column);
|
||||||
|
setSortDirection('asc');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 추가 모달 표시
|
||||||
|
const showAddJobModal = () => {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
setIsEditMode(false);
|
||||||
|
setEditData({
|
||||||
|
idx: '',
|
||||||
|
pdate: today,
|
||||||
|
status: '진행 중',
|
||||||
|
projectName: '',
|
||||||
|
requestpart: '',
|
||||||
|
type: '',
|
||||||
|
hrs: '8',
|
||||||
|
ot: '',
|
||||||
|
otStart: '',
|
||||||
|
otEnd: '',
|
||||||
|
description: ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 편집 모달 표시
|
||||||
|
const showEditJobModal = async (item) => {
|
||||||
|
try {
|
||||||
|
// 상세 정보 로드
|
||||||
|
const response = await fetch(`/Jobreport/GetJobDetail?id=${item.idx}`);
|
||||||
|
if (response.ok) {
|
||||||
|
const fullItem = await response.json();
|
||||||
|
item = fullItem.error ? item : fullItem;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to load full details, using truncated data:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsEditMode(true);
|
||||||
|
setEditData({
|
||||||
|
idx: item.idx || '',
|
||||||
|
pdate: item.pdate || '',
|
||||||
|
status: item.status || '',
|
||||||
|
projectName: item.projectName || '',
|
||||||
|
requestpart: item.requestpart || '',
|
||||||
|
type: item.type || '',
|
||||||
|
hrs: item.hrs || '',
|
||||||
|
ot: item.ot || '',
|
||||||
|
otStart: item.otStart || '',
|
||||||
|
otEnd: item.otEnd || '',
|
||||||
|
description: item.description || ''
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 저장 처리
|
||||||
|
const handleSave = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const formData = new URLSearchParams();
|
||||||
|
Object.keys(editData).forEach(key => {
|
||||||
|
if (key !== 'idx' || editData.idx) {
|
||||||
|
formData.append(key, editData[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = isEditMode ? '/Jobreport/Edit' : '/Jobreport/Add';
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${isEditMode ? '수정' : '추가'}에 실패했습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowEditModal(false);
|
||||||
|
await loadJobData();
|
||||||
|
showNotification(`업무일지가 성공적으로 ${isEditMode ? '수정' : '추가'}되었습니다.`, 'success');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving job:', error);
|
||||||
|
showNotification(`업무일지 ${isEditMode ? '수정' : '추가'} 중 오류가 발생했습니다: ` + error.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 삭제 처리
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!editData.idx) {
|
||||||
|
showNotification('삭제할 수 없는 항목입니다.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.confirm('정말로 이 업무일지를 삭제하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Jobreport/Delete/${editData.idx}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setShowEditModal(false);
|
||||||
|
await loadJobData();
|
||||||
|
showNotification('업무일지가 성공적으로 삭제되었습니다.', 'success');
|
||||||
|
} else {
|
||||||
|
throw new Error(result.message || '삭제에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting job:', error);
|
||||||
|
showNotification('업무일지 삭제 중 오류가 발생했습니다: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 엑셀 내보내기
|
||||||
|
const exportToExcel = () => {
|
||||||
|
if (filteredData.length === 0) {
|
||||||
|
showNotification('내보낼 데이터가 없습니다.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const periodText = filters.startDate && filters.endDate ? `_${filters.startDate}_${filters.endDate}` : '';
|
||||||
|
const headers = ['날짜', '상태', '프로젝트명', '요청부서', '타입', '업무내용', '근무시간', '초과근무'];
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...filteredData.map(item => [
|
||||||
|
formatDate(item.pdate),
|
||||||
|
item.status || '',
|
||||||
|
item.projectName || '',
|
||||||
|
item.requestpart || '',
|
||||||
|
item.type || '',
|
||||||
|
`"${(item.description || '').replace(/"/g, '""')}"`,
|
||||||
|
item.hrs || '',
|
||||||
|
item.ot || ''
|
||||||
|
].join(','))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `업무일지${periodText}_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 유틸리티 함수들
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('ko-KR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case '진행 중': return 'bg-blue-100 text-blue-800';
|
||||||
|
case '진행 완료': return 'bg-green-100 text-green-800';
|
||||||
|
case '대기': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 알림 표시 함수
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||||
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||||
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||||
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="ml-2">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지네이션
|
||||||
|
const maxPage = Math.ceil(filteredData.length / pageSize);
|
||||||
|
const startIndex = (currentPage - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const pageData = filteredData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
// 프로젝트 목록 업데이트
|
||||||
|
const uniqueProjects = [...new Set(jobData.map(item => item.projectName).filter(Boolean))];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 개발중 경고 메시지 */}
|
||||||
|
<div className="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
||||||
|
<p className="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 카드 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8 animate-slide-up">
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-primary-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">총 업무일수</p>
|
||||||
|
<p className="text-2xl font-bold text-white">{statistics.totalDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-success-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">오늘 근무시간</p>
|
||||||
|
<p className={`text-2xl font-bold ${statistics.todayHours < 8 ? 'text-red-300' : 'text-green-300'}`}>
|
||||||
|
{statistics.todayHours.toFixed(1)}h
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-white/60">(목표 8시간의 {statistics.todayProgress.toFixed(0)}%)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-warning-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">총 초과근무</p>
|
||||||
|
<p className="text-2xl font-bold text-white">{statistics.totalOT.toFixed(1)}h</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-purple-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">진행중 프로젝트</p>
|
||||||
|
<p className="text-2xl font-bold text-white">{statistics.activeProjects}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 필터 및 검색 */}
|
||||||
|
<div className="glass-effect rounded-lg mb-6 animate-slide-up">
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
{/* 좌측: 필터 컨트롤 */}
|
||||||
|
<div className="lg:col-span-2 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">조회기간</label>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={(e) => handleFilterChange('startDate', e.target.value)}
|
||||||
|
className="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
/>
|
||||||
|
<span className="flex items-center text-white/60 text-xs">~</span>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={(e) => handleFilterChange('endDate', e.target.value)}
|
||||||
|
className="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">상태</label>
|
||||||
|
<select
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) => handleFilterChange('status', e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
>
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="진행중">진행중</option>
|
||||||
|
<option value="완료">완료</option>
|
||||||
|
<option value="대기">대기</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">타입</label>
|
||||||
|
<select
|
||||||
|
value={filters.type}
|
||||||
|
onChange={(e) => handleFilterChange('type', e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
>
|
||||||
|
<option value="">전체</option>
|
||||||
|
<option value="개발">개발</option>
|
||||||
|
<option value="유지보수">유지보수</option>
|
||||||
|
<option value="분석">분석</option>
|
||||||
|
<option value="테스트">테스트</option>
|
||||||
|
<option value="문서작업">문서작업</option>
|
||||||
|
<option value="회의">회의</option>
|
||||||
|
<option value="기타">기타</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">프로젝트</label>
|
||||||
|
<select
|
||||||
|
value={filters.project}
|
||||||
|
onChange={(e) => handleFilterChange('project', e.target.value)}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
>
|
||||||
|
<option value="">전체</option>
|
||||||
|
{uniqueProjects.map(project => (
|
||||||
|
<option key={project} value={project}>{project}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">검색</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => handleFilterChange('search', e.target.value)}
|
||||||
|
placeholder="업무 내용 검색..."
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 액션 버튼들 */}
|
||||||
|
<div className="flex flex-col space-y-2 justify-center">
|
||||||
|
<button
|
||||||
|
onClick={showAddJobModal}
|
||||||
|
className="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
업무일지 추가
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={exportToExcel}
|
||||||
|
className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
엑셀 다운로드
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
|
</svg>
|
||||||
|
필터 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 테이블 */}
|
||||||
|
<div className="glass-effect rounded-lg overflow-hidden animate-slide-up custom-scrollbar">
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="min-w-full divide-y divide-white/20">
|
||||||
|
<thead className="bg-white/10">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('pdate')}
|
||||||
|
>
|
||||||
|
날짜 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('status')}
|
||||||
|
>
|
||||||
|
상태 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('hrs')}
|
||||||
|
>
|
||||||
|
근무시간 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('projectName')}
|
||||||
|
>
|
||||||
|
프로젝트명 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">업무내용</th>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('requestpart')}
|
||||||
|
>
|
||||||
|
요청부서 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors"
|
||||||
|
onClick={() => handleSort('type')}
|
||||||
|
>
|
||||||
|
타입 <svg className="w-4 h-4 inline ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="7" className="p-8 text-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : pageData.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="7" className="p-8 text-center">
|
||||||
|
<svg className="w-12 h-12 text-white/60 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-white/70">업무일지 데이터가 없습니다.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
pageData.map((item, index) => {
|
||||||
|
const hrs = parseFloat(item.hrs) || 0;
|
||||||
|
const ot = parseFloat(item.ot) || 0;
|
||||||
|
let workTimeDisplay = '';
|
||||||
|
|
||||||
|
if (hrs > 0) {
|
||||||
|
workTimeDisplay = hrs.toFixed(1);
|
||||||
|
if (ot > 0) {
|
||||||
|
workTimeDisplay += '+' + ot.toFixed(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
workTimeDisplay = '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr
|
||||||
|
key={item.idx || index}
|
||||||
|
className="hover:bg-white/10 cursor-pointer transition-colors"
|
||||||
|
onClick={() => showEditJobModal(item)}
|
||||||
|
>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{formatDate(item.pdate)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}`}>
|
||||||
|
{item.status || '-'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{workTimeDisplay}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span>{item.projectName || '-'}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-white">
|
||||||
|
<div className="max-w-xs truncate" title={item.description || ''}>
|
||||||
|
{item.description || '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{item.requestpart || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{item.type || '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이지네이션 */}
|
||||||
|
<div className="mt-6 flex items-center justify-between glass-effect rounded-lg p-4">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="text-sm text-white/80">페이지당 행 수:</span>
|
||||||
|
<select
|
||||||
|
value={pageSize}
|
||||||
|
onChange={(e) => {
|
||||||
|
setPageSize(parseInt(e.target.value));
|
||||||
|
setCurrentPage(1);
|
||||||
|
}}
|
||||||
|
className="bg-white/20 border border-white/30 rounded-md px-2 py-1 text-sm text-white"
|
||||||
|
>
|
||||||
|
<option value="10">10</option>
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50">50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
|
||||||
|
disabled={currentPage <= 1}
|
||||||
|
className="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
이전
|
||||||
|
</button>
|
||||||
|
<span className="text-sm text-white/80">{currentPage} / {maxPage}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setCurrentPage(Math.min(maxPage, currentPage + 1))}
|
||||||
|
disabled={currentPage >= maxPage}
|
||||||
|
className="px-3 py-1 border border-white/30 rounded-md text-sm text-white hover:bg-white/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 편집 모달 */}
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-6xl animate-slide-up">
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||||
|
</svg>
|
||||||
|
업무일지 {isEditMode ? '편집' : '추가'}
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowEditModal(false)}
|
||||||
|
className="text-white/70 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<form onSubmit={handleSave} className="p-6">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
||||||
|
{/* 좌측: 기본 정보 */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">날짜 *</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={editData.pdate || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, pdate: e.target.value}))}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">상태 *</label>
|
||||||
|
<select
|
||||||
|
value={editData.status || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, status: e.target.value}))}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
<option value="진행 중">진행 중</option>
|
||||||
|
<option value="완료">완료</option>
|
||||||
|
<option value="대기">대기</option>
|
||||||
|
<option value="보류">보류</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">요청부서</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.requestpart || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, requestpart: e.target.value}))}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">타입</label>
|
||||||
|
<select
|
||||||
|
value={editData.type || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, type: e.target.value}))}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="">선택하세요</option>
|
||||||
|
<option value="개발">개발</option>
|
||||||
|
<option value="유지보수">유지보수</option>
|
||||||
|
<option value="분석">분석</option>
|
||||||
|
<option value="테스트">테스트</option>
|
||||||
|
<option value="문서작업">문서작업</option>
|
||||||
|
<option value="회의">회의</option>
|
||||||
|
<option value="기타">기타</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">근무시간 (시간) *</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
value={editData.hrs || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, hrs: e.target.value}))}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">초과근무 (시간)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="any"
|
||||||
|
value={editData.ot || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, ot: e.target.value}))}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">초과근무 시작시간</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={editData.otStart || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, otStart: e.target.value}))}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">초과근무 종료시간</label>
|
||||||
|
<input
|
||||||
|
type="time"
|
||||||
|
value={editData.otEnd || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, otEnd: e.target.value}))}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 프로젝트명과 업무내용 */}
|
||||||
|
<div className="lg:col-span-3 space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">프로젝트명 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editData.projectName || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, projectName: e.target.value}))}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">업무내용 *</label>
|
||||||
|
<textarea
|
||||||
|
value={editData.description || ''}
|
||||||
|
onChange={(e) => setEditData(prev => ({...prev, description: e.target.value}))}
|
||||||
|
rows="15"
|
||||||
|
required
|
||||||
|
className="w-full h-full min-h-[360px] bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all resize-vertical"
|
||||||
|
placeholder="상세한 업무 내용을 입력하세요..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-between items-center bg-black/10 rounded-b-2xl">
|
||||||
|
{isEditMode && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
className="bg-danger-500 hover:bg-danger-600 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex space-x-3 ml-auto">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setShowEditModal(false)}
|
||||||
|
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={handleSave}
|
||||||
|
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors"
|
||||||
|
>
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
339
Project/Web/wwwroot/react/Kuntae.jsx
Normal file
339
Project/Web/wwwroot/react/Kuntae.jsx
Normal file
@@ -0,0 +1,339 @@
|
|||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function Kuntae() {
|
||||||
|
// 상태 관리
|
||||||
|
const [kuntaeData, setKuntaeData] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
startDate: '',
|
||||||
|
endDate: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 통계 데이터
|
||||||
|
const [statistics, setStatistics] = useState({
|
||||||
|
totalDays: 0,
|
||||||
|
approvedDays: 0,
|
||||||
|
pendingDays: 0,
|
||||||
|
rejectedDays: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트 마운트시 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
initializeFilters();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 초기 필터 설정 (이번 달)
|
||||||
|
const initializeFilters = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||||
|
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||||
|
|
||||||
|
setFilters({
|
||||||
|
startDate: firstDay.toISOString().split('T')[0],
|
||||||
|
endDate: lastDay.toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
|
||||||
|
// 초기 데이터 로드
|
||||||
|
loadKuntaeData({
|
||||||
|
startDate: firstDay.toISOString().split('T')[0],
|
||||||
|
endDate: lastDay.toISOString().split('T')[0]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 근태 데이터 로드
|
||||||
|
const loadKuntaeData = async (searchFilters = filters) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
let url = '/Kuntae/GetData';
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (searchFilters.startDate) params.append('startDate', searchFilters.startDate);
|
||||||
|
if (searchFilters.endDate) params.append('endDate', searchFilters.endDate);
|
||||||
|
|
||||||
|
if (params.toString()) {
|
||||||
|
url += '?' + params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: 데이터를 불러오는데 실패했습니다.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responseData = await response.json();
|
||||||
|
|
||||||
|
if (Array.isArray(responseData)) {
|
||||||
|
setKuntaeData(responseData);
|
||||||
|
updateStatistics(responseData);
|
||||||
|
} else if (responseData.error) {
|
||||||
|
throw new Error(responseData.error);
|
||||||
|
} else {
|
||||||
|
setKuntaeData([]);
|
||||||
|
updateStatistics([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading kuntae data:', error);
|
||||||
|
setKuntaeData([]);
|
||||||
|
updateStatistics([]);
|
||||||
|
showNotification('데이터를 불러오는데 실패했습니다: ' + error.message, 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 통계 업데이트
|
||||||
|
const updateStatistics = (data) => {
|
||||||
|
const totalDays = data.length;
|
||||||
|
const approvedDays = data.filter(item => item.status === '승인').length;
|
||||||
|
const pendingDays = data.filter(item => item.status === '대기').length;
|
||||||
|
const rejectedDays = data.filter(item => item.status === '반려').length;
|
||||||
|
|
||||||
|
setStatistics({
|
||||||
|
totalDays,
|
||||||
|
approvedDays,
|
||||||
|
pendingDays,
|
||||||
|
rejectedDays
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터 변경 핸들러
|
||||||
|
const handleFilterChange = (field, value) => {
|
||||||
|
setFilters(prev => ({ ...prev, [field]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색 실행
|
||||||
|
const handleSearch = () => {
|
||||||
|
loadKuntaeData();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 유틸리티 함수들
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleDateString('ko-KR');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case '승인': return 'bg-green-100 text-green-800';
|
||||||
|
case '대기': return 'bg-yellow-100 text-yellow-800';
|
||||||
|
case '반려': return 'bg-red-100 text-red-800';
|
||||||
|
default: return 'bg-gray-100 text-gray-800';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 알림 표시 함수
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||||
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||||
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||||
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="ml-2">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 개발중 경고 메시지 */}
|
||||||
|
<div className="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
|
||||||
|
<p className="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 검색 및 필터 섹션 */}
|
||||||
|
<div className="glass-effect rounded-lg p-6 mb-6 animate-slide-up">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.startDate}
|
||||||
|
onChange={(e) => handleFilterChange('startDate', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={filters.endDate}
|
||||||
|
onChange={(e) => handleFilterChange('endDate', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 bg-white/20 border border-white/30 rounded-md text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<button
|
||||||
|
onClick={handleSearch}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="w-full glass-effect text-white px-4 py-2 rounded-md hover:bg-white/30 transition-colors duration-200 flex items-center justify-center disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="loading inline-block w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></div>
|
||||||
|
로딩중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'조회'
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 카드 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-6 animate-slide-up">
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-primary-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">총 신청건</p>
|
||||||
|
<p className="text-2xl font-bold text-white">{statistics.totalDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-success-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">승인</p>
|
||||||
|
<p className="text-2xl font-bold text-green-300">{statistics.approvedDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-warning-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">대기</p>
|
||||||
|
<p className="text-2xl font-bold text-yellow-300">{statistics.pendingDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 card-hover">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="p-3 bg-red-500/20 rounded-lg">
|
||||||
|
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div className="ml-4">
|
||||||
|
<p className="text-sm font-medium text-white/80">반려</p>
|
||||||
|
<p className="text-2xl font-bold text-red-300">{statistics.rejectedDays}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 데이터 테이블 */}
|
||||||
|
<div className="glass-effect rounded-lg overflow-hidden animate-slide-up">
|
||||||
|
<div className="table-container custom-scrollbar" style={{ maxHeight: '600px', overflowY: 'auto' }}>
|
||||||
|
<table className="min-w-full divide-y divide-white/20">
|
||||||
|
<thead className="bg-white/10 sticky top-0">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">신청일</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">시작일</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">종료일</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">휴가종류</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">일수</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">상태</th>
|
||||||
|
<th className="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">사유</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="7" className="p-8 text-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<div className="loading inline-block w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin mr-3"></div>
|
||||||
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : kuntaeData.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="7" className="p-8 text-center">
|
||||||
|
<svg className="w-12 h-12 text-white/60 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
<p className="text-white/70">근태 데이터가 없습니다.</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
kuntaeData.map((item, index) => (
|
||||||
|
<tr key={item.idx || index} className="hover:bg-white/10 transition-colors">
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{formatDate(item.requestDate)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{formatDate(item.startDate)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{formatDate(item.endDate)}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{item.type || '-'}
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||||
|
{item.days || '0'}일
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}`}>
|
||||||
|
{item.status || '-'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-sm text-white">
|
||||||
|
<div className="max-w-xs truncate" title={item.reason || ''}>
|
||||||
|
{item.reason || '-'}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
308
Project/Web/wwwroot/react/LoginApp.jsx
Normal file
308
Project/Web/wwwroot/react/LoginApp.jsx
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
// LoginApp.jsx - React Login Component for GroupWare
|
||||||
|
const { useState, useEffect, useRef } = React;
|
||||||
|
|
||||||
|
function LoginApp() {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
gcode: '',
|
||||||
|
userId: '',
|
||||||
|
password: '',
|
||||||
|
rememberMe: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const [userGroups, setUserGroups] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState({ type: '', text: '', show: false });
|
||||||
|
const [isFormReady, setIsFormReady] = useState(false);
|
||||||
|
|
||||||
|
const gcodeRef = useRef(null);
|
||||||
|
const userIdRef = useRef(null);
|
||||||
|
const passwordRef = useRef(null);
|
||||||
|
|
||||||
|
// 메시지 표시 함수
|
||||||
|
const showMessage = (type, text) => {
|
||||||
|
setMessage({ type, text, show: true });
|
||||||
|
setTimeout(() => {
|
||||||
|
setMessage(prev => ({ ...prev, show: false }));
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 폼 데이터 업데이트
|
||||||
|
const handleInputChange = (e) => {
|
||||||
|
const { name, value, type, checked } = e.target;
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[name]: type === 'checkbox' ? checked : value
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 사용자 그룹 목록 로드
|
||||||
|
const loadUserGroups = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/DashBoard/GetUserGroups');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
// 유효한 그룹만 필터링
|
||||||
|
const validGroups = data.filter(group => group.gcode && group.name);
|
||||||
|
setUserGroups(validGroups);
|
||||||
|
|
||||||
|
// 이전 로그인 정보 로드
|
||||||
|
await loadPreviousLoginInfo();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('그룹 목록 로드 중 오류 발생:', error);
|
||||||
|
showMessage('error', '부서 목록을 불러오는 중 오류가 발생했습니다.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 이전 로그인 정보 로드
|
||||||
|
const loadPreviousLoginInfo = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Home/GetPreviousLoginInfo');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.Success && result.Data) {
|
||||||
|
const { Gcode, UserId } = result.Data;
|
||||||
|
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
gcode: Gcode || '',
|
||||||
|
userId: UserId ? UserId.split(';')[0] : ''
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 포커스 설정
|
||||||
|
setTimeout(() => {
|
||||||
|
if (Gcode && UserId) {
|
||||||
|
passwordRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
gcodeRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsFormReady(true);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('이전 로그인 정보 로드 중 오류 발생:', error);
|
||||||
|
setIsFormReady(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
gcodeRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 로그인 처리
|
||||||
|
const handleLogin = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const { gcode, userId, password, rememberMe } = formData;
|
||||||
|
|
||||||
|
// 유효성 검사
|
||||||
|
if (!gcode || !userId || !password) {
|
||||||
|
showMessage('error', '그룹코드/사용자ID/비밀번호를 입력해주세요.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Home/Login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
Gcode: gcode,
|
||||||
|
UserId: userId,
|
||||||
|
Password: password,
|
||||||
|
RememberMe: rememberMe
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
// 로그인 성공
|
||||||
|
showMessage('success', data.Message);
|
||||||
|
|
||||||
|
// WebView2에 로그인 성공 메시지 전송
|
||||||
|
if (window.chrome && window.chrome.webview) {
|
||||||
|
window.chrome.webview.postMessage('LOGIN_SUCCESS');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 리다이렉트 URL이 있으면 이동
|
||||||
|
if (data.RedirectUrl) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = data.RedirectUrl;
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 로그인 실패
|
||||||
|
showMessage('error', data.Message || '로그인에 실패했습니다.');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('로그인 요청 중 오류 발생:', error);
|
||||||
|
showMessage('error', '서버 연결에 실패했습니다. 다시 시도해주세요.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 실행
|
||||||
|
useEffect(() => {
|
||||||
|
loadUserGroups();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gradient-bg min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div className="w-full max-w-md">
|
||||||
|
{/* 로그인 카드 */}
|
||||||
|
<div className="glass-effect rounded-3xl p-8 card-hover animate-bounce-in">
|
||||||
|
{/* 로고 및 제목 */}
|
||||||
|
<div className="text-center mb-8 animate-fade-in">
|
||||||
|
<div className="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-white mb-2">GroupWare</h1>
|
||||||
|
<p className="text-white/70 text-sm">로그인하여 시스템에 접속하세요</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로그인 폼 */}
|
||||||
|
<form onSubmit={handleLogin} className="space-y-6 animate-slide-up">
|
||||||
|
{/* Gcode 드롭다운 */}
|
||||||
|
<div className="relative">
|
||||||
|
<select
|
||||||
|
ref={gcodeRef}
|
||||||
|
name="gcode"
|
||||||
|
value={formData.gcode}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white focus:outline-none focus:border-primary-400 input-focus appearance-none"
|
||||||
|
required
|
||||||
|
disabled={!isFormReady}
|
||||||
|
>
|
||||||
|
<option value="" className="text-gray-800">부서를 선택하세요</option>
|
||||||
|
{userGroups.map(group => (
|
||||||
|
<option key={group.gcode} value={group.gcode} className="text-gray-800">
|
||||||
|
{group.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="absolute right-3 top-3 pointer-events-none">
|
||||||
|
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 사용자 ID 입력 */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={userIdRef}
|
||||||
|
type="text"
|
||||||
|
name="userId"
|
||||||
|
value={formData.userId}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/60 focus:outline-none focus:border-primary-400 input-focus"
|
||||||
|
placeholder="사원번호"
|
||||||
|
required
|
||||||
|
disabled={!isFormReady}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-3">
|
||||||
|
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 비밀번호 입력 */}
|
||||||
|
<div className="relative">
|
||||||
|
<input
|
||||||
|
ref={passwordRef}
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="input-field w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/60 focus:outline-none focus:border-primary-400 input-focus"
|
||||||
|
placeholder="비밀번호"
|
||||||
|
required
|
||||||
|
disabled={!isFormReady}
|
||||||
|
/>
|
||||||
|
<div className="absolute right-3 top-3">
|
||||||
|
<svg className="w-5 h-5 text-white/40" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로그인 버튼 */}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={isLoading || !isFormReady}
|
||||||
|
className="w-full bg-primary-500 hover:bg-primary-600 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold py-3 px-4 rounded-xl transition-all duration-300 transform hover:scale-105 focus:outline-none focus:ring-2 focus:ring-primary-400 focus:ring-offset-2 focus:ring-offset-transparent"
|
||||||
|
>
|
||||||
|
<span className="flex items-center justify-center">
|
||||||
|
{isLoading ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
|
||||||
|
로그인 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
|
||||||
|
</svg>
|
||||||
|
로그인
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* 추가 옵션 */}
|
||||||
|
<div className="mt-6 text-center">
|
||||||
|
<div className="flex items-center justify-center space-x-4 text-sm">
|
||||||
|
<label className="flex items-center text-white/70 hover:text-white cursor-pointer transition-colors">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="rememberMe"
|
||||||
|
checked={formData.rememberMe}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
className="mr-2 w-4 h-4 text-primary-500 bg-white/10 border-white/20 rounded focus:ring-primary-400 focus:ring-2"
|
||||||
|
/>
|
||||||
|
로그인 정보 저장
|
||||||
|
</label>
|
||||||
|
<a href="#" className="text-primary-300 hover:text-primary-200 transition-colors">비밀번호 찾기</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 푸터 */}
|
||||||
|
<div className="text-center mt-6 animate-fade-in">
|
||||||
|
<p className="text-white/50 text-sm">
|
||||||
|
© 2024 GroupWare System. All rights reserved.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 메시지 표시 */}
|
||||||
|
{message.show && (
|
||||||
|
<div className={`fixed top-4 left-1/2 transform -translate-x-1/2 px-6 py-3 rounded-lg shadow-lg animate-slide-up ${
|
||||||
|
message.type === 'error' ? 'bg-red-500' : 'bg-green-500'
|
||||||
|
} text-white`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
{message.type === 'error' ? (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
) : (
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
|
||||||
|
)}
|
||||||
|
</svg>
|
||||||
|
<span>{message.text}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
884
Project/Web/wwwroot/react/Project.jsx
Normal file
884
Project/Web/wwwroot/react/Project.jsx
Normal file
@@ -0,0 +1,884 @@
|
|||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function Project() {
|
||||||
|
// 상태 관리
|
||||||
|
const [projects, setProjects] = useState([]);
|
||||||
|
const [filteredProjects, setFilteredProjects] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [currentUser, setCurrentUser] = useState('사용자');
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
|
// 모달 상태
|
||||||
|
const [showProjectModal, setShowProjectModal] = useState(false);
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [currentProjectIdx, setCurrentProjectIdx] = useState(null);
|
||||||
|
|
||||||
|
// 필터 상태
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
status: '진행',
|
||||||
|
search: '',
|
||||||
|
manager: 'my'
|
||||||
|
});
|
||||||
|
|
||||||
|
// UI 상태
|
||||||
|
const [showFilters, setShowFilters] = useState(false);
|
||||||
|
const [showColumnDropdown, setShowColumnDropdown] = useState(false);
|
||||||
|
const [visibleColumns, setVisibleColumns] = useState({
|
||||||
|
serial: false,
|
||||||
|
plant: false,
|
||||||
|
line: false,
|
||||||
|
package: false,
|
||||||
|
staff: false,
|
||||||
|
process: false,
|
||||||
|
expire: false,
|
||||||
|
delivery: false,
|
||||||
|
design: false,
|
||||||
|
electric: false,
|
||||||
|
program: false,
|
||||||
|
budgetDue: false,
|
||||||
|
budget: false,
|
||||||
|
jasmin: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// 프로젝트 폼 상태
|
||||||
|
const [projectForm, setProjectForm] = useState({
|
||||||
|
idx: 0,
|
||||||
|
name: '',
|
||||||
|
process: '',
|
||||||
|
sdate: '',
|
||||||
|
edate: '',
|
||||||
|
ddate: '',
|
||||||
|
odate: '',
|
||||||
|
userManager: '',
|
||||||
|
status: '진행',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// 통계 상태
|
||||||
|
const [statusCounts, setStatusCounts] = useState({
|
||||||
|
진행: 0,
|
||||||
|
완료: 0,
|
||||||
|
대기: 0,
|
||||||
|
중단: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트 마운트시 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
getCurrentUser();
|
||||||
|
loadProjects();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 필터 변경시 프로젝트 목록 새로 로드
|
||||||
|
useEffect(() => {
|
||||||
|
loadProjects();
|
||||||
|
}, [filters.status, filters.manager]);
|
||||||
|
|
||||||
|
// 검색어 변경시 필터링
|
||||||
|
useEffect(() => {
|
||||||
|
filterData();
|
||||||
|
}, [filters.search, projects]);
|
||||||
|
|
||||||
|
// 현재 사용자 정보 가져오기
|
||||||
|
const getCurrentUser = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Common/GetCurrentUser');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success && data.Data) {
|
||||||
|
const userName = data.Data.userName || data.Data.name || '사용자';
|
||||||
|
setCurrentUser(userName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting current user:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 목록 로드
|
||||||
|
const loadProjects = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Project/GetProjects?status=${filters.status}&userFilter=${filters.manager}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
const projectData = data.Data || [];
|
||||||
|
setProjects(projectData);
|
||||||
|
setFilteredProjects(projectData);
|
||||||
|
updateStatusCounts(projectData);
|
||||||
|
|
||||||
|
if (data.CurrentUser) {
|
||||||
|
setCurrentUser(data.CurrentUser);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error:', data.Message);
|
||||||
|
showNotification('데이터를 불러오는데 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('데이터를 불러오는데 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 상태별 카운트 업데이트
|
||||||
|
const updateStatusCounts = (projectData) => {
|
||||||
|
const counts = { 진행: 0, 완료: 0, 대기: 0, 중단: 0 };
|
||||||
|
|
||||||
|
projectData.forEach(project => {
|
||||||
|
const status = project.상태 || '진행';
|
||||||
|
counts[status] = (counts[status] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
setStatusCounts(counts);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 데이터 필터링
|
||||||
|
const filterData = () => {
|
||||||
|
if (!filters.search.trim()) {
|
||||||
|
setFilteredProjects(projects);
|
||||||
|
setCurrentPage(1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = filters.search.toLowerCase();
|
||||||
|
const filtered = projects.filter(project => {
|
||||||
|
return (project.프로젝트명 && project.프로젝트명.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.프로젝트공정 && project.프로젝트공정.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.자산번호 && project.자산번호.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.장비모델 && project.장비모델.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.시리얼번호 && project.시리얼번호.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.프로젝트관리자 && project.프로젝트관리자.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.설계담당 && project.설계담당.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.전장담당 && project.전장담당.toLowerCase().includes(searchTerm)) ||
|
||||||
|
(project.프로그램담당 && project.프로그램담당.toLowerCase().includes(searchTerm));
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredProjects(filtered);
|
||||||
|
setCurrentPage(1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 추가 모달 표시
|
||||||
|
const showAddProjectModal = () => {
|
||||||
|
setCurrentProjectIdx(null);
|
||||||
|
setProjectForm({
|
||||||
|
idx: 0,
|
||||||
|
name: '',
|
||||||
|
process: '',
|
||||||
|
sdate: '',
|
||||||
|
edate: '',
|
||||||
|
ddate: '',
|
||||||
|
odate: '',
|
||||||
|
userManager: '',
|
||||||
|
status: '진행',
|
||||||
|
memo: ''
|
||||||
|
});
|
||||||
|
setShowProjectModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 편집
|
||||||
|
const editProject = async (idx) => {
|
||||||
|
setCurrentProjectIdx(idx);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Project/GetProject?id=${idx}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success && data.Data) {
|
||||||
|
const project = data.Data;
|
||||||
|
setProjectForm({
|
||||||
|
idx: project.idx,
|
||||||
|
name: project.프로젝트명 || '',
|
||||||
|
process: project.프로젝트공정 || '',
|
||||||
|
sdate: project.시작일 || '',
|
||||||
|
edate: project.완료일 || '',
|
||||||
|
ddate: project.만료일 || '',
|
||||||
|
odate: project.출고일 || '',
|
||||||
|
userManager: project.프로젝트관리자 || '',
|
||||||
|
status: project.상태 || '진행',
|
||||||
|
memo: project.memo || ''
|
||||||
|
});
|
||||||
|
setShowProjectModal(true);
|
||||||
|
} else {
|
||||||
|
showNotification('프로젝트 정보를 불러오는데 실패했습니다: ' + data.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('프로젝트 정보를 불러오는데 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 저장 (추가/수정)
|
||||||
|
const saveProject = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const projectData = {
|
||||||
|
...projectForm,
|
||||||
|
idx: currentProjectIdx ? parseInt(projectForm.idx) : 0,
|
||||||
|
sdate: projectForm.sdate || null,
|
||||||
|
edate: projectForm.edate || null,
|
||||||
|
ddate: projectForm.ddate || null,
|
||||||
|
odate: projectForm.odate || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = currentProjectIdx ? '/Project/UpdateProject' : '/Project/CreateProject';
|
||||||
|
const method = currentProjectIdx ? 'PUT' : 'POST';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: method,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(projectData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
showNotification(data.Message || (currentProjectIdx ? '프로젝트가 수정되었습니다.' : '프로젝트가 추가되었습니다.'), 'success');
|
||||||
|
setShowProjectModal(false);
|
||||||
|
loadProjects();
|
||||||
|
} else {
|
||||||
|
showNotification('오류: ' + data.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('저장 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 프로젝트 삭제
|
||||||
|
const deleteProject = async () => {
|
||||||
|
if (!currentProjectIdx) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Project/DeleteProject?id=${currentProjectIdx}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
showNotification(data.Message || '프로젝트가 삭제되었습니다.', 'success');
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
setShowProjectModal(false);
|
||||||
|
loadProjects();
|
||||||
|
} else {
|
||||||
|
showNotification('오류: ' + data.Message, 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('삭제 중 오류가 발생했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 필터 초기화
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
status: '진행',
|
||||||
|
search: '',
|
||||||
|
manager: 'my'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 엑셀 다운로드
|
||||||
|
const exportToExcel = () => {
|
||||||
|
if (filteredProjects.length === 0) {
|
||||||
|
showNotification('내보낼 데이터가 없습니다.', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = ['상태', '자산번호', '장비모델', '시리얼번호', '우선순위', '요청국가', '요청공장', '요청라인', '요청부서패키지', '요청자', '프로젝트공정', '시작일', '완료일', '만료일', '출고일', '프로젝트명', '프로젝트관리자', '설계담당', '전장담당', '프로그램담당', '예산만기일', '예산', '웹관리번호'];
|
||||||
|
const csvContent = [
|
||||||
|
headers.join(','),
|
||||||
|
...filteredProjects.map(project => [
|
||||||
|
project.상태 || '',
|
||||||
|
project.자산번호 || '',
|
||||||
|
project.장비모델 || '',
|
||||||
|
project.시리얼번호 || '',
|
||||||
|
project.우선순위 || '',
|
||||||
|
project.요청국가 || '',
|
||||||
|
project.요청공장 || '',
|
||||||
|
project.요청라인 || '',
|
||||||
|
project.요청부서패키지 || '',
|
||||||
|
project.요청자 || '',
|
||||||
|
project.프로젝트공정 || '',
|
||||||
|
project.시작일 || '',
|
||||||
|
project.완료일 || '',
|
||||||
|
project.만료일 || '',
|
||||||
|
project.출고일 || '',
|
||||||
|
project.프로젝트명 || '',
|
||||||
|
project.프로젝트관리자 || '',
|
||||||
|
project.설계담당 || '',
|
||||||
|
project.전장담당 || '',
|
||||||
|
project.프로그램담당 || '',
|
||||||
|
project.예산만기일 || '',
|
||||||
|
project.예산 || '',
|
||||||
|
project.웹관리번호 || ''
|
||||||
|
].join(','))
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
|
const link = document.createElement('a');
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
link.setAttribute('href', url);
|
||||||
|
link.setAttribute('download', `프로젝트목록_${new Date().toISOString().split('T')[0]}.csv`);
|
||||||
|
link.style.visibility = 'hidden';
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
document.body.removeChild(link);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 유틸리티 함수들
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
switch(status) {
|
||||||
|
case '진행': return 'bg-blue-500/20 text-blue-300';
|
||||||
|
case '완료': return 'bg-green-500/20 text-green-300';
|
||||||
|
case '대기': return 'bg-yellow-500/20 text-yellow-300';
|
||||||
|
case '중단': return 'bg-red-500/20 text-red-300';
|
||||||
|
default: return 'bg-gray-500/20 text-gray-300';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDateToMonthDay = (dateString) => {
|
||||||
|
if (!dateString) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
if (isNaN(date.getTime())) return dateString;
|
||||||
|
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||||
|
const day = String(date.getDate()).padStart(2, '0');
|
||||||
|
return `${month}-${day}`;
|
||||||
|
} catch (error) {
|
||||||
|
return dateString;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 알림 표시 함수
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||||
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||||
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||||
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="ml-2">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 페이지네이션 계산
|
||||||
|
const totalPages = Math.ceil(filteredProjects.length / itemsPerPage);
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const paginatedProjects = filteredProjects.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
const changePage = (page) => {
|
||||||
|
if (page >= 1 && page <= totalPages) {
|
||||||
|
setCurrentPage(page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 프로젝트 목록 */}
|
||||||
|
<div className="glass-effect rounded-lg overflow-hidden animate-slide-up">
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
|
||||||
|
</svg>
|
||||||
|
프로젝트 목록
|
||||||
|
</h2>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<button onClick={showAddProjectModal} className="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm" title="프로젝트 추가">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
프로젝트 추가
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setShowFilters(!showFilters)} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm" title="필터 표시/숨김">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707L13 14v6a1 1 0 01-.707.293l-4-1A1 1 0 018 19v-5L0.293 7.293A1 1 0 010 6.586V4z"></path>
|
||||||
|
</svg>
|
||||||
|
필터
|
||||||
|
<svg className={`w-4 h-4 ml-2 transition-transform ${showFilters ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 9l-7 7-7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 상태별 카드 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 p-4 border-b border-white/10">
|
||||||
|
<div className="bg-white/10 rounded-lg p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-blue-300 mb-1">{statusCounts.진행}</div>
|
||||||
|
<div className="text-sm text-white/60">진행</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/10 rounded-lg p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-green-300 mb-1">{statusCounts.완료}</div>
|
||||||
|
<div className="text-sm text-white/60">완료</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/10 rounded-lg p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-yellow-300 mb-1">{statusCounts.대기}</div>
|
||||||
|
<div className="text-sm text-white/60">대기</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white/10 rounded-lg p-4 text-center">
|
||||||
|
<div className="text-2xl font-bold text-red-300 mb-1">{statusCounts.중단}</div>
|
||||||
|
<div className="text-sm text-white/60">중단</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 필터 영역 */}
|
||||||
|
{showFilters && (
|
||||||
|
<div className="p-4 border-b border-white/10 animate-slide-up">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
||||||
|
{/* 좌측: 필터 컨트롤 */}
|
||||||
|
<div className="lg:col-span-2 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">상태</label>
|
||||||
|
<select
|
||||||
|
value={filters.status}
|
||||||
|
onChange={(e) => setFilters({...filters, status: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
>
|
||||||
|
<option value="진행">진행</option>
|
||||||
|
<option value="완료">완료</option>
|
||||||
|
<option value="대기">대기</option>
|
||||||
|
<option value="중단">중단</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">검색</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={filters.search}
|
||||||
|
onChange={(e) => setFilters({...filters, search: e.target.value})}
|
||||||
|
placeholder="프로젝트명 검색..."
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs font-medium text-white/80 mb-1">담당자</label>
|
||||||
|
<select
|
||||||
|
value={filters.manager}
|
||||||
|
onChange={(e) => setFilters({...filters, manager: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs"
|
||||||
|
>
|
||||||
|
<option value="my">내 프로젝트</option>
|
||||||
|
<option value="all">전체 프로젝트</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 우측: 액션 버튼들 */}
|
||||||
|
<div className="flex flex-wrap gap-2 justify-end">
|
||||||
|
<div className="relative">
|
||||||
|
<button onClick={() => setShowColumnDropdown(!showColumnDropdown)} className="bg-purple-500 hover:bg-purple-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="컬럼 표시/숨김">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2H9z"></path>
|
||||||
|
</svg>
|
||||||
|
컬럼 설정
|
||||||
|
</button>
|
||||||
|
{showColumnDropdown && (
|
||||||
|
<div className="absolute top-full right-0 bg-gray-800/95 backdrop-blur-sm border border-white/20 rounded-lg p-3 min-w-48 z-50 mt-1">
|
||||||
|
<div className="text-sm font-medium text-white/80 mb-2">표시할 컬럼 선택</div>
|
||||||
|
<div className="space-y-1 text-sm max-h-40 overflow-y-auto">
|
||||||
|
{Object.entries({
|
||||||
|
serial: '시리얼번호',
|
||||||
|
plant: '요청공장',
|
||||||
|
line: '요청라인',
|
||||||
|
package: '요청부서패키지',
|
||||||
|
staff: '요청자',
|
||||||
|
process: '프로젝트공정',
|
||||||
|
expire: '만료일',
|
||||||
|
delivery: '출고일',
|
||||||
|
design: '설계담당',
|
||||||
|
electric: '전장담당',
|
||||||
|
program: '프로그램담당',
|
||||||
|
budgetDue: '예산만기일',
|
||||||
|
budget: '예산',
|
||||||
|
jasmin: '웹관리번호'
|
||||||
|
}).map(([key, label]) => (
|
||||||
|
<label key={key} className="flex items-center text-white cursor-pointer hover:text-blue-300">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={visibleColumns[key]}
|
||||||
|
onChange={(e) => setVisibleColumns({...visibleColumns, [key]: e.target.checked})}
|
||||||
|
className="mr-2"
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button onClick={exportToExcel} className="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="엑셀 다운로드">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
|
||||||
|
</svg>
|
||||||
|
엑셀 다운로드
|
||||||
|
</button>
|
||||||
|
<button onClick={clearFilters} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg flex items-center transition-colors text-sm" title="필터 초기화">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
||||||
|
</svg>
|
||||||
|
필터 초기화
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 데이터 테이블 */}
|
||||||
|
<div className="px-4 py-3 bg-white/5">
|
||||||
|
<div className="text-sm text-white/70 font-medium">프로젝트 현황</div>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto custom-scrollbar">
|
||||||
|
<table className="w-full divide-y divide-white/20">
|
||||||
|
<thead className="bg-white/10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">상태</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 min-w-32">프로젝트명</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">자산번호</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28">장비모델</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">우선순위</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">요청국가</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28">프로젝트관리자</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">시작일</th>
|
||||||
|
<th className="px-4 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">완료일</th>
|
||||||
|
{visibleColumns.serial && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">시리얼번호</th>}
|
||||||
|
{visibleColumns.plant && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">요청공장</th>}
|
||||||
|
{visibleColumns.line && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">요청라인</th>}
|
||||||
|
{visibleColumns.package && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-28">요청부서패키지</th>}
|
||||||
|
{visibleColumns.staff && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">요청자</th>}
|
||||||
|
{visibleColumns.process && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">프로젝트공정</th>}
|
||||||
|
{visibleColumns.expire && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">만료일</th>}
|
||||||
|
{visibleColumns.delivery && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">출고일</th>}
|
||||||
|
{visibleColumns.design && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">설계담당</th>}
|
||||||
|
{visibleColumns.electric && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">전장담당</th>}
|
||||||
|
{visibleColumns.program && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">프로그램담당</th>}
|
||||||
|
{visibleColumns.budgetDue && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-24">예산만기일</th>}
|
||||||
|
{visibleColumns.budget && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider border-r border-white/20 w-20">예산</th>}
|
||||||
|
{visibleColumns.jasmin && <th className="px-3 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider w-24">웹관리번호</th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="23" className="px-6 py-4 text-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<div className="loading inline-block w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin mr-3"></div>
|
||||||
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : paginatedProjects.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="23" className="px-6 py-4 text-center text-white/60">데이터가 없습니다.</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
paginatedProjects.map(project => (
|
||||||
|
<tr key={project.idx} onClick={() => editProject(project.idx)} className="hover:bg-white/10 cursor-pointer transition-colors">
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatusClass(project.상태)}`}>
|
||||||
|
{project.상태 || '진행'}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20 font-medium">{project.프로젝트명 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{project.자산번호 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{project.장비모델 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{project.우선순위 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{project.요청국가 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{project.프로젝트관리자 || ''}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{formatDateToMonthDay(project.시작일)}</td>
|
||||||
|
<td className="px-4 py-4 text-sm border-r border-white/20">{formatDateToMonthDay(project.완료일)}</td>
|
||||||
|
{visibleColumns.serial && <td className="px-3 py-4 text-sm border-r border-white/20">{project.시리얼번호 || ''}</td>}
|
||||||
|
{visibleColumns.plant && <td className="px-3 py-4 text-sm border-r border-white/20">{project.요청공장 || ''}</td>}
|
||||||
|
{visibleColumns.line && <td className="px-3 py-4 text-sm border-r border-white/20">{project.요청라인 || ''}</td>}
|
||||||
|
{visibleColumns.package && <td className="px-3 py-4 text-sm border-r border-white/20">{project.요청부서패키지 || ''}</td>}
|
||||||
|
{visibleColumns.staff && <td className="px-3 py-4 text-sm border-r border-white/20">{project.요청자 || ''}</td>}
|
||||||
|
{visibleColumns.process && <td className="px-3 py-4 text-sm border-r border-white/20">{project.프로젝트공정 || ''}</td>}
|
||||||
|
{visibleColumns.expire && <td className="px-3 py-4 text-sm border-r border-white/20">{formatDateToMonthDay(project.만료일)}</td>}
|
||||||
|
{visibleColumns.delivery && <td className="px-3 py-4 text-sm border-r border-white/20">{formatDateToMonthDay(project.출고일)}</td>}
|
||||||
|
{visibleColumns.design && <td className="px-3 py-4 text-sm border-r border-white/20">{project.설계담당 || ''}</td>}
|
||||||
|
{visibleColumns.electric && <td className="px-3 py-4 text-sm border-r border-white/20">{project.전장담당 || ''}</td>}
|
||||||
|
{visibleColumns.program && <td className="px-3 py-4 text-sm border-r border-white/20">{project.프로그램담당 || ''}</td>}
|
||||||
|
{visibleColumns.budgetDue && <td className="px-3 py-4 text-sm border-r border-white/20">{formatDateToMonthDay(project.예산만기일)}</td>}
|
||||||
|
{visibleColumns.budget && <td className="px-3 py-4 text-sm border-r border-white/20">{project.예산 || ''}</td>}
|
||||||
|
{visibleColumns.jasmin && (
|
||||||
|
<td className="px-3 py-4 text-sm">
|
||||||
|
{project.웹관리번호 ? (
|
||||||
|
<a href={`https://scwa.amkor.co.kr/jasmine/view/${project.웹관리번호}`} target="_blank" rel="noopener noreferrer" className="text-blue-300 hover:text-blue-200 underline">
|
||||||
|
{project.웹관리번호}
|
||||||
|
</a>
|
||||||
|
) : ''}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 페이징 */}
|
||||||
|
<div className="px-4 py-4 border-t border-white/10">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="text-sm text-white/60">
|
||||||
|
총 {filteredProjects.length}개 중 {startIndex + 1}-{Math.min(endIndex, filteredProjects.length)}개 표시
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<button onClick={() => changePage(currentPage - 1)} disabled={currentPage <= 1} className="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="flex space-x-1">
|
||||||
|
{Array.from({length: Math.min(5, totalPages)}, (_, i) => {
|
||||||
|
const pageNum = Math.max(1, currentPage - 2) + i;
|
||||||
|
if (pageNum > totalPages) return null;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={pageNum}
|
||||||
|
onClick={() => changePage(pageNum)}
|
||||||
|
className={`px-3 py-1 rounded-md text-sm transition-colors ${
|
||||||
|
pageNum === currentPage
|
||||||
|
? 'bg-primary-500 text-white'
|
||||||
|
: 'bg-white/20 hover:bg-white/30 text-white'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{pageNum}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => changePage(currentPage + 1)} disabled={currentPage >= totalPages} className="px-3 py-1 bg-white/20 hover:bg-white/30 text-white rounded-md text-sm disabled:opacity-50 disabled:cursor-not-allowed">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 프로젝트 추가/편집 모달 */}
|
||||||
|
{showProjectModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-lg w-full max-w-2xl animate-slide-up">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex justify-between items-center mb-6">
|
||||||
|
<h3 className="text-xl font-semibold">{currentProjectIdx ? '프로젝트 편집' : '프로젝트 추가'}</h3>
|
||||||
|
<button onClick={() => setShowProjectModal(false)} className="text-white/60 hover:text-white">
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={saveProject}>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">프로젝트명 *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={projectForm.name}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, name: e.target.value})}
|
||||||
|
required
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">부서</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={projectForm.process}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, process: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">시작일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={projectForm.sdate}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, sdate: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">종료일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={projectForm.edate}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, edate: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">개발완료일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={projectForm.ddate}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, ddate: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">운영개시일</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={projectForm.odate}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, odate: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">담당자</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={projectForm.userManager}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, userManager: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">상태</label>
|
||||||
|
<select
|
||||||
|
value={projectForm.status}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, status: e.target.value})}
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="진행">진행</option>
|
||||||
|
<option value="완료">완료</option>
|
||||||
|
<option value="대기">대기</option>
|
||||||
|
<option value="중단">중단</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-4">
|
||||||
|
<label className="block text-sm font-medium text-white/80 mb-2">메모</label>
|
||||||
|
<textarea
|
||||||
|
value={projectForm.memo}
|
||||||
|
onChange={(e) => setProjectForm({...projectForm, memo: e.target.value})}
|
||||||
|
rows="3"
|
||||||
|
className="w-full bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
{currentProjectIdx && (
|
||||||
|
<button type="button" onClick={() => setShowDeleteModal(true)} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition-colors">
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<div className="flex space-x-2 ml-auto">
|
||||||
|
<button type="button" onClick={() => setShowProjectModal(false)} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md transition-colors">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-md transition-colors">
|
||||||
|
저장
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 삭제 확인 모달 */}
|
||||||
|
{showDeleteModal && (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-lg w-full max-w-md animate-slide-up">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center mb-4">
|
||||||
|
<svg className="w-8 h-8 text-red-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
|
||||||
|
</svg>
|
||||||
|
<h3 className="text-lg font-semibold">삭제 확인</h3>
|
||||||
|
</div>
|
||||||
|
<p className="text-white/80 mb-6">선택한 프로젝트를 삭제하시겠습니까?<br><span className="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</span></p>
|
||||||
|
<div className="flex justify-end space-x-2">
|
||||||
|
<button onClick={() => setShowDeleteModal(false)} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md transition-colors">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button onClick={deleteProject} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-md transition-colors">
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 로딩 인디케이터 */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm z-40">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
처리 중...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 외부 클릭시 드롭다운 닫기 */}
|
||||||
|
{showColumnDropdown && (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-10"
|
||||||
|
onClick={() => setShowColumnDropdown(false)}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
147
Project/Web/wwwroot/react/TestApp.jsx
Normal file
147
Project/Web/wwwroot/react/TestApp.jsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// TestApp.jsx - React Test Component for GroupWare
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function TestApp() {
|
||||||
|
const [status, setStatus] = useState('loading');
|
||||||
|
const [counter, setCounter] = useState(0);
|
||||||
|
const [serverTime, setServerTime] = useState('');
|
||||||
|
const [apiTest, setApiTest] = useState({ status: 'pending', message: '' });
|
||||||
|
|
||||||
|
// 컴포넌트가 마운트될 때 실행
|
||||||
|
useEffect(() => {
|
||||||
|
// React가 정상적으로 로드되었음을 표시
|
||||||
|
setTimeout(() => {
|
||||||
|
setStatus('success');
|
||||||
|
setServerTime(new Date().toLocaleString('ko-KR'));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// API 테스트
|
||||||
|
testAPI();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// GroupWare API 테스트 함수
|
||||||
|
const testAPI = async () => {
|
||||||
|
try {
|
||||||
|
// Home 컨트롤러 테스트
|
||||||
|
const response = await fetch('/Home');
|
||||||
|
if (response.ok) {
|
||||||
|
setApiTest({ status: 'success', message: 'API 연결 성공' });
|
||||||
|
} else {
|
||||||
|
setApiTest({ status: 'warning', message: `API 응답: ${response.status}` });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setApiTest({ status: 'error', message: `API 오류: ${error.message}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const buttonStyle = {
|
||||||
|
padding: '10px 20px',
|
||||||
|
margin: '5px',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '14px'
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`status ${status === 'success' ? 'success' : 'loading'}`}>
|
||||||
|
{status === 'success' ? (
|
||||||
|
<div>
|
||||||
|
<h3>✅ React 컴포넌트가 성공적으로 로드되었습니다!</h3>
|
||||||
|
<p><strong>현재 시간:</strong> {serverTime}</p>
|
||||||
|
<p><strong>파일 위치:</strong> /react/TestApp.jsx</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h3>React 컴포넌트를 로딩 중입니다...</h3>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<div>
|
||||||
|
<div className="status">
|
||||||
|
<h3>📊 상태 관리 테스트</h3>
|
||||||
|
<p><strong>카운터:</strong> {counter}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setCounter(counter + 1)}
|
||||||
|
style={{
|
||||||
|
...buttonStyle,
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
증가 (+1)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCounter(counter - 1)}
|
||||||
|
style={{
|
||||||
|
...buttonStyle,
|
||||||
|
backgroundColor: '#dc3545',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
감소 (-1)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCounter(0)}
|
||||||
|
style={{
|
||||||
|
...buttonStyle,
|
||||||
|
backgroundColor: '#6c757d',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
리셋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`status ${
|
||||||
|
apiTest.status === 'success' ? 'success' :
|
||||||
|
apiTest.status === 'error' ? 'error' : 'loading'
|
||||||
|
}`}>
|
||||||
|
<h3>🌐 GroupWare API 연결 테스트</h3>
|
||||||
|
<p><strong>상태:</strong> {apiTest.message}</p>
|
||||||
|
<p><strong>테스트 엔드포인트:</strong> /Home</p>
|
||||||
|
<button
|
||||||
|
onClick={testAPI}
|
||||||
|
style={{
|
||||||
|
...buttonStyle,
|
||||||
|
backgroundColor: '#28a745',
|
||||||
|
color: 'white'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
API 다시 테스트
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status">
|
||||||
|
<h3>📋 React + OWIN 통합 테스트 체크리스트</h3>
|
||||||
|
<ul style={{ textAlign: 'left' }}>
|
||||||
|
<li>✅ OWIN 정적 파일 서빙</li>
|
||||||
|
<li>✅ React 라이브러리 로딩 (CDN)</li>
|
||||||
|
<li>✅ JSX 파일 분리 및 로딩</li>
|
||||||
|
<li>✅ JSX 컴파일 (Babel)</li>
|
||||||
|
<li>✅ React Hooks (useState, useEffect)</li>
|
||||||
|
<li>✅ 이벤트 핸들링</li>
|
||||||
|
<li>✅ API 호출 (fetch)</li>
|
||||||
|
<li>✅ 반응형 UI 업데이트</li>
|
||||||
|
<li>✅ 컴포넌트 모듈화</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status success">
|
||||||
|
<h3>🎉 통합 테스트 결과</h3>
|
||||||
|
<p>GroupWare + OWIN + React 환경이 성공적으로 구성되었습니다!</p>
|
||||||
|
<p><strong>다음 단계:</strong></p>
|
||||||
|
<ul style={{ textAlign: 'left' }}>
|
||||||
|
<li>React Router 추가 (SPA 라우팅)</li>
|
||||||
|
<li>상태 관리 라이브러리 추가 (Redux/Zustand)</li>
|
||||||
|
<li>UI 컴포넌트 라이브러리 추가 (Material-UI/Ant Design)</li>
|
||||||
|
<li>번들링 도구 설정 (Webpack/Vite)</li>
|
||||||
|
<li>TypeScript 지원 추가</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
811
Project/Web/wwwroot/react/Todo.jsx
Normal file
811
Project/Web/wwwroot/react/Todo.jsx
Normal file
@@ -0,0 +1,811 @@
|
|||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function Todo() {
|
||||||
|
// 상태 관리
|
||||||
|
const [todos, setTodos] = useState([]);
|
||||||
|
const [activeTodos, setActiveTodos] = useState([]);
|
||||||
|
const [completedTodos, setCompletedTodos] = useState([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [currentTab, setCurrentTab] = useState('active');
|
||||||
|
const [currentEditId, setCurrentEditId] = useState(null);
|
||||||
|
|
||||||
|
// 모달 상태
|
||||||
|
const [showAddModal, setShowAddModal] = useState(false);
|
||||||
|
const [showEditModal, setShowEditModal] = useState(false);
|
||||||
|
|
||||||
|
// 폼 상태
|
||||||
|
const [todoForm, setTodoForm] = useState({
|
||||||
|
title: '',
|
||||||
|
remark: '',
|
||||||
|
expire: '',
|
||||||
|
seqno: 0,
|
||||||
|
flag: false,
|
||||||
|
request: '',
|
||||||
|
status: '0'
|
||||||
|
});
|
||||||
|
|
||||||
|
const [editForm, setEditForm] = useState({
|
||||||
|
idx: 0,
|
||||||
|
title: '',
|
||||||
|
remark: '',
|
||||||
|
expire: '',
|
||||||
|
seqno: 0,
|
||||||
|
flag: false,
|
||||||
|
request: '',
|
||||||
|
status: '0'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 컴포넌트 마운트시 할일 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
loadTodos();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 할일 목록을 활성/완료로 분리
|
||||||
|
useEffect(() => {
|
||||||
|
const active = todos.filter(todo => (todo.status || '0') !== '5');
|
||||||
|
const completed = todos.filter(todo => (todo.status || '0') === '5');
|
||||||
|
setActiveTodos(active);
|
||||||
|
setCompletedTodos(completed);
|
||||||
|
}, [todos]);
|
||||||
|
|
||||||
|
// 할일 목록 로드
|
||||||
|
const loadTodos = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Todo/GetTodos');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
setTodos(data.Data || []);
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '할일 목록을 불러올 수 없습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('할일 목록 로드 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 새 할일 추가
|
||||||
|
const addTodo = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!todoForm.remark.trim()) {
|
||||||
|
showNotification('할일 내용을 입력해주세요.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Todo/CreateTodo', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...todoForm,
|
||||||
|
seqno: parseInt(todoForm.seqno),
|
||||||
|
expire: todoForm.expire || null,
|
||||||
|
request: todoForm.request || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
setShowAddModal(false);
|
||||||
|
setTodoForm({
|
||||||
|
title: '',
|
||||||
|
remark: '',
|
||||||
|
expire: '',
|
||||||
|
seqno: 0,
|
||||||
|
flag: false,
|
||||||
|
request: '',
|
||||||
|
status: '0'
|
||||||
|
});
|
||||||
|
loadTodos();
|
||||||
|
showNotification(data.Message || '할일이 추가되었습니다.', 'success');
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '할일 추가에 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('할일 추가 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 할일 수정
|
||||||
|
const updateTodo = async () => {
|
||||||
|
if (!editForm.remark.trim()) {
|
||||||
|
showNotification('할일 내용을 입력해주세요.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Todo/UpdateTodo', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
...editForm,
|
||||||
|
seqno: parseInt(editForm.seqno),
|
||||||
|
expire: editForm.expire || null,
|
||||||
|
request: editForm.request || null
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setCurrentEditId(null);
|
||||||
|
loadTodos();
|
||||||
|
showNotification(data.Message || '할일이 수정되었습니다.', 'success');
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '할일 수정에 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('할일 수정 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 상태 업데이트 (편집 모달에서 바로 서버에 반영)
|
||||||
|
const updateTodoStatus = async (status) => {
|
||||||
|
if (!currentEditId) return;
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
...editForm,
|
||||||
|
status: status,
|
||||||
|
seqno: parseInt(editForm.seqno),
|
||||||
|
expire: editForm.expire || null,
|
||||||
|
request: editForm.request || null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!formData.remark.trim()) {
|
||||||
|
showNotification('할일 내용을 입력해주세요.', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch('/Todo/UpdateTodo', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
setShowEditModal(false);
|
||||||
|
setCurrentEditId(null);
|
||||||
|
loadTodos();
|
||||||
|
showNotification(`상태가 '${getStatusText(status)}'(으)로 변경되었습니다.`, 'success');
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '상태 변경에 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('상태 변경 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 할일 편집 모달 열기
|
||||||
|
const editTodo = async (id) => {
|
||||||
|
setCurrentEditId(id);
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Todo/GetTodo?id=${id}`);
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success && data.Data) {
|
||||||
|
const todo = data.Data;
|
||||||
|
setEditForm({
|
||||||
|
idx: todo.idx,
|
||||||
|
title: todo.title || '',
|
||||||
|
remark: todo.remark || '',
|
||||||
|
expire: todo.expire ? new Date(todo.expire).toISOString().split('T')[0] : '',
|
||||||
|
seqno: todo.seqno || 0,
|
||||||
|
flag: todo.flag || false,
|
||||||
|
request: todo.request || '',
|
||||||
|
status: todo.status || '0'
|
||||||
|
});
|
||||||
|
setShowEditModal(true);
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '할일 정보를 불러올 수 없습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('할일 조회 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 할일 삭제
|
||||||
|
const deleteTodo = async (id) => {
|
||||||
|
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/Todo/DeleteTodo?id=${id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.Success) {
|
||||||
|
loadTodos();
|
||||||
|
showNotification(data.Message || '할일이 삭제되었습니다.', 'success');
|
||||||
|
} else {
|
||||||
|
showNotification(data.Message || '할일 삭제에 실패했습니다.', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('할일 삭제 중 오류:', error);
|
||||||
|
showNotification('서버 연결에 실패했습니다.', 'error');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 유틸리티 함수들
|
||||||
|
const getStatusClass = (status) => {
|
||||||
|
switch(status) {
|
||||||
|
case '0': return 'bg-gray-500/20 text-gray-300';
|
||||||
|
case '1': return 'bg-primary-500/20 text-primary-300';
|
||||||
|
case '2': return 'bg-danger-500/20 text-danger-300';
|
||||||
|
case '3': return 'bg-warning-500/20 text-warning-300';
|
||||||
|
case '5': return 'bg-success-500/20 text-success-300';
|
||||||
|
default: return 'bg-white/10 text-white/50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
switch(status) {
|
||||||
|
case '0': return '대기';
|
||||||
|
case '1': return '진행';
|
||||||
|
case '2': return '취소';
|
||||||
|
case '3': return '보류';
|
||||||
|
case '5': return '완료';
|
||||||
|
default: return '대기';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSeqnoClass = (seqno) => {
|
||||||
|
switch(seqno) {
|
||||||
|
case 1: return 'bg-primary-500/20 text-primary-300';
|
||||||
|
case 2: return 'bg-warning-500/20 text-warning-300';
|
||||||
|
case 3: return 'bg-danger-500/20 text-danger-300';
|
||||||
|
default: return 'bg-white/10 text-white/50';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSeqnoText = (seqno) => {
|
||||||
|
switch(seqno) {
|
||||||
|
case 1: return '중요';
|
||||||
|
case 2: return '매우 중요';
|
||||||
|
case 3: return '긴급';
|
||||||
|
default: return '보통';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
return new Date(dateString).toLocaleDateString('ko-KR');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 알림 표시 함수
|
||||||
|
const showNotification = (message, type = 'info') => {
|
||||||
|
const colors = {
|
||||||
|
info: 'bg-blue-500/90 backdrop-blur-sm',
|
||||||
|
success: 'bg-green-500/90 backdrop-blur-sm',
|
||||||
|
warning: 'bg-yellow-500/90 backdrop-blur-sm',
|
||||||
|
error: 'bg-red-500/90 backdrop-blur-sm'
|
||||||
|
};
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `fixed top-4 right-4 ${colors[type]} text-white px-4 py-3 rounded-lg z-50 transition-all duration-300 transform translate-x-0 opacity-100 shadow-lg border border-white/20`;
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="ml-2">${message}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(0)';
|
||||||
|
notification.style.opacity = '1';
|
||||||
|
}, 10);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
notification.style.transform = 'translateX(100%)';
|
||||||
|
notification.style.opacity = '0';
|
||||||
|
setTimeout(() => notification.remove(), 300);
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 할일 행 렌더링
|
||||||
|
const renderTodoRow = (todo, includeOkdate = false) => {
|
||||||
|
const statusClass = getStatusClass(todo.status);
|
||||||
|
const statusText = getStatusText(todo.status);
|
||||||
|
|
||||||
|
const flagClass = todo.flag ? 'bg-warning-500/20 text-warning-300' : 'bg-white/10 text-white/50';
|
||||||
|
const flagText = todo.flag ? '고정' : '일반';
|
||||||
|
|
||||||
|
const seqnoClass = getSeqnoClass(todo.seqno);
|
||||||
|
const seqnoText = getSeqnoText(todo.seqno);
|
||||||
|
|
||||||
|
const expireText = formatDate(todo.expire);
|
||||||
|
const isExpired = todo.expire && new Date(todo.expire) < new Date();
|
||||||
|
const expireClass = isExpired ? 'text-danger-400' : 'text-white/80';
|
||||||
|
|
||||||
|
const okdateText = formatDate(todo.okdate);
|
||||||
|
const okdateClass = todo.okdate ? 'text-success-400' : 'text-white/80';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={todo.idx} className="hover:bg-white/5 transition-colors cursor-pointer" onClick={() => editTodo(todo.idx)}>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${statusClass}`}>
|
||||||
|
{statusText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${flagClass}`}>
|
||||||
|
{flagText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-6 py-4 text-white">{todo.title || '제목 없음'}</td>
|
||||||
|
<td className="px-6 py-4 text-white/80 max-w-xs truncate">{todo.remark || ''}</td>
|
||||||
|
<td className="px-6 py-4 text-white/80">{todo.request || '-'}</td>
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${seqnoClass}`}>
|
||||||
|
{seqnoText}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className={`px-6 py-4 whitespace-nowrap ${expireClass}`}>{expireText}</td>
|
||||||
|
{includeOkdate && <td className={`px-6 py-4 whitespace-nowrap ${okdateClass}`}>{okdateText}</td>}
|
||||||
|
<td className="px-6 py-4 whitespace-nowrap text-sm" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<button onClick={() => editTodo(todo.idx)} className="text-primary-400 hover:text-primary-300 mr-3 transition-colors">
|
||||||
|
수정
|
||||||
|
</button>
|
||||||
|
<button onClick={() => deleteTodo(todo.idx)} className="text-danger-400 hover:text-danger-300 transition-colors">
|
||||||
|
삭제
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
{/* 할일 목록 */}
|
||||||
|
<div className="glass-effect rounded-2xl overflow-hidden animate-slide-up">
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
|
||||||
|
</svg>
|
||||||
|
내 할일 목록
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => setShowAddModal(true)} className="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center text-sm">
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
새 할일 추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 탭 메뉴 */}
|
||||||
|
<div className="px-6 py-2 border-b border-white/10">
|
||||||
|
<div className="flex space-x-1 bg-white/5 rounded-lg p-1">
|
||||||
|
<button onClick={() => setCurrentTab('active')} className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||||
|
currentTab === 'active'
|
||||||
|
? 'text-white bg-white/20 shadow-sm'
|
||||||
|
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>진행중인 할일</span>
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-primary-500/30 text-primary-200 rounded-full">{activeTodos.length}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<button onClick={() => setCurrentTab('completed')} className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all duration-200 ${
|
||||||
|
currentTab === 'completed'
|
||||||
|
? 'text-white bg-white/20 shadow-sm'
|
||||||
|
: 'text-white/60 hover:text-white hover:bg-white/10'
|
||||||
|
}`}>
|
||||||
|
<div className="flex items-center justify-center space-x-2">
|
||||||
|
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>완료된 할일</span>
|
||||||
|
<span className="px-2 py-0.5 text-xs bg-success-500/30 text-success-200 rounded-full">{completedTodos.length}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 진행중인 할일 테이블 */}
|
||||||
|
{currentTab === 'active' && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">진행상태</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">플래그</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">제목</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">내용</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">요청자</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">중요도</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">만료일</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="8" className="p-8 text-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<div className="loading inline-block w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin mr-3"></div>
|
||||||
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : activeTodos.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="8" className="px-6 py-8 text-center text-white/50">
|
||||||
|
진행중인 할일이 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
activeTodos.map(todo => renderTodoRow(todo, false))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 완료된 할일 테이블 */}
|
||||||
|
{currentTab === 'completed' && (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full">
|
||||||
|
<thead className="bg-white/10">
|
||||||
|
<tr>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">진행상태</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">플래그</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">제목</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">내용</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">요청자</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">중요도</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">만료일</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">완료일</th>
|
||||||
|
<th className="px-6 py-4 text-left text-xs font-medium text-white/70 uppercase tracking-wider">작업</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-white/10">
|
||||||
|
{isLoading ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="9" className="p-8 text-center">
|
||||||
|
<div className="inline-flex items-center">
|
||||||
|
<div className="loading inline-block w-5 h-5 border-3 border-white/30 border-t-white rounded-full animate-spin mr-3"></div>
|
||||||
|
<span className="text-white/80">데이터를 불러오는 중...</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : completedTodos.length === 0 ? (
|
||||||
|
<tr>
|
||||||
|
<td colSpan="9" className="px-6 py-8 text-center text-white/50">
|
||||||
|
완료된 할일이 없습니다
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
) : (
|
||||||
|
completedTodos.map(todo => renderTodoRow(todo, true))
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 로딩 인디케이터 */}
|
||||||
|
{isLoading && (
|
||||||
|
<div className="fixed top-4 right-4 bg-white/20 backdrop-blur-sm rounded-full px-4 py-2 text-white text-sm">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
처리 중...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 새 할일 추가 모달 */}
|
||||||
|
{showAddModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
새 할일 추가
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => setShowAddModal(false)} className="text-white/70 hover:text-white transition-colors">
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<div className="p-6">
|
||||||
|
<form onSubmit={addTodo} className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">제목 (선택사항)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={todoForm.title}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, title: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="할일 제목을 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">만료일 (선택사항)</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={todoForm.expire}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, expire: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">내용 *</label>
|
||||||
|
<textarea
|
||||||
|
value={todoForm.remark}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, remark: e.target.value})}
|
||||||
|
rows="3"
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="할일 내용을 입력하세요 (필수)"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">요청자</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={todoForm.request}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, request: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="업무 요청자를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">진행상태</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{value: '0', label: '대기', class: 'bg-gray-500/20 text-gray-300'},
|
||||||
|
{value: '1', label: '진행', class: 'bg-primary-500/20 text-primary-300'},
|
||||||
|
{value: '3', label: '보류', class: 'bg-warning-500/20 text-warning-300'},
|
||||||
|
{value: '2', label: '취소', class: 'bg-danger-500/20 text-danger-300'},
|
||||||
|
{value: '5', label: '완료', class: 'bg-success-500/20 text-success-300'}
|
||||||
|
].map(status => (
|
||||||
|
<button
|
||||||
|
key={status.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setTodoForm({...todoForm, status: status.value})}
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
todoForm.status === status.value
|
||||||
|
? status.class + ' border-' + status.class.split(' ')[0].replace('bg-', '').replace('/20', '/30')
|
||||||
|
: 'bg-white/10 text-white/50 border border-white/20 hover:bg-white/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">중요도</label>
|
||||||
|
<select
|
||||||
|
value={todoForm.seqno}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, seqno: parseInt(e.target.value)})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="0">보통</option>
|
||||||
|
<option value="1">중요</option>
|
||||||
|
<option value="2">매우 중요</option>
|
||||||
|
<option value="3">긴급</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<label className="flex items-center text-white/70 text-sm font-medium">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={todoForm.flag}
|
||||||
|
onChange={(e) => setTodoForm({...todoForm, flag: e.target.checked})}
|
||||||
|
className="mr-2 text-primary-500 focus:ring-primary-400 focus:ring-offset-0 rounded"
|
||||||
|
/>
|
||||||
|
플래그 (상단 고정)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-end space-x-3">
|
||||||
|
<button type="button" onClick={() => setShowAddModal(false)} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button type="submit" form="todoForm" onClick={addTodo} className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center">
|
||||||
|
<svg className="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
|
||||||
|
</svg>
|
||||||
|
추가
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 수정 모달 */}
|
||||||
|
{showEditModal && (
|
||||||
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50">
|
||||||
|
<div className="flex items-center justify-center min-h-screen p-4">
|
||||||
|
<div className="glass-effect rounded-2xl w-full max-w-2xl animate-slide-up">
|
||||||
|
{/* 모달 헤더 */}
|
||||||
|
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||||
|
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||||
|
</svg>
|
||||||
|
할일 수정
|
||||||
|
</h2>
|
||||||
|
<button onClick={() => {setShowEditModal(false); setCurrentEditId(null);}} className="text-white/70 hover:text-white transition-colors">
|
||||||
|
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 내용 */}
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">제목 (선택사항)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.title}
|
||||||
|
onChange={(e) => setEditForm({...editForm, title: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="할일 제목을 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">만료일 (선택사항)</label>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={editForm.expire}
|
||||||
|
onChange={(e) => setEditForm({...editForm, expire: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">내용 *</label>
|
||||||
|
<textarea
|
||||||
|
value={editForm.remark}
|
||||||
|
onChange={(e) => setEditForm({...editForm, remark: e.target.value})}
|
||||||
|
rows="3"
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="할일 내용을 입력하세요 (필수)"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">요청자</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={editForm.request}
|
||||||
|
onChange={(e) => setEditForm({...editForm, request: e.target.value})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
placeholder="업무 요청자를 입력하세요"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">진행상태</label>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{[
|
||||||
|
{value: '0', label: '대기', class: 'bg-gray-500/20 text-gray-300'},
|
||||||
|
{value: '1', label: '진행', class: 'bg-primary-500/20 text-primary-300'},
|
||||||
|
{value: '3', label: '보류', class: 'bg-warning-500/20 text-warning-300'},
|
||||||
|
{value: '2', label: '취소', class: 'bg-danger-500/20 text-danger-300'},
|
||||||
|
{value: '5', label: '완료', class: 'bg-success-500/20 text-success-300'}
|
||||||
|
].map(status => (
|
||||||
|
<button
|
||||||
|
key={status.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateTodoStatus(status.value)}
|
||||||
|
className={`px-3 py-1 rounded-lg text-xs font-medium transition-all ${
|
||||||
|
editForm.status === status.value
|
||||||
|
? status.class + ' border-' + status.class.split(' ')[0].replace('bg-', '').replace('/20', '/30')
|
||||||
|
: 'bg-white/10 text-white/50 border border-white/20 hover:bg-white/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{status.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-white/70 text-sm font-medium mb-2">중요도</label>
|
||||||
|
<select
|
||||||
|
value={editForm.seqno}
|
||||||
|
onChange={(e) => setEditForm({...editForm, seqno: parseInt(e.target.value)})}
|
||||||
|
className="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all"
|
||||||
|
>
|
||||||
|
<option value="0">보통</option>
|
||||||
|
<option value="1">중요</option>
|
||||||
|
<option value="2">매우 중요</option>
|
||||||
|
<option value="3">긴급</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center text-white/70 text-sm font-medium mt-6">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={editForm.flag}
|
||||||
|
onChange={(e) => setEditForm({...editForm, flag: e.target.checked})}
|
||||||
|
className="mr-2 text-primary-500 focus:ring-primary-400 focus:ring-offset-0 rounded"
|
||||||
|
/>
|
||||||
|
플래그 (상단 고정)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 모달 푸터 */}
|
||||||
|
<div className="px-6 py-4 border-t border-white/10 flex justify-end space-x-3">
|
||||||
|
<button onClick={() => {setShowEditModal(false); setCurrentEditId(null);}} className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
||||||
|
취소
|
||||||
|
</button>
|
||||||
|
<button onClick={updateTodo} className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors">
|
||||||
|
수정
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
270
Project/Web/wwwroot/react/react-common.html
Normal file
270
Project/Web/wwwroot/react/react-common.html
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>공용코드관리 (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
|
||||||
|
<!-- Tailwind 설정 -->
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
50: '#fffbeb',
|
||||||
|
100: '#fef3c7',
|
||||||
|
200: '#fde68a',
|
||||||
|
300: '#fcd34d',
|
||||||
|
400: '#fbbf24',
|
||||||
|
500: '#f59e0b',
|
||||||
|
600: '#d97706',
|
||||||
|
700: '#b45309',
|
||||||
|
800: '#92400e',
|
||||||
|
900: '#78350f',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#ef4444',
|
||||||
|
600: '#dc2626',
|
||||||
|
700: '#b91c1c',
|
||||||
|
800: '#991b1b',
|
||||||
|
900: '#7f1d1d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 스크롤바 스타일링 */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 애니메이션 */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 셀렉트 박스 옵션 스타일링 */
|
||||||
|
select option {
|
||||||
|
background-color: #374151 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:hover {
|
||||||
|
background-color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:checked {
|
||||||
|
background-color: #6366F1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 테이블 셀 텍스트 오버플로우 처리 */
|
||||||
|
.truncate {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 값(문자열) 열 최대 너비 제한 */
|
||||||
|
.svalue-cell {
|
||||||
|
max-width: 128px; /* w-32 = 128px */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 개발중 경고 스타일 */
|
||||||
|
.dev-warning {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(245, 158, 11, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<!-- 로딩 스켈레톤 UI -->
|
||||||
|
<div id="react-common-app" class="animate-fade-in">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="flex gap-6 h-[calc(100vh-200px)]">
|
||||||
|
<!-- 좌측 스켈레톤 -->
|
||||||
|
<div class="w-80">
|
||||||
|
<div class="glass-effect rounded-2xl h-full animate-pulse">
|
||||||
|
<div class="p-4 border-b border-white/10">
|
||||||
|
<div class="h-6 bg-white/20 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4 space-y-3">
|
||||||
|
<div class="h-16 bg-white/10 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-white/10 rounded-lg"></div>
|
||||||
|
<div class="h-16 bg-white/10 rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 우측 스켈레톤 -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="glass-effect rounded-2xl h-full animate-pulse">
|
||||||
|
<div class="p-4 border-b border-white/10 flex justify-between items-center">
|
||||||
|
<div class="h-6 bg-white/20 rounded w-1/3"></div>
|
||||||
|
<div class="h-8 bg-white/20 rounded w-20"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="h-10 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-10 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-10 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-10 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 네비게이션 컴포넌트 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
|
||||||
|
<!-- 개발중 경고 컴포넌트 -->
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- App 컴포넌트 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="common" />
|
||||||
|
<CommonCode />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 공용코드 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonCode"></script>
|
||||||
|
|
||||||
|
<!-- 앱 초기화 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-common-app'));
|
||||||
|
root.render(<App />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
297
Project/Web/wwwroot/react/react-dashboard.html
Normal file
297
Project/Web/wwwroot/react/react-dashboard.html
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="version" content="v2.0-20250905-react">
|
||||||
|
<title>근태현황 대시보드 (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
50: '#fffbeb',
|
||||||
|
100: '#fef3c7',
|
||||||
|
200: '#fde68a',
|
||||||
|
300: '#fcd34d',
|
||||||
|
400: '#fbbf24',
|
||||||
|
500: '#f59e0b',
|
||||||
|
600: '#d97706',
|
||||||
|
700: '#b45309',
|
||||||
|
800: '#92400e',
|
||||||
|
900: '#78350f',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#ef4444',
|
||||||
|
600: '#dc2626',
|
||||||
|
700: '#b91c1c',
|
||||||
|
800: '#991b1b',
|
||||||
|
900: '#7f1d1d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.5s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.3s ease-out',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(10px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 스크롤바 스타일링 */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* React 컴포넌트 로딩 스타일 */
|
||||||
|
.react-dashboard-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-skeleton {
|
||||||
|
background: linear-gradient(90deg, rgba(255,255,255,0.1) 25%, rgba(255,255,255,0.2) 50%, rgba(255,255,255,0.1) 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: loading 1.5s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loading {
|
||||||
|
0% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="react-dashboard-app" class="react-dashboard-container">
|
||||||
|
<!-- 로딩 화면 -->
|
||||||
|
<div class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<!-- 로딩 헤더 -->
|
||||||
|
<div class="text-center mb-8">
|
||||||
|
<div class="loading-skeleton h-10 w-80 mx-auto rounded-lg mb-4"></div>
|
||||||
|
<div class="loading-skeleton h-6 w-60 mx-auto rounded-lg mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-4 w-40 mx-auto rounded-lg"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로딩 통계 카드들 -->
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-5 gap-6 mb-8">
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-8 w-12 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-success-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<div class="loading-skeleton w-6 h-6 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-8 w-12 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-warning-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<div class="loading-skeleton w-6 h-6 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-8 w-12 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-primary-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<div class="loading-skeleton w-6 h-6 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="loading-skeleton h-4 w-20 rounded mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-8 w-12 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-danger-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<div class="loading-skeleton w-6 h-6 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<div class="loading-skeleton h-4 w-20 rounded mb-2"></div>
|
||||||
|
<div class="loading-skeleton h-8 w-12 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-12 h-12 bg-purple-500/20 rounded-full flex items-center justify-center">
|
||||||
|
<div class="loading-skeleton w-6 h-6 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로딩 추가 정보 -->
|
||||||
|
<div class="glass-effect rounded-2xl p-6">
|
||||||
|
<div class="loading-skeleton h-8 w-32 rounded mb-4"></div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-success-500/20 rounded-full mx-auto mb-2">
|
||||||
|
<div class="loading-skeleton w-full h-full rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mx-auto mb-1"></div>
|
||||||
|
<div class="loading-skeleton h-3 w-24 rounded mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-warning-500/20 rounded-full mx-auto mb-2">
|
||||||
|
<div class="loading-skeleton w-full h-full rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mx-auto mb-1"></div>
|
||||||
|
<div class="loading-skeleton h-3 w-24 rounded mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-primary-500/20 rounded-full mx-auto mb-2">
|
||||||
|
<div class="loading-skeleton w-full h-full rounded-full"></div>
|
||||||
|
</div>
|
||||||
|
<div class="loading-skeleton h-4 w-16 rounded mx-auto mb-1"></div>
|
||||||
|
<div class="loading-skeleton h-3 w-24 rounded mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 로딩 텍스트 -->
|
||||||
|
<div class="text-center mt-8">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto mb-4"></div>
|
||||||
|
<p class="text-white/70">React Dashboard 컴포넌트를 로딩 중입니다...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- Dashboard 컴포넌트 -->
|
||||||
|
<script type="text/babel" src="/react/component/DashboardApp"></script>
|
||||||
|
|
||||||
|
<!-- 앱 초기화 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
console.log('✅ App 컴포넌트 렌더링 시작');
|
||||||
|
console.log('📊 CommonNavigation 사용 가능:', typeof CommonNavigation);
|
||||||
|
console.log('📊 DashboardApp 사용 가능:', typeof DashboardApp);
|
||||||
|
console.log('📊 DevWarning 사용 가능:', typeof DevWarning);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="dashboard" />
|
||||||
|
<DashboardApp />
|
||||||
|
<DevWarning show={false} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 루트 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-dashboard-app'));
|
||||||
|
root.render(<App />);
|
||||||
|
|
||||||
|
console.log('✅ React Dashboard 앱이 마운트되었습니다.');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
261
Project/Web/wwwroot/react/react-jobreport.html
Normal file
261
Project/Web/wwwroot/react/react-jobreport.html
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>업무일지 (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 스크롤바 스타일링 */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 애니메이션 */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 셀렉트 박스 옵션 스타일링 */
|
||||||
|
select option {
|
||||||
|
background-color: #374151 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:hover {
|
||||||
|
background-color: #4B5563 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:checked {
|
||||||
|
background-color: #6366F1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 개발중 경고 스타일 */
|
||||||
|
.dev-warning {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(245, 158, 11, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<!-- 로딩 스켈레톤 UI -->
|
||||||
|
<div id="react-jobreport-app" class="animate-fade-in">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
{/* 스켈레톤 UI */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 경고 메시지 스켈레톤 */}
|
||||||
|
<div className="bg-white/10 rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="h-6 bg-white/20 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 카드 스켈레톤 */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<div className="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div className="h-4 bg-white/20 rounded w-20 mb-2"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded w-12"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div className="h-4 bg-white/20 rounded w-24 mb-2"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded w-16"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div className="h-4 bg-white/20 rounded w-20 mb-2"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded w-14"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<div className="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div className="h-4 bg-white/20 rounded w-28 mb-2"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded w-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 필터 스켈레톤 */}
|
||||||
|
<div className="glass-effect rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="grid grid-cols-3 gap-4">
|
||||||
|
<div className="col-span-2 space-y-3">
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="h-8 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-8 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-8 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div className="h-8 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-8 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="h-10 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-10 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-10 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 스켈레톤 */}
|
||||||
|
<div className="glass-effect rounded-lg p-4 animate-pulse">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="grid grid-cols-7 gap-4">
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div className="h-6 bg-white/20 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="grid grid-cols-7 gap-4">
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-4">
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-7 gap-4">
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div className="h-4 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- 앱 컴포넌트 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="jobreport" />
|
||||||
|
<JobReport />
|
||||||
|
<DevWarning />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 업무일지 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/JobReport"></script>
|
||||||
|
|
||||||
|
<!-- 앱 초기화 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-jobreport-app'));
|
||||||
|
root.render(<App />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
120
Project/Web/wwwroot/react/react-jsx-test.html
Normal file
120
Project/Web/wwwroot/react/react-jsx-test.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>React JSX Test - GroupWare</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #f0f0f0;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
.header p {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.2em;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.tech-info {
|
||||||
|
background-color: #e3f2fd;
|
||||||
|
border: 1px solid #90caf9;
|
||||||
|
color: #0d47a1;
|
||||||
|
margin: 20px 0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.version-info {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🚀 React JSX Integration</h1>
|
||||||
|
<p>GroupWare + OWIN + React + JSX 모듈화 테스트</p>
|
||||||
|
<div class="tech-info">
|
||||||
|
<strong>기술 스택:</strong> C# WinForms + OWIN + React 18 + Babel JSX + 모듈화된 컴포넌트
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="react-app">
|
||||||
|
<div class="status loading">
|
||||||
|
React JSX 컴포넌트를 로딩 중입니다...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="version-info">
|
||||||
|
<p><strong>환경:</strong> .NET Framework 4.6 + OWIN + React 18.2.0</p>
|
||||||
|
<p><strong>포트:</strong> 7979 | <strong>파일 위치:</strong> /react-jsx-test.html</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React & ReactDOM CDN -->
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||||
|
<!-- Babel standalone for JSX -->
|
||||||
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- JSX Component Import -->
|
||||||
|
<script type="text/babel" src="/react/component/TestApp"></script>
|
||||||
|
|
||||||
|
<!-- App Initialization -->
|
||||||
|
<script type="text/babel">
|
||||||
|
// JSX 컴포넌트를 DOM에 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-app'));
|
||||||
|
root.render(<TestApp />);
|
||||||
|
|
||||||
|
console.log('✅ React JSX 컴포넌트가 성공적으로 마운트되었습니다.');
|
||||||
|
console.log('📁 컴포넌트 파일: /react/TestApp.jsx');
|
||||||
|
console.log('🌐 테스트 페이지: /react-jsx-test.html');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
241
Project/Web/wwwroot/react/react-kuntae.html
Normal file
241
Project/Web/wwwroot/react/react-kuntae.html
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<title>근태관리 (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-top: 3px solid #ffffff;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
max-height: 600px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 스크롤바 스타일링 */
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 애니메이션 */
|
||||||
|
.animate-fade-in {
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 개발중 경고 스타일 */
|
||||||
|
.dev-warning {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: linear-gradient(135deg, #fbbf24, #f59e0b);
|
||||||
|
color: white;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(245, 158, 11, 0.3);
|
||||||
|
z-index: 1000;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dev-warning .description {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.9;
|
||||||
|
margin: 2px 0 0 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<!-- 로딩 스켈레톤 UI -->
|
||||||
|
<div id="react-kuntae-app" class="animate-fade-in">
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="space-y-6">
|
||||||
|
{/* 경고 메시지 스켈레톤 */}
|
||||||
|
<div class="bg-white/10 rounded-lg p-4 animate-pulse">
|
||||||
|
<div class="h-6 bg-white/20 rounded w-3/4"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 필터 스켈레톤 */}
|
||||||
|
<div class="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div class="grid grid-cols-3 gap-4">
|
||||||
|
<div class="h-16 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-16 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-16 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 통계 카드 스켈레톤 */}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||||
|
<div class="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-20 mb-2"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded w-12"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16 mb-2"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded w-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16 mb-2"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded w-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="glass-effect rounded-lg p-6 animate-pulse">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="w-12 h-12 bg-white/20 rounded-lg mr-4"></div>
|
||||||
|
<div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16 mb-2"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded w-8"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 테이블 스켈레톤 */}
|
||||||
|
<div class="glass-effect rounded-lg p-4 animate-pulse">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="grid grid-cols-7 gap-4">
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
<div class="h-6 bg-white/20 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="grid grid-cols-7 gap-4">
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-4">
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
<div class="h-4 bg-white/10 rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- 근태관리 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/Kuntae"></script>
|
||||||
|
|
||||||
|
<!-- 앱 컴포넌트 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="kuntae" />
|
||||||
|
<Kuntae />
|
||||||
|
<DevWarning />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 루트 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-kuntae-app'));
|
||||||
|
root.render(<App />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
177
Project/Web/wwwroot/react/react-login.html
Normal file
177
Project/Web/wwwroot/react/react-login.html
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>로그인 - GroupWare (React)</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
|
<script>
|
||||||
|
tailwind.config = {
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
primary: {
|
||||||
|
50: '#eff6ff',
|
||||||
|
100: '#dbeafe',
|
||||||
|
200: '#bfdbfe',
|
||||||
|
300: '#93c5fd',
|
||||||
|
400: '#60a5fa',
|
||||||
|
500: '#3b82f6',
|
||||||
|
600: '#2563eb',
|
||||||
|
700: '#1d4ed8',
|
||||||
|
800: '#1e40af',
|
||||||
|
900: '#1e3a8a',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
50: '#f0fdf4',
|
||||||
|
100: '#dcfce7',
|
||||||
|
200: '#bbf7d0',
|
||||||
|
300: '#86efac',
|
||||||
|
400: '#4ade80',
|
||||||
|
500: '#22c55e',
|
||||||
|
600: '#16a34a',
|
||||||
|
700: '#15803d',
|
||||||
|
800: '#166534',
|
||||||
|
900: '#14532d',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
50: '#fffbeb',
|
||||||
|
100: '#fef3c7',
|
||||||
|
200: '#fde68a',
|
||||||
|
300: '#fcd34d',
|
||||||
|
400: '#fbbf24',
|
||||||
|
500: '#f59e0b',
|
||||||
|
600: '#d97706',
|
||||||
|
700: '#b45309',
|
||||||
|
800: '#92400e',
|
||||||
|
900: '#78350f',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
50: '#fef2f2',
|
||||||
|
100: '#fee2e2',
|
||||||
|
200: '#fecaca',
|
||||||
|
300: '#fca5a5',
|
||||||
|
400: '#f87171',
|
||||||
|
500: '#ef4444',
|
||||||
|
600: '#dc2626',
|
||||||
|
700: '#b91c1c',
|
||||||
|
800: '#991b1b',
|
||||||
|
900: '#7f1d1d',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
'fade-in': 'fadeIn 0.6s ease-in-out',
|
||||||
|
'slide-up': 'slideUp 0.4s ease-out',
|
||||||
|
'bounce-in': 'bounceIn 0.6s ease-out',
|
||||||
|
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
fadeIn: {
|
||||||
|
'0%': { opacity: '0' },
|
||||||
|
'100%': { opacity: '1' },
|
||||||
|
},
|
||||||
|
slideUp: {
|
||||||
|
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||||
|
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||||
|
},
|
||||||
|
bounceIn: {
|
||||||
|
'0%': { transform: 'scale(0.3)', opacity: '0' },
|
||||||
|
'50%': { transform: 'scale(1.05)' },
|
||||||
|
'70%': { transform: 'scale(0.9)' },
|
||||||
|
'100%': { transform: 'scale(1)', opacity: '1' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
.input-focus {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.input-focus:focus {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
.floating-label {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.input-field:focus + .floating-label,
|
||||||
|
.input-field:not(:placeholder-shown) + .floating-label {
|
||||||
|
transform: translateY(-1.5rem) scale(0.85);
|
||||||
|
color: #3b82f6;
|
||||||
|
}
|
||||||
|
/* 드롭다운 스타일 */
|
||||||
|
select.input-field option {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
select.input-field:focus option:checked {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
}
|
||||||
|
select.input-field option:hover {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
/* React 컴포넌트 전용 스타일 */
|
||||||
|
.react-login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="react-login-app" class="react-login-container">
|
||||||
|
<div class="gradient-bg min-h-screen flex items-center justify-center p-4">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<div class="glass-effect rounded-3xl p-8 card-hover">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="w-16 h-16 bg-white/20 rounded-full flex items-center justify-center mx-auto mb-4">
|
||||||
|
<svg class="w-8 h-8 text-white animate-pulse" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-2xl font-bold text-white mb-2">GroupWare</h1>
|
||||||
|
<p class="text-white/70 text-sm">React 로그인 컴포넌트를 로딩 중입니다...</p>
|
||||||
|
<div class="mt-6">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-white mx-auto"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React & ReactDOM CDN -->
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||||
|
<!-- Babel standalone for JSX -->
|
||||||
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- React Login Component -->
|
||||||
|
<script type="text/babel" src="/react/component/LoginApp"></script>
|
||||||
|
|
||||||
|
<!-- App Initialization -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-login-app'));
|
||||||
|
root.render(<LoginApp />);
|
||||||
|
|
||||||
|
console.log('✅ React 로그인 페이지가 성공적으로 마운트되었습니다.');
|
||||||
|
console.log('📁 컴포넌트 파일: /react/LoginApp.jsx');
|
||||||
|
console.log('🌐 페이지 URL: /react/login');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
191
Project/Web/wwwroot/react/react-project.html
Normal file
191
Project/Web/wwwroot/react/react-project.html
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>프로젝트 관리 - GroupWare (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slide-up {
|
||||||
|
animation: slideUp 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-500 {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-primary-600 {
|
||||||
|
background-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-primary-600:hover {
|
||||||
|
background-color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-green-500 {
|
||||||
|
background-color: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-green-600 {
|
||||||
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-green-600:hover {
|
||||||
|
background-color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-red-500 {
|
||||||
|
background-color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-red-600 {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hover\:bg-red-600:hover {
|
||||||
|
background-color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:hover {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:checked {
|
||||||
|
background-color: #3b82f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
select option:focus {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid #fff;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<div id="react-project">
|
||||||
|
<!-- 스켈레톤 로딩 UI -->
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="glass-effect rounded-lg overflow-hidden animate-pulse">
|
||||||
|
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div class="h-6 bg-white/20 rounded w-40"></div>
|
||||||
|
<div class="flex space-x-3">
|
||||||
|
<div class="h-10 bg-white/20 rounded w-32"></div>
|
||||||
|
<div class="h-10 bg-white/20 rounded w-24"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 p-4">
|
||||||
|
<div class="bg-white/10 rounded-lg p-4">
|
||||||
|
<div class="h-8 bg-white/20 rounded mb-2"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 rounded-lg p-4">
|
||||||
|
<div class="h-8 bg-white/20 rounded mb-2"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 rounded-lg p-4">
|
||||||
|
<div class="h-8 bg-white/20 rounded mb-2"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16"></div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white/10 rounded-lg p-4">
|
||||||
|
<div class="h-8 bg-white/20 rounded mb-2"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-16"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="h-4 bg-white/20 rounded w-full"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-3/4"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- Project 컴포넌트 -->
|
||||||
|
<script type="text/babel" src="/react/component/Project"></script>
|
||||||
|
|
||||||
|
<!-- 앱 초기화 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="project" />
|
||||||
|
<Project />
|
||||||
|
<DevWarning />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 루트 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-project'));
|
||||||
|
root.render(<App />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
197
Project/Web/wwwroot/react/react-test.html
Normal file
197
Project/Web/wwwroot/react/react-test.html
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>React Test Page - GroupWare</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: white;
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.loading {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeaa7;
|
||||||
|
color: #856404;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>🚀 React Integration Test</h1>
|
||||||
|
<p>GroupWare + OWIN + React 통합 테스트</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="react-app">
|
||||||
|
<div class="status loading">
|
||||||
|
React 컴포넌트를 로딩 중입니다...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React & ReactDOM CDN -->
|
||||||
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||||
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||||
|
<!-- Babel standalone for JSX -->
|
||||||
|
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- React Component -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
// 메인 React 컴포넌트
|
||||||
|
function ReactTestApp() {
|
||||||
|
const [status, setStatus] = useState('loading');
|
||||||
|
const [counter, setCounter] = useState(0);
|
||||||
|
const [serverTime, setServerTime] = useState('');
|
||||||
|
const [apiTest, setApiTest] = useState({ status: 'pending', message: '' });
|
||||||
|
|
||||||
|
// 컴포넌트가 마운트될 때 실행
|
||||||
|
useEffect(() => {
|
||||||
|
// React가 정상적으로 로드되었음을 표시
|
||||||
|
setTimeout(() => {
|
||||||
|
setStatus('success');
|
||||||
|
setServerTime(new Date().toLocaleString('ko-KR'));
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// API 테스트 (GroupWare의 기존 컨트롤러 테스트)
|
||||||
|
testAPI();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// GroupWare API 테스트 함수
|
||||||
|
const testAPI = async () => {
|
||||||
|
try {
|
||||||
|
// Home 컨트롤러 테스트 (기존에 있을 것으로 예상)
|
||||||
|
const response = await fetch('/Home');
|
||||||
|
if (response.ok) {
|
||||||
|
setApiTest({ status: 'success', message: 'API 연결 성공' });
|
||||||
|
} else {
|
||||||
|
setApiTest({ status: 'warning', message: `API 응답: ${response.status}` });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setApiTest({ status: 'error', message: `API 오류: ${error.message}` });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={`status ${status === 'success' ? 'success' : 'loading'}`}>
|
||||||
|
{status === 'success' ? (
|
||||||
|
<div>
|
||||||
|
<h3>✅ React 컴포넌트가 성공적으로 로드되었습니다!</h3>
|
||||||
|
<p><strong>현재 시간:</strong> {serverTime}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<h3>React 컴포넌트를 로딩 중입니다...</h3>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{status === 'success' && (
|
||||||
|
<div>
|
||||||
|
<div className="status">
|
||||||
|
<h3>📊 상태 관리 테스트</h3>
|
||||||
|
<p><strong>카운터:</strong> {counter}</p>
|
||||||
|
<button
|
||||||
|
onClick={() => setCounter(counter + 1)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
marginRight: '10px',
|
||||||
|
backgroundColor: '#007bff',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
증가 (+1)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setCounter(0)}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
backgroundColor: '#6c757d',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
리셋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`status ${
|
||||||
|
apiTest.status === 'success' ? 'success' :
|
||||||
|
apiTest.status === 'error' ? 'error' : 'loading'
|
||||||
|
}`}>
|
||||||
|
<h3>🌐 API 연결 테스트</h3>
|
||||||
|
<p><strong>상태:</strong> {apiTest.message}</p>
|
||||||
|
<button
|
||||||
|
onClick={testAPI}
|
||||||
|
style={{
|
||||||
|
padding: '10px 20px',
|
||||||
|
backgroundColor: '#28a745',
|
||||||
|
color: 'white',
|
||||||
|
border: 'none',
|
||||||
|
borderRadius: '5px',
|
||||||
|
cursor: 'pointer'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
API 다시 테스트
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="status">
|
||||||
|
<h3>📋 통합 테스트 체크리스트</h3>
|
||||||
|
<ul style={{ textAlign: 'left' }}>
|
||||||
|
<li>✅ OWIN 정적 파일 서빙</li>
|
||||||
|
<li>✅ React 라이브러리 로딩 (CDN)</li>
|
||||||
|
<li>✅ JSX 컴파일 (Babel)</li>
|
||||||
|
<li>✅ React Hooks (useState, useEffect)</li>
|
||||||
|
<li>✅ 이벤트 핸들링</li>
|
||||||
|
<li>✅ API 호출 (fetch)</li>
|
||||||
|
<li>✅ 반응형 UI 업데이트</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// React 컴포넌트를 DOM에 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-app'));
|
||||||
|
root.render(<ReactTestApp />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
115
Project/Web/wwwroot/react/react-todo.html
Normal file
115
Project/Web/wwwroot/react/react-todo.html
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||||||
|
<meta http-equiv="Pragma" content="no-cache">
|
||||||
|
<meta http-equiv="Expires" content="0">
|
||||||
|
<meta name="version" content="v1.0-20250127">
|
||||||
|
<title>할일 관리 - GroupWare (React)</title>
|
||||||
|
<link rel="stylesheet" href="/lib/css/tailwind.min.css">
|
||||||
|
<script src="/lib/js/tailwind-config.js"></script>
|
||||||
|
<style>
|
||||||
|
.glass-effect {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
.gradient-bg {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
}
|
||||||
|
.card-hover {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
.card-hover:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-track {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
border: 3px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid #fff;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
|
||||||
|
<div id="react-todo">
|
||||||
|
<!-- 스켈레톤 로딩 UI -->
|
||||||
|
<div class="container mx-auto px-4 py-8">
|
||||||
|
<div class="glass-effect rounded-2xl overflow-hidden animate-pulse">
|
||||||
|
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||||
|
<div class="h-6 bg-white/20 rounded w-40"></div>
|
||||||
|
<div class="h-10 bg-white/20 rounded w-32"></div>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="h-4 bg-white/20 rounded w-full"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-3/4"></div>
|
||||||
|
<div class="h-4 bg-white/20 rounded w-1/2"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- React Local -->
|
||||||
|
<script crossorigin src="/lib/js/react.development.js"></script>
|
||||||
|
<script crossorigin src="/lib/js/react-dom.development.js"></script>
|
||||||
|
<script src="/lib/js/babel.min.js"></script>
|
||||||
|
|
||||||
|
<!-- 공통 컴포넌트 로드 -->
|
||||||
|
<script type="text/babel" src="/react/component/CommonNavigation"></script>
|
||||||
|
<script type="text/babel" src="/react/component/DevWarning"></script>
|
||||||
|
|
||||||
|
<!-- Todo 컴포넌트 -->
|
||||||
|
<script type="text/babel" src="/react/component/Todo"></script>
|
||||||
|
|
||||||
|
<!-- 앱 초기화 -->
|
||||||
|
<script type="text/babel">
|
||||||
|
const { useState, useEffect } = React;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<CommonNavigation currentPage="todo" />
|
||||||
|
<Todo />
|
||||||
|
<DevWarning />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 루트 렌더링
|
||||||
|
const root = ReactDOM.createRoot(document.getElementById('react-todo'));
|
||||||
|
root.render(<App />);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
|
<Reference Include="System.ServiceProcess" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
<Reference Include="System.Data.DataSetExtensions" />
|
<Reference Include="System.Data.DataSetExtensions" />
|
||||||
<Reference Include="Microsoft.CSharp" />
|
<Reference Include="Microsoft.CSharp" />
|
||||||
@@ -49,6 +50,9 @@
|
|||||||
<AutoGen>True</AutoGen>
|
<AutoGen>True</AutoGen>
|
||||||
<DesignTime>True</DesignTime>
|
<DesignTime>True</DesignTime>
|
||||||
</Compile>
|
</Compile>
|
||||||
|
<Compile Include="MailService.cs">
|
||||||
|
<SubType>Component</SubType>
|
||||||
|
</Compile>
|
||||||
<Compile Include="Program.cs" />
|
<Compile Include="Program.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Properties\Settings.Designer.cs">
|
<Compile Include="Properties\Settings.Designer.cs">
|
||||||
|
|||||||
36
Sub/Console_SendMail/InstallService.bat
Normal file
36
Sub/Console_SendMail/InstallService.bat
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
@echo off
|
||||||
|
echo EETGW Mail Service 설치 중...
|
||||||
|
|
||||||
|
REM 관리자 권한 확인
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo 관리자 권한이 확인되었습니다.
|
||||||
|
) else (
|
||||||
|
echo 이 스크립트는 관리자 권한으로 실행해야 합니다.
|
||||||
|
echo 관리자 권한으로 다시 실행해주세요.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 서비스 설치
|
||||||
|
Console_SendMail.exe -install
|
||||||
|
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo.
|
||||||
|
echo 서비스 설치가 완료되었습니다.
|
||||||
|
echo 서비스를 시작하시겠습니까? (Y/N)
|
||||||
|
set /p choice="선택: "
|
||||||
|
if /i "%choice%"=="Y" (
|
||||||
|
net start EETGWMailService
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo 서비스가 성공적으로 시작되었습니다.
|
||||||
|
) else (
|
||||||
|
echo 서비스 시작에 실패했습니다.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) else (
|
||||||
|
echo 서비스 설치에 실패했습니다.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
112
Sub/Console_SendMail/MailService.cs
Normal file
112
Sub/Console_SendMail/MailService.cs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
using System;
|
||||||
|
using System.ServiceProcess;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace Console_SendMail
|
||||||
|
{
|
||||||
|
public partial class MailService : ServiceBase
|
||||||
|
{
|
||||||
|
private CancellationTokenSource _cancellationTokenSource;
|
||||||
|
private Task _serviceTask;
|
||||||
|
|
||||||
|
public MailService()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
ServiceName = "EETGWMailService";
|
||||||
|
CanStop = true;
|
||||||
|
CanPauseAndContinue = false;
|
||||||
|
AutoLog = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStart(string[] args)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스가 시작됩니다.", EventLogEntryType.Information);
|
||||||
|
|
||||||
|
_cancellationTokenSource = new CancellationTokenSource();
|
||||||
|
_serviceTask = Task.Run(() => DoWork(_cancellationTokenSource.Token));
|
||||||
|
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스가 성공적으로 시작되었습니다.", EventLogEntryType.Information);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, $"서비스 시작 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnStop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스를 중지합니다.", EventLogEntryType.Information);
|
||||||
|
|
||||||
|
_cancellationTokenSource?.Cancel();
|
||||||
|
|
||||||
|
if (_serviceTask != null)
|
||||||
|
{
|
||||||
|
// 최대 30초 대기
|
||||||
|
if (!_serviceTask.Wait(TimeSpan.FromSeconds(30)))
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, "서비스 중지 시간 초과", EventLogEntryType.Warning);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스가 중지되었습니다.", EventLogEntryType.Information);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, $"서비스 중지 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_cancellationTokenSource?.Dispose();
|
||||||
|
_serviceTask?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DoWork(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 기존 프로그램의 while 루프 로직을 여기로 이동
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스 작업을 시작합니다.", EventLogEntryType.Information);
|
||||||
|
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 기존 메일 처리 로직 실행
|
||||||
|
Program.ExecuteMailOperations();
|
||||||
|
|
||||||
|
// 1초 대기 (cancellation token으로 중단 가능)
|
||||||
|
if (cancellationToken.WaitHandle.WaitOne(1000))
|
||||||
|
{
|
||||||
|
break; // 취소 요청 시 종료
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
EventLog.WriteEntry(ServiceName, $"메일 처리 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
|
||||||
|
|
||||||
|
// 오류 발생 시 5초 대기 후 재시도
|
||||||
|
if (cancellationToken.WaitHandle.WaitOne(5000))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EventLog.WriteEntry(ServiceName, "메일 서비스 작업이 종료되었습니다.", EventLogEntryType.Information);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
// MailService
|
||||||
|
//
|
||||||
|
this.ServiceName = "EETGWMailService";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.ServiceProcess;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static System.Net.Mime.MediaTypeNames;
|
||||||
|
|
||||||
namespace Console_SendMail
|
namespace Console_SendMail
|
||||||
{
|
{
|
||||||
@@ -62,15 +66,175 @@ namespace Console_SendMail
|
|||||||
return list_to;
|
return list_to;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
// 명령행 인수 처리
|
||||||
|
if (args.Length > 0)
|
||||||
|
{
|
||||||
|
string command = args[0].ToLower();
|
||||||
|
switch (command)
|
||||||
|
{
|
||||||
|
case "-install":
|
||||||
|
case "/install":
|
||||||
|
InstallService();
|
||||||
|
return;
|
||||||
|
case "-uninstall":
|
||||||
|
case "/uninstall":
|
||||||
|
UninstallService();
|
||||||
|
return;
|
||||||
|
case "-console":
|
||||||
|
case "/console":
|
||||||
|
RunAsConsole();
|
||||||
|
return;
|
||||||
|
case "-help":
|
||||||
|
case "/help":
|
||||||
|
case "/?":
|
||||||
|
ShowHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Console.WriteLine($"mail start ver 2508051140");
|
// 서비스로 실행
|
||||||
|
if (Environment.UserInteractive)
|
||||||
|
{
|
||||||
|
// 대화형 모드에서는 콘솔로 실행
|
||||||
|
RunAsConsole();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 서비스로 실행
|
||||||
|
ServiceBase[] ServicesToRun;
|
||||||
|
ServicesToRun = new ServiceBase[]
|
||||||
|
{
|
||||||
|
new MailService()
|
||||||
|
};
|
||||||
|
ServiceBase.Run(ServicesToRun);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ShowHelp()
|
||||||
|
{
|
||||||
|
Console.WriteLine("EETGW Mail Service");
|
||||||
|
Console.WriteLine("사용법:");
|
||||||
|
Console.WriteLine(" Console_SendMail.exe - 서비스로 실행");
|
||||||
|
Console.WriteLine(" Console_SendMail.exe -console - 콘솔 모드로 실행");
|
||||||
|
Console.WriteLine(" Console_SendMail.exe -install - 서비스 설치");
|
||||||
|
Console.WriteLine(" Console_SendMail.exe -uninstall - 서비스 제거");
|
||||||
|
Console.WriteLine(" Console_SendMail.exe -help - 도움말 표시");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void InstallService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string servicePath = $"\"{System.Reflection.Assembly.GetExecutingAssembly().Location}\"";
|
||||||
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "sc.exe",
|
||||||
|
Arguments = $"create EETGWMailService binPath= \"{servicePath}\" DisplayName= \"EETGW Mail Service\" start= auto",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (Process process = Process.Start(startInfo))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
if (process.ExitCode == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("서비스가 성공적으로 설치되었습니다.");
|
||||||
|
Console.WriteLine("서비스를 시작하려면 다음 명령을 실행하세요:");
|
||||||
|
Console.WriteLine("net start EETGWMailService");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string error = process.StandardError.ReadToEnd();
|
||||||
|
Console.WriteLine($"서비스 설치 실패: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"서비스 설치 중 오류 발생: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UninstallService()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ProcessStartInfo startInfo = new ProcessStartInfo
|
||||||
|
{
|
||||||
|
FileName = "sc.exe",
|
||||||
|
Arguments = "delete EETGWMailService",
|
||||||
|
UseShellExecute = false,
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
CreateNoWindow = true
|
||||||
|
};
|
||||||
|
|
||||||
|
using (Process process = Process.Start(startInfo))
|
||||||
|
{
|
||||||
|
process.WaitForExit();
|
||||||
|
if (process.ExitCode == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("서비스가 성공적으로 제거되었습니다.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string error = process.StandardError.ReadToEnd();
|
||||||
|
Console.WriteLine($"서비스 제거 실패: {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"서비스 제거 중 오류 발생: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RunAsConsole()
|
||||||
|
{
|
||||||
|
// 중복실행 방지 체크
|
||||||
|
if (!CheckSingleInstance())
|
||||||
|
{
|
||||||
|
return; // 프로그램 종료
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"mail start ver 2508051140 (콘솔 모드)");
|
||||||
|
Console.WriteLine("종료하려면 Ctrl+C를 누르세요.");
|
||||||
|
|
||||||
|
// Ctrl+C 핸들러 설정
|
||||||
|
Console.CancelKeyPress += (sender, e) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine("\n메일 서비스를 종료합니다...");
|
||||||
|
e.Cancel = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
|
ExecuteMailOperations();
|
||||||
|
Thread.Sleep(1000); // 1초 대기
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"오류 발생: {ex.Message}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Console.WriteLine("mail end");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 메일 관련 작업들을 실행하는 메서드 (서비스와 콘솔에서 공통 사용)
|
||||||
|
/// </summary>
|
||||||
|
public static void ExecuteMailOperations()
|
||||||
|
{
|
||||||
//메일대기내역전송
|
//메일대기내역전송
|
||||||
var tsSendMail = DateTime.Now - ChkSendMailTime;
|
var tsSendMail = DateTime.Now - ChkSendMailTime;
|
||||||
if (tsSendMail.TotalMilliseconds > 1000)
|
if (tsSendMail.TotalMilliseconds > 1000)
|
||||||
@@ -93,7 +257,6 @@ namespace Console_SendMail
|
|||||||
var tsPrjUpdateweek = DateTime.Now - ChkMakePrjUpdateWeekTime;
|
var tsPrjUpdateweek = DateTime.Now - ChkMakePrjUpdateWeekTime;
|
||||||
if (tsPrjUpdateweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
if (tsPrjUpdateweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_MakeUpdateRequireProject(); }
|
try { Mail_MakeUpdateRequireProject(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkMakePrjUpdateWeekTime = DateTime.Now; }
|
finally { ChkMakePrjUpdateWeekTime = DateTime.Now; }
|
||||||
@@ -103,19 +266,16 @@ namespace Console_SendMail
|
|||||||
var tsScheDayweek = DateTime.Now - ChkMakeSchDayWeekTime;
|
var tsScheDayweek = DateTime.Now - ChkMakeSchDayWeekTime;
|
||||||
if (tsScheDayweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
if (tsScheDayweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_MakeScheduleDayWeek(); }
|
try { Mail_MakeScheduleDayWeek(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkMakeSchDayWeekTime = DateTime.Now; }
|
finally { ChkMakeSchDayWeekTime = DateTime.Now; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
///스케쥴 기한 알림(일)
|
///스케쥴 기한 알림(일)
|
||||||
var tsScheDay = DateTime.Now - ChkMakeSchDay;
|
var tsScheDay = DateTime.Now - ChkMakeSchDay;
|
||||||
if (tsScheDay.TotalMinutes > 30 && DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
|
if (tsScheDay.TotalMinutes > 30 && DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
|
||||||
DateTime.Now.DayOfWeek != DayOfWeek.Sunday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
DateTime.Now.DayOfWeek != DayOfWeek.Sunday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_MakeScheduleDay(); }
|
try { Mail_MakeScheduleDay(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkMakeSchDay = DateTime.Now; }
|
finally { ChkMakeSchDay = DateTime.Now; }
|
||||||
@@ -125,7 +285,6 @@ namespace Console_SendMail
|
|||||||
var tsNoSchedule = DateTime.Now - ChkNoSchedule;
|
var tsNoSchedule = DateTime.Now - ChkNoSchedule;
|
||||||
if (tsNoSchedule.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
if (tsNoSchedule.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_NoSchedule(); }
|
try { Mail_NoSchedule(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkNoSchedule = DateTime.Now; }
|
finally { ChkNoSchedule = DateTime.Now; }
|
||||||
@@ -135,7 +294,6 @@ namespace Console_SendMail
|
|||||||
var tsjobweek = DateTime.Now - ChkJObreportWeek;
|
var tsjobweek = DateTime.Now - ChkJObreportWeek;
|
||||||
if (tsjobweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
|
if (tsjobweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_JobReportWeek(); }
|
try { Mail_JobReportWeek(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkJObreportWeek = DateTime.Now; }
|
finally { ChkJObreportWeek = DateTime.Now; }
|
||||||
@@ -149,7 +307,6 @@ namespace Console_SendMail
|
|||||||
DateTime.Now.DayOfWeek != DayOfWeek.Monday &&
|
DateTime.Now.DayOfWeek != DayOfWeek.Monday &&
|
||||||
DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
|
DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_JobReportDay(); }
|
try { Mail_JobReportDay(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkJobreportDay = DateTime.Now; }
|
finally { ChkJobreportDay = DateTime.Now; }
|
||||||
@@ -162,16 +319,35 @@ namespace Console_SendMail
|
|||||||
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
|
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
|
||||||
DateTime.Now.Hour >= 17)
|
DateTime.Now.Hour >= 17)
|
||||||
{
|
{
|
||||||
|
|
||||||
try { Mail_Take_a_rest_remind(); }
|
try { Mail_Take_a_rest_remind(); }
|
||||||
catch { }
|
catch { }
|
||||||
finally { ChkTakeARest = DateTime.Now; }
|
finally { ChkTakeARest = DateTime.Now; }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 중복실행 방지 체크
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>단일 인스턴스인 경우 true, 중복실행인 경우 false</returns>
|
||||||
|
static bool CheckSingleInstance()
|
||||||
|
{
|
||||||
|
string processName = Process.GetCurrentProcess().ProcessName;
|
||||||
|
Process[] processes = Process.GetProcessesByName(processName);
|
||||||
|
|
||||||
|
if (processes.Length > 1)
|
||||||
|
{
|
||||||
|
// 중복실행 감지
|
||||||
|
string message = $"⚠️ 프로그램이 이미 실행 중입니다!\n\n" +
|
||||||
|
"동시에 여러 개의 프로그램을 실행할 수 없습니다.\n\n" +
|
||||||
|
"해결방법을 선택하세요:";
|
||||||
|
|
||||||
|
Console.WriteLine(message);
|
||||||
|
// 현재 실행을 취소
|
||||||
|
return false;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine("mail end");
|
return true; // 단일 인스턴스
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
7
Sub/Console_SendMail/RunConsole.bat
Normal file
7
Sub/Console_SendMail/RunConsole.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
echo EETGW Mail Service - 콘솔 모드로 실행
|
||||||
|
|
||||||
|
REM 콘솔 모드로 실행
|
||||||
|
Console_SendMail.exe -console
|
||||||
|
|
||||||
|
pause
|
||||||
34
Sub/Console_SendMail/UninstallService.bat
Normal file
34
Sub/Console_SendMail/UninstallService.bat
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
@echo off
|
||||||
|
echo EETGW Mail Service 제거 중...
|
||||||
|
|
||||||
|
REM 관리자 권한 확인
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo 관리자 권한이 확인되었습니다.
|
||||||
|
) else (
|
||||||
|
echo 이 스크립트는 관리자 권한으로 실행해야 합니다.
|
||||||
|
echo 관리자 권한으로 다시 실행해주세요.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 서비스 중지
|
||||||
|
echo 서비스를 중지합니다...
|
||||||
|
net stop EETGWMailService 2>nul
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo 서비스가 중지되었습니다.
|
||||||
|
) else (
|
||||||
|
echo 서비스가 실행 중이 아니거나 중지에 실패했습니다.
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 서비스 제거
|
||||||
|
Console_SendMail.exe -uninstall
|
||||||
|
|
||||||
|
if %errorLevel% == 0 (
|
||||||
|
echo 서비스 제거가 완료되었습니다.
|
||||||
|
) else (
|
||||||
|
echo 서비스 제거에 실패했습니다.
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
Reference in New Issue
Block a user