..
This commit is contained in:
@@ -173,9 +173,21 @@
|
||||
<Reference Include="Microsoft.Web.WebView2.Wpf, Version=1.0.2210.55, Culture=neutral, PublicKeyToken=2a8ab48044d2601e, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Microsoft.Web.WebView2.1.0.2210.55\lib\net45\Microsoft.Web.WebView2.Wpf.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="NetOffice, Version=1.8.1.0, Culture=neutral, PublicKeyToken=297f57b43ae7c1de, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NetOfficeFw.Core.1.8.1\lib\net40\NetOffice.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="Newtonsoft.Json, Version=13.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="OfficeApi, Version=1.8.1.0, Culture=neutral, PublicKeyToken=a39beb0835c43c8e, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NetOfficeFw.Core.1.8.1\lib\net40\OfficeApi.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="OutlookApi, Version=1.8.1.0, Culture=neutral, PublicKeyToken=b118031aaa1097f3, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NetOfficeFw.Outlook.1.8.1\lib\net40\OutlookApi.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="System">
|
||||
<HintPath>..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.6\System.dll</HintPath>
|
||||
</Reference>
|
||||
@@ -214,6 +226,10 @@
|
||||
<Reference Include="System.Drawing" />
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
<Reference Include="System.Xml" />
|
||||
<Reference Include="VBIDEApi, Version=1.8.1.0, Culture=neutral, PublicKeyToken=931cec8882205047, processorArchitecture=MSIL">
|
||||
<HintPath>..\packages\NetOfficeFw.Core.1.8.1\lib\net40\VBIDEApi.dll</HintPath>
|
||||
<EmbedInteropTypes>False</EmbedInteropTypes>
|
||||
</Reference>
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="Winsock Orcas">
|
||||
<HintPath>..\DLL\Winsock Orcas.dll</HintPath>
|
||||
@@ -369,6 +385,8 @@
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.Customs.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.UserGroup.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.UserAuth.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.License.cs" />
|
||||
<Compile Include="Web\MachineBridge\MachineBridge.PartList.cs" />
|
||||
<Compile Include="Web\MachineBridge\WebSocketServer.cs" />
|
||||
<Compile Include="Web\Model\PageModel.cs" />
|
||||
<Compile Include="Web\Model\ProjectModel.cs" />
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<ErrorReportUrlHistory />
|
||||
<FallbackCulture>ko-KR</FallbackCulture>
|
||||
<VerifyUploadedFiles>false</VerifyUploadedFiles>
|
||||
<ProjectView>ProjectFiles</ProjectView>
|
||||
<ProjectView>ShowAllFiles</ProjectView>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<EnableSecurityDebugging>false</EnableSecurityDebugging>
|
||||
|
||||
320
Project/Web/MachineBridge/MachineBridge.License.cs
Normal file
320
Project/Web/MachineBridge/MachineBridge.License.cs
Normal file
@@ -0,0 +1,320 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using FCOMMON;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
public partial class MachineBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 라이선스 목록 조회
|
||||
/// </summary>
|
||||
public string License_GetList()
|
||||
{
|
||||
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, expire, name, Version, MeterialNo, Supply, qty,
|
||||
uids, SerialNo, Remark, sdate, edate, manu, wuid, wdate
|
||||
FROM EETGW_License WITH (nolock)
|
||||
WHERE gcode = @gcode
|
||||
ORDER BY expire DESC, name, sdate", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
|
||||
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),
|
||||
expire = !reader.IsDBNull(2) && reader.GetBoolean(2),
|
||||
name = reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||
version = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||
meterialNo = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||
supply = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||
qty = reader.IsDBNull(7) ? 0 : reader.GetInt32(7),
|
||||
uids = reader.IsDBNull(8) ? "" : reader.GetString(8),
|
||||
serialNo = reader.IsDBNull(9) ? "" : reader.GetString(9),
|
||||
remark = reader.IsDBNull(10) ? "" : reader.GetString(10),
|
||||
sdate = reader.IsDBNull(11) ? "" : reader.GetString(11),
|
||||
edate = reader.IsDBNull(12) ? "" : reader.GetString(12),
|
||||
manu = reader.IsDBNull(13) ? "" : reader.GetString(13),
|
||||
wuid = reader.IsDBNull(14) ? "" : reader.GetString(14),
|
||||
wdate = reader.IsDBNull(15) ? "" : reader.GetDateTime(15).ToString("yyyy-MM-dd HH:mm:ss")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Data = list });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "라이선스 목록 조회 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라이선스 추가
|
||||
/// </summary>
|
||||
public string License_Add(string name, string version, string meterialNo, string supply,
|
||||
int qty, string uids, string serialNo, string remark, string sdate, string edate,
|
||||
string manu, bool expire)
|
||||
{
|
||||
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(@"
|
||||
INSERT INTO EETGW_License
|
||||
(gcode, expire, name, manu, Supply, qty, uids, sdate, edate, Remark, wuid, wdate, Version, SerialNo, MeterialNo)
|
||||
VALUES
|
||||
(@gcode, @expire, @name, @manu, @Supply, @qty, @uids, @sdate, @edate, @Remark, @wuid, @wdate, @Version, @SerialNo, @MeterialNo);
|
||||
SELECT SCOPE_IDENTITY();", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@expire", SqlDbType.Bit).Value = expire;
|
||||
cmd.Parameters.Add("@name", SqlDbType.NVarChar).Value = name ?? "";
|
||||
cmd.Parameters.Add("@manu", SqlDbType.NVarChar).Value = manu ?? "";
|
||||
cmd.Parameters.Add("@Supply", SqlDbType.NVarChar).Value = supply ?? "";
|
||||
cmd.Parameters.Add("@qty", SqlDbType.Int).Value = qty;
|
||||
cmd.Parameters.Add("@uids", SqlDbType.NVarChar).Value = uids ?? "";
|
||||
cmd.Parameters.Add("@sdate", SqlDbType.VarChar).Value = string.IsNullOrEmpty(sdate) ? DateTime.Now.ToString("yyyy-MM-dd") : sdate;
|
||||
cmd.Parameters.Add("@edate", SqlDbType.VarChar).Value = (object)edate ?? DBNull.Value;
|
||||
cmd.Parameters.Add("@Remark", SqlDbType.NVarChar).Value = remark ?? "";
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now;
|
||||
cmd.Parameters.Add("@Version", SqlDbType.NVarChar).Value = version ?? "";
|
||||
cmd.Parameters.Add("@SerialNo", SqlDbType.NVarChar).Value = serialNo ?? "";
|
||||
cmd.Parameters.Add("@MeterialNo", SqlDbType.NVarChar).Value = meterialNo ?? "";
|
||||
|
||||
var idx = Convert.ToInt32(cmd.ExecuteScalar());
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "라이선스가 추가되었습니다.", Data = new { idx } });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "라이선스 추가 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라이선스 수정
|
||||
/// </summary>
|
||||
public string License_Update(int idx, string name, string version, string meterialNo,
|
||||
string supply, int qty, string uids, string serialNo, string remark, string sdate,
|
||||
string edate, string manu, bool expire)
|
||||
{
|
||||
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(@"
|
||||
UPDATE EETGW_License SET
|
||||
expire = @expire, name = @name, manu = @manu, Supply = @Supply,
|
||||
qty = @qty, uids = @uids, sdate = @sdate, edate = @edate,
|
||||
Remark = @Remark, wuid = @wuid, wdate = @wdate,
|
||||
Version = @Version, SerialNo = @SerialNo, MeterialNo = @MeterialNo
|
||||
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;
|
||||
cmd.Parameters.Add("@expire", SqlDbType.Bit).Value = expire;
|
||||
cmd.Parameters.Add("@name", SqlDbType.NVarChar).Value = name ?? "";
|
||||
cmd.Parameters.Add("@manu", SqlDbType.NVarChar).Value = manu ?? "";
|
||||
cmd.Parameters.Add("@Supply", SqlDbType.NVarChar).Value = supply ?? "";
|
||||
cmd.Parameters.Add("@qty", SqlDbType.Int).Value = qty;
|
||||
cmd.Parameters.Add("@uids", SqlDbType.NVarChar).Value = uids ?? "";
|
||||
cmd.Parameters.Add("@sdate", SqlDbType.VarChar).Value = string.IsNullOrEmpty(sdate) ? DateTime.Now.ToString("yyyy-MM-dd") : sdate;
|
||||
cmd.Parameters.Add("@edate", SqlDbType.VarChar).Value = (object)edate ?? DBNull.Value;
|
||||
cmd.Parameters.Add("@Remark", SqlDbType.NVarChar).Value = remark ?? "";
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
cmd.Parameters.Add("@wdate", SqlDbType.DateTime).Value = DateTime.Now;
|
||||
cmd.Parameters.Add("@Version", SqlDbType.NVarChar).Value = version ?? "";
|
||||
cmd.Parameters.Add("@SerialNo", SqlDbType.NVarChar).Value = serialNo ?? "";
|
||||
cmd.Parameters.Add("@MeterialNo", SqlDbType.NVarChar).Value = meterialNo ?? "";
|
||||
|
||||
var cnt = cmd.ExecuteNonQuery();
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "라이선스가 수정되었습니다.", Data = new { UpdatedCount = cnt } });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "라이선스 수정 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라이선스 삭제
|
||||
/// </summary>
|
||||
public string License_Delete(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;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
DELETE FROM EETGW_License
|
||||
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;
|
||||
|
||||
var cnt = cmd.ExecuteNonQuery();
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "라이선스가 삭제되었습니다.", Data = new { DeletedCount = cnt } });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "라이선스 삭제 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 라이선스 폴더 열기
|
||||
/// </summary>
|
||||
public string License_OpenFolder(int idx)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
var serverpath = DBM.getCodeSvalue("55", "01");
|
||||
if (string.IsNullOrEmpty(serverpath) || !Directory.Exists(serverpath))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "프로젝트 기본경로가 존재하지 않습니다.\\n\\n공용정보->공용코드->55-01 데이터를 설정 하시기 바랍니다." });
|
||||
}
|
||||
|
||||
var folderPath = Path.Combine(serverpath, "Data", "License", idx.ToString());
|
||||
|
||||
// 폴더가 없으면 생성
|
||||
if (!Directory.Exists(folderPath))
|
||||
{
|
||||
Directory.CreateDirectory(folderPath);
|
||||
}
|
||||
|
||||
// 탐색기로 폴더 열기
|
||||
Process.Start("explorer.exe", folderPath);
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "폴더를 열었습니다.", Data = new { Path = folderPath } });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "폴더 열기 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV로 내보내기
|
||||
/// </summary>
|
||||
public string License_ExportCSV(string filePath)
|
||||
{
|
||||
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, expire, name, Version, MeterialNo, Supply, qty,
|
||||
uids, SerialNo, Remark, sdate, edate, manu
|
||||
FROM EETGW_License WITH (nolock)
|
||||
WHERE gcode = @gcode
|
||||
ORDER BY expire DESC, name, sdate", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("idx,expire,name,Version,MeterialNo,Supply,qty,uids,SerialNo,Remark,sdate,edate,manu");
|
||||
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
var values = new List<string>();
|
||||
for (int i = 0; i < reader.FieldCount; i++)
|
||||
{
|
||||
var value = reader.IsDBNull(i) ? "" : reader.GetValue(i).ToString();
|
||||
// CSV 이스케이프 처리
|
||||
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n"))
|
||||
{
|
||||
value = "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
values.Add(value);
|
||||
}
|
||||
sb.AppendLine(string.Join(",", values));
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "CSV 파일이 생성되었습니다.", Data = new { Path = filePath } });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "CSV 내보내기 중 오류가 발생했습니다: " + ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,9 @@ using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using FCOMMON;
|
||||
using NetOffice;
|
||||
using Outlook = NetOffice.OutlookApi;
|
||||
using NetOffice.OutlookApi.Enums;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
@@ -71,5 +74,164 @@ namespace Project.Web
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메일 데이터 추가 (발송 대기열)
|
||||
/// </summary>
|
||||
public string Mail_AddData(string cate, string subject, string fromlist, string tolist, string cc, string bcc, string body)
|
||||
{
|
||||
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(@"
|
||||
INSERT INTO MailData
|
||||
(gcode, cate, pdate, subject, fromlist, tolist, cc, bcc, body, SendOK, wuid, wdate)
|
||||
VALUES
|
||||
(@gcode, @cate, @pdate, @subject, @fromlist, @tolist, @cc, @bcc, @body, 0, @wuid, GETDATE())", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@cate", SqlDbType.VarChar).Value = cate ?? "";
|
||||
cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
cmd.Parameters.Add("@subject", SqlDbType.VarChar).Value = subject ?? "";
|
||||
cmd.Parameters.Add("@fromlist", SqlDbType.VarChar).Value = fromlist ?? "";
|
||||
cmd.Parameters.Add("@tolist", SqlDbType.VarChar).Value = tolist ?? "";
|
||||
cmd.Parameters.Add("@cc", SqlDbType.VarChar).Value = cc ?? "";
|
||||
cmd.Parameters.Add("@bcc", SqlDbType.VarChar).Value = bcc ?? "";
|
||||
cmd.Parameters.Add("@body", SqlDbType.VarChar).Value = body ?? "";
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
|
||||
int affected = cmd.ExecuteNonQuery();
|
||||
|
||||
if (affected > 0)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "메일이 발송 대기열에 추가되었습니다." });
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "메일 등록에 실패했습니다." });
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 메일 직접 발송 (SMTP)
|
||||
/// </summary>
|
||||
public string Mail_SendDirect(string cate, string subject, string fromlist, string tolist, string cc, string bcc, string body)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
// SMTP 직접 발송
|
||||
var mailserver = info.mailserver ?? "scwa.amkor.co.kr";
|
||||
var mc = new System.Net.Mail.SmtpClient(mailserver);
|
||||
var msg = new System.Net.Mail.MailMessage(
|
||||
string.IsNullOrEmpty(fromlist) ? "gw@amkor.co.kr" : fromlist,
|
||||
tolist,
|
||||
subject,
|
||||
body
|
||||
);
|
||||
|
||||
if (!string.IsNullOrEmpty(bcc)) msg.Bcc.Add(bcc);
|
||||
if (!string.IsNullOrEmpty(cc)) msg.CC.Add(cc);
|
||||
msg.IsBodyHtml = true;
|
||||
|
||||
mc.Send(msg);
|
||||
|
||||
// 발송 성공 시 MailData에도 저장 (SendOK=1)
|
||||
var connStr = Project.Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
INSERT INTO MailData
|
||||
(gcode, cate, pdate, subject, fromlist, tolist, cc, bcc, body, SendOK, SendMsg, wuid, wdate, suid, sdate)
|
||||
VALUES
|
||||
(@gcode, @cate, @pdate, @subject, @fromlist, @tolist, @cc, @bcc, @body, 1, @SendMsg, @wuid, GETDATE(), @wuid, GETDATE())", conn);
|
||||
|
||||
cmd.Parameters.Add("@gcode", SqlDbType.VarChar).Value = info.Login.gcode;
|
||||
cmd.Parameters.Add("@cate", SqlDbType.VarChar).Value = cate ?? "";
|
||||
cmd.Parameters.Add("@pdate", SqlDbType.VarChar).Value = DateTime.Now.ToString("yyyy-MM-dd");
|
||||
cmd.Parameters.Add("@subject", SqlDbType.VarChar).Value = subject ?? "";
|
||||
cmd.Parameters.Add("@fromlist", SqlDbType.VarChar).Value = string.IsNullOrEmpty(fromlist) ? "gw@amkor.co.kr" : fromlist;
|
||||
cmd.Parameters.Add("@tolist", SqlDbType.VarChar).Value = tolist ?? "";
|
||||
cmd.Parameters.Add("@cc", SqlDbType.VarChar).Value = cc ?? "";
|
||||
cmd.Parameters.Add("@bcc", SqlDbType.VarChar).Value = bcc ?? "";
|
||||
cmd.Parameters.Add("@body", SqlDbType.VarChar).Value = body ?? "";
|
||||
cmd.Parameters.Add("@SendMsg", SqlDbType.VarChar).Value = "Direct Send";
|
||||
cmd.Parameters.Add("@wuid", SqlDbType.VarChar).Value = info.Login.no;
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "메일이 발송되었습니다." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = $"메일 발송 실패: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Outlook으로 메일 미리보기/발송
|
||||
/// </summary>
|
||||
public string Mail_SendOutlook(string subject, string _tolist, string cc, string bcc, string body)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(info.Login.no) || string.IsNullOrEmpty(info.Login.gcode))
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "로그인이 필요합니다." });
|
||||
}
|
||||
|
||||
// Outlook COM 객체 생성
|
||||
var tolist = new string[] { "Chikyun.kim@amkor.co.kr" }; //dr.tolist.Split(',');
|
||||
Outlook.Application outlookApplication = new Outlook.Application();
|
||||
|
||||
foreach (var to in tolist)
|
||||
{
|
||||
if (to.isEmpty()) continue;
|
||||
var newMail = outlookApplication.CreateItem(OlItemType.olMailItem) as Outlook.MailItem;
|
||||
newMail.Display();
|
||||
newMail.Subject = subject.Trim(); // dr.title;
|
||||
newMail.To = to;
|
||||
newMail.CC = cc;
|
||||
newMail.BCC = bcc;
|
||||
// newMail.BodyFormat = OlBodyFormat.olFormatHTML;
|
||||
newMail.HTMLBody = body
|
||||
.Replace("{USER}", FCOMMON.info.Login.nameK)
|
||||
.Replace("{EUSER}", FCOMMON.info.Login.nameE)
|
||||
.Replace("{EMAIL}", FCOMMON.info.Login.email)
|
||||
.Replace("%7BEMAIL%7D", FCOMMON.info.Login.email)
|
||||
.Replace("{HP}", FCOMMON.info.Login.hp)
|
||||
.Replace("{TEL}", FCOMMON.info.Login.tel)
|
||||
.Replace("{ITEM}", subject) + newMail.HTMLBody;
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "Outlook 메일 창이 열렸습니다." });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = $"Outlook 실행 실패: {ex.Message}" });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
247
Project/Web/MachineBridge/MachineBridge.PartList.cs
Normal file
247
Project/Web/MachineBridge/MachineBridge.PartList.cs
Normal file
@@ -0,0 +1,247 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.SqlClient;
|
||||
using Newtonsoft.Json;
|
||||
using FCOMMON;
|
||||
|
||||
namespace Project.Web
|
||||
{
|
||||
public partial class MachineBridge
|
||||
{
|
||||
/// <summary>
|
||||
/// 프로젝트별 파트리스트 조회
|
||||
/// </summary>
|
||||
public string PartList_GetList(int projectIdx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var conn = new SqlConnection(connStr))
|
||||
{
|
||||
conn.Open();
|
||||
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
SELECT
|
||||
idx, no, Project, ItemGroup, ItemModel, ItemUnit, ItemName,
|
||||
ItemSid, ItemSupply, ItemSupplyidx, ItemManu, Item,
|
||||
option1, qty, qtyn, price, amt, remark, qtybuy
|
||||
FROM ProjectsPart
|
||||
WHERE Project = @ProjectIdx
|
||||
ORDER BY ItemGroup, option1, no, ItemName
|
||||
", conn);
|
||||
|
||||
cmd.Parameters.Add("@ProjectIdx", SqlDbType.Int).Value = projectIdx;
|
||||
|
||||
var list = new List<object>();
|
||||
using (var reader = cmd.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
list.Add(new
|
||||
{
|
||||
idx = reader.GetInt32(0),
|
||||
no = reader.IsDBNull(1) ? 0 : reader.GetInt32(1),
|
||||
Project = reader.GetInt32(2),
|
||||
itemgroup = reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||
itemmodel = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||
itemunit = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||
itemname = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||
itemsid = reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||
itemsupply = reader.IsDBNull(8) ? "" : reader.GetString(8),
|
||||
itemsupplyidx = reader.IsDBNull(9) ? 0 : reader.GetInt32(9),
|
||||
itemmanu = reader.IsDBNull(10) ? "" : reader.GetString(10),
|
||||
item = reader.IsDBNull(11) ? "" : reader.GetInt32(11).ToString(),
|
||||
option1 = reader.IsDBNull(12) ? "" : reader.GetString(12),
|
||||
qty = reader.IsDBNull(13) ? 0 : reader.GetInt32(13),
|
||||
qtyn = reader.IsDBNull(14) ? 0 : reader.GetInt32(14),
|
||||
price = reader.IsDBNull(15) ? 0.0 : (double)reader.GetDecimal(15),
|
||||
amt = reader.IsDBNull(16) ? 0.0 : (double)reader.GetDecimal(16),
|
||||
remark = reader.IsDBNull(17) ? "" : reader.GetString(17),
|
||||
qtybuy = reader.IsDBNull(18) ? 0 : reader.GetInt32(18)
|
||||
});
|
||||
}
|
||||
} var result = new
|
||||
{
|
||||
Success = true,
|
||||
Data = list
|
||||
};
|
||||
return JsonConvert.SerializeObject(result);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = false,
|
||||
Message = "파트리스트 조회 실패: " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파트리스트 항목 저장 (추가/수정)
|
||||
/// </summary>
|
||||
public string PartList_Save(
|
||||
int idx,
|
||||
int projectIdx,
|
||||
string itemgroup,
|
||||
string itemname,
|
||||
string item,
|
||||
string itemmodel,
|
||||
string itemscale,
|
||||
string itemunit,
|
||||
double qty,
|
||||
double price,
|
||||
string itemsupply,
|
||||
int itemsupplyidx,
|
||||
string itemmanu,
|
||||
string itemsid,
|
||||
string option1,
|
||||
string remark,
|
||||
int no,
|
||||
double qtybuy)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var con = new SqlConnection(connStr))
|
||||
{
|
||||
con.Open();
|
||||
|
||||
if (idx == 0 || idx == -1) // 새로 추가
|
||||
{
|
||||
using (var cmd = new SqlCommand(@"
|
||||
INSERT INTO ProjectsPart (
|
||||
Project, ItemGroup, ItemName, Item, ItemModel,
|
||||
ItemUnit, Qty, Price, ItemSupply,
|
||||
ItemSupplyIdx, ItemManu, ItemSid, option1,
|
||||
remark, no, qtybuy, wuid, wdate
|
||||
) VALUES (
|
||||
@Project, @ItemGroup, @ItemName, @Item, @ItemModel,
|
||||
@ItemUnit, @Qty, @Price, @ItemSupply,
|
||||
@ItemSupplyIdx, @ItemManu, @ItemSid, @option1,
|
||||
@remark, @no, @qtybuy, @wuid, @wdate
|
||||
)
|
||||
", con))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@Project", projectIdx);
|
||||
cmd.Parameters.AddWithValue("@ItemGroup", itemgroup ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemName", itemname ?? "");
|
||||
cmd.Parameters.AddWithValue("@Item", item ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemModel", itemmodel ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemUnit", itemunit ?? "");
|
||||
cmd.Parameters.AddWithValue("@Qty", qty);
|
||||
cmd.Parameters.AddWithValue("@Price", price);
|
||||
cmd.Parameters.AddWithValue("@ItemSupply", itemsupply ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemSupplyIdx", itemsupplyidx);
|
||||
cmd.Parameters.AddWithValue("@ItemManu", itemmanu ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemSid", itemsid ?? "");
|
||||
cmd.Parameters.AddWithValue("@option1", option1 ?? "");
|
||||
cmd.Parameters.AddWithValue("@remark", remark ?? "");
|
||||
cmd.Parameters.AddWithValue("@no", no);
|
||||
cmd.Parameters.AddWithValue("@qtybuy", qtybuy);
|
||||
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||
cmd.Parameters.AddWithValue("@wdate", DateTime.Now);
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
else // 수정
|
||||
{
|
||||
using (var cmd = new SqlCommand(@"
|
||||
UPDATE ProjectsPart SET
|
||||
ItemGroup = @ItemGroup,
|
||||
ItemName = @ItemName,
|
||||
Item = @Item,
|
||||
ItemModel = @ItemModel,
|
||||
ItemUnit = @ItemUnit,
|
||||
Qty = @Qty,
|
||||
Price = @Price,
|
||||
ItemSupply = @ItemSupply,
|
||||
ItemSupplyIdx = @ItemSupplyIdx,
|
||||
ItemManu = @ItemManu,
|
||||
ItemSid = @ItemSid,
|
||||
option1 = @option1,
|
||||
remark = @remark,
|
||||
no = @no,
|
||||
qtybuy = @qtybuy
|
||||
WHERE idx = @idx
|
||||
", con))
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@idx", idx);
|
||||
cmd.Parameters.AddWithValue("@ItemGroup", itemgroup ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemName", itemname ?? "");
|
||||
cmd.Parameters.AddWithValue("@Item", item ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemModel", itemmodel ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemUnit", itemunit ?? "");
|
||||
cmd.Parameters.AddWithValue("@Qty", qty);
|
||||
cmd.Parameters.AddWithValue("@Price", price);
|
||||
cmd.Parameters.AddWithValue("@ItemSupply", itemsupply ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemSupplyIdx", itemsupplyidx);
|
||||
cmd.Parameters.AddWithValue("@ItemManu", itemmanu ?? "");
|
||||
cmd.Parameters.AddWithValue("@ItemSid", itemsid ?? "");
|
||||
cmd.Parameters.AddWithValue("@option1", option1 ?? "");
|
||||
cmd.Parameters.AddWithValue("@remark", remark ?? "");
|
||||
cmd.Parameters.AddWithValue("@no", no);
|
||||
cmd.Parameters.AddWithValue("@qtybuy", qtybuy);
|
||||
|
||||
cmd.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = true,
|
||||
Message = "저장되었습니다."
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = false,
|
||||
Message = "저장 실패: " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 파트리스트 항목 삭제
|
||||
/// </summary>
|
||||
public string PartList_Delete(int idx)
|
||||
{
|
||||
try
|
||||
{
|
||||
var connStr = Properties.Settings.Default.CS;
|
||||
using (var con = new SqlConnection(connStr))
|
||||
{
|
||||
con.Open();
|
||||
|
||||
var cmd = new SqlCommand(@"
|
||||
DELETE FROM ProjectsPart WHERE idx = @idx
|
||||
", con);
|
||||
|
||||
cmd.Parameters.Add("@idx", SqlDbType.Int).Value = idx;
|
||||
cmd.ExecuteNonQuery();
|
||||
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = true,
|
||||
Message = "삭제되었습니다."
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = false,
|
||||
Message = "삭제 실패: " + ex.Message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -606,6 +606,64 @@ namespace Project.Web
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 프로젝트 히스토리 저장
|
||||
/// </summary>
|
||||
public string Project_SaveHistory(int idx, int pidx, string pdate, int progress, string remark)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cs = Properties.Settings.Default.gwcs;
|
||||
using (var cn = new SqlConnection(cs))
|
||||
{
|
||||
cn.Open();
|
||||
string sql;
|
||||
|
||||
if (idx > 0)
|
||||
{
|
||||
// 수정
|
||||
sql = @"UPDATE ProjectsHistory
|
||||
SET remark = @remark, progress = @progress, wdate = GETDATE(), wuid = @wuid
|
||||
WHERE idx = @idx";
|
||||
}
|
||||
else
|
||||
{
|
||||
// 신규 등록
|
||||
sql = @"INSERT INTO ProjectsHistory (pidx, pdate, progress, remark, wuid, wdate)
|
||||
VALUES (@pidx, @pdate, @progress, @remark, @wuid, GETDATE())";
|
||||
}
|
||||
|
||||
using (var cmd = new SqlCommand(sql, cn))
|
||||
{
|
||||
if (idx > 0)
|
||||
{
|
||||
cmd.Parameters.AddWithValue("@idx", idx);
|
||||
}
|
||||
cmd.Parameters.AddWithValue("@pidx", pidx);
|
||||
cmd.Parameters.AddWithValue("@pdate", pdate);
|
||||
cmd.Parameters.AddWithValue("@progress", progress);
|
||||
cmd.Parameters.AddWithValue("@remark", remark ?? "");
|
||||
cmd.Parameters.AddWithValue("@wuid", info.Login.no);
|
||||
|
||||
int affected = cmd.ExecuteNonQuery();
|
||||
|
||||
if (affected > 0)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = true, Message = "저장되었습니다." });
|
||||
}
|
||||
else
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = "저장에 실패했습니다." });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return JsonConvert.SerializeObject(new { Success = false, Message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 프로젝트 일일 메모 조회
|
||||
/// </summary>
|
||||
|
||||
@@ -101,12 +101,27 @@ namespace Project.Web
|
||||
{
|
||||
try
|
||||
{
|
||||
var productVersion = Application.ProductVersion;
|
||||
var maxVersion = DBM.GetMaxVersion();
|
||||
var hasNewVersion = false;
|
||||
|
||||
if (!string.IsNullOrEmpty(maxVersion))
|
||||
{
|
||||
var verchk = productVersion.CompareTo(maxVersion);
|
||||
if (verchk < 0)
|
||||
{
|
||||
hasNewVersion = true;
|
||||
}
|
||||
}
|
||||
|
||||
return JsonConvert.SerializeObject(new
|
||||
{
|
||||
Success = true,
|
||||
ProductName = Application.ProductName,
|
||||
ProductVersion = Application.ProductVersion,
|
||||
DisplayVersion = $"{Application.ProductName} v{Application.ProductVersion}"
|
||||
ProductVersion = productVersion,
|
||||
DisplayVersion = $"{Application.ProductName} v{productVersion}",
|
||||
MaxVersion = maxVersion,
|
||||
HasNewVersion = hasNewVersion
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -986,6 +986,171 @@ namespace Project.Web
|
||||
}
|
||||
break;
|
||||
|
||||
case "MAIL_ADD_DATA":
|
||||
{
|
||||
string cate = json.cate ?? "";
|
||||
string subject = json.subject ?? "";
|
||||
string fromlist = json.fromlist ?? "";
|
||||
string tolist = json.tolist ?? "";
|
||||
string cc = json.cc ?? "";
|
||||
string bcc = json.bcc ?? "";
|
||||
string body = json.body ?? "";
|
||||
string result = _bridge.Mail_AddData(cate, subject, fromlist, tolist, cc, bcc, body);
|
||||
var response = new { type = "MAIL_ADD_DATA_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "MAIL_SEND_DIRECT":
|
||||
{
|
||||
string cate = json.cate ?? "";
|
||||
string subject = json.subject ?? "";
|
||||
string fromlist = json.fromlist ?? "";
|
||||
string tolist = json.tolist ?? "";
|
||||
string cc = json.cc ?? "";
|
||||
string bcc = json.bcc ?? "";
|
||||
string body = json.body ?? "";
|
||||
string result = _bridge.Mail_SendDirect(cate, subject, fromlist, tolist, cc, bcc, body);
|
||||
var response = new { type = "MAIL_SEND_DIRECT_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "MAIL_SEND_OUTLOOK":
|
||||
{
|
||||
string subject = json.subject ?? "";
|
||||
string tolist = json.tolist ?? "";
|
||||
string cc = json.cc ?? "";
|
||||
string bcc = json.bcc ?? "";
|
||||
string body = json.body ?? "";
|
||||
string result = _bridge.Mail_SendOutlook(subject, tolist, cc, bcc, body);
|
||||
var response = new { type = "MAIL_SEND_OUTLOOK_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== License API (라이선스 관리) =====
|
||||
case "LICENSE_GET_LIST":
|
||||
{
|
||||
string result = _bridge.License_GetList();
|
||||
var response = new { type = "LICENSE_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "LICENSE_ADD":
|
||||
{
|
||||
string name = json.name ?? "";
|
||||
string version = json.version ?? "";
|
||||
string meterialNo = json.meterialNo ?? "";
|
||||
string supply = json.supply ?? "";
|
||||
int qty = json.qty ?? 0;
|
||||
string uids = json.uids ?? "";
|
||||
string serialNo = json.serialNo ?? "";
|
||||
string remark = json.remark ?? "";
|
||||
string sdate = json.sdate ?? "";
|
||||
string edate = json.edate ?? "";
|
||||
string manu = json.manu ?? "";
|
||||
bool expire = json.expire ?? false;
|
||||
string result = _bridge.License_Add(name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire);
|
||||
var response = new { type = "LICENSE_ADD_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "LICENSE_UPDATE":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string name = json.name ?? "";
|
||||
string version = json.version ?? "";
|
||||
string meterialNo = json.meterialNo ?? "";
|
||||
string supply = json.supply ?? "";
|
||||
int qty = json.qty ?? 0;
|
||||
string uids = json.uids ?? "";
|
||||
string serialNo = json.serialNo ?? "";
|
||||
string remark = json.remark ?? "";
|
||||
string sdate = json.sdate ?? "";
|
||||
string edate = json.edate ?? "";
|
||||
string manu = json.manu ?? "";
|
||||
bool expire = json.expire ?? false;
|
||||
string result = _bridge.License_Update(idx, name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire);
|
||||
var response = new { type = "LICENSE_UPDATE_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "LICENSE_DELETE":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.License_Delete(idx);
|
||||
var response = new { type = "LICENSE_DELETE_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "LICENSE_OPEN_FOLDER":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.License_OpenFolder(idx);
|
||||
var response = new { type = "LICENSE_OPEN_FOLDER_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "LICENSE_EXPORT_CSV":
|
||||
{
|
||||
string filePath = json.filePath ?? "";
|
||||
string result = _bridge.License_ExportCSV(filePath);
|
||||
var response = new { type = "LICENSE_EXPORT_CSV_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== PartList API (파트리스트) =====
|
||||
case "PARTLIST_GET_LIST":
|
||||
{
|
||||
int projectIdx = json.projectIdx ?? 0;
|
||||
string result = _bridge.PartList_GetList(projectIdx);
|
||||
var response = new { type = "PARTLIST_LIST_DATA", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "PARTLIST_SAVE":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
int projectIdx = json.projectIdx ?? 0;
|
||||
string itemgroup = json.itemgroup ?? "";
|
||||
string itemname = json.itemname ?? "";
|
||||
string item = json.item ?? "";
|
||||
string itemmodel = json.itemmodel ?? "";
|
||||
string itemscale = json.itemscale ?? "";
|
||||
string itemunit = json.itemunit ?? "";
|
||||
double qty = json.qty ?? 0.0;
|
||||
double price = json.price ?? 0.0;
|
||||
string itemsupply = json.itemsupply ?? "";
|
||||
int itemsupplyidx = json.itemsupplyidx ?? 0;
|
||||
string itemmanu = json.itemmanu ?? "";
|
||||
string itemsid = json.itemsid ?? "";
|
||||
string option1 = json.option1 ?? "";
|
||||
string remark = json.remark ?? "";
|
||||
int no = json.no ?? 0;
|
||||
double qtybuy = json.qtybuy ?? 0.0;
|
||||
string result = _bridge.PartList_Save(idx, projectIdx, itemgroup, itemname, item, itemmodel, itemscale, itemunit, qty, price, itemsupply, itemsupplyidx, itemmanu, itemsid, option1, remark, no, qtybuy);
|
||||
var response = new { type = "PARTLIST_SAVE_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "PARTLIST_DELETE":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
string result = _bridge.PartList_Delete(idx);
|
||||
var response = new { type = "PARTLIST_DELETE_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
// ===== Customs API (업체정보) =====
|
||||
case "CUSTOMS_GET_LIST":
|
||||
{
|
||||
@@ -1295,6 +1460,19 @@ namespace Project.Web
|
||||
}
|
||||
break;
|
||||
|
||||
case "PROJECT_SAVE_HISTORY":
|
||||
{
|
||||
int idx = json.idx ?? 0;
|
||||
int pidx = json.pidx ?? 0;
|
||||
string pdate = json.pdate ?? "";
|
||||
int progress = json.progress ?? 0;
|
||||
string remark = json.remark ?? "";
|
||||
string result = _bridge.Project_SaveHistory(idx, pidx, pdate, progress, remark);
|
||||
var response = new { type = "PROJECT_SAVE_HISTORY_RESULT", data = JsonConvert.DeserializeObject(result) };
|
||||
await Send(socket, JsonConvert.SerializeObject(response));
|
||||
}
|
||||
break;
|
||||
|
||||
case "PROJECT_GET_DAILY_MEMO":
|
||||
{
|
||||
int projectIdx = json.projectIdx ?? 0;
|
||||
|
||||
@@ -3,26 +3,16 @@
|
||||
<configSections>
|
||||
</configSections>
|
||||
<connectionStrings>
|
||||
<add name="Project.Properties.Settings.gwcs" connectionString="Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True"
|
||||
providerName="System.Data.SqlClient" />
|
||||
<add name="EEEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntities1" connectionString="metadata=res://*/ModelMain.csdl|res://*/ModelMain.ssdl|res://*/ModelMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework""
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesMain" connectionString="metadata=res://*/AdoNetEFMain.csdl|res://*/AdoNetEFMain.ssdl|res://*/AdoNetEFMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework""
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="S1ACCESS300Entities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesPurchase" connectionString="metadata=res://*/ModelPurchase.csdl|res://*/ModelPurchase.ssdl|res://*/ModelPurchase.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesCommon" connectionString="metadata=res://*/ModelCommon.csdl|res://*/ModelCommon.ssdl|res://*/ModelCommon.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesJobreport" connectionString="metadata=res://*/ModelJobreport.csdl|res://*/ModelJobreport.ssdl|res://*/ModelJobreport.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesProject" connectionString="metadata=res://*/ModelProject.csdl|res://*/ModelProject.ssdl|res://*/ModelProject.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True"
|
||||
providerName="System.Data.EntityClient" />
|
||||
<add name="Project.Properties.Settings.CS" connectionString="Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True"
|
||||
providerName="System.Data.SqlClient" />
|
||||
<add name="Project.Properties.Settings.gwcs" connectionString="Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True" providerName="System.Data.SqlClient" />
|
||||
<add name="EEEntities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntities1" connectionString="metadata=res://*/ModelMain.csdl|res://*/ModelMain.ssdl|res://*/ModelMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesMain" connectionString="metadata=res://*/AdoNetEFMain.csdl|res://*/AdoNetEFMain.ssdl|res://*/AdoNetEFMain.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework"" providerName="System.Data.EntityClient" />
|
||||
<add name="S1ACCESS300Entities" connectionString="metadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;provider=System.Data.SqlClient;provider connection string="data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesPurchase" connectionString="metadata=res://*/ModelPurchase.csdl|res://*/ModelPurchase.ssdl|res://*/ModelPurchase.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesCommon" connectionString="metadata=res://*/ModelCommon.csdl|res://*/ModelCommon.ssdl|res://*/ModelCommon.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesJobreport" connectionString="metadata=res://*/ModelJobreport.csdl|res://*/ModelJobreport.ssdl|res://*/ModelJobreport.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="EEEntitiesProject" connectionString="metadata=res://*/ModelProject.csdl|res://*/ModelProject.ssdl|res://*/ModelProject.msl;provider=System.Data.SqlClient;provider connection string="data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework"TrustServerCertificate=True" providerName="System.Data.EntityClient" />
|
||||
<add name="Project.Properties.Settings.CS" connectionString="Data Source=K4FASQL.kr.ds.amkor.com,50150;Initial Catalog=EE;Persist Security Info=True;User ID=eeadm;Password=uJnU8a8q&DJ+ug-D;Encrypt=False;TrustServerCertificate=True" providerName="System.Data.SqlClient" />
|
||||
</connectionStrings>
|
||||
<runtime>
|
||||
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
|
||||
166
Project/fMain.Designer.cs
generated
166
Project/fMain.Designer.cs
generated
@@ -78,8 +78,6 @@
|
||||
this.업무분류및형태설정ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.자동입력ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem2 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.교육목록ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.비용절감ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.라이선스ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.mn_jago = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.관리ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -102,14 +100,6 @@
|
||||
this.근태마감ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.대체시간이월ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.personalInventoryToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.mn_docu = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.메모장ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.메일내역ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.기타ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.품목검색ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.대쉬보드ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem17 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.webview2TestToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.즐겨찾기ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.btDev = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.purchaseImportToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -123,17 +113,11 @@
|
||||
this.addSIdDataToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.계정목록ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.그룹정보ToolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem1 = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.임의테이블조작ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.mailBackupToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.메일자동발신테스트ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem5 = new System.Windows.Forms.ToolStripSeparator();
|
||||
this.아이템비활성화하기ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.sPR설정ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.toolStripMenuItem13 = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.프로젝트스케쥴담당자사번업데이트ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.버젼확인ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.설명서ToolStripMenuItem = new System.Windows.Forms.ToolStripMenuItem();
|
||||
this.tabControl1 = new System.Windows.Forms.TabControl();
|
||||
this.toolStrip1 = new System.Windows.Forms.ToolStrip();
|
||||
this.toolStripMenuItem8 = new System.Windows.Forms.ToolStripMenuItem();
|
||||
@@ -236,12 +220,8 @@
|
||||
this.btSetting,
|
||||
this.commonToolStripMenuItem,
|
||||
this.managementToolStripMenuItem,
|
||||
this.mn_docu,
|
||||
this.기타ToolStripMenuItem,
|
||||
this.즐겨찾기ToolStripMenuItem,
|
||||
this.btDev,
|
||||
this.버젼확인ToolStripMenuItem,
|
||||
this.설명서ToolStripMenuItem});
|
||||
this.btDev});
|
||||
this.menuStrip1.Location = new System.Drawing.Point(1, 1);
|
||||
this.menuStrip1.Name = "menuStrip1";
|
||||
this.menuStrip1.Size = new System.Drawing.Size(1094, 27);
|
||||
@@ -368,8 +348,6 @@
|
||||
this.mn_purchase,
|
||||
this.mn_project,
|
||||
this.mn_dailyhistory,
|
||||
this.교육목록ToolStripMenuItem,
|
||||
this.비용절감ToolStripMenuItem,
|
||||
this.라이선스ToolStripMenuItem,
|
||||
this.mn_jago,
|
||||
this.휴가관리ToolStripMenuItem,
|
||||
@@ -508,7 +486,6 @@
|
||||
this.mn_dailyhistory.Name = "mn_dailyhistory";
|
||||
this.mn_dailyhistory.Size = new System.Drawing.Size(203, 24);
|
||||
this.mn_dailyhistory.Text = "업무관리";
|
||||
this.mn_dailyhistory.Click += new System.EventHandler(this.업무일지ToolStripMenuItem1_Click);
|
||||
//
|
||||
// 목록ToolStripMenuItem1
|
||||
//
|
||||
@@ -548,22 +525,6 @@
|
||||
this.toolStripMenuItem2.Name = "toolStripMenuItem2";
|
||||
this.toolStripMenuItem2.Size = new System.Drawing.Size(231, 6);
|
||||
//
|
||||
// 교육목록ToolStripMenuItem
|
||||
//
|
||||
this.교육목록ToolStripMenuItem.Name = "교육목록ToolStripMenuItem";
|
||||
this.교육목록ToolStripMenuItem.Size = new System.Drawing.Size(203, 24);
|
||||
this.교육목록ToolStripMenuItem.Text = "교육목록";
|
||||
this.교육목록ToolStripMenuItem.Click += new System.EventHandler(this.교육목록ToolStripMenuItem_Click);
|
||||
//
|
||||
// 비용절감ToolStripMenuItem
|
||||
//
|
||||
this.비용절감ToolStripMenuItem.ForeColor = System.Drawing.Color.Black;
|
||||
this.비용절감ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("비용절감ToolStripMenuItem.Image")));
|
||||
this.비용절감ToolStripMenuItem.Name = "비용절감ToolStripMenuItem";
|
||||
this.비용절감ToolStripMenuItem.Size = new System.Drawing.Size(203, 24);
|
||||
this.비용절감ToolStripMenuItem.Text = "비용절감";
|
||||
this.비용절감ToolStripMenuItem.Click += new System.EventHandler(this.비용절감ToolStripMenuItem_Click);
|
||||
//
|
||||
// 라이선스ToolStripMenuItem
|
||||
//
|
||||
this.라이선스ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("라이선스ToolStripMenuItem.Image")));
|
||||
@@ -740,68 +701,6 @@
|
||||
this.personalInventoryToolStripMenuItem.Visible = false;
|
||||
this.personalInventoryToolStripMenuItem.Click += new System.EventHandler(this.personalInventoryToolStripMenuItem_Click);
|
||||
//
|
||||
// mn_docu
|
||||
//
|
||||
this.mn_docu.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.메모장ToolStripMenuItem,
|
||||
this.메일내역ToolStripMenuItem});
|
||||
this.mn_docu.Image = ((System.Drawing.Image)(resources.GetObject("mn_docu.Image")));
|
||||
this.mn_docu.Name = "mn_docu";
|
||||
this.mn_docu.Size = new System.Drawing.Size(65, 23);
|
||||
this.mn_docu.Text = "문서";
|
||||
//
|
||||
// 메모장ToolStripMenuItem
|
||||
//
|
||||
this.메모장ToolStripMenuItem.Name = "메모장ToolStripMenuItem";
|
||||
this.메모장ToolStripMenuItem.Size = new System.Drawing.Size(152, 24);
|
||||
this.메모장ToolStripMenuItem.Text = "메모장";
|
||||
this.메모장ToolStripMenuItem.Click += new System.EventHandler(this.메모장ToolStripMenuItem_Click);
|
||||
//
|
||||
// 메일내역ToolStripMenuItem
|
||||
//
|
||||
this.메일내역ToolStripMenuItem.Name = "메일내역ToolStripMenuItem";
|
||||
this.메일내역ToolStripMenuItem.Size = new System.Drawing.Size(152, 24);
|
||||
this.메일내역ToolStripMenuItem.Text = "메일 내역";
|
||||
this.메일내역ToolStripMenuItem.Click += new System.EventHandler(this.메일내역ToolStripMenuItem_Click);
|
||||
//
|
||||
// 기타ToolStripMenuItem
|
||||
//
|
||||
this.기타ToolStripMenuItem.DropDownItems.AddRange(new System.Windows.Forms.ToolStripItem[] {
|
||||
this.품목검색ToolStripMenuItem,
|
||||
this.대쉬보드ToolStripMenuItem,
|
||||
this.toolStripMenuItem17,
|
||||
this.webview2TestToolStripMenuItem});
|
||||
this.기타ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("기타ToolStripMenuItem.Image")));
|
||||
this.기타ToolStripMenuItem.Name = "기타ToolStripMenuItem";
|
||||
this.기타ToolStripMenuItem.Size = new System.Drawing.Size(65, 23);
|
||||
this.기타ToolStripMenuItem.Text = "기타";
|
||||
//
|
||||
// 품목검색ToolStripMenuItem
|
||||
//
|
||||
this.품목검색ToolStripMenuItem.Name = "품목검색ToolStripMenuItem";
|
||||
this.품목검색ToolStripMenuItem.Size = new System.Drawing.Size(171, 24);
|
||||
this.품목검색ToolStripMenuItem.Text = "품목 검색";
|
||||
this.품목검색ToolStripMenuItem.Click += new System.EventHandler(this.품목검색ToolStripMenuItem_Click);
|
||||
//
|
||||
// 대쉬보드ToolStripMenuItem
|
||||
//
|
||||
this.대쉬보드ToolStripMenuItem.Name = "대쉬보드ToolStripMenuItem";
|
||||
this.대쉬보드ToolStripMenuItem.Size = new System.Drawing.Size(171, 24);
|
||||
this.대쉬보드ToolStripMenuItem.Text = "대쉬보드";
|
||||
this.대쉬보드ToolStripMenuItem.Click += new System.EventHandler(this.대쉬보드ToolStripMenuItem_Click);
|
||||
//
|
||||
// toolStripMenuItem17
|
||||
//
|
||||
this.toolStripMenuItem17.Name = "toolStripMenuItem17";
|
||||
this.toolStripMenuItem17.Size = new System.Drawing.Size(168, 6);
|
||||
//
|
||||
// webview2TestToolStripMenuItem
|
||||
//
|
||||
this.webview2TestToolStripMenuItem.Name = "webview2TestToolStripMenuItem";
|
||||
this.webview2TestToolStripMenuItem.Size = new System.Drawing.Size(171, 24);
|
||||
this.webview2TestToolStripMenuItem.Text = "Webview2 Test";
|
||||
this.webview2TestToolStripMenuItem.Click += new System.EventHandler(this.webview2TestToolStripMenuItem_Click);
|
||||
//
|
||||
// 즐겨찾기ToolStripMenuItem
|
||||
//
|
||||
this.즐겨찾기ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("즐겨찾기ToolStripMenuItem.Image")));
|
||||
@@ -823,11 +722,7 @@
|
||||
this.addSIdDataToolStripMenuItem,
|
||||
this.계정목록ToolStripMenuItem,
|
||||
this.그룹정보ToolStripMenuItem1,
|
||||
this.toolStripMenuItem1,
|
||||
this.임의테이블조작ToolStripMenuItem,
|
||||
this.mailBackupToolStripMenuItem,
|
||||
this.메일자동발신테스트ToolStripMenuItem,
|
||||
this.toolStripMenuItem5,
|
||||
this.아이템비활성화하기ToolStripMenuItem,
|
||||
this.sPR설정ToolStripMenuItem,
|
||||
this.toolStripMenuItem13,
|
||||
@@ -916,21 +811,6 @@
|
||||
this.그룹정보ToolStripMenuItem1.Text = "그룹정보";
|
||||
this.그룹정보ToolStripMenuItem1.Click += new System.EventHandler(this.그룹정보ToolStripMenuItem1_Click);
|
||||
//
|
||||
// toolStripMenuItem1
|
||||
//
|
||||
this.toolStripMenuItem1.ForeColor = System.Drawing.Color.HotPink;
|
||||
this.toolStripMenuItem1.Name = "toolStripMenuItem1";
|
||||
this.toolStripMenuItem1.Size = new System.Drawing.Size(302, 24);
|
||||
this.toolStripMenuItem1.Text = "Staff Grid";
|
||||
this.toolStripMenuItem1.Click += new System.EventHandler(this.toolStripMenuItem1_Click);
|
||||
//
|
||||
// 임의테이블조작ToolStripMenuItem
|
||||
//
|
||||
this.임의테이블조작ToolStripMenuItem.Name = "임의테이블조작ToolStripMenuItem";
|
||||
this.임의테이블조작ToolStripMenuItem.Size = new System.Drawing.Size(302, 24);
|
||||
this.임의테이블조작ToolStripMenuItem.Text = "구매내역 suuply 다시 설정 하기";
|
||||
this.임의테이블조작ToolStripMenuItem.Click += new System.EventHandler(this.임의테이블조작ToolStripMenuItem_Click);
|
||||
//
|
||||
// mailBackupToolStripMenuItem
|
||||
//
|
||||
this.mailBackupToolStripMenuItem.Name = "mailBackupToolStripMenuItem";
|
||||
@@ -938,18 +818,6 @@
|
||||
this.mailBackupToolStripMenuItem.Text = "Mail Backup";
|
||||
this.mailBackupToolStripMenuItem.Click += new System.EventHandler(this.mailBackupToolStripMenuItem_Click);
|
||||
//
|
||||
// 메일자동발신테스트ToolStripMenuItem
|
||||
//
|
||||
this.메일자동발신테스트ToolStripMenuItem.Name = "메일자동발신테스트ToolStripMenuItem";
|
||||
this.메일자동발신테스트ToolStripMenuItem.Size = new System.Drawing.Size(302, 24);
|
||||
this.메일자동발신테스트ToolStripMenuItem.Text = "메일자동발신(테스트)";
|
||||
this.메일자동발신테스트ToolStripMenuItem.Click += new System.EventHandler(this.메일자동발신테스트ToolStripMenuItem_Click);
|
||||
//
|
||||
// toolStripMenuItem5
|
||||
//
|
||||
this.toolStripMenuItem5.Name = "toolStripMenuItem5";
|
||||
this.toolStripMenuItem5.Size = new System.Drawing.Size(299, 6);
|
||||
//
|
||||
// 아이템비활성화하기ToolStripMenuItem
|
||||
//
|
||||
this.아이템비활성화하기ToolStripMenuItem.Name = "아이템비활성화하기ToolStripMenuItem";
|
||||
@@ -978,22 +846,6 @@
|
||||
this.프로젝트스케쥴담당자사번업데이트ToolStripMenuItem.Text = "프로젝트스케쥴담당자사번업데이트";
|
||||
this.프로젝트스케쥴담당자사번업데이트ToolStripMenuItem.Click += new System.EventHandler(this.프로젝트스케쥴담당자사번업데이트ToolStripMenuItem_Click);
|
||||
//
|
||||
// 버젼확인ToolStripMenuItem
|
||||
//
|
||||
this.버젼확인ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("버젼확인ToolStripMenuItem.Image")));
|
||||
this.버젼확인ToolStripMenuItem.Name = "버젼확인ToolStripMenuItem";
|
||||
this.버젼확인ToolStripMenuItem.Size = new System.Drawing.Size(93, 23);
|
||||
this.버젼확인ToolStripMenuItem.Text = "버젼확인";
|
||||
this.버젼확인ToolStripMenuItem.Click += new System.EventHandler(this.버젼확인ToolStripMenuItem_Click);
|
||||
//
|
||||
// 설명서ToolStripMenuItem
|
||||
//
|
||||
this.설명서ToolStripMenuItem.Image = ((System.Drawing.Image)(resources.GetObject("설명서ToolStripMenuItem.Image")));
|
||||
this.설명서ToolStripMenuItem.Name = "설명서ToolStripMenuItem";
|
||||
this.설명서ToolStripMenuItem.Size = new System.Drawing.Size(79, 23);
|
||||
this.설명서ToolStripMenuItem.Text = "설명서";
|
||||
this.설명서ToolStripMenuItem.Click += new System.EventHandler(this.설명서ToolStripMenuItem_Click);
|
||||
//
|
||||
// tabControl1
|
||||
//
|
||||
this.tabControl1.Appearance = System.Windows.Forms.TabAppearance.FlatButtons;
|
||||
@@ -1146,7 +998,6 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem itemsToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripStatusLabel sbLogin;
|
||||
private System.Windows.Forms.ToolStripMenuItem codesToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem mn_docu;
|
||||
private System.Windows.Forms.ToolStripMenuItem managementToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem personalInventoryToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem userInfoToolStripMenuItem;
|
||||
@@ -1175,12 +1026,8 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem 권한설정ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 관리ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 재고현황ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem1;
|
||||
private System.Windows.Forms.ToolStripMenuItem 임의테이블조작ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 메일양식ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 메일내역ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem mailBackupToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 메모장ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStrip toolStrip1;
|
||||
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem6;
|
||||
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem7;
|
||||
@@ -1189,32 +1036,24 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem 근태입력ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 근로명부ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripStatusLabel lbSvr;
|
||||
private System.Windows.Forms.ToolStripMenuItem 비용절감ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 목록ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton1;
|
||||
private System.Windows.Forms.ToolStripButton toolStripButton2;
|
||||
private System.Windows.Forms.ToolStripMenuItem 목록ToolStripMenuItem1;
|
||||
private System.Windows.Forms.ToolStripMenuItem 자동입력ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 기타ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 품목검색ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem layoutToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 교육목록ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 양식ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 설명서ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 라이선스ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 휴일연장근무승인ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem9;
|
||||
private System.Windows.Forms.ToolStripMenuItem 휴일연장근무집계표출력ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 메일자동발신테스트ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 출근부출력ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 휴가신청ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem10;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem5;
|
||||
private System.Windows.Forms.ToolStripMenuItem 아이템비활성화하기ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 휴가사용현황ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 개인별근태원장ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 근태입력오류확인ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 버젼확인ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem11;
|
||||
private System.Windows.Forms.ToolStripMenuItem 근태마감ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem sPR설정ToolStripMenuItem;
|
||||
@@ -1233,7 +1072,6 @@
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem15;
|
||||
private System.Windows.Forms.ToolStripMenuItem 개인별근태집계표ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 연차현황ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 대쉬보드ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripStatusLabel sbLoginUseTime;
|
||||
private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem16;
|
||||
private System.Windows.Forms.ToolStripMenuItem 담당자별업무현황ToolStripMenuItem;
|
||||
@@ -1242,8 +1080,6 @@
|
||||
private System.Windows.Forms.ToolStripMenuItem 업무분류및형태설정ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 대체시간이월ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripMenuItem 월별NRCR기준금액입력ToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem17;
|
||||
private System.Windows.Forms.ToolStripMenuItem webview2TestToolStripMenuItem;
|
||||
private System.Windows.Forms.ToolStripStatusLabel sbWeb;
|
||||
private System.Windows.Forms.ToolStripStatusLabel sbChat;
|
||||
private System.Windows.Forms.ToolStripSeparator toolStripMenuItem18;
|
||||
|
||||
@@ -183,8 +183,6 @@ namespace Project
|
||||
this.mn_jago.Visible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_jago);
|
||||
//this.mn_eq.Visible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_equipment);
|
||||
this.mn_kuntae.Visible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_workday);
|
||||
this.mn_docu.Visible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_docu);
|
||||
//this.mn_logdata.Visible = FCOMMON.Util.getBit(FCOMMON.info.Login.gpermission, (int)FCOMMON.eGroupPermission.menu_logdata);
|
||||
|
||||
//220421
|
||||
FCOMMON.info.Disable_8hourover = Pub.setting.Disable8HourOver;
|
||||
@@ -377,12 +375,7 @@ namespace Project
|
||||
|
||||
|
||||
}
|
||||
void menu_save_cost()
|
||||
{
|
||||
string formkey = "SAVECOST";
|
||||
if (!ShowForm(formkey))
|
||||
AddForm(formkey, new FPJ0000.fSaveCostList());
|
||||
}
|
||||
|
||||
|
||||
void menu_dayhistory()
|
||||
{
|
||||
@@ -680,23 +673,6 @@ namespace Project
|
||||
Menu_Dashboard();
|
||||
}
|
||||
|
||||
private void 메일전송ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
if (FCOMMON.info.Login.level < 10)
|
||||
{
|
||||
FCOMMON.Util.MsgE("테스트 기능이므로 개발자만 사용가능 합니다.");
|
||||
return;
|
||||
}
|
||||
FCM0000.fSendMail f = new FCM0000.fSendMail();
|
||||
//f.MdiParent = this;
|
||||
f.Show();
|
||||
}
|
||||
|
||||
private void 업무일지ToolStripMenuItem1_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void workReportImportToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
@@ -801,12 +777,7 @@ namespace Project
|
||||
if (!ShowForm(formkey))
|
||||
AddForm(formkey, new FCM0000.fInventoryJagoList());
|
||||
}
|
||||
void Menu_Note()
|
||||
{
|
||||
string formkey = "NOTELIST";
|
||||
if (!ShowForm(formkey))
|
||||
AddForm(formkey, new FPJ0000.Note.fNote());
|
||||
}
|
||||
|
||||
private void 재고현황ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
Menu_InventoryList();
|
||||
@@ -908,11 +879,6 @@ namespace Project
|
||||
FCOMMON.Util.MsgI("complete");
|
||||
}
|
||||
|
||||
private void sMTRepairLogToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void 메일양식ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
string formkey = "MAILFORM";
|
||||
@@ -920,20 +886,6 @@ namespace Project
|
||||
AddForm(formkey, new FCM0000.fMailform());
|
||||
}
|
||||
|
||||
private void 메일내역ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
string formkey = "MAILLIST";
|
||||
if (!ShowForm(formkey))
|
||||
AddForm(formkey, new FCM0000.Mail.fMailList());
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void pMP데이터베이스업데이트ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
private void mailBackupToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
var f = new FCM0000.Mail.fMailBackup();
|
||||
@@ -941,11 +893,6 @@ namespace Project
|
||||
f.Show();
|
||||
}
|
||||
|
||||
private void 메모장ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
Menu_Note();
|
||||
}
|
||||
|
||||
|
||||
private void toolStripMenuItem7_Click(object sender, EventArgs e)
|
||||
{
|
||||
@@ -976,11 +923,7 @@ namespace Project
|
||||
AddForm(formkey, new FBS0000.fWorkTableUser());
|
||||
}
|
||||
|
||||
private void 비용절감ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
menu_save_cost();
|
||||
}
|
||||
|
||||
|
||||
private void 목록ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
menu_projecT_list();
|
||||
@@ -1018,12 +961,6 @@ namespace Project
|
||||
AddForm(formkey, new FPJ0000.fProjectLayout());
|
||||
}
|
||||
|
||||
private void 교육목록ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
string formkey = "EDLIST";
|
||||
if (!ShowForm(formkey))
|
||||
AddForm(formkey, new FED0000.fEdulist());
|
||||
}
|
||||
|
||||
private void 양식ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
@@ -1032,16 +969,6 @@ namespace Project
|
||||
AddForm(formkey, new FCM0000.fJRForm());
|
||||
}
|
||||
|
||||
private void 설명서ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
// 로컬 PDF 파일 열기
|
||||
var pdfPath = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Manual.pdf");
|
||||
if (System.IO.File.Exists(pdfPath))
|
||||
Util.RunExplorer(pdfPath);
|
||||
else
|
||||
Util.MsgE("설명서 파일을 찾을 수 없습니다.");
|
||||
}
|
||||
|
||||
private void 라이선스ToolStripMenuItem_Click(object sender, EventArgs e)
|
||||
{
|
||||
string formkey = "LICENSE";
|
||||
|
||||
@@ -150,94 +150,6 @@
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIkAA/CByYwUOGgQgTfvDgwINChQw9IHD4UGBEABAuUETo
|
||||
oaNEDxg1erTo4IGDiQAGNFCA4QABAg4jdkxpoYOABAkUGKD4UaUFARUCMLCwk6NGCkADRKBQNKEHDB6E
|
||||
RpDQlCNUoRYkSJhQYKPFAyqZThjbleMBDxw07CxQYMKGBRs9vNTQcaGHBXjjjuS4t2LFgAA7
|
||||
</value>
|
||||
</data>
|
||||
<data name="managementToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQQAP/cpv/GcNvb292RIP+kG/+5T6m5rMStgK13Jv+wOt2TJKmwrP/zcZOru////wAA
|
||||
AP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAQACwAAAAAEAAQAAAIZQAhCBxIsKBABgQBGCTIoCEEBgEWDmRAgMAAAwUgKJQ4
|
||||
QIEDBwciSmw4QMDHBRIBBCiQAIGABw8aSBSosgHMmCkH2oQpc6bAnTh9QgDa0ydRoUMffCw6s4FJB0wl
|
||||
OoWKdGiDBgEBADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="mn_docu.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAJrM+3OSsKfS++zx9uXt9Yis1FdwkZW51ElVa8fj/bba/NXb5PL2+o276b3d/VJh
|
||||
e7TR4ENLXNXn8KLD536kwIyzzJ/E2KjL3t7n7ykxQz5FVa/W/OLp8I+w1P///////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAItgA9CBxIcOCHgx4kQIBwwcKBChQiBgjgASEECQQGZNRI
|
||||
oGMDAxU9QCCQoGRJBygZNPgo8AKDBB1iyuzAoOYDgRZeonSgoGfPDQxuejjwcsLMAkgFBBVY4aVPBRui
|
||||
ClCKQCCFojGRIm0ggEBVDxQG8IQqdSoAAhGsijWqdSsADmk9BBBLdqoAAHgxaBAYgAHPsnjxDtjrwcAC
|
||||
Bhw5KOaAYQCEDHwNSH7wAAGCCBE0aMggtKBnggEBADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="기타ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAKnrVsfvlYnVOHy8KnfJLJneRWqyJLvth1W7GzGTEVWnHT2aFIPNMkuiGmrKJGKt
|
||||
Io7eOXa5KU64GDiWE1yqIHK3KHa5J0WeGGC/IG+1JW/FKGjCJHnRLWrOIP///////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIqwA/CBTooeDAgwg9DFjoAWHCAQEAFBjQ0CHBAQAEFLBQ
|
||||
8WDBjxEARPjY8YPChREiZshg4EFJhQFiFmBQAAIECh8vLoTAgAEBAgIUeGhwoaHCghUE/NTAgcCFAxAW
|
||||
GDWZQamGDRgwIEAgYUJBjh4MCODAAYODBRMkSEhQMAJLARQUNODgwGsCtiYjHBDgsuCCDl4NmqxQAK7R
|
||||
v3gHemD5QK4HtBMSEyRplOTBgAA7
|
||||
</value>
|
||||
</data>
|
||||
<data name="즐겨찾기ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIMPAEWi6azap1WzS9LusYrSbApexXLHV+z41vH559Ltw8ns+pe75hBs0iCZEP//////
|
||||
/yH/C05FVFNDQVBFMi4wAwEBAAAh+QQBAAAPACwAAAAAEAAQAAAIiwAfCBxIsKDBBw4SOjg4MGGDBgwY
|
||||
JDzo4OEBBAgUMGiwkGBFBAcODAAAYMEAjh4ZIBgwQAAAAgZOdkTIQEGCAQRICoAZACVNBQACkHxpQEDP
|
||||
jg5qLhgKQIDTowIrJoA5NGKDABIbNpjqlEGBAguyag3QEiLYsDOjPgwQYEFYsQUdRpSY1qDCugzzBgQA
|
||||
Ow==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btDev.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAKxoR8VkLxFw1feVPITSWv+eQv7Qo0Cc6OyIN/v7+3PLTSCZEFy17Wa6XuT1x2bG
|
||||
Q3nNUU6vRXPAa9mLXMTkwJZEHJt7LL5aJ/z8/O2KONx3L/ubP/r6+rtVI////////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIpgA/CBxIsOBADBgEeljoweAHDB06eIi4QICAhRwOdjAQ
|
||||
kaMDCgwELOiQ8WGHAQYMFIjIgMEBCBEzQkSwoUCBCR0OSGigIKZJDQM2cKxwoAGBBx0ykISoIcOGiAcO
|
||||
EFCAVCkHphueAtgaIYKFpks7NM0qFIDFCxk0kPyQQCzZiB0CbLAqsO1YslnTrq0r9m4GvSUHcoioobDa
|
||||
vQU5DIar2KFgxYEHBgQAOw==
|
||||
</value>
|
||||
</data>
|
||||
<data name="버젼확인ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAGm6/idTd4yTmF+v8Xa37KvW+lyh3KHJ62aq41ee2bXZ98nm/2mt5W2Ck5XN/C1c
|
||||
hEZieho8WXXA/2Gn4P39/W+y6V+l3qjP8Njt/lx2izxPYGyv51Oa1EJWZ////////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIqgA/CPzgoaBBDwMTEoQgoGGDBhAQKvSQAcOCBQUcaHwg
|
||||
USBFDARCEqhQgYEEjh47gKxwweAFBAgkREDooYMCAhs8XGCAwMOEmB1o2qywYSdMnxMABCVocwMDngUP
|
||||
GLAAYCZTBTARHPAgdWpVoQV+TrBgoGwCA1+ZOkgwduuBBAk4pCWogUBcDnjxAgjQkS4BAAMCD9jrgcJE
|
||||
DQ8eBAjwYKZhhQQPFoRMuXJAADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="설명서ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQAAP/99v/qaOvOSem4M+zSSv/ypf/ug//1w//2zP/xnv/te//zrf/0uv/41/nWdufB
|
||||
MP/vkevTVf/rcv/0s//wlv/57OvRM//vi+/OQtaXIuuYEuvTLNyhJ+vHUP///////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIoAA/CBxIsCBBDwgTKizogUCEhwQIYLBgYYOHgR4cKPQA
|
||||
oMEBBAgsfsjoAQGDCQsKJEhAAcKBChYQajyZkiWECwYMAHiAkAAAlCop4FRA9ABPDxgqABVqQIGEpxQG
|
||||
IMTQoCaEphICaFXAAaEABCmZZtUawECGi0gRHGigloFWCgzOYhRAt0OHASg1yD24cUAFDRcNMhwAWLBB
|
||||
D4UNMwz8ISAAOw==
|
||||
</value>
|
||||
</data>
|
||||
<data name="codesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQAAHan1azQ4ldvj9vp9HSQruTr80lVa+vx9pu811SRuXifuj13uYyz2VFhe4Gt2UNL
|
||||
XPL1+Orv+ufu9YOqxYyzzNHW3SkxQz5FVWag2T6CuZe3y5G0t+Do7+r0/77a9f///yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQAAAAAACwAAAAAEAAQAAAIrwA/CBxIsKDAAQESIkBAYYICBQQICCAgMMAACQMyaswo
|
||||
oUKDih0SZMiwoKTJBQcEVDyAoEMHDy5hdnAg4eMHBBIQeNjJcyeDAjYRRNAQs+hMoAIpRNjQs6eDAgYE
|
||||
TshpVCYAqAIV5GzKU0GBB1klMKjqEgMHsB8IiOW60+wFgQQgIGDgoC4AABgwADjw9oOAChAkSChAmIPh
|
||||
AxUswBXAuEEDAwYePLhwwYJNg5gFBgQAOw==
|
||||
</value>
|
||||
</data>
|
||||
<data name="메일양식ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAHWSrbTY+6nU/I+74/r8/drj7FlxkUlVa9Xp/eLs9cvT2oWpxG+bwqPQ+57N++v1
|
||||
/lJgeabK7JnB5kNLXJG0z5nA1oyw0SkxQz5FVb7e/aC91tHl8qXB2n2gu////////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIpAA/CBxIsKDBgwQ9KFzIsKHCDRUqUFhAsQOAiwAMQFBY
|
||||
gYDHjyAJKNjogQIBChoQZAgQAEGCiQUOKFxAIEMEDhsQPEDAQEKDBzI9dKiZIYOFowwENPg5QeHQlRIi
|
||||
SJAwYIADBwWaegCQIMAACQEEKK2KFYNCrgMihBXbwEHVBGY9GFCQIEGBu3jvKrhw1oBfCBAOHJgwAQOG
|
||||
CyQdKlaIsLHjggEBADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="mn_purchase.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
@@ -342,6 +254,54 @@
|
||||
Nq7P2vgxc+6aH3lHuKt0Ou1PJpM2vUR2cdVbXL34iF+O65kFQWDmnQCSJEn+WCxm00tc/2IyS5K0Yb4X
|
||||
QIpGoz5RFG16YM1mc6d5L4AUiUR8oVDIJjPP81vm/wJIgiD4eJ5/8O98rT+Jli/+ECJFiAAAAABJRU5E
|
||||
rkJggg==
|
||||
</value>
|
||||
</data>
|
||||
<data name="managementToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQQAP/cpv/GcNvb292RIP+kG/+5T6m5rMStgK13Jv+wOt2TJKmwrP/zcZOru////wAA
|
||||
AP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAQACwAAAAAEAAQAAAIZQAhCBxIsKBABgQBGCTIoCEEBgEWDmRAgMAAAwUgKJQ4
|
||||
QIEDBwciSmw4QMDHBRIBBCiQAIGABw8aSBSosgHMmCkH2oQpc6bAnTh9QgDa0ydRoUMffCw6s4FJB0wl
|
||||
OoWKdGiDBgEBADs=
|
||||
</value>
|
||||
</data>
|
||||
<data name="즐겨찾기ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIMPAEWi6azap1WzS9LusYrSbApexXLHV+z41vH559Ltw8ns+pe75hBs0iCZEP//////
|
||||
/yH/C05FVFNDQVBFMi4wAwEBAAAh+QQBAAAPACwAAAAAEAAQAAAIiwAfCBxIsKDBBw4SOjg4MGGDBgwY
|
||||
JDzo4OEBBAgUMGiwkGBFBAcODAAAYMEAjh4ZIBgwQAAAAgZOdkTIQEGCAQRICoAZACVNBQACkHxpQEDP
|
||||
jg5qLhgKQIDTowIrJoA5NGKDABIbNpjqlEGBAguyag3QEiLYsDOjPgwQYEFYsQUdRpSY1qDCugzzBgQA
|
||||
Ow==
|
||||
</value>
|
||||
</data>
|
||||
<data name="btDev.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAKxoR8VkLxFw1feVPITSWv+eQv7Qo0Cc6OyIN/v7+3PLTSCZEFy17Wa6XuT1x2bG
|
||||
Q3nNUU6vRXPAa9mLXMTkwJZEHJt7LL5aJ/z8/O2KONx3L/ubP/r6+rtVI////////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIpgA/CBxIsOBADBgEeljoweAHDB06eIi4QICAhRwOdjAQ
|
||||
kaMDCgwELOiQ8WGHAQYMFIjIgMEBCBEzQkSwoUCBCR0OSGigIKZJDQM2cKxwoAGBBx0ykISoIcOGiAcO
|
||||
EFCAVCkHphueAtgaIYKFpks7NM0qFIDFCxk0kPyQQCzZiB0CbLAqsO1YslnTrq0r9m4GvSUHcoioobDa
|
||||
vQU5DIar2KFgxYEHBgQAOw==
|
||||
</value>
|
||||
</data>
|
||||
<data name="codesToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQAAHan1azQ4ldvj9vp9HSQruTr80lVa+vx9pu811SRuXifuj13uYyz2VFhe4Gt2UNL
|
||||
XPL1+Orv+ufu9YOqxYyzzNHW3SkxQz5FVWag2T6CuZe3y5G0t+Do7+r0/77a9f///yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQAAAAAACwAAAAAEAAQAAAIrwA/CBxIsKDAAQESIkBAYYICBQQICCAgMMAACQMyaswo
|
||||
oUKDih0SZMiwoKTJBQcEVDyAoEMHDy5hdnAg4eMHBBIQeNjJcyeDAjYRRNAQs+hMoAIpRNjQs6eDAgYE
|
||||
TshpVCYAqAIV5GzKU0GBB1klMKjqEgMHsB8IiOW60+wFgQQgIGDgoC4AABgwADjw9oOAChAkSChAmIPh
|
||||
AxUswBXAuEEDAwYePLhwwYJNg5gFBgQAOw==
|
||||
</value>
|
||||
</data>
|
||||
<data name="메일양식ToolStripMenuItem.Image" type="System.Drawing.Bitmap, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>
|
||||
R0lGODlhEAAQAIQfAHWSrbTY+6nU/I+74/r8/drj7FlxkUlVa9Xp/eLs9cvT2oWpxG+bwqPQ+57N++v1
|
||||
/lJgeabK7JnB5kNLXJG0z5nA1oyw0SkxQz5FVb7e/aC91tHl8qXB2n2gu////////yH/C05FVFNDQVBF
|
||||
Mi4wAwEBAAAh+QQBAAAfACwAAAAAEAAQAAAIpAA/CBxIsKDBgwQ9KFzIsKHCDRUqUFhAsQOAiwAMQFBY
|
||||
gYDHjyAJKNjogQIBChoQZAgQAEGCiQUOKFxAIEMEDhsQPEDAQEKDBzI9dKiZIYOFowwENPg5QeHQlRIi
|
||||
SJAwYIADBwWaegCQIMAACQEEKK2KFYNCrgMihBXbwEHVBGY9GFCQIEGBu3jvKrhw1oBfCBAOHJgwAQOG
|
||||
CyQdKlaIsLHjggEBADs=
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="toolStrip1.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
@@ -445,14 +405,14 @@
|
||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8
|
||||
YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAIHSURBVDhPY2CAgZnGrAxLtTMZluosYFim08KwWFcdLL5K
|
||||
i4dhmVYhwzLteQzLdFoZlqlLwfWggKU6qxmW6fyH48U6PxiWaHoyLNO8jiK+VOsJwyotCTTNWjYgSYG1
|
||||
lv/9Tub8T7lc/z/3ZMX/eXNiP9Q05f9LvdzwP/Jc2X/VbV5QQ7T7UA1YpjOBf435seQr9d+Kb/X8L7nZ
|
||||
lv/9Tub8T7lc/z/3ZMX/eXNiP9TU5/9LvdzwP/Jc2X/VbV5QQ7T7UA1YpjOBf435seQr9d+Kb/X8L7nZ
|
||||
8//QzJz/jzrT/l+dlPkTJAbDJrtDTzAs0bmGasBSXcHiW911MEUdh+r/Xy+N+L+/1f7v9UKvzz3bCuEG
|
||||
FN/qugBSj2oAAwND8c3uuTBFlZc6lp0vDDl4Odnt28Ugtf/bKpz2Fl/s+AiSK7jZ8w5dLxjAXFB0s6cE
|
||||
xH/RGiH+uMzz/5UMrT9Psm3UCm/26hff6vpcdKvnPLpeMCi+02NcfKPrOIz/uNjT81Gpx38wLvHyAKu5
|
||||
2V1afKu7BUUjMii+0y0GYz+p8MqCGfCk0jsTJFZ/v54j99ZEPhRNuMCjCs++J+Ve/0H4cblXL7o8QfC4
|
||||
zHMj3AulHhvQ5fGC//vrOV50RZ1+0xP3H4RfdkaeAomhq0MB/w70a/490Nv3/0DvmfsHen//P9D7HxlD
|
||||
xc6A1ezv0UBo3DaR/e+B3vnH2uv/oWvChUFq/x7omQfSy/B3f28vugJiMUgvw78DfbZ/D/Qs/3+gdxUp
|
||||
GKQHpBcAG6GqRqSz0QwAAAAASUVORK5CYII=
|
||||
xc6A1ezv0UBo3DaR/e+B3vmHmuv/oWvChUFq/x7omQfSy/B3f28vugJiMUgvw78DfbZ/D/Qs/3+gdxUp
|
||||
GKQHpBcAByWqM8i7D2wAAAAASUVORK5CYII=
|
||||
</value>
|
||||
</data>
|
||||
<metadata name="$this.TrayHeight" type="System.Int32, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
|
||||
|
||||
@@ -6,6 +6,8 @@ import { PatchList } from '@/pages/PatchList';
|
||||
import { BugReport } from '@/pages/BugReport';
|
||||
import { MailList } from '@/pages/MailList';
|
||||
import { Customs } from '@/pages/Customs';
|
||||
import { LicenseList } from '@/components/license/LicenseList';
|
||||
import { PartList } from '@/pages/PartList';
|
||||
import { comms } from '@/communication';
|
||||
import { UserInfo } from '@/types';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
@@ -101,6 +103,8 @@ export default function App() {
|
||||
<Route path="/patch-list" element={<PatchList />} />
|
||||
<Route path="/bug-report" element={<BugReport />} />
|
||||
<Route path="/mail-list" element={<MailList />} />
|
||||
<Route path="/license" element={<LicenseList />} />
|
||||
<Route path="/partlist" element={<PartList />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
{/* Tailwind Breakpoint Indicator - 개발용 */}
|
||||
|
||||
@@ -42,6 +42,8 @@ import type {
|
||||
BoardItem,
|
||||
MailItem,
|
||||
CustomItem,
|
||||
LicenseItem,
|
||||
PartListItem,
|
||||
} from '@/types';
|
||||
|
||||
// WebView2 환경 감지
|
||||
@@ -464,6 +466,21 @@ class CommunicationLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public async saveProjectHistory(historyData: { idx?: number; pidx: number; pdate: string; progress: number; remark: string }): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Project_SaveHistory(
|
||||
historyData.idx || 0,
|
||||
historyData.pidx,
|
||||
historyData.pdate,
|
||||
historyData.progress,
|
||||
historyData.remark
|
||||
);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('PROJECT_SAVE_HISTORY', 'PROJECT_SAVE_HISTORY_RESULT', historyData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== Login API =====
|
||||
|
||||
@@ -1361,6 +1378,64 @@ class CommunicationLayer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 데이터 추가 (발송 대기열)
|
||||
* @param cate 분류
|
||||
* @param subject 제목
|
||||
* @param fromlist 발신자
|
||||
* @param tolist 수신자
|
||||
* @param cc 참조
|
||||
* @param bcc 숨은참조
|
||||
* @param body 내용
|
||||
* @returns ApiResponse
|
||||
*/
|
||||
public async addMailData(cate: string, subject: string, fromlist: string, tolist: string, cc: string, bcc: string, body: string): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Mail_AddData(cate, subject, fromlist, tolist, cc, bcc, body);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('MAIL_ADD_DATA', 'MAIL_ADD_DATA_RESULT', { cate, subject, fromlist, tolist, cc, bcc, body });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 메일 직접 발송 (SMTP)
|
||||
* @param cate 분류
|
||||
* @param subject 제목
|
||||
* @param fromlist 발신자
|
||||
* @param tolist 수신자
|
||||
* @param cc 참조
|
||||
* @param bcc 숨은참조
|
||||
* @param body 내용
|
||||
* @returns ApiResponse
|
||||
*/
|
||||
public async sendMailDirect(cate: string, subject: string, fromlist: string, tolist: string, cc: string, bcc: string, body: string): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Mail_SendDirect(cate, subject, fromlist, tolist, cc, bcc, body);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('MAIL_SEND_DIRECT', 'MAIL_SEND_DIRECT_RESULT', { cate, subject, fromlist, tolist, cc, bcc, body });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Outlook으로 메일 미리보기/발송
|
||||
* @param subject 제목
|
||||
* @param tolist 수신자
|
||||
* @param cc 참조
|
||||
* @param bcc 숨은참조
|
||||
* @param body 내용
|
||||
* @returns ApiResponse
|
||||
*/
|
||||
public async sendMailOutlook(subject: string, tolist: string, cc: string, bcc: string, body: string): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.Mail_SendOutlook(subject, tolist, cc, bcc, body);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('MAIL_SEND_OUTLOOK', 'MAIL_SEND_OUTLOOK_RESULT', { subject, tolist, cc, bcc, body });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 업체정보 목록 조회
|
||||
* @param searchKey 검색어
|
||||
@@ -1388,6 +1463,175 @@ class CommunicationLayer {
|
||||
return this.wsRequest<ApiResponse<CustomItem>>('CUSTOMS_GET_DETAIL', 'CUSTOMS_DETAIL_DATA', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 목록 조회
|
||||
* @returns ApiResponse<LicenseItem[]>
|
||||
*/
|
||||
public async getLicenseList(): Promise<ApiResponse<LicenseItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_GetList();
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<LicenseItem[]>>('LICENSE_GET_LIST', 'LICENSE_LIST_DATA', {});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 추가
|
||||
*/
|
||||
public async addLicense(
|
||||
name: string,
|
||||
version: string,
|
||||
meterialNo: string,
|
||||
supply: string,
|
||||
qty: number,
|
||||
uids: string,
|
||||
serialNo: string,
|
||||
remark: string,
|
||||
sdate: string,
|
||||
edate: string,
|
||||
manu: string,
|
||||
expire: boolean
|
||||
): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_Add(name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('LICENSE_ADD', 'LICENSE_ADD_RESULT', {
|
||||
name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 수정
|
||||
*/
|
||||
public async updateLicense(
|
||||
idx: number,
|
||||
name: string,
|
||||
version: string,
|
||||
meterialNo: string,
|
||||
supply: string,
|
||||
qty: number,
|
||||
uids: string,
|
||||
serialNo: string,
|
||||
remark: string,
|
||||
sdate: string,
|
||||
edate: string,
|
||||
manu: string,
|
||||
expire: boolean
|
||||
): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_Update(idx, name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('LICENSE_UPDATE', 'LICENSE_UPDATE_RESULT', {
|
||||
idx, name, version, meterialNo, supply, qty, uids, serialNo, remark, sdate, edate, manu, expire
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 삭제
|
||||
*/
|
||||
public async deleteLicense(idx: number): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_Delete(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('LICENSE_DELETE', 'LICENSE_DELETE_RESULT', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 폴더 열기
|
||||
*/
|
||||
public async openLicenseFolder(idx: number): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_OpenFolder(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('LICENSE_OPEN_FOLDER', 'LICENSE_OPEN_FOLDER_RESULT', { idx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 라이선스 CSV 내보내기
|
||||
*/
|
||||
public async exportLicenseCSV(filePath: string): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.License_ExportCSV(filePath);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('LICENSE_EXPORT_CSV', 'LICENSE_EXPORT_CSV_RESULT', { filePath });
|
||||
}
|
||||
}
|
||||
|
||||
// ===== PartList API =====
|
||||
|
||||
/**
|
||||
* 프로젝트별 파트리스트 조회
|
||||
*/
|
||||
public async getPartList(projectIdx: number): Promise<ApiResponse<PartListItem[]>> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PartList_GetList(projectIdx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse<PartListItem[]>>('PARTLIST_GET_LIST', 'PARTLIST_LIST_DATA', { projectIdx });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파트리스트 항목 저장 (추가/수정)
|
||||
*/
|
||||
public async savePartList(
|
||||
idx: number,
|
||||
projectIdx: number,
|
||||
itemgroup: string,
|
||||
itemname: string,
|
||||
item: string,
|
||||
itemmodel: string,
|
||||
itemscale: string,
|
||||
itemunit: string,
|
||||
qty: number,
|
||||
price: number,
|
||||
itemsupply: string,
|
||||
itemsupplyidx: number,
|
||||
itemmanu: string,
|
||||
itemsid: string,
|
||||
option1: string,
|
||||
remark: string,
|
||||
no: number,
|
||||
qtybuy: number
|
||||
): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PartList_Save(
|
||||
idx, projectIdx, itemgroup, itemname, item, itemmodel, itemscale,
|
||||
itemunit, qty, price, itemsupply, itemsupplyidx, itemmanu, itemsid,
|
||||
option1, remark, no, qtybuy
|
||||
);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('PARTLIST_SAVE', 'PARTLIST_SAVE_RESULT', {
|
||||
idx, projectIdx, itemgroup, itemname, item, itemmodel, itemscale,
|
||||
itemunit, qty, price, itemsupply, itemsupplyidx, itemmanu, itemsid,
|
||||
option1, remark, no, qtybuy
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 파트리스트 항목 삭제
|
||||
*/
|
||||
public async deletePartList(idx: number): Promise<ApiResponse> {
|
||||
if (isWebView && machine) {
|
||||
const result = await machine.PartList_Delete(idx);
|
||||
return JSON.parse(result);
|
||||
} else {
|
||||
return this.wsRequest<ApiResponse>('PARTLIST_DELETE', 'PARTLIST_DELETE_RESULT', { idx });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const comms = new CommunicationLayer();
|
||||
|
||||
@@ -262,7 +262,9 @@ export function JobreportEditModal({
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{/* 헤더 */}
|
||||
<div className="px-6 py-4 border-b border-white/10 flex items-center justify-between sticky top-0 bg-slate-800/95 backdrop-blur z-10">
|
||||
<div className={`px-6 py-4 border-b border-white/10 flex items-center justify-between sticky top-0 backdrop-blur z-10 ${
|
||||
editingItem ? 'bg-slate-800/95' : 'bg-primary-600/30'
|
||||
}`}>
|
||||
<h2 className="text-xl font-semibold text-white flex items-center">
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
{editingItem ? '업무일지 수정' : '업무일지 등록'}
|
||||
|
||||
@@ -22,8 +22,11 @@ import {
|
||||
Building,
|
||||
Star,
|
||||
Bug,
|
||||
Settings,
|
||||
Key,
|
||||
} from 'lucide-react';
|
||||
import { clsx } from 'clsx';
|
||||
import { comms } from '@/communication';
|
||||
import { UserInfoDialog } from '@/components/user/UserInfoDialog';
|
||||
import { UserGroupDialog } from '@/components/user/UserGroupDialog';
|
||||
import { KuntaeErrorCheckDialog } from '@/components/kuntae/KuntaeErrorCheckDialog';
|
||||
@@ -39,6 +42,7 @@ interface NavItem {
|
||||
icon: React.ElementType;
|
||||
label: string;
|
||||
action?: string;
|
||||
className?: string; // 추가: 클래스 이름
|
||||
}
|
||||
|
||||
interface SubMenu {
|
||||
@@ -54,6 +58,7 @@ interface MenuItem {
|
||||
label: string;
|
||||
submenu?: SubMenu;
|
||||
action?: string;
|
||||
className?: string; // gold 등 스타일 적용용
|
||||
}
|
||||
|
||||
interface DropdownMenuConfig {
|
||||
@@ -78,6 +83,13 @@ const leftDropdownMenus: DropdownMenuConfig[] = [
|
||||
{ type: 'action', icon: AlertTriangle, label: '오류검사', action: 'kuntaeErrorCheck' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '관리',
|
||||
icon: Settings,
|
||||
items: [
|
||||
{ type: 'link', path: '/license', icon: Key, label: '라이선스' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 좌측 단독 액션 버튼
|
||||
@@ -90,44 +102,54 @@ const rightNavItems: NavItem[] = [
|
||||
];
|
||||
|
||||
// 드롭다운 메뉴 (2단계 지원)
|
||||
const dropdownMenus: DropdownMenuConfig[] = [
|
||||
{
|
||||
label: '공용정보',
|
||||
icon: Database,
|
||||
items: [
|
||||
{ type: 'link', path: '/common', icon: Code, label: '공용코드' },
|
||||
{ type: 'link', path: '/items', icon: Package, label: '품목정보' },
|
||||
{ type: 'link', path: '/customs', icon: Building, label: '업체정보' },
|
||||
{
|
||||
type: 'submenu',
|
||||
icon: Users,
|
||||
label: '사용자',
|
||||
submenu: {
|
||||
label: '사용자',
|
||||
const getDropdownMenus = (userLevel: number, userCode: string): DropdownMenuConfig[] => {
|
||||
const mailListItem = {
|
||||
type: 'link' as const,
|
||||
path: '/mail-list',
|
||||
icon: Mail,
|
||||
label: '메일 내역',
|
||||
className: (userCode === '395552') ? 'text-[gold] font-bold' : '',
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
label: '공용정보',
|
||||
icon: Database,
|
||||
items: [
|
||||
{ type: 'link', path: '/common', icon: Code, label: '공용코드' },
|
||||
{ type: 'link', path: '/items', icon: Package, label: '품목정보' },
|
||||
{ type: 'link', path: '/customs', icon: Building, label: '업체정보' },
|
||||
{
|
||||
type: 'submenu',
|
||||
icon: Users,
|
||||
items: [
|
||||
{ icon: User, label: '정보', action: 'userInfo' },
|
||||
{ path: '/user/list', icon: Users, label: '목록' },
|
||||
{ path: '/user/auth', icon: Shield, label: '권한' },
|
||||
{ icon: Users, label: '그룹정보', action: 'userGroup' },
|
||||
],
|
||||
label: '사용자',
|
||||
submenu: {
|
||||
label: '사용자',
|
||||
icon: Users,
|
||||
items: [
|
||||
{ icon: User, label: '정보', action: 'userInfo' },
|
||||
{ path: '/user/list', icon: Users, label: '목록' },
|
||||
{ path: '/user/auth', icon: Shield, label: '권한' },
|
||||
{ icon: Users, label: '그룹정보', action: 'userGroup' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{ type: 'link', path: '/monthly-work', icon: CalendarDays, label: '월별근무표' },
|
||||
{ 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: '/bug-report', icon: Bug, label: '버그 신고' },
|
||||
{ type: 'link', path: '/mail-list', icon: Mail, label: '메일 내역' },
|
||||
],
|
||||
},
|
||||
];
|
||||
{ type: 'link', path: '/monthly-work', icon: CalendarDays, label: '월별근무표' },
|
||||
{ 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: '/bug-report', icon: Bug, label: '버그 신고' },
|
||||
...(userLevel >= 9 || userCode === '395552' ? [mailListItem] : []),
|
||||
],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
function DropdownNavMenu({
|
||||
menu,
|
||||
@@ -194,7 +216,7 @@ function DropdownNavMenu({
|
||||
'flex items-center space-x-2 px-4 py-2 text-sm transition-colors',
|
||||
isActive
|
||||
? 'bg-white/20 text-white'
|
||||
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: (item.className || 'text-white/70 hover:bg-white/10 hover:text-white')
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -326,7 +348,7 @@ function MobileDropdownMenu({
|
||||
'flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-white/20 text-white'
|
||||
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: (item.className || 'text-white/70 hover:bg-white/10 hover:text-white')
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -373,7 +395,7 @@ function MobileDropdownMenu({
|
||||
'flex items-center space-x-3 px-4 py-2 rounded-lg transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-white/20 text-white'
|
||||
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
||||
: (item.className || 'text-white/70 hover:bg-white/10 hover:text-white')
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -409,6 +431,27 @@ export function Header(_props: HeaderProps) {
|
||||
const [showUserGroupDialog, setShowUserGroupDialog] = useState(false);
|
||||
const [showKuntaeErrorCheckDialog, setShowKuntaeErrorCheckDialog] = useState(false);
|
||||
const [showFavoriteDialog, setShowFavoriteDialog] = useState(false);
|
||||
const [userLevel, setUserLevel] = useState<number>(0);
|
||||
const [userCode, setUserCode] = useState<string>('');
|
||||
|
||||
// 사용자 정보 로드
|
||||
useEffect(() => {
|
||||
const loadUserInfo = async () => {
|
||||
try {
|
||||
const loginStatus = await comms.checkLoginStatus();
|
||||
console.log('Login Status:', loginStatus);
|
||||
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
|
||||
const user = loginStatus.User as { Level?: number; Id?: string };
|
||||
setUserLevel(user.Level || 0);
|
||||
setUserCode(user.Id || '');
|
||||
console.log('userLevel:', user.Level, 'userCode:', user.Id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 로드 오류:', error);
|
||||
}
|
||||
};
|
||||
loadUserInfo();
|
||||
}, []);
|
||||
|
||||
const handleAction = (action: string) => {
|
||||
if (action === 'userInfo') {
|
||||
@@ -485,7 +528,7 @@ export function Header(_props: HeaderProps) {
|
||||
{/* Desktop Navigation - Right */}
|
||||
<nav className="hidden lg:flex items-center space-x-1">
|
||||
{/* 드롭다운 메뉴들 (공용정보) */}
|
||||
{dropdownMenus.map((menu) => (
|
||||
{getDropdownMenus(userLevel, userCode).map((menu) => (
|
||||
<DropdownNavMenu key={menu.label} menu={menu} onAction={handleAction} />
|
||||
))}
|
||||
|
||||
@@ -574,7 +617,7 @@ export function Header(_props: HeaderProps) {
|
||||
<div className="border-t border-white/10 my-2" />
|
||||
|
||||
{/* 우측 드롭다운 메뉴들 (공용정보) */}
|
||||
{dropdownMenus.map((menu) => (
|
||||
{getDropdownMenus(userLevel, userCode).map((menu) => (
|
||||
<MobileDropdownMenu
|
||||
key={menu.label}
|
||||
menu={menu}
|
||||
|
||||
@@ -12,6 +12,7 @@ interface StatusBarProps {
|
||||
export function StatusBar({ userName, userDept, isConnected }: StatusBarProps) {
|
||||
const [currentTime, setCurrentTime] = useState(new Date());
|
||||
const [versionDisplay, setVersionDisplay] = useState('');
|
||||
const [hasNewVersion, setHasNewVersion] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
|
||||
@@ -25,6 +26,7 @@ export function StatusBar({ userName, userDept, isConnected }: StatusBarProps) {
|
||||
const result = await comms.getAppVersion();
|
||||
if (result.Success) {
|
||||
setVersionDisplay(result.DisplayVersion);
|
||||
setHasNewVersion(result.HasNewVersion || false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('버전 정보 로드 오류:', error);
|
||||
@@ -41,8 +43,13 @@ export function StatusBar({ userName, userDept, isConnected }: StatusBarProps) {
|
||||
</div>
|
||||
|
||||
{/* Center: App Version */}
|
||||
<div className="text-white/50">
|
||||
<div className={`font-medium ${hasNewVersion ? 'text-yellow-400 animate-pulse' : 'text-white/50'}`}>
|
||||
{versionDisplay || 'Loading...'}
|
||||
{hasNewVersion && (
|
||||
<span className="ml-2 text-xs bg-yellow-500/20 text-yellow-400 px-2 py-0.5 rounded animate-pulse">
|
||||
업데이트 필요
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right: Connection Status & Time */}
|
||||
|
||||
293
Project/frontend/src/components/license/LicenseEditDialog.tsx
Normal file
293
Project/frontend/src/components/license/LicenseEditDialog.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
import { X, Save, Trash2 } from 'lucide-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { LicenseItem } from '@/types';
|
||||
|
||||
interface LicenseEditDialogProps {
|
||||
item: LicenseItem | null;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSave: (data: Partial<LicenseItem>) => Promise<void>;
|
||||
onDelete?: (idx: number) => Promise<void>;
|
||||
}
|
||||
|
||||
export function LicenseEditDialog({ item, isOpen, onClose, onSave, onDelete }: LicenseEditDialogProps) {
|
||||
const [formData, setFormData] = useState<Partial<LicenseItem>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
setFormData({
|
||||
idx: item.idx,
|
||||
expire: item.expire || false,
|
||||
name: item.name || '',
|
||||
version: item.version || '',
|
||||
meterialNo: item.meterialNo || '',
|
||||
supply: item.supply || '',
|
||||
qty: item.qty || 1,
|
||||
uids: item.uids || '',
|
||||
serialNo: item.serialNo || '',
|
||||
remark: item.remark || '',
|
||||
sdate: item.sdate ? item.sdate.split('T')[0] : '',
|
||||
edate: item.edate ? item.edate.split('T')[0] : '',
|
||||
manu: item.manu || '',
|
||||
});
|
||||
}
|
||||
}, [item]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!formData.name?.trim()) {
|
||||
alert('제품명을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await onSave(formData);
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Save failed:', error);
|
||||
alert('저장에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
if (!formData.idx) return;
|
||||
if (!confirm('삭제하시겠습니까?')) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
if (onDelete) {
|
||||
await onDelete(formData.idx);
|
||||
}
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error);
|
||||
alert('삭제에 실패했습니다.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
||||
<div className="glass-effect rounded-lg w-full max-w-3xl max-h-[90vh] overflow-y-auto m-4" onClick={(e) => e.stopPropagation()}>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10">
|
||||
<h2 className="text-xl font-semibold text-white">
|
||||
{formData.idx ? '라이선스 수정' : '라이선스 추가'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/70 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* 기본 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
|
||||
<span>기본 정보</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-1 flex items-center">
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.expire || false}
|
||||
onChange={(e) => setFormData({ ...formData, expire: e.target.checked })}
|
||||
className="w-4 h-4"
|
||||
/>
|
||||
<span className="text-sm text-white/70">만료</span>
|
||||
</label>
|
||||
</div>
|
||||
<div className="col-span-5">
|
||||
<label className="block text-sm text-white/70 mb-1">제품명 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<label className="block text-sm text-white/70 mb-1">버전</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.version || ''}
|
||||
onChange={(e) => setFormData({ ...formData, version: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-3">
|
||||
<label className="block text-sm text-white/70 mb-1">자재번호</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.meterialNo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, meterialNo: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 공급 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
|
||||
<span>공급 정보</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">공급업체</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.supply || ''}
|
||||
onChange={(e) => setFormData({ ...formData, supply: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">제조사</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.manu || ''}
|
||||
onChange={(e) => setFormData({ ...formData, manu: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 사용 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
|
||||
<span>사용 정보</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-12 gap-4">
|
||||
<div className="col-span-2">
|
||||
<label className="block text-sm text-white/70 mb-1">수량</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.qty || 1}
|
||||
onChange={(e) => setFormData({ ...formData, qty: parseInt(e.target.value) || 1 })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
<label className="block text-sm text-white/70 mb-1">사용자</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.uids || ''}
|
||||
onChange={(e) => setFormData({ ...formData, uids: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-6">
|
||||
<label className="block text-sm text-white/70 mb-1">S/N</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.serialNo || ''}
|
||||
onChange={(e) => setFormData({ ...formData, serialNo: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 기간 정보 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
|
||||
<span>기간 정보</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">시작일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.sdate || ''}
|
||||
onChange={(e) => setFormData({ ...formData, sdate: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-white/70 mb-1">종료일</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.edate || ''}
|
||||
onChange={(e) => setFormData({ ...formData, edate: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 비고 */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-semibold text-white/90 flex items-center space-x-2 border-b border-white/10 pb-2">
|
||||
<span>비고</span>
|
||||
</h3>
|
||||
<textarea
|
||||
value={formData.remark || ''}
|
||||
onChange={(e) => setFormData({ ...formData, remark: e.target.value })}
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded text-white focus:outline-none focus:border-blue-500 resize-none"
|
||||
placeholder="추가 메모를 입력하세요..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between p-4 border-t border-white/10">
|
||||
<div>
|
||||
{formData.idx && onDelete && (
|
||||
<button
|
||||
onClick={handleDelete}
|
||||
disabled={saving}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-red-500 hover:bg-red-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
<span>삭제</span>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 disabled:bg-gray-800 text-white rounded-lg transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
<span>{saving ? '저장 중...' : '저장'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
387
Project/frontend/src/components/license/LicenseList.tsx
Normal file
387
Project/frontend/src/components/license/LicenseList.tsx
Normal file
@@ -0,0 +1,387 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Plus,
|
||||
FolderOpen,
|
||||
Download,
|
||||
Search,
|
||||
X,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { LicenseEditDialog } from './LicenseEditDialog';
|
||||
import type { LicenseItem } from '@/types';
|
||||
|
||||
export function LicenseList() {
|
||||
const [list, setList] = useState<LicenseItem[]>([]);
|
||||
const [filteredList, setFilteredList] = useState<LicenseItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<LicenseItem | null>(null);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
// Pagination
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 25;
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
applyFilter();
|
||||
}, [searchText, list]);
|
||||
|
||||
const loadData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await comms.getLicenseList();
|
||||
if (response.Success && response.Data) {
|
||||
setList(response.Data);
|
||||
} else {
|
||||
alert(response.Message || '라이선스 목록을 불러오는데 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load license list:', error);
|
||||
alert('라이선스 목록을 불러오는데 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const applyFilter = () => {
|
||||
if (!searchText.trim()) {
|
||||
setFilteredList(list);
|
||||
return;
|
||||
}
|
||||
|
||||
const search = searchText.toLowerCase();
|
||||
const filtered = list.filter((item) => {
|
||||
return (
|
||||
item.name?.toLowerCase().includes(search) ||
|
||||
item.version?.toLowerCase().includes(search) ||
|
||||
item.supply?.toLowerCase().includes(search) ||
|
||||
item.manu?.toLowerCase().includes(search) ||
|
||||
item.serialNo?.toLowerCase().includes(search) ||
|
||||
item.meterialNo?.toLowerCase().includes(search) ||
|
||||
item.remark?.toLowerCase().includes(search)
|
||||
);
|
||||
});
|
||||
setFilteredList(filtered);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
const handleAdd = () => {
|
||||
setSelectedItem({
|
||||
expire: false,
|
||||
name: '',
|
||||
version: '',
|
||||
meterialNo: '',
|
||||
supply: '',
|
||||
qty: 1,
|
||||
uids: '',
|
||||
serialNo: '',
|
||||
remark: '',
|
||||
sdate: new Date().toISOString().split('T')[0],
|
||||
edate: '',
|
||||
manu: '',
|
||||
});
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleRowClick = (item: LicenseItem) => {
|
||||
setSelectedItem(item);
|
||||
setIsDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSave = async (formData: Partial<LicenseItem>) => {
|
||||
if (!formData.name?.trim()) {
|
||||
alert('제품명을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
let response;
|
||||
|
||||
if (formData.idx) {
|
||||
// Update
|
||||
response = await comms.updateLicense(
|
||||
formData.idx,
|
||||
formData.name!,
|
||||
formData.version || '',
|
||||
formData.meterialNo || '',
|
||||
formData.supply || '',
|
||||
formData.qty || 1,
|
||||
formData.uids || '',
|
||||
formData.serialNo || '',
|
||||
formData.remark || '',
|
||||
formData.sdate || '',
|
||||
formData.edate || '',
|
||||
formData.manu || '',
|
||||
formData.expire || false
|
||||
);
|
||||
} else {
|
||||
// Add
|
||||
response = await comms.addLicense(
|
||||
formData.name!,
|
||||
formData.version || '',
|
||||
formData.meterialNo || '',
|
||||
formData.supply || '',
|
||||
formData.qty || 1,
|
||||
formData.uids || '',
|
||||
formData.serialNo || '',
|
||||
formData.remark || '',
|
||||
formData.sdate || '',
|
||||
formData.edate || '',
|
||||
formData.manu || '',
|
||||
formData.expire || false
|
||||
);
|
||||
}
|
||||
|
||||
if (response.Success) {
|
||||
alert(response.Message || '저장되었습니다.');
|
||||
await loadData();
|
||||
} else {
|
||||
alert(response.Message || '저장에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to save license:', error);
|
||||
alert('저장에 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (idx: number) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await comms.deleteLicense(idx);
|
||||
if (response.Success) {
|
||||
alert(response.Message || '삭제되었습니다.');
|
||||
await loadData();
|
||||
} else {
|
||||
alert(response.Message || '삭제에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete license:', error);
|
||||
alert('삭제에 실패했습니다.');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenFolder = async (item: LicenseItem, e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (!item.idx) {
|
||||
alert('저장된 자료만 폴더를 열 수 있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await comms.openLicenseFolder(item.idx);
|
||||
if (!response.Success) {
|
||||
alert(response.Message || '폴더 열기에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to open folder:', error);
|
||||
alert('폴더 열기에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCSV = async () => {
|
||||
const filename = `license_${new Date().toISOString().split('T')[0]}.csv`;
|
||||
const filepath = `C:\\Temp\\${filename}`;
|
||||
|
||||
try {
|
||||
const response = await comms.exportLicenseCSV(filepath);
|
||||
if (response.Success) {
|
||||
alert(`CSV 파일이 생성되었습니다.\n\n${filepath}`);
|
||||
} else {
|
||||
alert(response.Message || 'CSV 내보내기에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to export CSV:', error);
|
||||
alert('CSV 내보내기에 실패했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setIsDialogOpen(false);
|
||||
setSelectedItem(null);
|
||||
};
|
||||
|
||||
// Pagination
|
||||
const totalPages = Math.ceil(filteredList.length / pageSize);
|
||||
const paginatedList = filteredList.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize
|
||||
);
|
||||
|
||||
const goToPreviousPage = () => {
|
||||
setCurrentPage((prev) => Math.max(1, prev - 1));
|
||||
};
|
||||
|
||||
const goToNextPage = () => {
|
||||
setCurrentPage((prev) => Math.min(totalPages, prev + 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold text-white">라이선스 관리</h1>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={handleAdd}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-500 hover:bg-blue-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span>추가</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleExportCSV}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-green-500 hover:bg-green-600 disabled:bg-gray-600 text-white rounded-lg transition-colors"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
<span>CSV</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex-1 relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-white/50" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
placeholder="검색 (제품명, 버전, 공급업체, 제조사, S/N, 자재번호, 비고)"
|
||||
className="w-full pl-10 pr-10 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:border-blue-500"
|
||||
/>
|
||||
{searchText && (
|
||||
<button
|
||||
onClick={() => setSearchText('')}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-white/50 hover:text-white"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="glass-effect rounded-lg overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="bg-white/10">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-center text-sm font-semibold text-white border-r border-white/10 w-16">상태</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '25%' }}>제품명</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '25%' }}>버전</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10 w-20">수량</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-white border-r border-white/10" style={{ width: '12%' }}>사용자</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-semibold text-white" style={{ width: '15%' }}>S/N</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-white/70">
|
||||
로딩 중...
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!loading && paginatedList.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-4 py-8 text-center text-white/70">
|
||||
데이터가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{!loading &&
|
||||
paginatedList.map((item) => (
|
||||
<tr
|
||||
key={item.idx}
|
||||
onClick={() => handleRowClick(item)}
|
||||
className={`border-t border-white/10 hover:bg-white/10 cursor-pointer transition-colors ${
|
||||
item.expire ? 'bg-red-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-4 py-3 text-center border-r border-white/10">
|
||||
<div className="flex justify-center" title={item.expire ? '만료' : '유효'}>
|
||||
{item.expire ? (
|
||||
<XCircle className="w-5 h-5 text-red-500" />
|
||||
) : (
|
||||
<CheckCircle className="w-5 h-5 text-green-500" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-white border-r border-white/10 max-w-xs">
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={(e) => handleOpenFolder(item, e)}
|
||||
className="p-1 text-yellow-400 hover:text-yellow-300 transition-colors flex-shrink-0"
|
||||
title="폴더 열기"
|
||||
>
|
||||
<FolderOpen className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="break-words">{item.name}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-white border-r border-white/10 break-words">{item.version}</td>
|
||||
<td className="px-4 py-3 text-sm text-white border-r border-white/10">{item.qty}</td>
|
||||
<td className="px-4 py-3 text-sm text-white border-r border-white/10 break-words max-w-[8rem]">{item.uids}</td>
|
||||
<td className="px-4 py-3 text-sm text-white break-words">{item.serialNo}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-4 py-3 border-t border-white/10">
|
||||
<div className="text-sm text-white/70">
|
||||
전체 {filteredList.length}건 중 {(currentPage - 1) * pageSize + 1}~
|
||||
{Math.min(currentPage * pageSize, filteredList.length)}건
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={goToPreviousPage}
|
||||
disabled={currentPage === 1}
|
||||
className="p-2 text-white/70 hover:text-white disabled:text-white/30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-sm text-white">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-2 text-white/70 hover:text-white disabled:text-white/30 disabled:cursor-not-allowed"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Edit Dialog */}
|
||||
<LicenseEditDialog
|
||||
item={selectedItem}
|
||||
isOpen={isDialogOpen}
|
||||
onClose={handleCloseDialog}
|
||||
onSave={handleSave}
|
||||
onDelete={handleDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
304
Project/frontend/src/components/mail/MailTestDialog.tsx
Normal file
304
Project/frontend/src/components/mail/MailTestDialog.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Mail, Send } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
|
||||
interface MailTestDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function MailTestDialog({ isOpen, onClose }: MailTestDialogProps) {
|
||||
const [formData, setFormData] = useState({
|
||||
cate: '테스트',
|
||||
subject: '',
|
||||
fromlist: '',
|
||||
tolist: '',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
body: '',
|
||||
});
|
||||
const [processing, setProcessing] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadUserEmail = async () => {
|
||||
try {
|
||||
const response = await comms.checkLoginStatus();
|
||||
if (response.Success && response.IsLoggedIn && response.User) {
|
||||
const user = response.User as { Email?: string };
|
||||
if (user.Email) {
|
||||
setFormData(prev => ({ ...prev, fromlist: user.Email || '' }));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('사용자 정보 로드 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
loadUserEmail();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && isOpen) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
window.addEventListener('keydown', handleEscape);
|
||||
return () => window.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
}, [isOpen, onClose]);
|
||||
|
||||
const handleSubmit = async (mode: 'queue' | 'direct' | 'outlook' = 'queue') => {
|
||||
if (!formData.subject.trim()) {
|
||||
alert('제목을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!formData.tolist.trim()) {
|
||||
alert('수신자를 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
if (!formData.body.trim()) {
|
||||
alert('내용을 입력해주세요.');
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
try {
|
||||
let response;
|
||||
if (mode === 'outlook') {
|
||||
// Outlook 미리보기
|
||||
response = await comms.sendMailOutlook(
|
||||
formData.subject,
|
||||
formData.tolist,
|
||||
formData.cc,
|
||||
formData.bcc,
|
||||
formData.body
|
||||
);
|
||||
} else if (mode === 'direct') {
|
||||
// 직접 발송
|
||||
response = await comms.sendMailDirect(
|
||||
formData.cate,
|
||||
formData.subject,
|
||||
formData.fromlist,
|
||||
formData.tolist,
|
||||
formData.cc,
|
||||
formData.bcc,
|
||||
formData.body
|
||||
);
|
||||
} else {
|
||||
// 발송 대기열에 추가
|
||||
response = await comms.addMailData(
|
||||
formData.cate,
|
||||
formData.subject,
|
||||
formData.fromlist,
|
||||
formData.tolist,
|
||||
formData.cc,
|
||||
formData.bcc,
|
||||
formData.body
|
||||
);
|
||||
}
|
||||
|
||||
if (response.Success) {
|
||||
alert(response.Message || '처리되었습니다.');
|
||||
if (mode !== 'outlook') {
|
||||
onClose();
|
||||
// 폼 초기화
|
||||
setFormData({
|
||||
cate: '테스트',
|
||||
subject: '',
|
||||
fromlist: formData.fromlist, // 발신자는 유지
|
||||
tolist: '',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
body: '',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
alert(response.Message || '메일 처리에 실패했습니다.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('메일 처리 오류:', error);
|
||||
alert('메일 처리 중 오류가 발생했습니다.');
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[10000] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="relative w-full max-w-3xl glass-effect-solid rounded-2xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<Mail className="w-5 h-5 text-primary-400" />
|
||||
<h2 className="text-lg font-semibold text-white">메일 테스트</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 rounded-lg text-white/60 hover:text-white hover:bg-white/10 transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
{/* 분류 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">분류</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.cate}
|
||||
onChange={(e) => setFormData({ ...formData, cate: e.target.value })}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 제목 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">제목 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.subject}
|
||||
onChange={(e) => setFormData({ ...formData, subject: e.target.value })}
|
||||
placeholder="메일 제목을 입력하세요"
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 발신자 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">발신자</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.fromlist}
|
||||
onChange={(e) => setFormData({ ...formData, fromlist: e.target.value })}
|
||||
placeholder="발신자 이메일 (쉼표로 구분)"
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 수신자 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">수신자 *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.tolist}
|
||||
onChange={(e) => setFormData({ ...formData, tolist: e.target.value })}
|
||||
placeholder="수신자 이메일 (쉼표로 구분)"
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 참조 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">참조 (CC)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.cc}
|
||||
onChange={(e) => setFormData({ ...formData, cc: e.target.value })}
|
||||
placeholder="참조 이메일 (쉼표로 구분)"
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 숨은참조 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">숨은참조 (BCC)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.bcc}
|
||||
onChange={(e) => setFormData({ ...formData, bcc: e.target.value })}
|
||||
placeholder="숨은참조 이메일 (쉼표로 구분)"
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
<div>
|
||||
<label className="block text-white/80 text-sm font-medium mb-2">내용 *</label>
|
||||
<textarea
|
||||
value={formData.body}
|
||||
onChange={(e) => setFormData({ ...formData, body: e.target.value })}
|
||||
placeholder="메일 내용을 입력하세요 (HTML 가능)"
|
||||
rows={8}
|
||||
className="w-full px-3 py-2 bg-white/10 border border-white/20 rounded-lg text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="text-white/50 text-xs">
|
||||
* 메일은 발송 대기열에 추가됩니다. 실제 발송은 메일 서비스가 처리합니다.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10 bg-black/20">
|
||||
<button
|
||||
onClick={onClose}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 rounded-lg bg-white/10 hover:bg-white/20 text-white transition-colors disabled:opacity-50"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubmit('queue')}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 rounded-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
처리중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Mail className="w-4 h-4" />
|
||||
대기열 추가
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubmit('direct')}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 rounded-lg bg-green-500 hover:bg-green-600 text-white transition-colors flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
처리중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-4 h-4" />
|
||||
즉시 발송
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSubmit('outlook')}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 rounded-lg bg-orange-500 hover:bg-orange-600 text-white transition-colors flex items-center gap-2 disabled:opacity-50"
|
||||
>
|
||||
{processing ? (
|
||||
<>
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
처리중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Mail className="w-4 h-4" />
|
||||
Outlook
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
519
Project/frontend/src/components/project/PartListDialog.tsx
Normal file
519
Project/frontend/src/components/project/PartListDialog.tsx
Normal file
@@ -0,0 +1,519 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { X, Save, Trash2, Plus, RefreshCw } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { PartListItem } from '@/types';
|
||||
|
||||
interface PartListDialogProps {
|
||||
projectIdx: number;
|
||||
projectName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function PartListDialog({ projectIdx, projectName, onClose }: PartListDialogProps) {
|
||||
const [parts, setParts] = useState<PartListItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||
const [editForm, setEditForm] = useState<Partial<PartListItem>>({});
|
||||
|
||||
// ESC 키 핸들러
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (editingIdx !== null) {
|
||||
setEditingIdx(null);
|
||||
setEditForm({});
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
return () => window.removeEventListener('keydown', handleKeyDown);
|
||||
}, [editingIdx, onClose]);
|
||||
|
||||
// 데이터 로드
|
||||
const loadParts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('[PartList] 로드 시작, projectIdx:', projectIdx);
|
||||
const result = await comms.getPartList(projectIdx);
|
||||
console.log('[PartList] 결과:', result);
|
||||
if (result.Success && result.Data) {
|
||||
console.log('[PartList] 데이터 개수:', result.Data.length);
|
||||
setParts(result.Data);
|
||||
} else {
|
||||
console.error('[PartList] 실패:', result.Message);
|
||||
alert(result.Message || '파트리스트 로드 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('파트리스트 로드 실패:', error);
|
||||
alert('파트리스트 로드 중 오류: ' + error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadParts();
|
||||
}, [projectIdx]);
|
||||
|
||||
// 편집 시작
|
||||
const startEdit = (part: PartListItem) => {
|
||||
setEditingIdx(part.idx);
|
||||
setEditForm({ ...part });
|
||||
};
|
||||
|
||||
// 편집 취소
|
||||
const cancelEdit = () => {
|
||||
setEditingIdx(null);
|
||||
setEditForm({});
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!editForm.itemname || !editForm.item) {
|
||||
alert('품명과 자재번호는 필수입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await comms.savePartList(
|
||||
editingIdx || 0,
|
||||
projectIdx,
|
||||
editForm.itemgroup || '',
|
||||
editForm.itemname || '',
|
||||
editForm.item || '',
|
||||
editForm.itemmodel || '',
|
||||
editForm.itemscale || '',
|
||||
editForm.itemunit || '',
|
||||
editForm.qty || 0,
|
||||
editForm.price || 0,
|
||||
editForm.itemsupply || '',
|
||||
editForm.itemsupplyidx || 0,
|
||||
editForm.itemmanu || '',
|
||||
editForm.itemsid || '',
|
||||
editForm.option1 || '',
|
||||
editForm.remark || '',
|
||||
editForm.no || 0,
|
||||
editForm.qtybuy || 0
|
||||
);
|
||||
|
||||
if (result.Success) {
|
||||
await loadParts();
|
||||
cancelEdit();
|
||||
} else {
|
||||
alert(result.Message || '저장 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 실패:', error);
|
||||
alert('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제
|
||||
const handleDelete = async (idx: number) => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const result = await comms.deletePartList(idx);
|
||||
if (result.Success) {
|
||||
await loadParts();
|
||||
} else {
|
||||
alert(result.Message || '삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 실패:', error);
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 새 항목 추가
|
||||
const addNew = () => {
|
||||
setEditingIdx(-1);
|
||||
setEditForm({
|
||||
Project: projectIdx,
|
||||
itemgroup: '',
|
||||
itemname: '',
|
||||
item: '',
|
||||
itemmodel: '',
|
||||
itemscale: '',
|
||||
itemunit: 'EA',
|
||||
qty: 1,
|
||||
price: 0,
|
||||
itemsupply: '',
|
||||
itemsupplyidx: 0,
|
||||
itemmanu: '',
|
||||
itemsid: '',
|
||||
option1: '',
|
||||
remark: '',
|
||||
no: 0,
|
||||
qtybuy: 0,
|
||||
});
|
||||
};
|
||||
|
||||
// 금액 계산
|
||||
const getAmount = (qty: number, price: number) => qty * price;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-slate-800/95 backdrop-blur rounded-lg w-full max-w-7xl max-h-[90vh] flex flex-col shadow-2xl border border-white/10">
|
||||
{/* 헤더 */}
|
||||
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-primary-600/30 sticky top-0 z-10">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">파트리스트</h2>
|
||||
<p className="text-sm text-white/60">{projectName}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={addNew}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-primary-600 hover:bg-primary-500 text-white rounded transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span className="text-sm">추가</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={loadParts}
|
||||
disabled={loading}
|
||||
className="p-2 hover:bg-white/10 rounded transition-colors disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 text-white/70 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/10 rounded transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-white/70" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="flex-1 overflow-auto p-4">
|
||||
{loading && parts.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<RefreshCw className="w-8 h-8 text-primary-500 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full border-collapse">
|
||||
<thead className="sticky top-0 bg-slate-700/50 backdrop-blur z-10">
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-12">No</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-24">그룹</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium">품명</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">모델</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">규격</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-16">단위</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-20">수량</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-28">단가</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-32">금액</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">공급처</th>
|
||||
<th className="px-2 py-2 text-center text-xs text-white/70 font-medium w-20">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{parts.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<td colSpan={11} className="px-2 py-8 text-center text-white/40 text-sm">
|
||||
등록된 파트가 없습니다.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
parts.map((part) => {
|
||||
const isEditing = editingIdx === part.idx;
|
||||
return (
|
||||
<tr
|
||||
key={part.idx}
|
||||
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${
|
||||
isEditing ? 'bg-primary-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.no || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.no || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemgroup || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemgroup || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemname || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/90 text-xs font-medium">{part.itemname || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemmodel || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemmodel || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemscale || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemscale: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemscale || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemunit || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemunit || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.qty || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.qty?.toLocaleString() || 0}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.price || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.price?.toLocaleString() || 0}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<span className="text-white/90 text-xs font-medium">
|
||||
{getAmount(
|
||||
isEditing ? editForm.qty || 0 : part.qty || 0,
|
||||
isEditing ? editForm.price || 0 : part.price || 0
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsupply || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemsupply || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
||||
title="저장"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
||||
title="취소"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => startEdit(part)}
|
||||
className="p-1 hover:bg-white/10 text-white/70 rounded transition-colors text-xs"
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(part.idx)}
|
||||
className="p-1 hover:bg-red-500/20 text-red-400 rounded transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* 새 항목 추가 행 */}
|
||||
{editingIdx === -1 && (
|
||||
<tr className="border-b border-white/5 bg-primary-500/10">
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.no || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemgroup || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="그룹"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemname || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="품명 *"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemmodel || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="모델"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemscale || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemscale: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="규격"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemunit || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="단위"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.qty || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.price || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<span className="text-white/90 text-xs font-medium">
|
||||
{getAmount(editForm.qty || 0, editForm.price || 0).toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsupply || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="공급처"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
||||
title="저장"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
||||
title="취소"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 합계 */}
|
||||
{parts.length > 0 && (
|
||||
<div className="p-4 border-t border-white/10 bg-slate-900/50">
|
||||
<div className="flex justify-end gap-4 text-sm">
|
||||
<span className="text-white/70">
|
||||
총 <span className="text-white font-medium">{parts.length}</span>개 항목
|
||||
</span>
|
||||
<span className="text-white/70">
|
||||
합계: <span className="text-primary-400 font-medium">
|
||||
{parts.reduce((sum, part) => sum + getAmount(part.qty || 0, part.price || 0), 0).toLocaleString()}
|
||||
</span>원
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export { ProjectDetailDialog } from './ProjectDetailDialog';
|
||||
export { PartListDialog } from './PartListDialog';
|
||||
|
||||
@@ -101,6 +101,24 @@ export function Dashboard() {
|
||||
setUrgentTodos(allUrgentTodos.slice(start, end));
|
||||
}, [todoPage, allUrgentTodos]);
|
||||
|
||||
// ESC 키로 모달 닫기
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (showTodoAddModal) {
|
||||
setShowTodoAddModal(false);
|
||||
} else if (showTodoEditModal) {
|
||||
setShowTodoEditModal(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (showTodoAddModal || showTodoEditModal) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
return () => document.removeEventListener('keydown', handleEscape);
|
||||
}
|
||||
}, [showTodoAddModal, showTodoEditModal]);
|
||||
|
||||
const loadDashboardData = useCallback(async () => {
|
||||
try {
|
||||
// 오늘 날짜 (로컬 시간 기준)
|
||||
@@ -225,6 +243,7 @@ export function Dashboard() {
|
||||
|
||||
const getPriorityText = (seqno: number) => {
|
||||
switch (seqno) {
|
||||
case -1: return '낮음';
|
||||
case 1: return '중요';
|
||||
case 2: return '매우 중요';
|
||||
case 3: return '긴급';
|
||||
@@ -234,6 +253,7 @@ export function Dashboard() {
|
||||
|
||||
const getPriorityClass = (seqno: number) => {
|
||||
switch (seqno) {
|
||||
case -1: return 'bg-white/5 text-white/40';
|
||||
case 1: return 'bg-primary-500/20 text-primary-300';
|
||||
case 2: return 'bg-warning-500/20 text-warning-300';
|
||||
case 3: return 'bg-danger-500/20 text-danger-300';
|
||||
@@ -782,10 +802,11 @@ export function Dashboard() {
|
||||
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>
|
||||
<option value={2}>매우 중요</option>
|
||||
<option value={1}>중요</option>
|
||||
<option value={0}>보통</option>
|
||||
<option value={-1}>낮음</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
@@ -844,9 +865,22 @@ export function Dashboard() {
|
||||
<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 className="flex items-center space-x-2">
|
||||
{editingTodo.status !== '5' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodoComplete}
|
||||
disabled={processing}
|
||||
className="bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center disabled:opacity-50 text-sm"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
완료 처리
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => setShowTodoEditModal(false)} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
@@ -929,10 +963,11 @@ export function Dashboard() {
|
||||
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>
|
||||
<option value={2}>매우 중요</option>
|
||||
<option value={1}>중요</option>
|
||||
<option value={0}>보통</option>
|
||||
<option value={-1}>낮음</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
@@ -950,40 +985,8 @@ export function Dashboard() {
|
||||
</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="px-6 py-4 border-t border-white/10 flex justify-end">
|
||||
<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}
|
||||
@@ -997,6 +1000,15 @@ export function Dashboard() {
|
||||
)}
|
||||
수정
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
Search,
|
||||
RefreshCw,
|
||||
Copy,
|
||||
Info,
|
||||
Plus,
|
||||
Calendar,
|
||||
} from 'lucide-react';
|
||||
@@ -567,8 +568,8 @@ export function Jobreport() {
|
||||
<thead className="bg-white/10">
|
||||
<tr>
|
||||
<th className="px-2 py-3 text-center text-xs font-medium text-white/70 uppercase w-10"></th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">날짜</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">프로젝트</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase w-24">날짜</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase" style={{ width: '35%' }}>프로젝트</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">업무형태</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">상태</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase">시간</th>
|
||||
@@ -596,38 +597,51 @@ export function Jobreport() {
|
||||
paginatedList.map((item) => (
|
||||
<tr
|
||||
key={item.idx}
|
||||
className={`hover:bg-white/5 transition-colors cursor-pointer ${item.type === '휴가' ? 'bg-gradient-to-r from-lime-400/30 via-emerald-400/20 to-teal-400/30' : ''}`}
|
||||
onClick={() => openEditModal(item)}
|
||||
className="hover:bg-white/5 transition-colors"
|
||||
>
|
||||
<td className="px-2 py-3 text-center">
|
||||
<button
|
||||
onClick={(e) => openCopyModal(item, e)}
|
||||
className="text-white/40 hover:text-primary-400 transition-colors"
|
||||
title="복사하여 새로 작성"
|
||||
>
|
||||
<Copy className="w-4 h-4" />
|
||||
</button>
|
||||
<td
|
||||
className="px-2 py-3 text-center cursor-pointer hover:bg-primary-500/10 transition-colors"
|
||||
onClick={(e) => openCopyModal(item, e)}
|
||||
title="복사하여 새로 작성"
|
||||
>
|
||||
<Copy className="w-4 h-4 mx-auto text-white/40" />
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white text-sm">{formatDate(item.pdate)}</td>
|
||||
<td className={`px-4 py-3 text-sm font-medium max-w-xs truncate ${item.pidx && item.pidx > 0 ? 'text-white' : 'text-white/50'}`} title={item.projectName}>
|
||||
{item.projectName || '-'}
|
||||
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{formatDate(item.pdate)}</td>
|
||||
<td className={`px-4 py-3 text-sm font-medium ${item.pidx && item.pidx > 0 ? 'text-white' : 'text-white/50'}`}>
|
||||
<div className="flex items-center space-x-2">
|
||||
{item.pidx && item.pidx > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
window.open(`#/project-detail/${item.pidx}`, '_blank');
|
||||
}}
|
||||
className="text-primary-400 hover:text-primary-300 transition-colors flex-shrink-0"
|
||||
title="프로젝트 정보 보기"
|
||||
>
|
||||
<Info className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<span className="truncate cursor-pointer" onClick={() => openEditModal(item)} title={item.projectName}>
|
||||
{item.projectName || '-'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white text-sm">{item.type || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm">
|
||||
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{item.type || '-'}</td>
|
||||
<td className="px-4 py-3 text-sm cursor-pointer" onClick={() => openEditModal(item)}>
|
||||
<span className={`px-2 py-1 rounded text-xs ${item.status?.includes('완료') ? 'bg-green-500/20 text-green-400' : 'bg-white/20 text-white/70'
|
||||
}`}>
|
||||
{item.status || '-'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-white text-sm">
|
||||
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>
|
||||
{item.hrs || 0}
|
||||
</td>
|
||||
{canViewOT && (
|
||||
<td className="px-4 py-3 text-white text-sm">
|
||||
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>
|
||||
{item.ot ? <span className="text-warning-400">{item.ot}</span> : '-'}
|
||||
</td>
|
||||
)}
|
||||
<td className="px-4 py-3 text-white text-sm">{item.name || item.id || '-'}</td>
|
||||
<td className="px-4 py-3 text-white text-sm cursor-pointer" onClick={() => openEditModal(item)}>{item.name || item.id || '-'}</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Mail, Search, RefreshCw, Calendar } from 'lucide-react';
|
||||
import { Mail, Search, RefreshCw, Calendar, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { MailItem, UserInfo } from '@/types';
|
||||
import { MailTestDialog } from '@/components/mail/MailTestDialog';
|
||||
|
||||
export function MailList() {
|
||||
const [mailList, setMailList] = useState<MailItem[]>([]);
|
||||
@@ -11,7 +12,10 @@ export function MailList() {
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [selectedItem, setSelectedItem] = useState<MailItem | null>(null);
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [showTestDialog, setShowTestDialog] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<UserInfo | null>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 25;
|
||||
|
||||
const formatDateLocal = (date: Date) => {
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
@@ -78,9 +82,17 @@ export function MailList() {
|
||||
alert('시작일은 종료일보다 늦을 수 없습니다.');
|
||||
return;
|
||||
}
|
||||
setCurrentPage(1);
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 페이징 계산
|
||||
const totalPages = Math.ceil(mailList.length / pageSize);
|
||||
const paginatedList = mailList.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize
|
||||
);
|
||||
|
||||
const handleRowClick = (item: MailItem) => {
|
||||
// 레벨 9 이상(개발자)만 상세보기 가능
|
||||
if (!currentUser || currentUser.Level < 9) {
|
||||
@@ -151,6 +163,16 @@ export function MailList() {
|
||||
)}
|
||||
조회
|
||||
</button>
|
||||
|
||||
{currentUser && currentUser.Level >= 9 && (
|
||||
<button
|
||||
onClick={() => setShowTestDialog(true)}
|
||||
className="h-10 bg-green-500 hover:bg-green-600 text-white px-6 rounded-lg transition-colors flex items-center justify-center"
|
||||
>
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
테스트
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -164,7 +186,7 @@ export function MailList() {
|
||||
<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">
|
||||
<div className="divide-y divide-white/10 max-h-[calc(100vh-380px)] overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="px-6 py-8 text-center">
|
||||
<div className="flex items-center justify-center">
|
||||
@@ -178,7 +200,7 @@ export function MailList() {
|
||||
<p className="text-white/50">조회된 데이터가 없습니다.</p>
|
||||
</div>
|
||||
) : (
|
||||
mailList.map((item) => (
|
||||
paginatedList.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'}`}
|
||||
@@ -215,6 +237,29 @@ export function MailList() {
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 페이징 */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2 px-6 py-3 border-t border-white/10">
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-1 rounded hover:bg-white/10 disabled:opacity-30 text-white/70"
|
||||
>
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
<span className="text-white/70 text-sm">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-1 rounded hover:bg-white/10 disabled:opacity-30 text-white/70"
|
||||
>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 상세 모달 */}
|
||||
@@ -283,6 +328,15 @@ export function MailList() {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 메일 테스트 다이얼로그 */}
|
||||
<MailTestDialog
|
||||
isOpen={showTestDialog}
|
||||
onClose={() => {
|
||||
setShowTestDialog(false);
|
||||
loadData(); // 목록 새로고침
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
588
Project/frontend/src/pages/PartList.tsx
Normal file
588
Project/frontend/src/pages/PartList.tsx
Normal file
@@ -0,0 +1,588 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
ClipboardList,
|
||||
Search,
|
||||
RefreshCw,
|
||||
Plus,
|
||||
Save,
|
||||
Trash2,
|
||||
X,
|
||||
DollarSign,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { PartListItem } from '@/types';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function PartList() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const projectIdx = parseInt(searchParams.get('idx') || '0');
|
||||
const projectName = searchParams.get('name') || '';
|
||||
|
||||
const [parts, setParts] = useState<PartListItem[]>([]);
|
||||
const [filteredParts, setFilteredParts] = useState<PartListItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchKey, setSearchKey] = useState('');
|
||||
const [editingIdx, setEditingIdx] = useState<number | null>(null);
|
||||
const [editForm, setEditForm] = useState<Partial<PartListItem>>({});
|
||||
const [showSummary, setShowSummary] = useState(false);
|
||||
|
||||
// 데이터 로드
|
||||
const loadParts = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('[PartList] 로드 시작, projectIdx:', projectIdx);
|
||||
const result = await comms.getPartList(projectIdx);
|
||||
console.log('[PartList] 결과:', result);
|
||||
if (result.Success && result.Data) {
|
||||
console.log('[PartList] 데이터 개수:', result.Data.length);
|
||||
setParts(result.Data);
|
||||
setFilteredParts(result.Data);
|
||||
} else {
|
||||
console.error('[PartList] 실패:', result.Message);
|
||||
alert(result.Message || '파트리스트 로드 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('파트리스트 로드 실패:', error);
|
||||
alert('파트리스트 로드 중 오류: ' + error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (projectIdx > 0) {
|
||||
loadParts();
|
||||
}
|
||||
}, [projectIdx]);
|
||||
|
||||
// 검색
|
||||
useEffect(() => {
|
||||
if (!searchKey.trim()) {
|
||||
setFilteredParts(parts);
|
||||
return;
|
||||
}
|
||||
|
||||
const search = searchKey.toLowerCase();
|
||||
const filtered = parts.filter((part) => {
|
||||
return (
|
||||
part.itemsid?.toLowerCase().includes(search) ||
|
||||
part.itemname?.toLowerCase().includes(search) ||
|
||||
part.itemmodel?.toLowerCase().includes(search)
|
||||
);
|
||||
});
|
||||
setFilteredParts(filtered);
|
||||
}, [searchKey, parts]);
|
||||
|
||||
// 편집 시작
|
||||
const startEdit = (part: PartListItem) => {
|
||||
setEditingIdx(part.idx);
|
||||
setEditForm({ ...part });
|
||||
};
|
||||
|
||||
// 편집 취소
|
||||
const cancelEdit = () => {
|
||||
setEditingIdx(null);
|
||||
setEditForm({});
|
||||
};
|
||||
|
||||
// 저장
|
||||
const handleSave = async () => {
|
||||
if (!editForm.itemname || !editForm.item) {
|
||||
alert('품명과 자재번호는 필수입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await comms.savePartList(
|
||||
editingIdx || 0,
|
||||
projectIdx,
|
||||
editForm.itemgroup || '',
|
||||
editForm.itemname || '',
|
||||
editForm.item || '',
|
||||
editForm.itemmodel || '',
|
||||
'', // itemscale 제거됨
|
||||
editForm.itemunit || '',
|
||||
editForm.qty || 0,
|
||||
editForm.price || 0,
|
||||
editForm.itemsupply || '',
|
||||
editForm.itemsupplyidx || 0,
|
||||
editForm.itemmanu || '',
|
||||
editForm.itemsid || '',
|
||||
editForm.option1 || '',
|
||||
editForm.remark || '',
|
||||
editForm.no || 0,
|
||||
editForm.qtybuy || 0
|
||||
);
|
||||
|
||||
if (result.Success) {
|
||||
await loadParts();
|
||||
cancelEdit();
|
||||
} else {
|
||||
alert(result.Message || '저장 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('저장 실패:', error);
|
||||
alert('저장 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 삭제
|
||||
const handleDelete = async (idx: number) => {
|
||||
if (!confirm('정말 삭제하시겠습니까?')) return;
|
||||
|
||||
try {
|
||||
const result = await comms.deletePartList(idx);
|
||||
if (result.Success) {
|
||||
await loadParts();
|
||||
} else {
|
||||
alert(result.Message || '삭제 실패');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('삭제 실패:', error);
|
||||
alert('삭제 중 오류가 발생했습니다.');
|
||||
}
|
||||
};
|
||||
|
||||
// 새 항목 추가
|
||||
const addNew = () => {
|
||||
setEditingIdx(-1);
|
||||
setEditForm({
|
||||
Project: projectIdx,
|
||||
itemgroup: '',
|
||||
itemname: '',
|
||||
item: '',
|
||||
itemmodel: '',
|
||||
itemunit: 'EA',
|
||||
qty: 1,
|
||||
price: 0,
|
||||
itemsupply: '',
|
||||
itemsupplyidx: 0,
|
||||
itemmanu: '',
|
||||
itemsid: '',
|
||||
option1: '',
|
||||
remark: '',
|
||||
no: 0,
|
||||
qtybuy: 0,
|
||||
});
|
||||
};
|
||||
|
||||
// 금액 계산
|
||||
const getAmount = (qty: number, price: number) => qty * price;
|
||||
|
||||
// 합계 계산
|
||||
const totalAmount = filteredParts.reduce((sum, part) => sum + getAmount(part.qty || 0, part.price || 0), 0);
|
||||
|
||||
// 그룹별 합계
|
||||
const groupSummary = filteredParts.reduce((acc, part) => {
|
||||
const group = part.itemgroup || '미분류';
|
||||
if (!acc[group]) {
|
||||
acc[group] = { count: 0, amount: 0 };
|
||||
}
|
||||
acc[group].count++;
|
||||
acc[group].amount += getAmount(part.qty || 0, part.price || 0);
|
||||
return acc;
|
||||
}, {} as Record<string, { count: number; amount: number }>);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* 헤더 */}
|
||||
<div className="glass-effect rounded-xl p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<ClipboardList className="w-6 h-6 text-amber-400" />
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-white">파트리스트</h1>
|
||||
<p className="text-sm text-white/60">{projectName}</p>
|
||||
</div>
|
||||
<span className="text-white/50 text-sm">({filteredParts.length}건)</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={addNew}
|
||||
className="flex items-center gap-2 px-3 py-1.5 bg-primary-600 hover:bg-primary-500 text-white rounded transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<span className="text-sm">추가</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={loadParts}
|
||||
disabled={loading}
|
||||
className="p-2 hover:bg-white/10 rounded transition-colors disabled:opacity-50"
|
||||
title="새로고침"
|
||||
>
|
||||
<RefreshCw className={`w-5 h-5 text-white/70 ${loading ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 검색 */}
|
||||
<div className="flex items-center gap-2 bg-white/5 rounded-lg px-3 py-2">
|
||||
<Search className="w-4 h-4 text-white/50" />
|
||||
<input
|
||||
type="text"
|
||||
value={searchKey}
|
||||
onChange={(e) => setSearchKey(e.target.value)}
|
||||
placeholder="SID, 품명, 모델로 검색..."
|
||||
className="flex-1 bg-transparent text-white placeholder-white/30 focus:outline-none text-sm"
|
||||
/>
|
||||
{searchKey && (
|
||||
<button onClick={() => setSearchKey('')} className="text-white/50 hover:text-white/70">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 테이블 */}
|
||||
<div className="glass-effect rounded-xl overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
{loading && parts.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<RefreshCw className="w-8 h-8 text-primary-500 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-700/50 sticky top-0 z-10">
|
||||
<tr className="border-b border-white/10">
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-12">No</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-24">그룹</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-24">SID</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium">품명</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">모델</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-16">단위</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-20">수량</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-28">단가</th>
|
||||
<th className="px-2 py-2 text-right text-xs text-white/70 font-medium w-32">금액</th>
|
||||
<th className="px-2 py-2 text-left text-xs text-white/70 font-medium w-32">공급처</th>
|
||||
<th className="px-2 py-2 text-center text-xs text-white/70 font-medium w-20">작업</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredParts.length === 0 && !loading ? (
|
||||
<tr>
|
||||
<td colSpan={11} className="px-2 py-8 text-center text-white/40 text-sm">
|
||||
{searchKey ? '검색 결과가 없습니다.' : '등록된 파트가 없습니다.'}
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredParts.map((part) => {
|
||||
const isEditing = editingIdx === part.idx;
|
||||
return (
|
||||
<tr
|
||||
key={part.idx}
|
||||
className={`border-b border-white/5 hover:bg-white/5 transition-colors ${
|
||||
isEditing ? 'bg-primary-500/10' : ''
|
||||
}`}
|
||||
>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.no || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.no || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemgroup || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemgroup || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsid || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsid: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemsid || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemname || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/90 text-xs font-medium">{part.itemname || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemmodel || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemmodel || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemunit || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemunit || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.qty || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.qty?.toLocaleString() || 0}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.price || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.price?.toLocaleString() || 0}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<span className="text-white/90 text-xs font-medium">
|
||||
{getAmount(
|
||||
isEditing ? editForm.qty || 0 : part.qty || 0,
|
||||
isEditing ? editForm.price || 0 : part.price || 0
|
||||
).toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsupply || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-white/70 text-xs">{part.itemsupply || ''}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
{isEditing ? (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
||||
title="저장"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
||||
title="취소"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={() => startEdit(part)}
|
||||
className="p-1 hover:bg-white/10 text-white/70 rounded transition-colors text-xs"
|
||||
>
|
||||
편집
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(part.idx)}
|
||||
className="p-1 hover:bg-red-500/20 text-red-400 rounded transition-colors"
|
||||
title="삭제"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
{/* 새 항목 추가 행 */}
|
||||
{editingIdx === -1 && (
|
||||
<tr className="border-b border-white/5 bg-primary-500/10">
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.no || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, no: parseInt(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemgroup || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemgroup: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="그룹"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsid || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsid: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="SID"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemname || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemname: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="품명 *"
|
||||
required
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemmodel || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemmodel: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="모델"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemunit || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemunit: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="단위"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.qty || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, qty: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<input
|
||||
type="number"
|
||||
value={editForm.price || 0}
|
||||
onChange={(e) => setEditForm({ ...editForm, price: parseFloat(e.target.value) || 0 })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none text-right"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
<span className="text-white/90 text-xs font-medium">
|
||||
{getAmount(editForm.qty || 0, editForm.price || 0).toLocaleString()}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.itemsupply || ''}
|
||||
onChange={(e) => setEditForm({ ...editForm, itemsupply: e.target.value })}
|
||||
className="w-full bg-slate-700/50 text-white text-xs px-2 py-1 rounded border border-white/10 focus:border-primary-500 focus:outline-none"
|
||||
placeholder="공급처"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-2 py-2">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="p-1 hover:bg-green-500/20 text-green-400 rounded transition-colors"
|
||||
title="저장"
|
||||
>
|
||||
<Save className="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="p-1 hover:bg-white/10 text-white/50 rounded transition-colors"
|
||||
title="취소"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 하단 정보 */}
|
||||
<div className="flex gap-4">
|
||||
{/* 좌측: 비용 요약 */}
|
||||
<div className="glass-effect rounded-xl p-4 flex-1">
|
||||
<button
|
||||
onClick={() => setShowSummary(!showSummary)}
|
||||
className="flex items-center gap-2 text-amber-400 hover:text-amber-300 mb-3"
|
||||
>
|
||||
<DollarSign className="w-5 h-5" />
|
||||
<span className="font-medium">비용 요약</span>
|
||||
</button>
|
||||
|
||||
{showSummary && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(groupSummary).map(([group, data]) => (
|
||||
<div key={group} className="flex items-center justify-between text-sm border-b border-white/5 pb-2">
|
||||
<span className="text-white/70">{group}</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-white/50 text-xs">{data.count}건</span>
|
||||
<span className="text-primary-400 font-medium">{data.amount.toLocaleString()}원</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 우측: 합계 */}
|
||||
<div className="glass-effect rounded-xl p-4 min-w-[300px]">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-white/70 text-sm">총 항목</span>
|
||||
<span className="text-white font-medium">{filteredParts.length}개</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-white/70 text-sm">총 금액</span>
|
||||
<span className="text-amber-400 font-bold text-lg">{totalAmount.toLocaleString()}원</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,10 +8,14 @@ import {
|
||||
User,
|
||||
Calendar,
|
||||
ExternalLink,
|
||||
ClipboardList,
|
||||
Mail,
|
||||
Edit2,
|
||||
} from 'lucide-react';
|
||||
import { comms } from '@/communication';
|
||||
import { ProjectListItem, ProjectListResponse } from '@/types';
|
||||
import { ProjectDetailDialog } from '@/components/project';
|
||||
import clsx from 'clsx';
|
||||
|
||||
// 상태별 색상 매핑
|
||||
const statusColors: Record<string, { text: string; bg: string }> = {
|
||||
@@ -29,6 +33,11 @@ export function Project() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedProject, setSelectedProject] = useState<ProjectListItem | null>(null);
|
||||
const [showDetailDialog, setShowDetailDialog] = useState(false);
|
||||
const [expandedProject, setExpandedProject] = useState<number | null>(null);
|
||||
const [projectHistory, setProjectHistory] = useState<any[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [editingHistory, setEditingHistory] = useState<any | null>(null);
|
||||
const [editRemark, setEditRemark] = useState('');
|
||||
|
||||
// 필터 상태
|
||||
const [categories, setCategories] = useState<string[]>([]);
|
||||
@@ -37,13 +46,15 @@ export function Project() {
|
||||
const [selectedProcess, setSelectedProcess] = useState('전체');
|
||||
const [userFilter, setUserFilter] = useState('');
|
||||
const [currentUserName, setCurrentUserName] = useState('');
|
||||
const [userLevel, setUserLevel] = useState<number>(0);
|
||||
const [userCode, setUserCode] = useState<string>('');
|
||||
|
||||
// 상태 필터 체크박스
|
||||
const [statusChecks, setStatusChecks] = useState({
|
||||
검토: true,
|
||||
진행: true,
|
||||
대기: false,
|
||||
보류: false,
|
||||
보류: true,
|
||||
완료: true,
|
||||
'완료(보고)': false,
|
||||
취소: false,
|
||||
@@ -82,9 +93,12 @@ export function Project() {
|
||||
try {
|
||||
const loginStatus = await comms.checkLoginStatus();
|
||||
if (loginStatus.Success && loginStatus.IsLoggedIn && loginStatus.User) {
|
||||
const userName = (loginStatus.User as { NameK?: string }).NameK || loginStatus.User.Name || '';
|
||||
const user = loginStatus.User as { NameK?: string; Level?: number; Code?: string };
|
||||
const userName = user.NameK || loginStatus.User.Name || '';
|
||||
setCurrentUserName(userName);
|
||||
setUserFilter(userName);
|
||||
setUserLevel(user.Level || 0);
|
||||
setUserCode(user.Code || '');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('로그인 정보 로드 오류:', error);
|
||||
@@ -155,8 +169,11 @@ export function Project() {
|
||||
const filtered = projects.filter(
|
||||
(p) =>
|
||||
p.name?.toLowerCase().includes(key) ||
|
||||
p.userManager?.toLowerCase().includes(key) ||
|
||||
p.usermain?.toLowerCase().includes(key) ||
|
||||
p.name_champion?.toLowerCase().includes(key) ||
|
||||
p.name_design?.toLowerCase().includes(key) ||
|
||||
p.name_epanel?.toLowerCase().includes(key) ||
|
||||
p.name_software?.toLowerCase().includes(key) ||
|
||||
p.reqstaff?.toLowerCase().includes(key) ||
|
||||
p.orderno?.toLowerCase().includes(key) ||
|
||||
p.memo?.toLowerCase().includes(key)
|
||||
);
|
||||
@@ -186,6 +203,82 @@ export function Project() {
|
||||
setStatusChecks((prev) => ({ ...prev, [status]: !prev[status as keyof typeof prev] }));
|
||||
};
|
||||
|
||||
// 히스토리 토글 (편집 아이콘 클릭)
|
||||
const toggleHistory = async (projectIdx: number) => {
|
||||
if (expandedProject === projectIdx) {
|
||||
setExpandedProject(null);
|
||||
setProjectHistory([]);
|
||||
setEditingHistory(null);
|
||||
} else {
|
||||
setExpandedProject(projectIdx);
|
||||
setLoadingHistory(true);
|
||||
try {
|
||||
const result = await comms.getProjectHistory(projectIdx);
|
||||
if (result.Success && result.Data) {
|
||||
setProjectHistory(result.Data as any[]);
|
||||
} else {
|
||||
setProjectHistory([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('히스토리 로드 오류:', error);
|
||||
setProjectHistory([]);
|
||||
} finally {
|
||||
setLoadingHistory(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 히스토리 편집 시작
|
||||
const startEditHistory = (history: any) => {
|
||||
setEditingHistory(history);
|
||||
setEditRemark(history.remark || '');
|
||||
};
|
||||
|
||||
// 새 히스토리 추가 시작
|
||||
const startAddHistory = (projectIdx: number) => {
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
setEditingHistory({ pidx: projectIdx, pdate: today, progress: 0, remark: '', isNew: true });
|
||||
setEditRemark('');
|
||||
};
|
||||
|
||||
// 히스토리 저장
|
||||
const saveHistory = async () => {
|
||||
if (!editingHistory) return;
|
||||
|
||||
try {
|
||||
const historyData = {
|
||||
idx: editingHistory.idx || 0,
|
||||
pidx: editingHistory.pidx,
|
||||
pdate: editingHistory.pdate,
|
||||
progress: editingHistory.progress || 0,
|
||||
remark: editRemark,
|
||||
};
|
||||
|
||||
const result = await comms.saveProjectHistory(historyData);
|
||||
|
||||
if (result.Success) {
|
||||
// 저장 성공 후 히스토리 다시 로드
|
||||
const historyResult = await comms.getProjectHistory(editingHistory.pidx);
|
||||
if (historyResult.Success && historyResult.Data) {
|
||||
setProjectHistory(historyResult.Data as any[]);
|
||||
}
|
||||
} else {
|
||||
alert(result.Message || '저장에 실패했습니다.');
|
||||
}
|
||||
|
||||
setEditingHistory(null);
|
||||
setEditRemark('');
|
||||
} catch (error) {
|
||||
console.error('히스토리 저장 오류:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 편집 취소
|
||||
const cancelEdit = () => {
|
||||
setEditingHistory(null);
|
||||
setEditRemark('');
|
||||
};
|
||||
|
||||
// 페이징 계산
|
||||
const totalPages = Math.ceil(filteredProjects.length / pageSize);
|
||||
const paginatedProjects = filteredProjects.slice(
|
||||
@@ -326,7 +419,7 @@ export function Project() {
|
||||
<tr className="text-white/60 text-left">
|
||||
<th className="px-3 py-2 w-16">상태</th>
|
||||
<th className="px-3 py-2">프로젝트명</th>
|
||||
<th className="px-3 py-2 w-20">담당</th>
|
||||
<th className="px-3 py-2 w-20">챔피언</th>
|
||||
<th className="px-3 py-2 w-28">요청자</th>
|
||||
<th className="px-3 py-2 w-20 text-center">진행률</th>
|
||||
<th className="px-3 py-2 w-24">시작</th>
|
||||
@@ -351,33 +444,40 @@ export function Project() {
|
||||
) : (
|
||||
paginatedProjects.map((project) => {
|
||||
const statusColor = statusColors[project.status] || { text: 'text-white', bg: 'bg-white/10' };
|
||||
const isSelected = selectedProject?.idx === project.idx;
|
||||
const rowBg = project.bHighlight
|
||||
? 'bg-lime-500/10'
|
||||
: project.bCost
|
||||
? 'bg-yellow-500/10'
|
||||
: project.bmajoritem
|
||||
? 'bg-pink-500/10'
|
||||
: '';
|
||||
const isExpanded = expandedProject === project.idx;
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={project.idx}
|
||||
onClick={() => handleSelectProject(project)}
|
||||
className={`cursor-pointer transition-colors ${rowBg} ${
|
||||
isSelected ? 'bg-primary-500/20' : 'hover:bg-white/5'
|
||||
}`}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${statusColor.bg} ${statusColor.text}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`px-3 py-2 ${statusColor.text}`}>
|
||||
<div className="truncate max-w-xs" title={project.name}>
|
||||
{project.name}
|
||||
</div>
|
||||
</td>
|
||||
<>
|
||||
<tr
|
||||
key={project.idx}
|
||||
className={clsx(
|
||||
'border-b border-white/10 cursor-pointer hover:bg-white/5',
|
||||
isExpanded && 'bg-primary-900/30'
|
||||
)}
|
||||
onClick={() => toggleHistory(project.idx)}
|
||||
>
|
||||
<td className="px-3 py-2">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${statusColor.bg} ${statusColor.text}`}>
|
||||
{project.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className={`px-3 py-2 ${statusColor.text}`}>
|
||||
<div className="truncate max-w-xs" title={project.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleSelectProject(project);
|
||||
}}
|
||||
className="text-primary-300 hover:text-primary-200 transition-colors"
|
||||
title="편집"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</button>
|
||||
<span className="font-regular text-white/90">{project.name}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2 text-white/70">{project.name_champion || project.userManager}</td>
|
||||
<td className="px-3 py-2 text-white/70 text-xs">
|
||||
<div>{project.ReqLine}</div>
|
||||
@@ -400,20 +500,115 @@ export function Project() {
|
||||
<div className="text-white/40">{formatDate(project.edate)}</div>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{project.jasmin && project.jasmin > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openJasmin(project.jasmin);
|
||||
}}
|
||||
className="text-primary-400 hover:text-primary-300"
|
||||
title="자스민 열기"
|
||||
<div className="flex items-center gap-2">
|
||||
{project.jasmin && project.jasmin > 0 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openJasmin(project.jasmin);
|
||||
}}
|
||||
className="text-primary-400 hover:text-primary-300"
|
||||
title="자스민 열기"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
{(userLevel >= 9 || userCode === '395552') && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const w = window as any;
|
||||
if (w.CefSharp) {
|
||||
w.CefSharp.BindObjectAsync('bridge').then(() => {
|
||||
w.bridge?.OpenMailHistory();
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="text-cyan-400 hover:text-cyan-300"
|
||||
title="메일내역"
|
||||
>
|
||||
<Mail className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<a
|
||||
href={`#/partlist?idx=${project.idx}&name=${encodeURIComponent(project.name)}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-amber-400 hover:text-amber-300"
|
||||
title="파트리스트"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<ClipboardList className="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr key={`history-${project.idx}`}>
|
||||
<td colSpan={8} className="px-3 py-2 bg-primary-950/50">
|
||||
<div className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-sm font-semibold text-primary-300">주간 업무 내용</div>
|
||||
<button
|
||||
onClick={() => startAddHistory(project.idx)}
|
||||
className="text-xs px-3 py-1 bg-primary-500/20 hover:bg-primary-500/30 text-primary-400 rounded transition-colors"
|
||||
>
|
||||
+ 새 내용 등록
|
||||
</button>
|
||||
</div>
|
||||
{loadingHistory ? (
|
||||
<div className="text-white/50 text-sm">로딩 중...</div>
|
||||
) : editingHistory ? (
|
||||
<div className="bg-white/10 rounded p-3 space-y-3">
|
||||
<div className="flex gap-4 text-xs text-white/60">
|
||||
<span className="text-primary-400 font-semibold">{formatDate(editingHistory.pdate)}</span>
|
||||
<span>진행률: {editingHistory.progress || 0}%</span>
|
||||
</div>
|
||||
<textarea
|
||||
value={editRemark}
|
||||
onChange={(e) => setEditRemark(e.target.value)}
|
||||
className="w-full h-32 px-3 py-2 bg-white/5 border border-white/10 rounded text-white text-sm resize-none"
|
||||
placeholder="업무 내용을 입력하세요..."
|
||||
/>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="px-3 py-1 bg-white/5 hover:bg-white/10 text-white/70 rounded text-sm transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
<button
|
||||
onClick={saveHistory}
|
||||
className="px-3 py-1 bg-primary-500/20 hover:bg-primary-500/30 text-primary-400 rounded text-sm transition-colors"
|
||||
>
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : projectHistory.length > 0 ? (
|
||||
<div
|
||||
className="bg-white/5 rounded p-3 border-l-2 border-primary-500 cursor-pointer hover:bg-white/10 transition-colors"
|
||||
onClick={() => startEditHistory(projectHistory[0])}
|
||||
>
|
||||
<div className="flex gap-4 mb-2 text-xs">
|
||||
<span className="text-primary-400 font-semibold">{formatDate(projectHistory[0].pdate)}</span>
|
||||
<span className="text-white/60">진행률: {projectHistory[0].progress || 0}%</span>
|
||||
<span className="text-white/40">{projectHistory[0].wname || ''}</span>
|
||||
</div>
|
||||
{projectHistory[0].remark ? (
|
||||
<div className="text-sm text-white/80 whitespace-pre-wrap">{projectHistory[0].remark}</div>
|
||||
) : (
|
||||
<div className="text-sm text-white/40 italic">내용이 비어있습니다. 클릭하여 입력하세요.</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-white/50 text-sm text-center py-4">
|
||||
업무 내용이 없습니다. 새 내용을 등록하세요.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
})
|
||||
)}
|
||||
@@ -452,6 +647,8 @@ export function Project() {
|
||||
onClose={handleCloseDialog}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ const getStatusClass = (status: string): string => {
|
||||
|
||||
const getPriorityText = (seqno: number): string => {
|
||||
switch (seqno) {
|
||||
case -1: return '낮음';
|
||||
case 1: return '중요';
|
||||
case 2: return '매우 중요';
|
||||
case 3: return '긴급';
|
||||
@@ -46,6 +47,7 @@ const getPriorityText = (seqno: number): string => {
|
||||
|
||||
const getPriorityClass = (seqno: number): string => {
|
||||
switch (seqno) {
|
||||
case -1: return 'bg-white/5 text-white/40';
|
||||
case 1: return 'bg-primary-500/20 text-primary-300';
|
||||
case 2: return 'bg-warning-500/20 text-warning-300';
|
||||
case 3: return 'bg-danger-500/20 text-danger-300';
|
||||
@@ -561,9 +563,22 @@ function TodoModal({
|
||||
<Plus className="w-5 h-5 mr-2" />
|
||||
{title}
|
||||
</h2>
|
||||
<button onClick={onClose} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
<div className="flex items-center space-x-2">
|
||||
{isEdit && onComplete && currentStatus !== '5' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onComplete}
|
||||
disabled={processing}
|
||||
className="bg-success-500 hover:bg-success-600 text-white px-3 py-1.5 rounded-lg transition-colors flex items-center disabled:opacity-50 text-sm"
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-1" />
|
||||
완료 처리
|
||||
</button>
|
||||
)}
|
||||
<button onClick={onClose} className="text-white/70 hover:text-white transition-colors">
|
||||
<X className="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 내용 */}
|
||||
@@ -640,10 +655,11 @@ function TodoModal({
|
||||
onChange={(e) => setFormData(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>
|
||||
<option value={2}>매우 중요</option>
|
||||
<option value={1}>중요</option>
|
||||
<option value={0}>보통</option>
|
||||
<option value={-1}>낮음</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
@@ -661,42 +677,8 @@ function TodoModal({
|
||||
</div>
|
||||
|
||||
{/* 푸터 */}
|
||||
<div className="px-6 py-4 border-t border-white/10 flex justify-between">
|
||||
{/* 왼쪽: 삭제 버튼 (편집 모드일 때만) */}
|
||||
<div>
|
||||
{isEdit && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
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="px-6 py-4 border-t border-white/10 flex justify-end">
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors"
|
||||
>
|
||||
취소
|
||||
</button>
|
||||
{isEdit && onComplete && currentStatus !== '5' && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onComplete}
|
||||
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={onSubmit}
|
||||
@@ -710,6 +692,17 @@ function TodoModal({
|
||||
)}
|
||||
{submitText}
|
||||
</button>
|
||||
{isEdit && onDelete && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDelete}
|
||||
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>
|
||||
</div>
|
||||
|
||||
@@ -64,7 +64,7 @@ export interface PurchaseItem {
|
||||
|
||||
// 상태 관련 타입
|
||||
export type TodoStatus = '0' | '1' | '2' | '3' | '5';
|
||||
export type TodoPriority = 0 | 1 | 2 | 3;
|
||||
export type TodoPriority = -1 | 0 | 1 | 2 | 3;
|
||||
|
||||
// 로그 타입
|
||||
export interface LogEntry {
|
||||
@@ -447,6 +447,7 @@ export interface MachineBridgeInterface {
|
||||
Project_GetProcesses(): Promise<string>;
|
||||
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_SaveHistory(idx: number, pidx: number, pdate: string, progress: number, remark: string): Promise<string>;
|
||||
Project_GetDailyMemo(projectIdx: number): Promise<string>;
|
||||
|
||||
// Note API (메모장)
|
||||
@@ -467,10 +468,26 @@ export interface MachineBridgeInterface {
|
||||
|
||||
// Mail API (메일 발신 내역)
|
||||
Mail_GetList(startDate: string, endDate: string, searchKey: string): Promise<string>;
|
||||
Mail_AddData(cate: string, subject: string, fromlist: string, tolist: string, cc: string, bcc: string, body: string): Promise<string>;
|
||||
Mail_SendDirect(cate: string, subject: string, fromlist: string, tolist: string, cc: string, bcc: string, body: string): Promise<string>;
|
||||
Mail_SendOutlook(subject: string, tolist: string, cc: string, bcc: string, body: string): Promise<string>;
|
||||
|
||||
// Customs API (업체정보)
|
||||
Customs_GetList(searchKey: string): Promise<string>;
|
||||
Customs_GetDetail(idx: number): Promise<string>;
|
||||
|
||||
// License API (라이선스 관리)
|
||||
License_GetList(): Promise<string>;
|
||||
License_Add(name: string, version: string, meterialNo: string, supply: string, qty: number, uids: string, serialNo: string, remark: string, sdate: string, edate: string, manu: string, expire: boolean): Promise<string>;
|
||||
License_Update(idx: number, name: string, version: string, meterialNo: string, supply: string, qty: number, uids: string, serialNo: string, remark: string, sdate: string, edate: string, manu: string, expire: boolean): Promise<string>;
|
||||
License_Delete(idx: number): Promise<string>;
|
||||
License_OpenFolder(idx: number): Promise<string>;
|
||||
License_ExportCSV(filePath: string): Promise<string>;
|
||||
|
||||
// PartList API (파트리스트)
|
||||
PartList_GetList(projectIdx: number): Promise<string>;
|
||||
PartList_Save(idx: number, projectIdx: number, itemgroup: string, itemname: string, item: string, itemmodel: string, itemscale: string, itemunit: string, qty: number, price: number, itemsupply: string, itemsupplyidx: number, itemmanu: string, itemsid: string, option1: string, remark: string, no: number, qtybuy: number): Promise<string>;
|
||||
PartList_Delete(idx: number): Promise<string>;
|
||||
}
|
||||
|
||||
// 사용자 권한 정보 타입
|
||||
@@ -503,6 +520,8 @@ export interface AppVersionInfo {
|
||||
ProductName: string;
|
||||
ProductVersion: string;
|
||||
DisplayVersion: string;
|
||||
MaxVersion?: string;
|
||||
HasNewVersion?: boolean;
|
||||
}
|
||||
|
||||
// 사용자 전체 정보 저장용 타입
|
||||
@@ -895,3 +914,48 @@ export interface CustomItem {
|
||||
name2: string;
|
||||
gcode: string;
|
||||
}
|
||||
|
||||
// 라이선스 타입
|
||||
export interface LicenseItem {
|
||||
idx?: number;
|
||||
gcode?: string;
|
||||
expire?: boolean;
|
||||
name?: string;
|
||||
version?: string;
|
||||
meterialNo?: string;
|
||||
supply?: string;
|
||||
qty?: number;
|
||||
uids?: string;
|
||||
serialNo?: string;
|
||||
remark?: string;
|
||||
sdate?: string;
|
||||
edate?: string;
|
||||
manu?: string;
|
||||
wuid?: string;
|
||||
wdate?: string;
|
||||
}
|
||||
|
||||
// 파트리스트 타입 (ProjectsPart 테이블)
|
||||
export interface PartListItem {
|
||||
idx: number;
|
||||
Project: number;
|
||||
itemgroup?: string;
|
||||
itemname: string;
|
||||
item: string; // 자재번호
|
||||
itemmodel?: string;
|
||||
itemscale?: string;
|
||||
itemunit?: string;
|
||||
qty?: number;
|
||||
price?: number;
|
||||
amt?: number; // 계산된 금액 (qty * price)
|
||||
itemsupply?: string;
|
||||
itemsupplyidx?: number;
|
||||
itemmanu?: string;
|
||||
itemsid?: string;
|
||||
option1?: string;
|
||||
remark?: string;
|
||||
no?: number;
|
||||
qtybuy?: number;
|
||||
wuid?: string;
|
||||
wdate?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="CsvHelper" version="30.0.1" targetFramework="net46" />
|
||||
<package id="EntityFramework" version="6.2.0" targetFramework="net45" />
|
||||
@@ -9,9 +9,11 @@
|
||||
<package id="Microsoft.ReportingServices.ReportViewerControl.Winforms" version="150.1586.0" targetFramework="net46" />
|
||||
<package id="Microsoft.SqlServer.Types" version="14.0.314.76" targetFramework="net46" />
|
||||
<package id="Microsoft.Web.WebView2" version="1.0.2210.55" targetFramework="net46" />
|
||||
<package id="NetOfficeFw.Core" version="1.8.1" targetFramework="net46" />
|
||||
<package id="NetOfficeFw.Outlook" version="1.8.1" targetFramework="net46" />
|
||||
<package id="Newtonsoft.Json" version="13.0.3" targetFramework="net46" />
|
||||
<package id="System.Buffers" version="4.5.1" targetFramework="net46" />
|
||||
<package id="System.Memory" version="4.5.5" targetFramework="net46" />
|
||||
<package id="System.Runtime.CompilerServices.Unsafe" version="4.7.1" targetFramework="net46" />
|
||||
<package id="System.ValueTuple" version="4.5.0" targetFramework="net46" />
|
||||
</packages>
|
||||
</packages>
|
||||
Reference in New Issue
Block a user