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>
|
||||
Reference in New Issue
Block a user