This commit is contained in:
backuppc
2025-12-05 17:33:12 +09:00
parent 8e8d1f91b4
commit 77f1ddab80
92 changed files with 4878 additions and 20435 deletions

View File

@@ -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" />

View File

@@ -9,7 +9,7 @@
<ErrorReportUrlHistory />
<FallbackCulture>ko-KR</FallbackCulture>
<VerifyUploadedFiles>false</VerifyUploadedFiles>
<ProjectView>ProjectFiles</ProjectView>
<ProjectView>ShowAllFiles</ProjectView>
</PropertyGroup>
<PropertyGroup>
<EnableSecurityDebugging>false</EnableSecurityDebugging>

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

View File

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

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

View File

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

View File

@@ -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)

View File

@@ -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;

View File

@@ -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&amp;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"
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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework&quot;"
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=&quot;data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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&amp;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&amp;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework&quot;" 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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;connect timeout=30;encrypt=False;trustservercertificate=True;MultipleActiveResultSets=True;App=EntityFramework&quot;" 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=&quot;data source=10.141.18.50;initial catalog=S1ACCESS300;persist security info=True;user id=amkoruser;password=AmkorUser!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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=&quot;data source=K4FASQL.kr.ds.amkor.com,50150;initial catalog=EE;persist security info=True;user id=eeadm;password=uJnU8a8q&amp;DJ+ug-D!;MultipleActiveResultSets=True;App=EntityFramework&quot;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&amp;DJ+ug-D;Encrypt=False;TrustServerCertificate=True" providerName="System.Data.SqlClient" />
</connectionStrings>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">

View File

@@ -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;

View File

@@ -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";

View File

@@ -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">

View File

@@ -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 - 개발용 */}

View File

@@ -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();

View File

@@ -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 ? '업무일지 수정' : '업무일지 등록'}

View File

@@ -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}

View File

@@ -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 */}

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

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

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

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

View File

@@ -1 +1,2 @@
export { ProjectDetailDialog } from './ProjectDetailDialog';
export { PartListDialog } from './PartListDialog';

View File

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

View File

@@ -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>
))
)}

View File

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

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

View File

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

View File

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

View File

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

View File

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