Add Dashboard todo edit/delete/complete features and Note view count tracking
This commit is contained in:
@@ -363,6 +363,9 @@
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.Holiday.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.Holyday.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.MailForm.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.MailData.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.Note.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.Board.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.UserGroup.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.UserAuth.cs" />
|
||||
<Compile Include="Web\MachineBridge\WebSocketServer.cs" />
|
||||
|
||||
158
Project/Web/MachineBridge/MachineBridge.Board.cs
Normal file
158
Project/Web/MachineBridge/MachineBridge.Board.cs
Normal file
@@ -0,0 +1,158 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using FCOMMON;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
public partial class MachineBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 게시판 목록 조회 (bidx로 구분: 5=패치내역, 기타=일반게시판)
|
||||
/// </summary>
|
||||
public string Board_GetList(int bidx, string searchKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Project.Properties.Settings.Default.CS;// Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var sql = @"
|
||||
SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark,
|
||||
dbo.getUserName(wuid) AS wuid_name
|
||||
FROM Board WITH (nolock)
|
||||
WHERE gcode = @gcode AND bidx = @bidx
|
||||
AND (ISNULL(title,'') LIKE @search OR ISNULL(contents,'') LIKE @search OR ISNULL(wuid,'') LIKE @search)
|
||||
ORDER BY wdate DESC";
|
||||
|
||||
if(bidx == 5) //패치내역은 모두가 다 확인할 수있도록 그룹코드를 제한하지 않는다
|
||||
{
|
||||
sql = @"
|
||||
SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark,
|
||||
dbo.getUserName(wuid) AS wuid_name
|
||||
FROM Board WITH (nolock)
|
||||
WHERE bidx = @bidx
|
||||
AND (ISNULL(title,'') LIKE @search OR ISNULL(contents,'') LIKE @search OR ISNULL(wuid,'') LIKE @search)
|
||||
ORDER BY wdate DESC";
|
||||
}
|
||||
|
||||
var cmd = new SqlCommand(sql, conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@bidx", SqlDbType.Int).Value = bidx;
|
||||
cmd.Parameters.Add("@search", SqlDbType.NVarChar).Value = $"%{searchKey}%";
|
||||
|
||||
var list = new List<object>();
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
idx = reader.GetInt32(0),
|
||||
bidx = reader.GetInt32(1),
|
||||
header = reader.IsDBNull(2) ? "" : (reader.GetBoolean(2) ? "공지" : ""),
|
||||
cate = reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||
title = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||
contents = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||
file = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||
guid = reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||
url = reader.IsDBNull(8) ? "" : reader.GetString(8),
|
||||
wuid = reader.IsDBNull(9) ? "" : reader.GetString(9),
|
||||
wdate = reader.IsDBNull(10) ? (DateTime?)null : reader.GetDateTime(10),
|
||||
project = reader.IsDBNull(11) ? "" : reader.GetInt32(11).ToString(),
|
||||
pidx = reader.IsDBNull(12) ? -1 : reader.GetInt32(12),
|
||||
gcode = reader.IsDBNull(13) ? "" : reader.GetString(13),
|
||||
close = reader.IsDBNull(14) ? false : reader.GetBoolean(14),
|
||||
remark = reader.IsDBNull(15) ? "" : reader.GetString(15),
|
||||
wuid_name = reader.IsDBNull(16) ? "" : reader.GetString(16)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = list });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 게시판 상세 조회
|
||||
/// </summary>
|
||||
public string Board_GetDetail(int idx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Project.Properties.Settings.Default.CS;//Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
SELECT idx, bidx, header, cate, title, contents, [file], guid, url, wuid, wdate, project, pidx, gcode, [close], remark,
|
||||
dbo.getUserName(wuid) AS wuid_name
|
||||
FROM Board WITH (nolock)
|
||||
WHERE idx = @idx AND gcode = @gcode", conn);
|
||||
|
||||
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var data = new
|
||||
{
|
||||
idx = reader.GetInt32(0),
|
||||
bidx = reader.GetInt32(1),
|
||||
header = reader.IsDBNull(2) ? "" : (reader.GetBoolean(2) ? "공지" : ""),
|
||||
cate = reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||
title = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||
contents = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||
file = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||
guid = reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||
url = reader.IsDBNull(8) ? "" : reader.GetString(8),
|
||||
wuid = reader.IsDBNull(9) ? "" : reader.GetString(9),
|
||||
wdate = reader.IsDBNull(10) ? (DateTime?)null : reader.GetDateTime(10),
|
||||
project = reader.IsDBNull(11) ? "" : reader.GetInt32(11).ToString(),
|
||||
pidx = reader.IsDBNull(12) ? -1 : reader.GetInt32(12),
|
||||
gcode = reader.IsDBNull(13) ? "" : reader.GetString(13),
|
||||
close = reader.IsDBNull(14) ? false : reader.GetBoolean(14),
|
||||
remark = reader.IsDBNull(15) ? "" : reader.GetString(15),
|
||||
wuid_name = reader.IsDBNull(16) ? "" : reader.GetString(16)
|
||||
};
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = data });
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Project/Web/MachineBridge/MachineBridge.MailData.cs
Normal file
75
Project/Web/MachineBridge/MachineBridge.MailData.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using FCOMMON;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
public partial class MachineBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 메일 발신 내역 조회
|
||||
/// </summary>
|
||||
public string Mail_GetList(string startDate, string endDate, string searchKey)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Project.Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
SELECT idx, gcode, subject, body, fromlist, tolist, cc AS cclist, bcc AS bcclist, project, cate, pdate
|
||||
FROM MailData WITH (nolock)
|
||||
WHERE gcode = @gcode
|
||||
AND (pdate BETWEEN @startDate AND @endDate)
|
||||
AND (ISNULL(subject,'') LIKE @search OR ISNULL(fromlist,'') LIKE @search OR ISNULL(tolist,'') LIKE @search OR ISNULL(cate,'') LIKE @search)
|
||||
ORDER BY pdate DESC", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@startDate", SqlDbType.VarChar).Value = startDate;
|
||||
cmd.Parameters.Add("@endDate", SqlDbType.VarChar).Value = endDate;
|
||||
cmd.Parameters.Add("@search", SqlDbType.NVarChar).Value = $"%{searchKey}%";
|
||||
|
||||
var list = new List<object>();
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
idx = reader.GetInt32(0),
|
||||
gcode = reader.IsDBNull(1) ? "" : reader.GetString(1),
|
||||
uid = "", // uid 컬럼 없음
|
||||
subject = reader.IsDBNull(2) ? "" : reader.GetString(2),
|
||||
htmlbody = reader.IsDBNull(3) ? "" : reader.GetString(3), // body를 htmlbody로 반환
|
||||
fromlist = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||
tolist = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||
cclist = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||
bcclist = reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||
project = reader.IsDBNull(8) ? "" : reader.GetInt32(8).ToString(),
|
||||
cate = reader.IsDBNull(9) ? "" : reader.GetString(9),
|
||||
wdate = reader.IsDBNull(10) ? "" : reader.GetString(10) // pdate를 wdate로 반환 (프론트엔드 호환)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = list });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
343
Project/Web/MachineBridge/MachineBridge.Note.cs
Normal file
343
Project/Web/MachineBridge/MachineBridge.Note.cs
Normal file
@@ -0,0 +1,343 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Newtonsoft.Json;
|
||||
using FCOMMON;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
public partial class MachineBridge
|
||||
{
|
||||
#region Note API
|
||||
|
||||
/// <summary>
|
||||
/// 메모장 목록 조회
|
||||
/// </summary>
|
||||
public string Note_GetList(string startDate, string endDate, string uid = "")
|
||||
{
|
||||
try
|
||||
{
|
||||
// 로그인 체크
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
var cmd = new SqlCommand();
|
||||
cmd.Connection = conn;
|
||||
|
||||
// 권한 체크: 레벨5 미만이면 자기 것만 보거나 공유된 것만 조회
|
||||
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport));
|
||||
|
||||
if (curLevel >= 5)
|
||||
{
|
||||
// 관리자: 모든 메모 조회 가능
|
||||
if (string.IsNullOrEmpty(uid))
|
||||
{
|
||||
cmd.CommandText = @"
|
||||
SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid,
|
||||
ISNULL(viewcount, 0) as viewcount, viewdate,
|
||||
'' as description, '' as description2
|
||||
FROM EETGW_Note WITH (nolock)
|
||||
WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate
|
||||
ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC";
|
||||
}
|
||||
else
|
||||
{
|
||||
cmd.CommandText = @"
|
||||
SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid,
|
||||
ISNULL(viewcount, 0) as viewcount, viewdate,
|
||||
'' as description, '' as description2
|
||||
FROM EETGW_Note WITH (nolock)
|
||||
WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND uid = @uid
|
||||
ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC";
|
||||
cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 일반 사용자: 자신이 작성했거나 공유된 메모만 조회
|
||||
cmd.CommandText = @"
|
||||
SELECT idx, gcode, pdate, title, uid, share, wuid, wdate, guid,
|
||||
ISNULL(viewcount, 0) as viewcount, viewdate,
|
||||
'' as description, '' as description2
|
||||
FROM EETGW_Note WITH (nolock)
|
||||
WHERE (gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND uid = @currentUid)
|
||||
OR (gcode = @gcode AND pdate BETWEEN @startDate AND @endDate AND ISNULL(share, 0) = 1)
|
||||
ORDER BY ISNULL(viewdate, '1900-01-01') DESC, ISNULL(viewcount, 0) DESC, pdate DESC";
|
||||
cmd.Parameters.Add("@currentUid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
}
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@startDate", SqlDbType.VarChar).Value = startDate;
|
||||
cmd.Parameters.Add("@endDate", SqlDbType.VarChar).Value = endDate;
|
||||
|
||||
var list = new List<object>();
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
idx = reader["idx"],
|
||||
gcode = reader["gcode"],
|
||||
pdate = reader["pdate"],
|
||||
title = reader["title"],
|
||||
uid = reader["uid"],
|
||||
share = reader["share"],
|
||||
wuid = reader["wuid"],
|
||||
wdate = reader["wdate"],
|
||||
guid = reader["guid"],
|
||||
viewcount = reader["viewcount"],
|
||||
viewdate = reader["viewdate"] != DBNull.Value ? reader["viewdate"] : null,
|
||||
description = reader["description"],
|
||||
description2 = reader["description2"]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = list });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메모장 상세 조회
|
||||
/// </summary>
|
||||
public string Note_GetDetail(int idx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var cs = Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(cs))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
// 조회수 증가 및 조회일 업데이트
|
||||
var updateCmd = new SqlCommand(@"
|
||||
UPDATE EETGW_Note
|
||||
SET viewcount = ISNULL(viewcount, 0) + 1, viewdate = GETDATE()
|
||||
WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
updateCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
updateCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
updateCmd.ExecuteNonQuery();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
SELECT idx, gcode, pdate, title, uid, description, description2, share, wuid, wdate, guid,
|
||||
ISNULL(viewcount, 0) as viewcount, viewdate
|
||||
FROM EETGW_Note WITH (nolock)
|
||||
WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var item = new
|
||||
{
|
||||
idx = reader["idx"],
|
||||
gcode = reader["gcode"],
|
||||
pdate = reader["pdate"],
|
||||
title = reader["title"],
|
||||
uid = reader["uid"],
|
||||
description = reader["description"],
|
||||
description2 = reader["description2"],
|
||||
share = reader["share"],
|
||||
wuid = reader["wuid"],
|
||||
wdate = reader["wdate"],
|
||||
guid = reader["guid"],
|
||||
viewcount = reader["viewcount"],
|
||||
viewdate = reader["viewdate"] != DBNull.Value ? reader["viewdate"] : null
|
||||
};
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = item });
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "데이터를 찾을 수 없습니다." });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메모장 추가
|
||||
/// </summary>
|
||||
public string Note_Add(string pdate, string title, string uid, string description, string description2, bool share, string guid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
// GUID가 비어있으면 생성
|
||||
if (string.IsNullOrEmpty(guid))
|
||||
{
|
||||
guid = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
using (var conn = new SqlConnection(cs))
|
||||
{
|
||||
conn.Open();
|
||||
var cmd = new SqlCommand(@"
|
||||
INSERT INTO EETGW_Note (gcode, pdate, title, uid, description, description2, share, wuid, wdate, guid)
|
||||
VALUES (@gcode, @pdate, @title, @uid, @description, @description2, @share, @wuid, @wdate, @guid);
|
||||
SELECT CAST(SCOPE_IDENTITY() AS INT);", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = pdate;
|
||||
cmd.Parameters.Add("@title", SqlDbType.NVarChar).Value = title;
|
||||
cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid;
|
||||
cmd.Parameters.Add("@description", SqlDbType.NVarChar).Value = description ?? "";
|
||||
cmd.Parameters.Add("@description2", SqlDbType.NText).Value = description2 ?? "";
|
||||
cmd.Parameters.Add("@share", SqlDbType.Bit).Value = share;
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now;
|
||||
cmd.Parameters.Add("@guid", SqlDbType.VarChar).Value = guid;
|
||||
|
||||
var newIdx = cmd.ExecuteScalar();
|
||||
return JsonConvert.SerializeObject(new { Success = true, Idx = newIdx });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메모장 수정
|
||||
/// </summary>
|
||||
public string Note_Edit(int idx, string pdate, string title, string uid, string description, string description2, bool share, string guid)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
// 권한 체크: 자신의 메모이거나 관리자인 경우만 수정 가능
|
||||
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport));
|
||||
|
||||
var checkCmd = new SqlCommand(@"
|
||||
SELECT uid FROM EETGW_Note WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
checkCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
checkCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
|
||||
var originalUid = checkCmd.ExecuteScalar()?.ToString();
|
||||
if (originalUid != info.Login.no && curLevel < 5)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 수정할 수 없습니다." });
|
||||
}
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
UPDATE EETGW_Note
|
||||
SET pdate = @pdate, title = @title, uid = @uid,
|
||||
description = @description, description2 = @description2,
|
||||
share = @share, guid = @guid, wuid = @wuid, wdate = @wdate
|
||||
WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = pdate;
|
||||
cmd.Parameters.Add("@title", SqlDbType.NVarChar).Value = title;
|
||||
cmd.Parameters.Add("@uid", SqlDbType.VarChar).Value = uid;
|
||||
cmd.Parameters.Add("@description", SqlDbType.NVarChar).Value = description ?? "";
|
||||
cmd.Parameters.Add("@description2", SqlDbType.NText).Value = description2 ?? "";
|
||||
cmd.Parameters.Add("@share", SqlDbType.Bit).Value = share;
|
||||
cmd.Parameters.Add("@guid", SqlDbType.VarChar).Value = guid;
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now;
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
return JsonConvert.SerializeObject(new { Success = true });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 멤모장 삭제
|
||||
/// </summary>
|
||||
public string Note_Delete(int idx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
// 권한 체크: 자신의 메모이거나 관리자인 경우만 삭제 가능
|
||||
int curLevel = Math.Max(info.Login.level, DBM.getAuth(DBM.eAuthType.jobreport));
|
||||
|
||||
var checkCmd = new SqlCommand(@"
|
||||
SELECT uid FROM EETGW_Note WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
checkCmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
checkCmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
|
||||
var originalUid = checkCmd.ExecuteScalar()?.ToString();
|
||||
if (originalUid != info.Login.no && curLevel < 5)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "타인의 자료는 삭제할 수 없습니다." });
|
||||
}
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
DELETE FROM EETGW_Note
|
||||
WHERE gcode = @gcode AND idx = @idx", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
return JsonConvert.SerializeObject(new { Success = true });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -836,6 +836,99 @@ namespace Project.Web
|
||||
break;
|
||||
|
||||
|
||||
// ===== Note API (메모장) =====
|
||||
case "NOTE_GET_LIST":
|
||||
{
|
||||
string startDate = json.startDate ?? "";
|
||||
string endDate = json.endDate ?? "";
|
||||
string uid = json.uid ?? "";
|
||||
string result = _bridge.Note_GetList(startDate, endDate, uid);
|
||||
var response = new { type = "NOTE_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "NOTE_GET_DETAIL":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.Note_GetDetail(idx);
|
||||
var response = new { type = "NOTE_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "NOTE_ADD":
|
||||
{
|
||||
string pdate = json.pdate ?? "";
|
||||
string title = json.title ?? "";
|
||||
string uid = json.uid ?? "";
|
||||
string description = json.description ?? "";
|
||||
string description2 = json.description2 ?? "";
|
||||
bool share = json.share ?? false;
|
||||
string guid = json.guid ?? "";
|
||||
string result = _bridge.Note_Add(pdate, title, uid, description, description2, share, guid);
|
||||
var response = new { type = "NOTE_ADDED", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "NOTE_EDIT":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string pdate = json.pdate ?? "";
|
||||
string title = json.title ?? "";
|
||||
string uid = json.uid ?? "";
|
||||
string description = json.description ?? "";
|
||||
string description2 = json.description2 ?? "";
|
||||
bool share = json.share ?? false;
|
||||
string guid = json.guid ?? "";
|
||||
string result = _bridge.Note_Edit(idx, pdate, title, uid, description, description2, share, guid);
|
||||
var response = new { type = "NOTE_EDITED", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "NOTE_DELETE":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.Note_Delete(idx);
|
||||
var response = new { type = "NOTE_DELETED", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== Board API (게시판 - 패치내역 등) =====
|
||||
case "BOARD_GET_LIST":
|
||||
{
|
||||
int bidx = json.bidx ?? 5;
|
||||
string searchKey = json.searchKey ?? "";
|
||||
string result = _bridge.Board_GetList(bidx, searchKey);
|
||||
var response = new { type = "BOARD_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "BOARD_GET_DETAIL":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.Board_GetDetail(idx);
|
||||
var response = new { type = "BOARD_DETAIL_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== Mail API (메일 발신 내역) =====
|
||||
case "MAIL_GET_LIST":
|
||||
{
|
||||
string startDate = json.startDate ?? "";
|
||||
string endDate = json.endDate ?? "";
|
||||
string searchKey = json.searchKey ?? "";
|
||||
string result = _bridge.Mail_GetList(startDate, endDate, searchKey);
|
||||
var response = new { type = "MAIL_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== Holiday API (월별근무표) =====
|
||||
case "HOLIDAY_GET_LIST":
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { HashRouter, Routes, Route } from 'react-router-dom';
|
||||
import { Layout } from '@/components/layout';
|
||||
import { Dashboard, Todo, Kuntae, Jobreport, Project, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserAuthPage } from '@/pages';
|
||||
import { Dashboard, Todo, Kuntae, Jobreport, Project, Login, CommonCodePage, ItemsPage, UserListPage, MonthlyWorkPage, MailFormPage, UserAuthPage, Note } from '@/pages';
|
||||
import { PatchList } from '@/pages/PatchList';
|
||||
import { MailList } from '@/pages/MailList';
|
||||
import { comms } from '@/communication';
|
||||
import { UserInfo } from '@/types';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -92,6 +94,9 @@ export default function App() {
|
||||
<Route path="/user/auth" element={<UserAuthPage />} />
|
||||
<Route path="/monthly-work" element={<MonthlyWorkPage />} />
|
||||
<Route path="/mail-form" element={<MailFormPage />} />
|
||||
<Route path="/note" element={<Note />} />
|
||||
<Route path="/patch-list" element={<PatchList />} />
|
||||
<Route path="/mail-list" element={<MailList />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
{/* Tailwind Breakpoint Indicator - 개발용 */}
|
||||
|
||||
@@ -1,3 +1,48 @@
|
||||
import type {
|
||||
ApiResponse,
|
||||
LoginStatusResponse,
|
||||
CheckAuthResponse,
|
||||
TodoModel,
|
||||
PurchaseItem,
|
||||
PurchaseCount,
|
||||
JobReportItem,
|
||||
JobReportUser,
|
||||
JobReportPermission,
|
||||
JobReportTypeItem,
|
||||
JobTypeItem,
|
||||
MailFormItem,
|
||||
UserGroupItem,
|
||||
PermissionInfo,
|
||||
AuthItem,
|
||||
AuthFieldInfo,
|
||||
AuthType,
|
||||
MyAuthInfo,
|
||||
ProjectSearchItem,
|
||||
NoteItem,
|
||||
KuntaeModel,
|
||||
HolydayBalance,
|
||||
HolyUser,
|
||||
HolyRequestUser,
|
||||
LoginResult,
|
||||
UserGroup,
|
||||
PreviousLoginInfo,
|
||||
UserInfoDetail,
|
||||
CommonCodeGroup,
|
||||
CommonCode,
|
||||
ItemInfo,
|
||||
ItemDetail,
|
||||
SupplierStaff,
|
||||
PurchaseHistoryItem,
|
||||
UserLevelInfo,
|
||||
GroupUser,
|
||||
UserFullData,
|
||||
AppVersionInfo,
|
||||
HolidayItem,
|
||||
MachineBridgeInterface,
|
||||
BoardItem,
|
||||
MailItem,
|
||||
} from '@/types';
|
||||
|
||||
// WebView2 환경 감지
|
||||
const isWebView = typeof window !== 'undefined' &&
|
||||
window.chrome?.webview?.hostObjects !== undefined;
|
||||
@@ -799,7 +844,7 @@ class CommunicationLayer {
|
||||
|
||||
public async getJobReportTypeList(sd: string, ed: string, uid: string = ''): Promise<ApiResponse<JobReportTypeItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Jobreport_GetTypeList(sd, ed, uid);
|
||||
const result = await machine.Jobreport_GetList(sd, ed, uid, '', '');
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<JobReportTypeItem[]>>('JOBREPORT_GET_TYPE_LIST', 'JOBREPORT_TYPE_LIST_DATA', { sd, ed, uid });
|
||||
@@ -1080,6 +1125,159 @@ class CommunicationLayer {
|
||||
return this.wsRequest<ApiResponse<{ idx: number, name: string, status: string }[]>>('PROJECT_GET_USER_PROJECTS', 'PROJECT_USER_PROJECTS_DATA');
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Note API (메모장) =====
|
||||
|
||||
/**
|
||||
* 메모장 목록 조회
|
||||
* @param startDate 시작일 (yyyy-MM-dd)
|
||||
* @param endDate 종료일 (yyyy-MM-dd)
|
||||
* @param uid 사용자 ID (빈 문자열이면 전체)
|
||||
* @returns ApiResponse<NoteItem[]>
|
||||
*/
|
||||
public async getNoteList(startDate: string, endDate: string, uid: string = ''): Promise<ApiResponse<NoteItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Note_GetList(startDate, endDate, uid);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<NoteItem[]>>('NOTE_GET_LIST', 'NOTE_LIST_DATA', { startDate, endDate, uid });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모장 상세 조회
|
||||
* @param idx 메모 인덱스
|
||||
* @returns ApiResponse<NoteItem>
|
||||
*/
|
||||
public async getNoteDetail(idx: number): Promise<ApiResponse<NoteItem>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Note_GetDetail(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<NoteItem>>('NOTE_GET_DETAIL', 'NOTE_DETAIL_DATA', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모장 추가
|
||||
* @param pdate 날짜 (yyyy-MM-dd)
|
||||
* @param title 제목
|
||||
* @param uid 작성자 ID
|
||||
* @param description 내용 (plain text)
|
||||
* @param description2 내용 (RTF - not used in web)
|
||||
* @param share 공유 여부
|
||||
* @param guid 폴더 GUID (빈 문자열이면 자동 생성)
|
||||
* @returns ApiResponse with new Idx
|
||||
*/
|
||||
public async addNote(
|
||||
pdate: string,
|
||||
title: string,
|
||||
uid: string,
|
||||
description: string,
|
||||
description2: string = '',
|
||||
share: boolean = false,
|
||||
guid: string = ''
|
||||
): Promise<ApiResponse<{ Idx: number }>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Note_Add(pdate, title, uid, description, description2, share, guid);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<{ Idx: number }>>('NOTE_ADD', 'NOTE_ADDED', {
|
||||
pdate, title, uid, description, description2, share, guid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모장 수정
|
||||
* @param idx 메모 인덱스
|
||||
* @param pdate 날짜 (yyyy-MM-dd)
|
||||
* @param title 제목
|
||||
* @param uid 작성자 ID
|
||||
* @param description 내용 (plain text)
|
||||
* @param description2 내용 (RTF - not used in web)
|
||||
* @param share 공유 여부
|
||||
* @param guid 폴더 GUID
|
||||
* @returns ApiResponse
|
||||
*/
|
||||
public async editNote(
|
||||
idx: number,
|
||||
pdate: string,
|
||||
title: string,
|
||||
uid: string,
|
||||
description: string,
|
||||
description2: string = '',
|
||||
share: boolean = false,
|
||||
guid: string = ''
|
||||
): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Note_Edit(idx, pdate, title, uid, description, description2, share, guid);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('NOTE_EDIT', 'NOTE_EDITED', {
|
||||
idx, pdate, title, uid, description, description2, share, guid
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메모장 삭제
|
||||
* @param idx 메모 인덱스
|
||||
* @returns ApiResponse
|
||||
*/
|
||||
public async deleteNote(idx: number): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Note_Delete(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('NOTE_DELETE', 'NOTE_DELETED', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 목록 조회
|
||||
* @param bidx 게시판 인덱스 (5=패치내역)
|
||||
* @param searchKey 검색어
|
||||
* @returns ApiResponse<BoardItem[]>
|
||||
*/
|
||||
public async getBoardList(bidx: number, searchKey: string = ''): Promise<ApiResponse<BoardItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Board_GetList(bidx, searchKey);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<BoardItem[]>>('BOARD_GET_LIST', 'BOARD_LIST_DATA', { bidx, searchKey });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 게시판 상세 조회
|
||||
* @param idx 게시판 글 인덱스
|
||||
* @returns ApiResponse<BoardItem>
|
||||
*/
|
||||
public async getBoardDetail(idx: number): Promise<ApiResponse<BoardItem>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Board_GetDetail(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<BoardItem>>('BOARD_GET_DETAIL', 'BOARD_DETAIL_DATA', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 발신 내역 조회
|
||||
* @param startDate 시작일 (yyyy-MM-dd)
|
||||
* @param endDate 종료일 (yyyy-MM-dd)
|
||||
* @param searchKey 검색어
|
||||
* @returns ApiResponse<MailItem[]>
|
||||
*/
|
||||
public async getMailList(startDate: string, endDate: string, searchKey: string = ''): Promise<ApiResponse<MailItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Mail_GetList(startDate, endDate, searchKey);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<MailItem[]>>('MAIL_GET_LIST', 'MAIL_LIST_DATA', { startDate, endDate, searchKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const comms = new CommunicationLayer();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, ChevronLeft, ChevronRight, Download } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { JobReportDayItem, HolidayItem } from '@/types';
|
||||
import { HolidayItem } from '@/types';
|
||||
|
||||
interface JobReportDayDialogProps {
|
||||
isOpen: boolean;
|
||||
@@ -32,8 +32,6 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
|
||||
const [dayColumns, setDayColumns] = useState<DayColumn[]>([]);
|
||||
const [userRows, setUserRows] = useState<UserRow[]>([]);
|
||||
const [currentUserId, setCurrentUserId] = useState<string>('');
|
||||
const [currentUserLevel, setCurrentUserLevel] = useState<number>(0);
|
||||
const [authLevel, setAuthLevel] = useState<number>(0);
|
||||
const [canViewAll, setCanViewAll] = useState<boolean>(false);
|
||||
|
||||
// 요일 배열
|
||||
@@ -54,12 +52,10 @@ export function JobReportDayDialog({ isOpen, onClose, initialMonth }: JobReportD
|
||||
const userId = loginStatus.User.Id;
|
||||
const userLevel = loginStatus.User.Level || 0;
|
||||
setCurrentUserId(userId);
|
||||
setCurrentUserLevel(userLevel);
|
||||
|
||||
// 업무일지(jobreport) 권한 가져오기
|
||||
const authResponse = await comms.checkAuth('jobreport', 5);
|
||||
const jobReportAuthLevel = authResponse.EffectiveLevel || 0;
|
||||
setAuthLevel(jobReportAuthLevel);
|
||||
|
||||
// 유효 권한 레벨 = Max(사용자레벨, 권한레벨)
|
||||
const effectiveLevel = Math.max(userLevel, jobReportAuthLevel);
|
||||
|
||||
@@ -115,6 +115,15 @@ const dropdownMenus: DropdownMenuConfig[] = [
|
||||
{ type: 'link', path: '/mail-form', icon: Mail, label: '메일양식' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '문서',
|
||||
icon: FileText,
|
||||
items: [
|
||||
{ type: 'link', path: '/note', icon: FileText, label: '메모장' },
|
||||
{ type: 'link', path: '/patch-list', icon: FileText, label: '패치 내역' },
|
||||
{ type: 'link', path: '/mail-list', icon: Mail, label: '메일 내역' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
function DropdownNavMenu({
|
||||
|
||||
283
Project/frontend/src/components/note/NoteEditModal.tsx
Normal file
283
Project/frontend/src/components/note/NoteEditModal.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Save, User, Calendar } from 'lucide-react';
|
||||
import { NoteItem } from '@/types';
|
||||
import { comms } from '@/communication';
|
||||
|
||||
interface NoteEditModalProps {
|
||||
isOpen: boolean;
|
||||
editingItem: NoteItem | null;
|
||||
processing: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (formData: {
|
||||
pdate: string;
|
||||
title: string;
|
||||
uid: string;
|
||||
description: string;
|
||||
share: boolean;
|
||||
guid: string;
|
||||
}) => void;
|
||||
initialEditMode?: boolean;
|
||||
}
|
||||
|
||||
export function NoteEditModal({
|
||||
isOpen,
|
||||
editingItem,
|
||||
processing,
|
||||
onClose,
|
||||
onSave,
|
||||
initialEditMode = false,
|
||||
}: NoteEditModalProps) {
|
||||
const [pdate, setPdate] = useState('');
|
||||
const [title, setTitle] = useState('');
|
||||
const [uid, setUid] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
const [share, setShare] = useState(false);
|
||||
const [guid, setGuid] = useState('');
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
|
||||
// 현재 로그인 사용자 정보 로드
|
||||
const [currentUserId, setCurrentUserId] = useState('');
|
||||
const [currentUserLevel, setCurrentUserLevel] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const loginStatus = await comms.checkLoginStatus();
|
||||
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
|
||||
setCurrentUserId(loginStatus.User.Id);
|
||||
setCurrentUserLevel(loginStatus.User.Level);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 정보 로드 오류:', error);
|
||||
}
|
||||
};
|
||||
loadUserInfo();
|
||||
}, []);
|
||||
|
||||
// 모달이 열릴 때 폼 데이터 초기화
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
if (editingItem) {
|
||||
// 기존 메모
|
||||
setPdate(editingItem.pdate ? editingItem.pdate.split('T')[0] : '');
|
||||
setTitle(editingItem.title || '');
|
||||
setUid(editingItem.uid || currentUserId);
|
||||
setDescription(editingItem.description || '');
|
||||
setShare(editingItem.share || false);
|
||||
setGuid(editingItem.guid || '');
|
||||
setIsEditMode(initialEditMode);
|
||||
} else {
|
||||
// 신규 메모 - 편집 모드로 시작
|
||||
setPdate(new Date().toISOString().split('T')[0]);
|
||||
setTitle('');
|
||||
setUid(currentUserId);
|
||||
setDescription('');
|
||||
setShare(false);
|
||||
setGuid('');
|
||||
setIsEditMode(true);
|
||||
}
|
||||
}
|
||||
}, [isOpen, editingItem, currentUserId, initialEditMode]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
onSave({ pdate, title, uid, description, share, guid });
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// 권한 체크: 자신의 메모이거나 관리자만 수정 가능
|
||||
const canEdit = !editingItem || editingItem.uid === currentUserId || currentUserLevel >= 5;
|
||||
const canChangeUser = currentUserLevel >= 5;
|
||||
const canChangeShare = !editingItem || editingItem.uid === currentUserId;
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-5xl max-h-[90vh] overflow-hidden border border-white/10">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
{!editingItem ? '새 메모 작성' : '메모 편집'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 본문 - 좌우 레이아웃 */}
|
||||
<form onSubmit={handleSubmit} className="overflow-y-auto max-h-[calc(90vh-140px)]">
|
||||
<div className="flex gap-6 p-6">
|
||||
{/* 좌측 - 정보 영역 */}
|
||||
<div className="w-64 flex-shrink-0 space-y-4">
|
||||
{/* 날짜 */}
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||
<Calendar className="w-4 h-4 mr-2" />
|
||||
날짜 {isEditMode && '*'}
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<input
|
||||
type="date"
|
||||
value={pdate}
|
||||
onChange={(e) => setPdate(e.target.value)}
|
||||
required
|
||||
disabled={!canEdit}
|
||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white text-sm">{pdate}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 작성자 */}
|
||||
<div>
|
||||
<label className="flex items-center text-sm font-medium text-white/70 mb-2">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
작성자 {isEditMode && '*'}
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<input
|
||||
type="text"
|
||||
value={uid}
|
||||
onChange={(e) => setUid(e.target.value)}
|
||||
required
|
||||
disabled={!canEdit || !canChangeUser}
|
||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white text-sm focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="사용자 ID"
|
||||
/>
|
||||
{!canChangeUser && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
관리자만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white text-sm">{uid}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 공유 여부 */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
공유 설정
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="note-share"
|
||||
checked={share}
|
||||
onChange={(e) => setShare(e.target.checked)}
|
||||
disabled={!canEdit || !canChangeShare}
|
||||
className="w-4 h-4 bg-white/20 border border-white/30 rounded focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
/>
|
||||
<label htmlFor="note-share" className="text-sm text-white/70 cursor-pointer">
|
||||
공유
|
||||
</label>
|
||||
</div>
|
||||
{!canChangeShare && (
|
||||
<p className="text-xs text-white/50 mt-1">
|
||||
본인 메모만 변경 가능
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="text-white text-sm">{share ? '공유됨' : '비공유'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{!canEdit && isEditMode && (
|
||||
<div className="bg-red-500/20 border border-red-500/50 rounded-lg p-3 text-red-300 text-xs">
|
||||
타인의 자료는 수정할 수 없습니다.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측 - 제목 및 내용 영역 */}
|
||||
<div className="flex-1 space-y-4">
|
||||
{/* 제목 */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
제목 {isEditMode && '*'}
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
required
|
||||
disabled={!canEdit}
|
||||
className="w-full h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder="메모 제목을 입력하세요"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-white text-lg font-semibold">{title}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="flex-1">
|
||||
<label className="text-sm font-medium text-white/70 mb-2 block">
|
||||
내용
|
||||
</label>
|
||||
{isEditMode ? (
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
disabled={!canEdit}
|
||||
rows={18}
|
||||
className="w-full bg-white/20 border border-white/30 rounded-lg p-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 disabled:opacity-50 disabled:cursor-not-allowed resize-none"
|
||||
placeholder="메모 내용을 입력하세요"
|
||||
/>
|
||||
) : (
|
||||
<div className="bg-white/10 border border-white/20 rounded-lg p-4 text-white whitespace-pre-wrap min-h-[400px]">
|
||||
{description || '내용이 없습니다.'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-end gap-3 px-6 py-4 border-t border-white/10 bg-white/5">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||||
disabled={processing}
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
{isEditMode && canEdit && (
|
||||
<button
|
||||
type="submit"
|
||||
onClick={handleSubmit}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 rounded-lg bg-primary-500 hover:bg-primary-600 text-white transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></div>
|
||||
처리 중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
저장
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
74
Project/frontend/src/components/note/NoteViewModal.tsx
Normal file
74
Project/frontend/src/components/note/NoteViewModal.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X, Trash2 } from 'lucide-react';
|
||||
import { NoteItem } from '@/types';
|
||||
|
||||
interface NoteViewModalProps {
|
||||
isOpen: boolean;
|
||||
note: NoteItem | null;
|
||||
onClose: () => void;
|
||||
onEdit: (note: NoteItem) => void;
|
||||
onDelete?: (note: NoteItem) => void;
|
||||
}
|
||||
|
||||
export function NoteViewModal({ isOpen, note, onClose, onEdit, onDelete }: NoteViewModalProps) {
|
||||
if (!isOpen || !note) return null;
|
||||
|
||||
const handleDelete = () => {
|
||||
if (window.confirm('정말 삭제하시겠습니까?')) {
|
||||
onDelete?.(note);
|
||||
}
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[80vh] overflow-hidden border border-white/10">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-bold text-white">{note.title || '제목 없음'}</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 본문 - 메모 내용만 표시 */}
|
||||
<div className="overflow-y-auto max-h-[calc(80vh-140px)] p-6">
|
||||
<div className="bg-white/10 border border-white/20 rounded-lg p-4 text-white whitespace-pre-wrap min-h-[300px]">
|
||||
{note.description || '내용이 없습니다.'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 버튼 */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-white/10 bg-white/5">
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
className="px-4 py-2 rounded-lg bg-danger-500 hover:bg-danger-600 text-white transition-colors flex items-center gap-2"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
삭제
|
||||
</button>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
onClose();
|
||||
onEdit(note);
|
||||
}}
|
||||
className="px-4 py-2 bg-success-500 hover:bg-success-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
}
|
||||
@@ -9,9 +9,20 @@ import {
|
||||
RefreshCw,
|
||||
ClipboardList,
|
||||
Clock,
|
||||
FileText,
|
||||
Share2,
|
||||
Lock,
|
||||
List,
|
||||
Plus,
|
||||
X,
|
||||
Loader2,
|
||||
Edit2,
|
||||
Trash2,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { TodoModel, PurchaseItem } from '@/types';
|
||||
import { TodoModel, TodoStatus, TodoPriority, PurchaseItem, NoteItem, JobReportItem } from '@/types';
|
||||
import { NoteViewModal } from '@/components/note/NoteViewModal';
|
||||
import { NoteEditModal } from '@/components/note/NoteEditModal';
|
||||
|
||||
interface StatCardProps {
|
||||
title: string;
|
||||
@@ -55,10 +66,31 @@ export function Dashboard() {
|
||||
const [urgentTodos, setUrgentTodos] = useState<TodoModel[]>([]);
|
||||
const [purchaseNRList, setPurchaseNRList] = useState<PurchaseItem[]>([]);
|
||||
const [purchaseCRList, setPurchaseCRList] = useState<PurchaseItem[]>([]);
|
||||
const [recentNotes, setRecentNotes] = useState<NoteItem[]>([]);
|
||||
|
||||
// 모달 상태
|
||||
const [showNRModal, setShowNRModal] = useState(false);
|
||||
const [showCRModal, setShowCRModal] = useState(false);
|
||||
const [showNoteModal, setShowNoteModal] = useState(false);
|
||||
const [showNoteEditModal, setShowNoteEditModal] = useState(false);
|
||||
const [showNoteAddModal, setShowNoteAddModal] = useState(false);
|
||||
const [selectedNote, setSelectedNote] = useState<NoteItem | null>(null);
|
||||
const [editingNote, setEditingNote] = useState<NoteItem | null>(null);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
// 할일 추가 모달 상태
|
||||
const [showTodoAddModal, setShowTodoAddModal] = useState(false);
|
||||
const [showTodoEditModal, setShowTodoEditModal] = useState(false);
|
||||
const [editingTodo, setEditingTodo] = useState<TodoModel | null>(null);
|
||||
const [todoFormData, setTodoFormData] = useState({
|
||||
title: '',
|
||||
remark: '',
|
||||
expire: '',
|
||||
seqno: 0 as TodoPriority,
|
||||
flag: false,
|
||||
request: '',
|
||||
status: '0' as TodoStatus,
|
||||
});
|
||||
|
||||
const loadDashboardData = useCallback(async () => {
|
||||
try {
|
||||
@@ -83,11 +115,13 @@ export function Dashboard() {
|
||||
urgentTodosResponse,
|
||||
allTodosResponse,
|
||||
jobreportResponse,
|
||||
notesResponse,
|
||||
] = await Promise.all([
|
||||
comms.getPurchaseWaitCount(),
|
||||
comms.getUrgentTodos(),
|
||||
comms.getTodos(),
|
||||
comms.getJobReportList(todayStr, todayStr, currentUserId, ''),
|
||||
comms.getNoteList('2000-01-01', todayStr, ''),
|
||||
]);
|
||||
|
||||
setPurchaseNR(purchaseCount.NR);
|
||||
@@ -99,17 +133,22 @@ export function Dashboard() {
|
||||
|
||||
if (allTodosResponse.Success && allTodosResponse.Data) {
|
||||
// 진행, 대기 상태의 할일만 카운트 (보류, 취소 제외)
|
||||
const pendingCount = allTodosResponse.Data.filter(t => t.status === '0' || t.status === '1').length;
|
||||
const pendingCount = allTodosResponse.Data.filter((t: TodoModel) => t.status === '0' || t.status === '1').length;
|
||||
setTodoCount(pendingCount);
|
||||
}
|
||||
|
||||
// 오늘 업무일지 작성시간 계산
|
||||
if (jobreportResponse.Success && jobreportResponse.Data) {
|
||||
const totalHrs = jobreportResponse.Data.reduce((acc, item) => acc + (item.hrs || 0), 0);
|
||||
const totalHrs = jobreportResponse.Data.reduce((acc: number, item: JobReportItem) => acc + (item.hrs || 0), 0);
|
||||
setTodayWorkHrs(totalHrs);
|
||||
} else {
|
||||
setTodayWorkHrs(0);
|
||||
}
|
||||
|
||||
// 최근 메모 목록 (최대 10개)
|
||||
if (notesResponse.Success && notesResponse.Data) {
|
||||
setRecentNotes(notesResponse.Data.slice(0, 10));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('대시보드 데이터 로드 오류:', error);
|
||||
} finally {
|
||||
@@ -199,23 +238,263 @@ export function Dashboard() {
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-white">오늘의 현황</h2>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="flex items-center space-x-2 px-4 py-2 glass-effect rounded-lg text-white/70 hover:text-white transition-colors"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
<span>새로고침</span>
|
||||
</button>
|
||||
</div>
|
||||
const handleNoteClick = async (note: NoteItem) => {
|
||||
try {
|
||||
const response = await comms.getNoteDetail(note.idx);
|
||||
if (response.Success && response.Data) {
|
||||
setSelectedNote(response.Data);
|
||||
setShowNoteModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메모 조회 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
const handleNoteAdd = () => {
|
||||
setEditingNote(null);
|
||||
setShowNoteAddModal(true);
|
||||
};
|
||||
|
||||
const handleNoteDelete = async (note: NoteItem) => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.deleteNote(note.idx);
|
||||
if (response.Success) {
|
||||
setShowNoteModal(false);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 오류:', error);
|
||||
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTodoAdd = () => {
|
||||
setTodoFormData({
|
||||
title: '',
|
||||
remark: '',
|
||||
expire: '',
|
||||
seqno: 0,
|
||||
flag: false,
|
||||
request: '',
|
||||
status: '0',
|
||||
});
|
||||
setShowTodoAddModal(true);
|
||||
};
|
||||
|
||||
const handleTodoEdit = (todo: TodoModel) => {
|
||||
setEditingTodo(todo);
|
||||
setTodoFormData({
|
||||
title: todo.title || '',
|
||||
remark: todo.remark,
|
||||
expire: todo.expire || '',
|
||||
seqno: todo.seqno as TodoPriority,
|
||||
flag: todo.flag,
|
||||
request: todo.request || '',
|
||||
status: todo.status as TodoStatus,
|
||||
});
|
||||
setShowTodoEditModal(true);
|
||||
};
|
||||
|
||||
const handleTodoSave = async () => {
|
||||
if (!todoFormData.remark.trim()) {
|
||||
alert('할일 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.createTodo(
|
||||
todoFormData.title,
|
||||
todoFormData.remark,
|
||||
todoFormData.expire || null,
|
||||
todoFormData.seqno,
|
||||
todoFormData.flag,
|
||||
todoFormData.request || null,
|
||||
todoFormData.status
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowTodoAddModal(false);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '할일 추가에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 추가 오류:', error);
|
||||
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTodoUpdate = async () => {
|
||||
if (!editingTodo || !todoFormData.remark.trim()) {
|
||||
alert('할일 내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.updateTodo(
|
||||
editingTodo.idx,
|
||||
todoFormData.title,
|
||||
todoFormData.remark,
|
||||
todoFormData.expire || null,
|
||||
todoFormData.seqno,
|
||||
todoFormData.flag,
|
||||
todoFormData.request || null,
|
||||
todoFormData.status
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowTodoEditModal(false);
|
||||
setEditingTodo(null);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '할일 수정에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 수정 오류:', error);
|
||||
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTodoDelete = async () => {
|
||||
if (!editingTodo) return;
|
||||
if (!confirm('정말로 이 할일을 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.deleteTodo(editingTodo.idx);
|
||||
if (response.Success) {
|
||||
setShowTodoEditModal(false);
|
||||
setEditingTodo(null);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '할일 삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 삭제 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTodoComplete = async () => {
|
||||
if (!editingTodo) return;
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.updateTodo(
|
||||
editingTodo.idx,
|
||||
editingTodo.title,
|
||||
editingTodo.remark,
|
||||
editingTodo.expire,
|
||||
editingTodo.seqno,
|
||||
editingTodo.flag,
|
||||
editingTodo.request,
|
||||
'5' // 완료 상태
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowTodoEditModal(false);
|
||||
setEditingTodo(null);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '할일 완료 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('할일 완료 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleNoteSave = async (formData: {
|
||||
pdate: string;
|
||||
title: string;
|
||||
uid: string;
|
||||
description: string;
|
||||
share: boolean;
|
||||
guid: string;
|
||||
}) => {
|
||||
if (!formData.pdate) {
|
||||
alert('날짜를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!formData.title.trim()) {
|
||||
alert('제목을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = editingNote
|
||||
? await comms.editNote(
|
||||
editingNote.idx,
|
||||
formData.pdate,
|
||||
formData.title,
|
||||
formData.uid,
|
||||
formData.description,
|
||||
'',
|
||||
formData.share,
|
||||
formData.guid
|
||||
)
|
||||
: await comms.addNote(
|
||||
formData.pdate,
|
||||
formData.title,
|
||||
formData.uid,
|
||||
formData.description,
|
||||
'',
|
||||
formData.share,
|
||||
formData.guid
|
||||
);
|
||||
|
||||
if (response.Success) {
|
||||
setShowNoteEditModal(false);
|
||||
setShowNoteAddModal(false);
|
||||
loadDashboardData();
|
||||
} else {
|
||||
alert(response.Message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 오류:', error);
|
||||
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex gap-6 animate-fade-in">
|
||||
{/* 메인 컨텐츠 */}
|
||||
<div className="flex-1 space-y-6">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-2xl font-bold text-white">오늘의 현황</h2>
|
||||
<button
|
||||
onClick={handleRefresh}
|
||||
disabled={refreshing}
|
||||
className="flex items-center space-x-2 px-4 py-2 glass-effect rounded-lg text-white/70 hover:text-white transition-colors"
|
||||
>
|
||||
<RefreshCw className={`w-4 h-4 ${refreshing ? 'animate-spin' : ''}`} />
|
||||
<span>새로고침</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 통계 카드 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<StatCard
|
||||
title="구매요청 (NR)"
|
||||
value={purchaseNR}
|
||||
@@ -244,21 +523,31 @@ export function Dashboard() {
|
||||
color="text-cyan-400"
|
||||
onClick={() => navigate('/jobreport')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 급한 할일 목록 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
{/* 할일 목록 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||
<AlertTriangle className="w-5 h-5 mr-2 text-warning-400" />
|
||||
급한 할일
|
||||
할일
|
||||
</h3>
|
||||
<button
|
||||
onClick={() => navigate('/todo')}
|
||||
className="text-sm text-primary-400 hover:text-primary-300 transition-colors"
|
||||
>
|
||||
전체보기
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleTodoAdd}
|
||||
className="p-1.5 rounded-lg bg-primary-500/20 text-primary-400 hover:bg-primary-500/30 transition-colors"
|
||||
title="할일 추가"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/todo')}
|
||||
className="p-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors"
|
||||
title="전체보기"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10">
|
||||
@@ -267,7 +556,7 @@ export function Dashboard() {
|
||||
<div
|
||||
key={todo.idx}
|
||||
className="px-6 py-4 hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => navigate('/todo')}
|
||||
onClick={() => handleTodoEdit(todo)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
@@ -302,25 +591,436 @@ export function Dashboard() {
|
||||
) : (
|
||||
<div className="px-6 py-8 text-center text-white/50">
|
||||
<CheckCircle className="w-12 h-12 mx-auto mb-3 text-success-400/50" />
|
||||
<p>급한 할일이 없습니다</p>
|
||||
<p>할일이 없습니다</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* NR 모달 */}
|
||||
{showNRModal && (
|
||||
<Modal title="구매요청 (NR) 목록" onClose={() => setShowNRModal(false)}>
|
||||
<PurchaseTable data={purchaseNRList} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* CR 모달 */}
|
||||
{showCRModal && (
|
||||
<Modal title="구매요청 (CR) 목록" onClose={() => setShowCRModal(false)}>
|
||||
<PurchaseTable data={purchaseCRList} />
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* 메모 보기 모달 */}
|
||||
<NoteViewModal
|
||||
isOpen={showNoteModal}
|
||||
note={selectedNote}
|
||||
onClose={() => setShowNoteModal(false)}
|
||||
onEdit={(note) => {
|
||||
setShowNoteModal(false);
|
||||
setEditingNote(note);
|
||||
setShowNoteEditModal(true);
|
||||
}}
|
||||
onDelete={handleNoteDelete}
|
||||
/>
|
||||
|
||||
{/* 메모 편집 모달 */}
|
||||
<NoteEditModal
|
||||
isOpen={showNoteEditModal}
|
||||
editingItem={editingNote}
|
||||
processing={processing}
|
||||
onClose={() => setShowNoteEditModal(false)}
|
||||
onSave={handleNoteSave}
|
||||
initialEditMode={true}
|
||||
/>
|
||||
|
||||
{/* 메모 추가 모달 */}
|
||||
<NoteEditModal
|
||||
isOpen={showNoteAddModal}
|
||||
editingItem={null}
|
||||
processing={processing}
|
||||
onClose={() => setShowNoteAddModal(false)}
|
||||
onSave={handleNoteSave}
|
||||
initialEditMode={true}
|
||||
/>
|
||||
|
||||
{/* 할일 추가 모달 */}
|
||||
{showTodoAddModal && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" onClick={() => setShowTodoAddModal(false)}>
|
||||
<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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<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">
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
할일 추가
|
||||
</h2>
|
||||
<button onClick={() => setShowTodoAddModal(false)} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="p-6 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={todoFormData.title}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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={todoFormData.expire}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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={todoFormData.remark}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">요청자</label>
|
||||
<input
|
||||
type="text"
|
||||
value={todoFormData.request}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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: '대기' },
|
||||
{ value: '1', label: '진행' },
|
||||
{ value: '3', label: '보류' },
|
||||
{ value: '2', label: '취소' },
|
||||
{ value: '5', label: '완료' },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${
|
||||
todoFormData.status === option.value
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">중요도</label>
|
||||
<select
|
||||
value={todoFormData.seqno}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, seqno: parseInt(e.target.value) as TodoPriority }))}
|
||||
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 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todoFormData.flag}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, flag: e.target.checked }))}
|
||||
className="mr-2 text-primary-500 focus:ring-primary-400 focus:ring-offset-0 rounded"
|
||||
/>
|
||||
플래그 (상단 고정)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-end space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTodoAddModal(false)}
|
||||
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodoSave}
|
||||
disabled={processing}
|
||||
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
추가
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 할일 수정 모달 */}
|
||||
{showTodoEditModal && editingTodo && (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm z-50" onClick={() => setShowTodoEditModal(false)}>
|
||||
<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"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<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">
|
||||
<Edit2 className="w-5 h-5 mr-2" />
|
||||
할일 수정
|
||||
</h2>
|
||||
<button onClick={() => setShowTodoEditModal(false)} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div className="p-6 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={todoFormData.title}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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={todoFormData.expire}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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={todoFormData.remark}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">요청자</label>
|
||||
<input
|
||||
type="text"
|
||||
value={todoFormData.request}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, 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: '대기' },
|
||||
{ value: '1', label: '진행' },
|
||||
{ value: '3', label: '보류' },
|
||||
{ value: '2', label: '취소' },
|
||||
{ value: '5', label: '완료' },
|
||||
].map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setTodoFormData(prev => ({ ...prev, status: option.value as TodoStatus }))}
|
||||
className={`px-3 py-1 rounded-lg text-xs font-medium border transition-all ${
|
||||
todoFormData.status === option.value
|
||||
? getStatusClass(option.value)
|
||||
: 'bg-white/10 text-white/50 border-white/20 hover:bg-white/20'
|
||||
}`}
|
||||
>
|
||||
{option.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-white/70 text-sm font-medium mb-2">중요도</label>
|
||||
<select
|
||||
value={todoFormData.seqno}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, seqno: parseInt(e.target.value) as TodoPriority }))}
|
||||
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 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todoFormData.flag}
|
||||
onChange={(e) => setTodoFormData(prev => ({ ...prev, flag: e.target.checked }))}
|
||||
className="mr-2 text-primary-500 focus:ring-primary-400 focus:ring-offset-0 rounded"
|
||||
/>
|
||||
플래그 (상단 고정)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-between">
|
||||
{/* 왼쪽: 삭제 버튼 */}
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodoDelete}
|
||||
disabled={processing}
|
||||
className="bg-danger-500 hover:bg-danger-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
삭제
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 오른쪽: 취소, 완료, 수정 버튼 */}
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowTodoEditModal(false)}
|
||||
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
{editingTodo.status !== '5' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodoComplete}
|
||||
disabled={processing}
|
||||
className="bg-success-500 hover:bg-success-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
완료 처리
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodoUpdate}
|
||||
disabled={processing}
|
||||
className="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors flex items-center disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Edit2 className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
수정
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* NR 모달 */}
|
||||
{showNRModal && (
|
||||
<Modal title="구매요청 (NR) 목록" onClose={() => setShowNRModal(false)}>
|
||||
<PurchaseTable data={purchaseNRList} />
|
||||
</Modal>
|
||||
)}
|
||||
{/* 우측 사이드바 - 메모 리스트 */}
|
||||
<div className="w-60 space-y-4">
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
|
||||
<h3 className="text-base font-semibold text-white flex items-center">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
최근 메모
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleNoteAdd}
|
||||
className="p-1.5 rounded-lg bg-primary-500/20 text-primary-400 hover:bg-primary-500/30 transition-colors"
|
||||
title="메모 추가"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => navigate('/note')}
|
||||
className="p-1.5 rounded-lg bg-white/10 text-white/70 hover:bg-white/20 transition-colors"
|
||||
title="전체보기"
|
||||
>
|
||||
<List className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CR 모달 */}
|
||||
{showCRModal && (
|
||||
<Modal title="구매요청 (CR) 목록" onClose={() => setShowCRModal(false)}>
|
||||
<PurchaseTable data={purchaseCRList} />
|
||||
</Modal>
|
||||
)}
|
||||
<div className="divide-y divide-white/10 max-h-[calc(100vh-200px)] overflow-y-auto">
|
||||
{recentNotes.length > 0 ? (
|
||||
recentNotes.map((note) => (
|
||||
<div
|
||||
key={note.idx}
|
||||
className="px-4 py-2.5 hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
onClick={() => handleNoteClick(note)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{note.share ? (
|
||||
<Share2 className="w-3 h-3 text-green-400 flex-shrink-0" />
|
||||
) : (
|
||||
<Lock className="w-3 h-3 text-blue-400 flex-shrink-0" />
|
||||
)}
|
||||
<p className="text-white text-sm truncate flex-1">
|
||||
{(note.title || '제목 없음').length > 15 ? `${(note.title || '제목 없음').substring(0, 15)}...` : (note.title || '제목 없음')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="px-4 py-8 text-center text-white/50">
|
||||
<FileText className="w-10 h-10 mx-auto mb-2 text-white/30" />
|
||||
<p className="text-sm">등록된 메모가 없습니다</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,12 +117,12 @@ export function Jobreport() {
|
||||
initialize();
|
||||
}, []);
|
||||
|
||||
// 초기화 완료 후 조회 실행
|
||||
// 초기화 완료 후 조회 실행 (최초 1회만)
|
||||
useEffect(() => {
|
||||
if (initialized && startDate && endDate && selectedUser) {
|
||||
handleSearchAndLoadToday();
|
||||
}
|
||||
}, [initialized, startDate, endDate, selectedUser]);
|
||||
}, [initialized]); // startDate, endDate, selectedUser 의존성 제거 (날짜 변경 시 자동 조회 방지)
|
||||
|
||||
// 검색 + 오늘 근무시간 로드 (순차 실행)
|
||||
const handleSearchAndLoadToday = async () => {
|
||||
@@ -373,6 +373,34 @@ export function Jobreport() {
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
// 빠른 날짜 선택 함수들
|
||||
const setToday = () => {
|
||||
const today = new Date();
|
||||
setStartDate(formatDateLocal(today));
|
||||
};
|
||||
|
||||
const setThisMonth = () => {
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
setStartDate(formatDateLocal(startOfMonth));
|
||||
setEndDate(formatDateLocal(endOfMonth));
|
||||
};
|
||||
|
||||
const setYesterday = () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
setStartDate(formatDateLocal(yesterday));
|
||||
};
|
||||
|
||||
const setLastMonth = () => {
|
||||
const now = new Date();
|
||||
const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
const lastMonthEnd = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
setStartDate(formatDateLocal(lastMonthStart));
|
||||
setEndDate(formatDateLocal(lastMonthEnd));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 검색 필터 */}
|
||||
@@ -381,6 +409,38 @@ export function Jobreport() {
|
||||
{/* 좌측: 필터 영역 */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* 빠른 날짜 선택 버튼 */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
onClick={setToday}
|
||||
className="h-8 bg-white/10 hover:bg-white/20 text-white text-xs px-3 rounded-lg transition-colors whitespace-nowrap"
|
||||
title="오늘 날짜로 설정"
|
||||
>
|
||||
오늘
|
||||
</button>
|
||||
<button
|
||||
onClick={setYesterday}
|
||||
className="h-8 bg-white/10 hover:bg-white/20 text-white text-xs px-3 rounded-lg transition-colors whitespace-nowrap"
|
||||
title="어제 날짜로 설정"
|
||||
>
|
||||
어제
|
||||
</button>
|
||||
<button
|
||||
onClick={setThisMonth}
|
||||
className="h-8 bg-white/10 hover:bg-white/20 text-white text-xs px-3 rounded-lg transition-colors whitespace-nowrap"
|
||||
title="이번 달 1일부터 말일까지"
|
||||
>
|
||||
이번달
|
||||
</button>
|
||||
<button
|
||||
onClick={setLastMonth}
|
||||
className="h-8 bg-white/10 hover:bg-white/20 text-white text-xs px-3 rounded-lg transition-colors whitespace-nowrap"
|
||||
title="저번달 1일부터 말일까지"
|
||||
>
|
||||
저번달
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 필터 입력 영역: 2행 2열 */}
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
|
||||
{/* 1행: 시작일, 담당자 */}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
Calendar,
|
||||
Search,
|
||||
Trash2,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
@@ -23,7 +22,7 @@ import { KuntaeEditModal, KuntaeFormData } from '@/components/kuntae/KuntaeEditM
|
||||
export function Kuntae() {
|
||||
const [kuntaeList, setKuntaeList] = useState<KuntaeModel[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [_processing, setProcessing] = useState(false);
|
||||
|
||||
// 검색 조건
|
||||
const [startDate, setStartDate] = useState('');
|
||||
@@ -441,7 +440,7 @@ export function Kuntae() {
|
||||
{item.contents || '-'}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white text-sm">
|
||||
{item.UserName || item.uname || item.uid}
|
||||
{item.UserName || item.uid}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white/50 text-xs">
|
||||
{item.extcate ? `${item.extcate}` : '-'}
|
||||
|
||||
288
Project/frontend/src/pages/MailList.tsx
Normal file
288
Project/frontend/src/pages/MailList.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Mail, Search, RefreshCw, Calendar } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { MailItem, UserInfo } from '@/types';
|
||||
|
||||
export function MailList() {
|
||||
const [mailList, setMailList] = useState<MailItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<MailItem | null>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<UserInfo | null>(null);
|
||||
|
||||
const formatDateLocal = (date: Date) => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 로그인 정보 로드
|
||||
const loadLoginInfo = async () => {
|
||||
try {
|
||||
const response = await comms.checkLoginStatus();
|
||||
if (response.Success && response.IsLoggedIn && response.User) {
|
||||
setCurrentUser(response.User);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 정보 로드 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadLoginInfo();
|
||||
|
||||
const now = new Date();
|
||||
const tenDaysAgo = new Date();
|
||||
tenDaysAgo.setDate(now.getDate() - 10);
|
||||
|
||||
const start = formatDateLocal(tenDaysAgo);
|
||||
const end = formatDateLocal(now);
|
||||
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
|
||||
// 날짜 설정 후 바로 데이터 로드
|
||||
setTimeout(() => {
|
||||
loadDataWithDates(start, end);
|
||||
}, 100);
|
||||
}, []);
|
||||
|
||||
const loadDataWithDates = async (start: string, end: string) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('메일내역 조회:', { start, end, searchKey });
|
||||
const response = await comms.getMailList(start, end, searchKey);
|
||||
console.log('메일내역 응답:', response);
|
||||
if (response.Success && response.Data) {
|
||||
setMailList(response.Data);
|
||||
} else {
|
||||
console.warn('메일내역 없음:', response.Message);
|
||||
setMailList([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메일내역 로드 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
if (!startDate || !endDate) return;
|
||||
await loadDataWithDates(startDate, endDate);
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
if (new Date(startDate) > new Date(endDate)) {
|
||||
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleRowClick = (item: MailItem) => {
|
||||
// 레벨 9 이상(개발자)만 상세보기 가능
|
||||
if (!currentUser || currentUser.Level < 9) {
|
||||
return;
|
||||
}
|
||||
setSelectedItem(item);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const yy = String(date.getFullYear()).slice(-2);
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
const hh = String(date.getHours()).padStart(2, '0');
|
||||
const mi = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${yy}.${mm}.${dd} ${hh}:${mi}`;
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 검색 필터 */}
|
||||
<div className="glass-effect rounded-2xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">기간</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
<span className="text-white/70">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">검색어</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="제목, 발신자, 수신자 등"
|
||||
className="flex-1 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={loading}
|
||||
className="h-10 bg-primary-500 hover:bg-primary-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메일 내역 목록 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||
<Mail className="w-5 h-5 mr-2" />
|
||||
메일 발신 내역
|
||||
</h3>
|
||||
<span className="text-white/60 text-sm">{mailList.length}건</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||||
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : mailList.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<Mail className="w-12 h-12 mx-auto mb-3 text-white/30" />
|
||||
<p className="text-white/50">조회된 데이터가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
mailList.map((item) => (
|
||||
<div
|
||||
key={item.idx}
|
||||
className={`px-6 py-4 transition-colors ${currentUser && currentUser.Level >= 9 ? 'hover:bg-white/5 cursor-pointer' : 'cursor-default'}`}
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{item.cate && (
|
||||
<span className="px-2 py-0.5 bg-primary-500/20 text-primary-400 text-xs rounded">
|
||||
{item.cate}
|
||||
</span>
|
||||
)}
|
||||
{item.project && (
|
||||
<span className="px-2 py-0.5 bg-white/10 text-white/70 text-xs rounded">
|
||||
{item.project}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-white font-medium mb-1">{item.subject}</h4>
|
||||
<div className="flex items-center gap-4 text-white/60 text-sm">
|
||||
<div>발신: {item.fromlist}</div>
|
||||
<div>수신: {item.tolist}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<div className="flex items-center text-white/60 text-xs">
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
{formatDate(item.wdate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세 모달 */}
|
||||
{showModal && selectedItem && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden border border-white/10">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItem.cate && (
|
||||
<span className="px-2 py-1 bg-primary-500/20 text-primary-400 text-sm rounded">
|
||||
{selectedItem.cate}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-xl font-bold text-white ml-2">{selectedItem.subject}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<span className="text-2xl">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-b border-white/10 space-y-2 text-sm">
|
||||
<div className="flex items-start gap-2 text-white/70">
|
||||
<span className="font-medium w-16">발신:</span>
|
||||
<span className="text-white">{selectedItem.fromlist}</span>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 text-white/70">
|
||||
<span className="font-medium w-16">수신:</span>
|
||||
<span className="text-white">{selectedItem.tolist}</span>
|
||||
</div>
|
||||
{selectedItem.cclist && (
|
||||
<div className="flex items-start gap-2 text-white/70">
|
||||
<span className="font-medium w-16">참조:</span>
|
||||
<span className="text-white">{selectedItem.cclist}</span>
|
||||
</div>
|
||||
)}
|
||||
{selectedItem.bcclist && (
|
||||
<div className="flex items-start gap-2 text-white/70">
|
||||
<span className="font-medium w-16">숨은참조:</span>
|
||||
<span className="text-white">{selectedItem.bcclist}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-2 text-white/60">
|
||||
<Calendar className="w-4 h-4" />
|
||||
{formatDate(selectedItem.wdate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-280px)] p-6">
|
||||
<div
|
||||
className="prose prose-invert max-w-none"
|
||||
dangerouslySetInnerHTML={{ __html: selectedItem.htmlbody }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t border-white/10 bg-white/5">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
452
Project/frontend/src/pages/Note.tsx
Normal file
452
Project/frontend/src/pages/Note.tsx
Normal file
@@ -0,0 +1,452 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Edit,
|
||||
Trash2,
|
||||
Share2,
|
||||
Lock,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { NoteItem } from '@/types';
|
||||
import { NoteEditModal } from '@/components/note/NoteEditModal';
|
||||
import { NoteViewModal } from '@/components/note/NoteViewModal';
|
||||
|
||||
export function Note() {
|
||||
const [noteList, setNoteList] = useState<NoteItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
// 검색 조건
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
|
||||
// 모달 상태
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showViewModal, setShowViewModal] = useState(false);
|
||||
const [editingItem, setEditingItem] = useState<NoteItem | null>(null);
|
||||
const [selectedNote, setSelectedNote] = useState<NoteItem | null>(null);
|
||||
|
||||
// 페이징 상태
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 10;
|
||||
|
||||
// 날짜 포맷 헬퍼 함수 (로컬 시간 기준)
|
||||
const formatDateLocal = (date: Date) => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
// 초기화 완료 플래그
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
// 날짜 초기화
|
||||
useEffect(() => {
|
||||
const now = new Date();
|
||||
// 2000년부터 현재까지 데이터 조회
|
||||
const startOfPeriod = new Date(2000, 0, 1);
|
||||
|
||||
const sd = formatDateLocal(startOfPeriod);
|
||||
const ed = formatDateLocal(now);
|
||||
|
||||
setStartDate(sd);
|
||||
setEndDate(ed);
|
||||
|
||||
// 초기화 완료 표시
|
||||
setInitialized(true);
|
||||
}, []);
|
||||
|
||||
// 초기화 완료 후 조회 실행 (최초 1회만)
|
||||
useEffect(() => {
|
||||
if (initialized && startDate && endDate) {
|
||||
handleSearch();
|
||||
}
|
||||
}, [initialized]);
|
||||
|
||||
// 데이터 로드
|
||||
const loadData = useCallback(async () => {
|
||||
if (!startDate || !endDate) return;
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('메모장 조회 요청:', { startDate, endDate });
|
||||
const response = await comms.getNoteList(startDate, endDate, '');
|
||||
console.log('메모장 조회 응답:', response);
|
||||
if (response.Success && response.Data) {
|
||||
console.log('메모장 데이터 개수:', response.Data.length);
|
||||
setNoteList(response.Data);
|
||||
} else {
|
||||
console.log('메모장 조회 실패 또는 데이터 없음:', response);
|
||||
setNoteList([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메모장 목록 로드 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [startDate, endDate]);
|
||||
|
||||
// 검색
|
||||
const handleSearch = async () => {
|
||||
if (new Date(startDate) > new Date(endDate)) {
|
||||
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
await loadData();
|
||||
};
|
||||
|
||||
// 새 메모 추가 모달
|
||||
const openAddModal = () => {
|
||||
setEditingItem(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
// 메모 클릭 (보기 모달)
|
||||
const handleNoteClick = async (item: NoteItem) => {
|
||||
try {
|
||||
const response = await comms.getNoteDetail(item.idx);
|
||||
if (response.Success && response.Data) {
|
||||
setSelectedNote(response.Data);
|
||||
setShowViewModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메모 조회 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 편집 모달
|
||||
const openEditModal = async (item: NoteItem) => {
|
||||
try {
|
||||
const response = await comms.getNoteDetail(item.idx);
|
||||
if (response.Success && response.Data) {
|
||||
setEditingItem(response.Data);
|
||||
setShowModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메모 조회 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async (formData: {
|
||||
pdate: string;
|
||||
title: string;
|
||||
uid: string;
|
||||
description: string;
|
||||
share: boolean;
|
||||
guid: string;
|
||||
}) => {
|
||||
if (!formData.pdate) {
|
||||
alert('날짜를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!formData.title.trim()) {
|
||||
alert('제목을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
let response;
|
||||
if (editingItem) {
|
||||
response = await comms.editNote(
|
||||
editingItem.idx,
|
||||
formData.pdate,
|
||||
formData.title,
|
||||
formData.uid,
|
||||
formData.description,
|
||||
'',
|
||||
formData.share,
|
||||
formData.guid
|
||||
);
|
||||
} else {
|
||||
response = await comms.addNote(
|
||||
formData.pdate,
|
||||
formData.title,
|
||||
formData.uid,
|
||||
formData.description,
|
||||
'',
|
||||
formData.share,
|
||||
formData.guid
|
||||
);
|
||||
}
|
||||
|
||||
if (response.Success) {
|
||||
setShowModal(false);
|
||||
loadData();
|
||||
} else {
|
||||
alert(response.Message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 오류:', error);
|
||||
alert('서버 연결에 실패했습니다: ' + (error instanceof Error ? error.message : String(error)));
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제
|
||||
const handleDelete = async (idx: number) => {
|
||||
if (!confirm('정말로 이 메모를 삭제하시겠습니까?')) return;
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
const response = await comms.deleteNote(idx);
|
||||
if (response.Success) {
|
||||
alert('삭제되었습니다.');
|
||||
loadData();
|
||||
} else {
|
||||
alert(response.Message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 오류:', error);
|
||||
alert('서버 연결에 실패했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 날짜 포맷 (YY.MM.DD)
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const yy = String(date.getFullYear()).slice(-2);
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
return `${yy}.${mm}.${dd}`;
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
// 필터링된 목록 (검색어 적용)
|
||||
const filteredList = noteList.filter(item => {
|
||||
if (!searchKey.trim()) return true;
|
||||
const search = searchKey.toLowerCase();
|
||||
return (
|
||||
item.title?.toLowerCase().includes(search) ||
|
||||
item.description?.toLowerCase().includes(search) ||
|
||||
item.uid?.toLowerCase().includes(search)
|
||||
);
|
||||
});
|
||||
|
||||
// 페이징 계산
|
||||
const totalPages = Math.ceil(filteredList.length / pageSize);
|
||||
const paginatedList = filteredList.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize
|
||||
);
|
||||
|
||||
// 검색 시 페이지 초기화
|
||||
const handleSearchWithReset = () => {
|
||||
setCurrentPage(1);
|
||||
handleSearch();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 검색 필터 */}
|
||||
<div className="glass-effect rounded-2xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">기간</label>
|
||||
<input
|
||||
type="date"
|
||||
value={startDate}
|
||||
onChange={(e) => setStartDate(e.target.value)}
|
||||
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
<span className="text-white/70">~</span>
|
||||
<input
|
||||
type="date"
|
||||
value={endDate}
|
||||
onChange={(e) => setEndDate(e.target.value)}
|
||||
className="w-36 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">검색어</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearchWithReset()}
|
||||
placeholder="제목, 내용, 작성자 등"
|
||||
className="w-60 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSearchWithReset}
|
||||
disabled={loading}
|
||||
className="h-10 bg-primary-500 hover:bg-primary-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
조회
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={openAddModal}
|
||||
className="h-10 bg-success-500 hover:bg-success-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
새 메모
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 메모 리스트 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
메모장 목록
|
||||
</h3>
|
||||
<span className="text-white/60 text-sm">{filteredList.length}건</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||||
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : filteredList.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-white/30" />
|
||||
<p className="text-white/50">조회된 데이터가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
paginatedList.map((item) => (
|
||||
<div
|
||||
key={item.idx}
|
||||
className="px-6 py-3 hover:bg-white/5 transition-colors cursor-pointer group"
|
||||
onClick={() => handleNoteClick(item)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
{item.share ? (
|
||||
<Share2 className="w-4 h-4 text-green-400 flex-shrink-0" />
|
||||
) : (
|
||||
<Lock className="w-4 h-4 text-blue-400 flex-shrink-0" />
|
||||
)}
|
||||
<p className="text-white text-sm font-medium truncate flex-1">
|
||||
{(item.title || '제목 없음').length > 15 ? `${(item.title || '제목 없음').substring(0, 15)}...` : (item.title || '제목 없음')}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 flex-shrink-0">
|
||||
<span className="text-white/60 text-xs">{item.uid || '-'}</span>
|
||||
<span className="text-white/60 text-xs">{formatDate(item.pdate)}</span>
|
||||
<span className="text-white/50 text-xs">조회 {item.viewcount || 0}</span>
|
||||
<div className="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openEditModal(item);
|
||||
}}
|
||||
className="text-white/40 hover:text-primary-400 transition-colors"
|
||||
title="편집"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(item.idx);
|
||||
}}
|
||||
className="text-white/40 hover:text-red-400 transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이징 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="px-6 py-4 border-t border-white/10 flex items-center justify-between">
|
||||
<div className="text-white/50 text-sm">
|
||||
총 {filteredList.length}건 중 {(currentPage - 1) * pageSize + 1}-{Math.min(currentPage * pageSize, filteredList.length)}건
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => setCurrentPage(1)}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<span className="text-white/70 px-3">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage(totalPages)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="px-3 py-1 rounded bg-white/10 text-white/70 hover:bg-white/20 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 메모 보기 모달 */}
|
||||
<NoteViewModal
|
||||
isOpen={showViewModal}
|
||||
note={selectedNote}
|
||||
onClose={() => setShowViewModal(false)}
|
||||
onEdit={(note) => {
|
||||
setShowViewModal(false);
|
||||
setEditingItem(note);
|
||||
setShowModal(true);
|
||||
}}
|
||||
onDelete={(note) => {
|
||||
setShowViewModal(false);
|
||||
handleDelete(note.idx);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 추가/수정 모달 */}
|
||||
<NoteEditModal
|
||||
isOpen={showModal}
|
||||
editingItem={editingItem}
|
||||
processing={processing}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
219
Project/frontend/src/pages/PatchList.tsx
Normal file
219
Project/frontend/src/pages/PatchList.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FileText, Search, RefreshCw, Calendar, User } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { BoardItem } from '@/types';
|
||||
|
||||
export function PatchList() {
|
||||
const [boardList, setBoardList] = useState<BoardItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<BoardItem | null>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('패치내역 조회:', { bidx: 5, searchKey });
|
||||
const response = await comms.getBoardList(5, searchKey); // bidx=5: 패치내역
|
||||
console.log('패치내역 응답:', response);
|
||||
if (response.Success && response.Data) {
|
||||
setBoardList(response.Data);
|
||||
} else {
|
||||
console.warn('패치내역 없음:', response.Message);
|
||||
setBoardList([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('패치내역 로드 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
const handleRowClick = async (item: BoardItem) => {
|
||||
try {
|
||||
const response = await comms.getBoardDetail(item.idx);
|
||||
if (response.Success && response.Data) {
|
||||
setSelectedItem(response.Data);
|
||||
setShowModal(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('상세 조회 오류:', error);
|
||||
alert('데이터를 불러오는 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const formatDate = (dateStr: string | null) => {
|
||||
if (!dateStr) return '-';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
const yy = String(date.getFullYear()).slice(-2);
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
return `${yy}.${mm}.${dd}`;
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
{/* 검색 필터 */}
|
||||
<div className="glass-effect rounded-2xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<label className="text-white/70 text-sm font-medium whitespace-nowrap">검색어</label>
|
||||
<input
|
||||
type="text"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
placeholder="제목, 내용, 작성자 등"
|
||||
className="flex-1 h-10 bg-white/20 border border-white/30 rounded-lg px-3 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={loading}
|
||||
className="h-10 bg-primary-500 hover:bg-primary-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center disabled:opacity-50"
|
||||
>
|
||||
{loading ? (
|
||||
<RefreshCw className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Search className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
조회
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 패치내역 목록 */}
|
||||
<div className="glass-effect rounded-2xl overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-white flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
패치 내역
|
||||
</h3>
|
||||
<span className="text-white/60 text-sm">{boardList.length}건</span>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-white/10 max-h-[calc(100vh-300px)] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
<RefreshCw className="w-5 h-5 mr-2 animate-spin text-white/50" />
|
||||
<span className="text-white/50">데이터를 불러오는 중...</span>
|
||||
</div>
|
||||
</div>
|
||||
) : boardList.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<FileText className="w-12 h-12 mx-auto mb-3 text-white/30" />
|
||||
<p className="text-white/50">조회된 데이터가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
boardList.map((item) => (
|
||||
<div
|
||||
key={item.idx}
|
||||
className="px-6 py-4 hover:bg-white/5 transition-colors cursor-pointer"
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
{item.header && (
|
||||
<span className="px-2 py-0.5 bg-primary-500/20 text-primary-400 text-xs rounded">
|
||||
{item.header}
|
||||
</span>
|
||||
)}
|
||||
{item.cate && (
|
||||
<span className="px-2 py-0.5 bg-white/10 text-white/70 text-xs rounded">
|
||||
{item.cate}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="text-white font-medium mb-1">{item.title}</h4>
|
||||
<p className="text-white/60 text-sm line-clamp-1">{item.contents}</p>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1 flex-shrink-0">
|
||||
<div className="flex items-center text-white/60 text-xs">
|
||||
<User className="w-3 h-3 mr-1" />
|
||||
{item.wuid_name || item.wuid}
|
||||
</div>
|
||||
<div className="flex items-center text-white/60 text-xs">
|
||||
<Calendar className="w-3 h-3 mr-1" />
|
||||
{formatDate(item.wdate)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 상세 모달 */}
|
||||
{showModal && selectedItem && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-gray-900 rounded-2xl shadow-2xl w-full max-w-4xl max-h-[90vh] overflow-hidden border border-white/10">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedItem.header && (
|
||||
<span className="px-2 py-1 bg-primary-500/20 text-primary-400 text-sm rounded">
|
||||
{selectedItem.header}
|
||||
</span>
|
||||
)}
|
||||
{selectedItem.cate && (
|
||||
<span className="px-2 py-1 bg-white/10 text-white/70 text-sm rounded">
|
||||
{selectedItem.cate}
|
||||
</span>
|
||||
)}
|
||||
<h2 className="text-xl font-bold text-white ml-2">{selectedItem.title}</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="text-white/50 hover:text-white transition-colors"
|
||||
>
|
||||
<span className="text-2xl">×</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center gap-4 text-sm text-white/60">
|
||||
<div className="flex items-center">
|
||||
<User className="w-4 h-4 mr-1" />
|
||||
{selectedItem.wuid_name || selectedItem.wuid}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<Calendar className="w-4 h-4 mr-1" />
|
||||
{formatDate(selectedItem.wdate)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-180px)] p-6">
|
||||
<div className="prose prose-invert max-w-none">
|
||||
<div className="text-white whitespace-pre-wrap">{selectedItem.contents}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end px-6 py-4 border-t border-white/10 bg-white/5">
|
||||
<button
|
||||
onClick={() => setShowModal(false)}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors"
|
||||
>
|
||||
닫기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -12,3 +12,4 @@ export { MonthlyWorkPage } from './MonthlyWork';
|
||||
export { MailFormPage } from './MailForm';
|
||||
export { UserGroupPage } from './UserGroup';
|
||||
export { default as UserAuthPage } from './UserAuth';
|
||||
export { Note } from './Note';
|
||||
|
||||
@@ -448,6 +448,20 @@ export interface MachineBridgeInterface {
|
||||
Project_GetList(statusFilter: string, category: string, process: string, userFilter: string, yearStart: string, yearEnd: string, dateType: string): Promise<string>;
|
||||
Project_GetHistory(projectIdx: number): Promise<string>;
|
||||
Project_GetDailyMemo(projectIdx: number): Promise<string>;
|
||||
|
||||
// Note API (메모장)
|
||||
Note_GetList(startDate: string, endDate: string, uid: string): Promise<string>;
|
||||
Note_GetDetail(idx: number): Promise<string>;
|
||||
Note_Add(pdate: string, title: string, uid: string, description: string, description2: string, share: boolean, guid: string): Promise<string>;
|
||||
Note_Edit(idx: number, pdate: string, title: string, uid: string, description: string, description2: string, share: boolean, guid: string): Promise<string>;
|
||||
Note_Delete(idx: number): Promise<string>;
|
||||
|
||||
// Board API (게시판 - 패치내역 등)
|
||||
Board_GetList(bidx: number, searchKey: string): Promise<string>;
|
||||
Board_GetDetail(idx: number): Promise<string>;
|
||||
|
||||
// Mail API (메일 발신 내역)
|
||||
Mail_GetList(startDate: string, endDate: string, searchKey: string): Promise<string>;
|
||||
}
|
||||
|
||||
// 사용자 권한 정보 타입
|
||||
@@ -512,6 +526,23 @@ export interface HolidayItem {
|
||||
wdate?: string;
|
||||
}
|
||||
|
||||
// 메모장 관련 타입 (EETGW_Note 테이블)
|
||||
export interface NoteItem {
|
||||
idx: number;
|
||||
gcode: string;
|
||||
pdate: string; // 날짜
|
||||
title: string; // 제목
|
||||
uid: string; // 작성자 ID
|
||||
description: string; // 내용 (plain text)
|
||||
description2: string; // 내용 (RTF format - not used in web)
|
||||
share: boolean; // 공유 여부
|
||||
wuid: string; // 등록자 ID
|
||||
wdate: string; // 등록일
|
||||
guid: string; // 폴더 GUID
|
||||
viewcount?: number; // 조회수
|
||||
viewdate?: string; // 최종 조회일
|
||||
}
|
||||
|
||||
// 메일양식 항목 타입
|
||||
export interface MailFormItem {
|
||||
idx: number;
|
||||
@@ -792,3 +823,39 @@ export interface JobReportDayData {
|
||||
holidays: HolidayItem[];
|
||||
}
|
||||
|
||||
// Board 게시판 타입 (패치내역 등)
|
||||
export interface BoardItem {
|
||||
idx: number;
|
||||
bidx: number;
|
||||
header: string;
|
||||
cate: string;
|
||||
title: string;
|
||||
contents: string;
|
||||
file: string;
|
||||
guid: string;
|
||||
url: string;
|
||||
wuid: string;
|
||||
wdate: string | null;
|
||||
project: string;
|
||||
pidx: number;
|
||||
gcode: string;
|
||||
close: boolean;
|
||||
remark: string;
|
||||
wuid_name: string;
|
||||
}
|
||||
|
||||
// Mail 발신 내역 타입
|
||||
export interface MailItem {
|
||||
idx: number;
|
||||
gcode: string;
|
||||
uid: string;
|
||||
subject: string;
|
||||
htmlbody: string;
|
||||
fromlist: string;
|
||||
tolist: string;
|
||||
cclist: string;
|
||||
bcclist: string;
|
||||
project: string;
|
||||
cate: string;
|
||||
wdate: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user