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:
ChiKyun Kim
2025-09-11 09:08:40 +09:00
parent 777fcd5d89
commit 6bd4f84192
49 changed files with 46882 additions and 102 deletions

View 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;
}
}
}

View 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_;
}
}
}
}

View 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; } // 데이터를기록한일시
}
}

View 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;
}
}
}

View 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")
};
}
}
}

View 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
});
}
}
}
}

View 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;
}
}
}

View 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"); // 탭
}
}
}

View 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; } // 데이터를기록한일시
}
}

View 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;
}
}
}

View 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")
};
}
}
}

View 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;
}
}
}

View 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 - 이 상태 페이지"
}
});
}
}
}

View 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
};
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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")
};
}
}
}

View 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>

View 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;
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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;
}

View 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>
);
};

View 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>
);
}

View 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>
);
};

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -35,6 +35,7 @@
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.ServiceProcess" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
@@ -49,6 +50,9 @@
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
</Compile>
<Compile Include="MailService.cs">
<SubType>Component</SubType>
</Compile>
<Compile Include="Program.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Properties\Settings.Designer.cs">

View File

@@ -0,0 +1,36 @@
@echo off
echo EETGW Mail Service 설치 중...
REM 관리자 권한 확인
net session >nul 2>&1
if %errorLevel% == 0 (
echo 관리자 권한이 확인되었습니다.
) else (
echo 이 스크립트는 관리자 권한으로 실행해야 합니다.
echo 관리자 권한으로 다시 실행해주세요.
pause
exit /b 1
)
REM 서비스 설치
Console_SendMail.exe -install
if %errorLevel% == 0 (
echo.
echo 서비스 설치가 완료되었습니다.
echo 서비스를 시작하시겠습니까? (Y/N)
set /p choice="선택: "
if /i "%choice%"=="Y" (
net start EETGWMailService
if %errorLevel% == 0 (
echo 서비스가 성공적으로 시작되었습니다.
) else (
echo 서비스 시작에 실패했습니다.
)
)
) else (
echo 서비스 설치에 실패했습니다.
)
echo.
pause

View File

@@ -0,0 +1,112 @@
using System;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
namespace Console_SendMail
{
public partial class MailService : ServiceBase
{
private CancellationTokenSource _cancellationTokenSource;
private Task _serviceTask;
public MailService()
{
InitializeComponent();
ServiceName = "EETGWMailService";
CanStop = true;
CanPauseAndContinue = false;
AutoLog = true;
}
protected override void OnStart(string[] args)
{
try
{
EventLog.WriteEntry(ServiceName, "메일 서비스가 시작됩니다.", EventLogEntryType.Information);
_cancellationTokenSource = new CancellationTokenSource();
_serviceTask = Task.Run(() => DoWork(_cancellationTokenSource.Token));
EventLog.WriteEntry(ServiceName, "메일 서비스가 성공적으로 시작되었습니다.", EventLogEntryType.Information);
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"서비스 시작 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
throw;
}
}
protected override void OnStop()
{
try
{
EventLog.WriteEntry(ServiceName, "메일 서비스를 중지합니다.", EventLogEntryType.Information);
_cancellationTokenSource?.Cancel();
if (_serviceTask != null)
{
// 최대 30초 대기
if (!_serviceTask.Wait(TimeSpan.FromSeconds(30)))
{
EventLog.WriteEntry(ServiceName, "서비스 중지 시간 초과", EventLogEntryType.Warning);
}
}
EventLog.WriteEntry(ServiceName, "메일 서비스가 중지되었습니다.", EventLogEntryType.Information);
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"서비스 중지 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
}
finally
{
_cancellationTokenSource?.Dispose();
_serviceTask?.Dispose();
}
}
private void DoWork(CancellationToken cancellationToken)
{
// 기존 프로그램의 while 루프 로직을 여기로 이동
EventLog.WriteEntry(ServiceName, "메일 서비스 작업을 시작합니다.", EventLogEntryType.Information);
while (!cancellationToken.IsCancellationRequested)
{
try
{
// 기존 메일 처리 로직 실행
Program.ExecuteMailOperations();
// 1초 대기 (cancellation token으로 중단 가능)
if (cancellationToken.WaitHandle.WaitOne(1000))
{
break; // 취소 요청 시 종료
}
}
catch (Exception ex)
{
EventLog.WriteEntry(ServiceName, $"메일 처리 중 오류 발생: {ex.Message}", EventLogEntryType.Error);
// 오류 발생 시 5초 대기 후 재시도
if (cancellationToken.WaitHandle.WaitOne(5000))
{
break;
}
}
}
EventLog.WriteEntry(ServiceName, "메일 서비스 작업이 종료되었습니다.", EventLogEntryType.Information);
}
private void InitializeComponent()
{
//
// MailService
//
this.ServiceName = "EETGWMailService";
}
}
}

View File

@@ -1,8 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using static System.Net.Mime.MediaTypeNames;
namespace Console_SendMail
{
@@ -62,116 +66,288 @@ namespace Console_SendMail
return list_to;
}
static void Main(string[] args)
{
Console.WriteLine($"mail start ver 2508051140");
while (true)
// 명령행 인수 처리
if (args.Length > 0)
{
//메일대기내역전송
var tsSendMail = DateTime.Now - ChkSendMailTime;
if (tsSendMail.TotalMilliseconds > 1000)
string command = args[0].ToLower();
switch (command)
{
try { SendMail(); }
catch { }
finally { ChkSendMailTime = DateTime.Now; }
case "-install":
case "/install":
InstallService();
return;
case "-uninstall":
case "/uninstall":
UninstallService();
return;
case "-console":
case "/console":
RunAsConsole();
return;
case "-help":
case "/help":
case "/?":
ShowHelp();
return;
}
//자동생성 메일 작성
var tsAutoMake = DateTime.Now - ChkMakeAutoTime;
if (tsAutoMake.TotalMinutes >= 10)
{
try { MakeAutoMail(); }
catch { }
finally { ChkMakeAutoTime = DateTime.Now; }
}
//프로젝트업데이트알림
var tsPrjUpdateweek = DateTime.Now - ChkMakePrjUpdateWeekTime;
if (tsPrjUpdateweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeUpdateRequireProject(); }
catch { }
finally { ChkMakePrjUpdateWeekTime = DateTime.Now; }
}
///스케쥴 기한 알림(주)
var tsScheDayweek = DateTime.Now - ChkMakeSchDayWeekTime;
if (tsScheDayweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeScheduleDayWeek(); }
catch { }
finally { ChkMakeSchDayWeekTime = DateTime.Now; }
}
///스케쥴 기한 알림(일)
var tsScheDay = DateTime.Now - ChkMakeSchDay;
if (tsScheDay.TotalMinutes > 30 && DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeScheduleDay(); }
catch { }
finally { ChkMakeSchDay = DateTime.Now; }
}
///스케쥴없음
var tsNoSchedule = DateTime.Now - ChkNoSchedule;
if (tsNoSchedule.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_NoSchedule(); }
catch { }
finally { ChkNoSchedule = DateTime.Now; }
}
///업무일지(주간)
var tsjobweek = DateTime.Now - ChkJObreportWeek;
if (tsjobweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
{
try { Mail_JobReportWeek(); }
catch { }
finally { ChkJObreportWeek = DateTime.Now; }
}
///업무일지(일)
var tsjobday = DateTime.Now - ChkJobreportDay;
if (tsjobday.TotalMinutes > 15 &&
DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
DateTime.Now.DayOfWeek != DayOfWeek.Monday &&
DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
{
try { Mail_JobReportDay(); }
catch { }
finally { ChkJobreportDay = DateTime.Now; }
}
///휴가신청(Remind) - 230611
var tsTakeaRest = DateTime.Now - ChkTakeARest;
if (tsTakeaRest.TotalMinutes > 15 &&
DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
DateTime.Now.Hour >= 17)
{
try { Mail_Take_a_rest_remind(); }
catch { }
finally { ChkTakeARest = DateTime.Now; }
}
}
Console.WriteLine("mail end");
// 서비스로 실행
if (Environment.UserInteractive)
{
// 대화형 모드에서는 콘솔로 실행
RunAsConsole();
}
else
{
// 서비스로 실행
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new MailService()
};
ServiceBase.Run(ServicesToRun);
}
}
private static void ShowHelp()
{
Console.WriteLine("EETGW Mail Service");
Console.WriteLine("사용법:");
Console.WriteLine(" Console_SendMail.exe - 서비스로 실행");
Console.WriteLine(" Console_SendMail.exe -console - 콘솔 모드로 실행");
Console.WriteLine(" Console_SendMail.exe -install - 서비스 설치");
Console.WriteLine(" Console_SendMail.exe -uninstall - 서비스 제거");
Console.WriteLine(" Console_SendMail.exe -help - 도움말 표시");
}
private static void InstallService()
{
try
{
string servicePath = $"\"{System.Reflection.Assembly.GetExecutingAssembly().Location}\"";
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = $"create EETGWMailService binPath= \"{servicePath}\" DisplayName= \"EETGW Mail Service\" start= auto",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
process.WaitForExit();
if (process.ExitCode == 0)
{
Console.WriteLine("서비스가 성공적으로 설치되었습니다.");
Console.WriteLine("서비스를 시작하려면 다음 명령을 실행하세요:");
Console.WriteLine("net start EETGWMailService");
}
else
{
string error = process.StandardError.ReadToEnd();
Console.WriteLine($"서비스 설치 실패: {error}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"서비스 설치 중 오류 발생: {ex.Message}");
}
}
private static void UninstallService()
{
try
{
ProcessStartInfo startInfo = new ProcessStartInfo
{
FileName = "sc.exe",
Arguments = "delete EETGWMailService",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using (Process process = Process.Start(startInfo))
{
process.WaitForExit();
if (process.ExitCode == 0)
{
Console.WriteLine("서비스가 성공적으로 제거되었습니다.");
}
else
{
string error = process.StandardError.ReadToEnd();
Console.WriteLine($"서비스 제거 실패: {error}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"서비스 제거 중 오류 발생: {ex.Message}");
}
}
private static void RunAsConsole()
{
// 중복실행 방지 체크
if (!CheckSingleInstance())
{
return; // 프로그램 종료
}
Console.WriteLine($"mail start ver 2508051140 (콘솔 모드)");
Console.WriteLine("종료하려면 Ctrl+C를 누르세요.");
// Ctrl+C 핸들러 설정
Console.CancelKeyPress += (sender, e) =>
{
Console.WriteLine("\n메일 서비스를 종료합니다...");
e.Cancel = false;
};
try
{
while (true)
{
ExecuteMailOperations();
Thread.Sleep(1000); // 1초 대기
}
}
catch (Exception ex)
{
Console.WriteLine($"오류 발생: {ex.Message}");
}
finally
{
Console.WriteLine("mail end");
}
}
/// <summary>
/// 메일 관련 작업들을 실행하는 메서드 (서비스와 콘솔에서 공통 사용)
/// </summary>
public static void ExecuteMailOperations()
{
//메일대기내역전송
var tsSendMail = DateTime.Now - ChkSendMailTime;
if (tsSendMail.TotalMilliseconds > 1000)
{
try { SendMail(); }
catch { }
finally { ChkSendMailTime = DateTime.Now; }
}
//자동생성 메일 작성
var tsAutoMake = DateTime.Now - ChkMakeAutoTime;
if (tsAutoMake.TotalMinutes >= 10)
{
try { MakeAutoMail(); }
catch { }
finally { ChkMakeAutoTime = DateTime.Now; }
}
//프로젝트업데이트알림
var tsPrjUpdateweek = DateTime.Now - ChkMakePrjUpdateWeekTime;
if (tsPrjUpdateweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeUpdateRequireProject(); }
catch { }
finally { ChkMakePrjUpdateWeekTime = DateTime.Now; }
}
///스케쥴 기한 알림(주)
var tsScheDayweek = DateTime.Now - ChkMakeSchDayWeekTime;
if (tsScheDayweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeScheduleDayWeek(); }
catch { }
finally { ChkMakeSchDayWeekTime = DateTime.Now; }
}
///스케쥴 기한 알림(일)
var tsScheDay = DateTime.Now - ChkMakeSchDay;
if (tsScheDay.TotalMinutes > 30 && DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_MakeScheduleDay(); }
catch { }
finally { ChkMakeSchDay = DateTime.Now; }
}
///스케쥴없음
var tsNoSchedule = DateTime.Now - ChkNoSchedule;
if (tsNoSchedule.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 10 && DateTime.Now.Hour <= 18)
{
try { Mail_NoSchedule(); }
catch { }
finally { ChkNoSchedule = DateTime.Now; }
}
///업무일지(주간)
var tsjobweek = DateTime.Now - ChkJObreportWeek;
if (tsjobweek.TotalMinutes > 30 && DateTime.Now.DayOfWeek == DayOfWeek.Monday && DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
{
try { Mail_JobReportWeek(); }
catch { }
finally { ChkJObreportWeek = DateTime.Now; }
}
///업무일지(일)
var tsjobday = DateTime.Now - ChkJobreportDay;
if (tsjobday.TotalMinutes > 15 &&
DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
DateTime.Now.DayOfWeek != DayOfWeek.Monday &&
DateTime.Now.Hour >= 9 && DateTime.Now.Hour <= 18)
{
try { Mail_JobReportDay(); }
catch { }
finally { ChkJobreportDay = DateTime.Now; }
}
///휴가신청(Remind) - 230611
var tsTakeaRest = DateTime.Now - ChkTakeARest;
if (tsTakeaRest.TotalMinutes > 15 &&
DateTime.Now.DayOfWeek != DayOfWeek.Saturday &&
DateTime.Now.DayOfWeek != DayOfWeek.Sunday &&
DateTime.Now.Hour >= 17)
{
try { Mail_Take_a_rest_remind(); }
catch { }
finally { ChkTakeARest = DateTime.Now; }
}
}
/// <summary>
/// 중복실행 방지 체크
/// </summary>
/// <returns>단일 인스턴스인 경우 true, 중복실행인 경우 false</returns>
static bool CheckSingleInstance()
{
string processName = Process.GetCurrentProcess().ProcessName;
Process[] processes = Process.GetProcessesByName(processName);
if (processes.Length > 1)
{
// 중복실행 감지
string message = $"⚠️ 프로그램이 이미 실행 중입니다!\n\n" +
"동시에 여러 개의 프로그램을 실행할 수 없습니다.\n\n" +
"해결방법을 선택하세요:";
Console.WriteLine(message);
// 현재 실행을 취소
return false;
}
return true; // 단일 인스턴스
}
}
}

View File

@@ -0,0 +1,7 @@
@echo off
echo EETGW Mail Service - 콘솔 모드로 실행
REM 콘솔 모드로 실행
Console_SendMail.exe -console
pause

View File

@@ -0,0 +1,34 @@
@echo off
echo EETGW Mail Service 제거 중...
REM 관리자 권한 확인
net session >nul 2>&1
if %errorLevel% == 0 (
echo 관리자 권한이 확인되었습니다.
) else (
echo 이 스크립트는 관리자 권한으로 실행해야 합니다.
echo 관리자 권한으로 다시 실행해주세요.
pause
exit /b 1
)
REM 서비스 중지
echo 서비스를 중지합니다...
net stop EETGWMailService 2>nul
if %errorLevel% == 0 (
echo 서비스가 중지되었습니다.
) else (
echo 서비스가 실행 중이 아니거나 중지에 실패했습니다.
)
REM 서비스 제거
Console_SendMail.exe -uninstall
if %errorLevel% == 0 (
echo 서비스 제거가 완료되었습니다.
) else (
echo 서비스 제거에 실패했습니다.
)
echo.
pause