업무일지 목록 및 상세화면 디자인 변경.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Owin;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Web;
|
||||
@@ -17,35 +18,228 @@ namespace Project.Web.Controllers
|
||||
}
|
||||
|
||||
// DELETE api/values/5
|
||||
public void Delete(int id)
|
||||
[HttpDelete]
|
||||
public HttpResponseMessage Delete(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (id <= 0)
|
||||
{
|
||||
throw new Exception("유효하지 않은 업무일지 ID입니다.");
|
||||
}
|
||||
|
||||
// 직접 SQL 삭제 실행
|
||||
string connectionString = Properties.Settings.Default.gwcs;
|
||||
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
string deleteSql = @"
|
||||
DELETE FROM JobReport
|
||||
WHERE idx = @idx AND gcode = @gcode";
|
||||
|
||||
using (var command = new System.Data.SqlClient.SqlCommand(deleteSql, connection))
|
||||
{
|
||||
command.Parameters.AddWithValue("@idx", id);
|
||||
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
|
||||
int rowsAffected = command.ExecuteNonQuery();
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
throw new Exception("업무일지를 찾을 수 없거나 삭제 권한이 없습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var jsonData = "{\"success\":true,\"message\":\"데이터가 성공적으로 삭제되었습니다.\"}";
|
||||
|
||||
var resp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
jsonData,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorResp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
$"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
return errorResp;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public string Add(FormCollection tbpdate)
|
||||
public string Add(FormCollection formData)
|
||||
{
|
||||
var vals = Request.GetQueryNameValuePairs();
|
||||
return string.Empty;
|
||||
try
|
||||
{
|
||||
// 폼 데이터에서 값 추출
|
||||
var pdate = formData["pdate"] ?? DateTime.Now.ToShortDateString();
|
||||
var status = formData["status"] ?? "";
|
||||
var projectName = formData["projectName"] ?? "";
|
||||
var requestpart = formData["requestpart"] ?? "";
|
||||
var type = formData["type"] ?? "";
|
||||
var description = formData["description"] ?? "";
|
||||
var otStart = formData["otStart"] ?? "";
|
||||
var otEnd = formData["otEnd"] ?? "";
|
||||
|
||||
decimal hrs = 0;
|
||||
decimal.TryParse(formData["hrs"], out hrs);
|
||||
|
||||
decimal ot = 0;
|
||||
decimal.TryParse(formData["ot"], out ot);
|
||||
|
||||
// 직접 SQL 삽입 실행
|
||||
string connectionString = Properties.Settings.Default.gwcs;
|
||||
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
string insertSql = @"
|
||||
INSERT INTO JobReport
|
||||
(gcode, pdate, projectName, uid, requestpart, status, type, description, hrs, ot, otStart, otEnd, wuid, wdate)
|
||||
VALUES
|
||||
(@gcode, @pdate, @projectName, @uid, @requestpart, @status, @type, @description, @hrs, @ot, @otStart, @otEnd, @wuid, @wdate)";
|
||||
|
||||
using (var command = new System.Data.SqlClient.SqlCommand(insertSql, connection))
|
||||
{
|
||||
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
command.Parameters.AddWithValue("@pdate", pdate);
|
||||
command.Parameters.AddWithValue("@projectName", projectName);
|
||||
command.Parameters.AddWithValue("@uid", FCOMMON.info.Login.no);
|
||||
command.Parameters.AddWithValue("@requestpart", requestpart);
|
||||
command.Parameters.AddWithValue("@status", status);
|
||||
command.Parameters.AddWithValue("@type", type);
|
||||
command.Parameters.AddWithValue("@description", description);
|
||||
command.Parameters.AddWithValue("@hrs", hrs);
|
||||
command.Parameters.AddWithValue("@ot", ot);
|
||||
command.Parameters.AddWithValue("@otStart", string.IsNullOrEmpty(otStart) ? (object)DBNull.Value : otStart);
|
||||
command.Parameters.AddWithValue("@otEnd", string.IsNullOrEmpty(otEnd) ? (object)DBNull.Value : otEnd);
|
||||
command.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||
command.Parameters.AddWithValue("@wdate", DateTime.Now);
|
||||
|
||||
command.ExecuteNonQuery();
|
||||
}
|
||||
}
|
||||
|
||||
return "{\"success\":true,\"message\":\"데이터가 성공적으로 저장되었습니다.\"}";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return $"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}";
|
||||
}
|
||||
}
|
||||
|
||||
//[HttpPost]
|
||||
//public string Edit([FromBody] string value)
|
||||
//{
|
||||
// var vals = Request.GetQueryNameValuePairs();
|
||||
|
||||
// var req = Request.GetRequestContext();
|
||||
|
||||
// return string.Empty;
|
||||
//}
|
||||
|
||||
[HttpPost]
|
||||
public string Edit(FormCollection value)
|
||||
public HttpResponseMessage Edit()
|
||||
{
|
||||
var vals = Request.GetQueryNameValuePairs();
|
||||
try
|
||||
{
|
||||
// Request.Form에서 직접 값 추출
|
||||
var idx = HttpContext.Current.Request.Form["idx"];
|
||||
var pdate = HttpContext.Current.Request.Form["pdate"] ?? DateTime.Now.ToShortDateString();
|
||||
var status = HttpContext.Current.Request.Form["status"] ?? "";
|
||||
var projectName = HttpContext.Current.Request.Form["projectName"] ?? "";
|
||||
var requestpart = HttpContext.Current.Request.Form["requestpart"] ?? "";
|
||||
var type = HttpContext.Current.Request.Form["type"] ?? "";
|
||||
var description = HttpContext.Current.Request.Form["description"] ?? "";
|
||||
var otStart = HttpContext.Current.Request.Form["otStart"] ?? "";
|
||||
var otEnd = HttpContext.Current.Request.Form["otEnd"] ?? "";
|
||||
|
||||
decimal hrs = 0;
|
||||
decimal.TryParse(HttpContext.Current.Request.Form["hrs"], out hrs);
|
||||
|
||||
decimal ot = 0;
|
||||
decimal.TryParse(HttpContext.Current.Request.Form["ot"], out ot);
|
||||
|
||||
var req = Request.GetRequestContext();
|
||||
int idxNum = 0;
|
||||
int.TryParse(idx, out idxNum);
|
||||
|
||||
return string.Empty;
|
||||
if (idxNum <= 0)
|
||||
{
|
||||
throw new Exception("유효하지 않은 업무일지 ID입니다.");
|
||||
}
|
||||
|
||||
// 직접 SQL 업데이트 실행
|
||||
string connectionString = Properties.Settings.Default.gwcs;
|
||||
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
string updateSql = @"
|
||||
UPDATE JobReport
|
||||
SET pdate = @pdate,
|
||||
status = @status,
|
||||
projectName = @projectName,
|
||||
requestpart = @requestpart,
|
||||
type = @type,
|
||||
description = @description,
|
||||
hrs = @hrs,
|
||||
ot = @ot,
|
||||
otStart = @otStart,
|
||||
otEnd = @otEnd,
|
||||
wuid = @wuid,
|
||||
wdate = @wdate
|
||||
WHERE idx = @idx AND gcode = @gcode";
|
||||
|
||||
using (var command = new System.Data.SqlClient.SqlCommand(updateSql, connection))
|
||||
{
|
||||
command.Parameters.AddWithValue("@idx", idxNum);
|
||||
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
command.Parameters.AddWithValue("@pdate", pdate);
|
||||
command.Parameters.AddWithValue("@status", status);
|
||||
command.Parameters.AddWithValue("@projectName", projectName);
|
||||
command.Parameters.AddWithValue("@requestpart", requestpart);
|
||||
command.Parameters.AddWithValue("@type", type);
|
||||
command.Parameters.AddWithValue("@description", description);
|
||||
command.Parameters.AddWithValue("@hrs", hrs);
|
||||
command.Parameters.AddWithValue("@ot", ot);
|
||||
command.Parameters.AddWithValue("@otStart", string.IsNullOrEmpty(otStart) ? (object)DBNull.Value : otStart);
|
||||
command.Parameters.AddWithValue("@otEnd", string.IsNullOrEmpty(otEnd) ? (object)DBNull.Value : otEnd);
|
||||
command.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
|
||||
command.Parameters.AddWithValue("@wdate", DateTime.Now);
|
||||
|
||||
int rowsAffected = command.ExecuteNonQuery();
|
||||
|
||||
if (rowsAffected == 0)
|
||||
{
|
||||
throw new Exception("업무일지를 찾을 수 없거나 수정 권한이 없습니다.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var jsonData = "{\"success\":true,\"message\":\"데이터가 성공적으로 수정되었습니다.\"}";
|
||||
|
||||
var resp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
jsonData,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorResp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
$"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
return errorResp;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@@ -315,5 +509,317 @@ namespace Project.Web.Controllers
|
||||
|
||||
return resp;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public HttpResponseMessage GetJobDetail(int id)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 특정 업무일지의 전체 정보 조회
|
||||
string connectionString = Properties.Settings.Default.gwcs;
|
||||
|
||||
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
string selectSql = @"
|
||||
SELECT idx, pdate, gcode, uid as id, '' as name, '' as process, type, '' as svalue,
|
||||
hrs, ot, requestpart, '' as package, '' as userProcess, status, projectName,
|
||||
description, '' as ww, otStart, otEnd, ot as ot2, '' as otReason,
|
||||
'' as grade, '' as indate, '' as outdate, pidx
|
||||
FROM JobReport WITH (NOLOCK)
|
||||
WHERE gcode = @gcode AND uid = @uid AND idx = @idx";
|
||||
|
||||
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
|
||||
{
|
||||
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
command.Parameters.AddWithValue("@uid", FCOMMON.info.Login.no);
|
||||
command.Parameters.AddWithValue("@idx", id);
|
||||
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
if (reader.Read())
|
||||
{
|
||||
var item = new
|
||||
{
|
||||
idx = reader["idx"],
|
||||
pdate = reader["pdate"],
|
||||
gcode = reader["gcode"],
|
||||
id = reader["id"],
|
||||
name = reader["name"],
|
||||
process = reader["process"],
|
||||
type = reader["type"],
|
||||
svalue = reader["svalue"],
|
||||
hrs = reader["hrs"],
|
||||
ot = reader["ot"],
|
||||
requestpart = reader["requestpart"],
|
||||
package = reader["package"],
|
||||
userProcess = reader["userProcess"],
|
||||
status = reader["status"],
|
||||
projectName = reader["projectName"],
|
||||
description = reader["description"], // 전체 내용
|
||||
ww = reader["ww"],
|
||||
otStart = reader["otStart"],
|
||||
otEnd = reader["otEnd"],
|
||||
ot2 = reader["ot2"],
|
||||
otReason = reader["otReason"],
|
||||
grade = reader["grade"],
|
||||
indate = reader["indate"],
|
||||
outdate = reader["outdate"],
|
||||
pidx = reader["pidx"]
|
||||
};
|
||||
|
||||
// JSON 형태로 변환
|
||||
decimal hrs = 0;
|
||||
decimal ot = 0;
|
||||
int idx = 0;
|
||||
int pidx = 0;
|
||||
|
||||
try { hrs = Convert.ToDecimal(item.hrs); } catch { hrs = 0; }
|
||||
try { ot = Convert.ToDecimal(item.ot); } catch { ot = 0; }
|
||||
try { idx = Convert.ToInt32(item.idx); } catch { idx = 0; }
|
||||
try { pidx = Convert.ToInt32(item.pidx); } catch { pidx = 0; }
|
||||
|
||||
var desc = EscapeJsonString(item.description?.ToString() ?? ""); // 전체 내용
|
||||
var pdate = EscapeJsonString(item.pdate?.ToString() ?? "");
|
||||
var status = EscapeJsonString(item.status?.ToString() ?? "");
|
||||
var type = EscapeJsonString(item.type?.ToString() ?? "");
|
||||
var projectName = EscapeJsonString(item.projectName?.ToString() ?? "");
|
||||
var requestpart = EscapeJsonString(item.requestpart?.ToString() ?? "");
|
||||
var otStart = EscapeJsonString(item.otStart?.ToString() ?? "");
|
||||
var otEnd = EscapeJsonString(item.otEnd?.ToString() ?? "");
|
||||
|
||||
var jsonData = "{";
|
||||
jsonData += $"\"pdate\":\"{pdate}\",";
|
||||
jsonData += $"\"status\":\"{status}\",";
|
||||
jsonData += $"\"type\":\"{type}\",";
|
||||
jsonData += $"\"projectName\":\"{projectName}\",";
|
||||
jsonData += $"\"requestpart\":\"{requestpart}\",";
|
||||
jsonData += $"\"hrs\":{hrs},";
|
||||
jsonData += $"\"ot\":{ot},";
|
||||
jsonData += $"\"description\":\"{desc}\",";
|
||||
jsonData += $"\"otStart\":\"{otStart}\",";
|
||||
jsonData += $"\"otEnd\":\"{otEnd}\",";
|
||||
jsonData += $"\"idx\":{idx},";
|
||||
jsonData += $"\"pidx\":{pidx}";
|
||||
jsonData += "}";
|
||||
|
||||
var resp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
jsonData,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 데이터를 찾을 수 없는 경우
|
||||
var errorResp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
"{\"error\":\"데이터를 찾을 수 없습니다.\"}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
return errorResp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorResp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
$"{{\"error\":\"{ex.Message}\"}}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
return errorResp;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public HttpResponseMessage GetJobData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var gets = Request.GetQueryNameValuePairs();
|
||||
var startDateParam = gets.Where(t => t.Key == "startDate").FirstOrDefault();
|
||||
var endDateParam = gets.Where(t => t.Key == "endDate").FirstOrDefault();
|
||||
|
||||
var startDate = startDateParam.Key != null ? startDateParam.Value : null;
|
||||
var endDate = endDateParam.Key != null ? endDateParam.Value : null;
|
||||
|
||||
// 날짜 파라미터 처리
|
||||
string sd, ed;
|
||||
if (!string.IsNullOrEmpty(startDate) && !string.IsNullOrEmpty(endDate))
|
||||
{
|
||||
sd = startDate;
|
||||
ed = endDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 기본값: 오늘부터 -2주
|
||||
var now = DateTime.Now;
|
||||
var twoWeeksAgo = now.AddDays(-14);
|
||||
sd = twoWeeksAgo.ToShortDateString();
|
||||
ed = now.ToShortDateString();
|
||||
}
|
||||
|
||||
// 직접 SQL로 데이터 조회
|
||||
string connectionString = Properties.Settings.Default.gwcs;
|
||||
var jobReports = new List<dynamic>();
|
||||
|
||||
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
|
||||
{
|
||||
connection.Open();
|
||||
|
||||
string selectSql = @"
|
||||
SELECT idx, pdate, gcode, uid as id, '' as name, '' as process, type, '' as svalue,
|
||||
hrs, ot, requestpart, '' as package, '' as userProcess, status, projectName,
|
||||
description, '' as ww, otStart, otEnd, ot as ot2, '' as otReason,
|
||||
'' as grade, '' as indate, '' as outdate, pidx
|
||||
FROM JobReport WITH (NOLOCK)
|
||||
WHERE gcode = @gcode AND uid = @uid AND pdate BETWEEN @startDate AND @endDate
|
||||
ORDER BY pdate DESC";
|
||||
|
||||
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
|
||||
{
|
||||
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
|
||||
command.Parameters.AddWithValue("@uid", FCOMMON.info.Login.no);
|
||||
command.Parameters.AddWithValue("@startDate", sd);
|
||||
command.Parameters.AddWithValue("@endDate", ed);
|
||||
|
||||
using (var reader = command.ExecuteReader())
|
||||
{
|
||||
while (reader.Read())
|
||||
{
|
||||
jobReports.Add(new
|
||||
{
|
||||
idx = reader["idx"],
|
||||
pdate = reader["pdate"],
|
||||
gcode = reader["gcode"],
|
||||
id = reader["id"],
|
||||
name = reader["name"],
|
||||
process = reader["process"],
|
||||
type = reader["type"],
|
||||
svalue = reader["svalue"],
|
||||
hrs = reader["hrs"],
|
||||
ot = reader["ot"],
|
||||
requestpart = reader["requestpart"],
|
||||
package = reader["package"],
|
||||
userProcess = reader["userProcess"],
|
||||
status = reader["status"],
|
||||
projectName = reader["projectName"],
|
||||
description = reader["description"],
|
||||
ww = reader["ww"],
|
||||
otStart = reader["otStart"],
|
||||
otEnd = reader["otEnd"],
|
||||
ot2 = reader["ot2"],
|
||||
otReason = reader["otReason"],
|
||||
grade = reader["grade"],
|
||||
indate = reader["indate"],
|
||||
outdate = reader["outdate"],
|
||||
pidx = reader["pidx"]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// JSON 형태로 변환
|
||||
var jsonData = "[";
|
||||
bool first = true;
|
||||
|
||||
if (jobReports != null)
|
||||
{
|
||||
foreach (var item in jobReports)
|
||||
{
|
||||
if (!first) jsonData += ",";
|
||||
first = false;
|
||||
|
||||
// DBNull 처리를 위한 안전한 변환
|
||||
decimal hrs = 0;
|
||||
decimal ot = 0;
|
||||
int idx = 0;
|
||||
int pidx = 0;
|
||||
|
||||
try { hrs = Convert.ToDecimal(item.hrs); } catch { hrs = 0; }
|
||||
try { ot = Convert.ToDecimal(item.ot); } catch { ot = 0; }
|
||||
try { idx = Convert.ToInt32(item.idx); } catch { idx = 0; }
|
||||
try { pidx = Convert.ToInt32(item.pidx); } catch { pidx = 0; }
|
||||
|
||||
// 안전한 JSON 문자열 이스케이프 처리 및 25자 제한
|
||||
var fullDesc = item.description?.ToString() ?? "";
|
||||
var desc = EscapeJsonString(fullDesc.Length > 25 ? fullDesc.Substring(0, 25) + "..." : fullDesc);
|
||||
var pdate = EscapeJsonString(item.pdate?.ToString() ?? "");
|
||||
var ww = EscapeJsonString(item.ww?.ToString() ?? "");
|
||||
var name = EscapeJsonString(item.name?.ToString() ?? "");
|
||||
var status = EscapeJsonString(item.status?.ToString() ?? "");
|
||||
var type = EscapeJsonString(item.type?.ToString() ?? "");
|
||||
var projectName = EscapeJsonString(item.projectName?.ToString() ?? "");
|
||||
var requestpart = EscapeJsonString(item.requestpart?.ToString() ?? "");
|
||||
var userProcess = EscapeJsonString(item.userProcess?.ToString() ?? "");
|
||||
|
||||
jsonData += "{";
|
||||
jsonData += $"\"pdate\":\"{pdate}\",";
|
||||
jsonData += $"\"ww\":\"{ww}\",";
|
||||
jsonData += $"\"name\":\"{name}\",";
|
||||
jsonData += $"\"status\":\"{status}\",";
|
||||
jsonData += $"\"type\":\"{type}\",";
|
||||
jsonData += $"\"projectName\":\"{projectName}\",";
|
||||
jsonData += $"\"requestpart\":\"{requestpart}\",";
|
||||
jsonData += $"\"userProcess\":\"{userProcess}\",";
|
||||
jsonData += $"\"hrs\":{hrs},";
|
||||
jsonData += $"\"ot\":{ot},";
|
||||
jsonData += $"\"description\":\"{desc}\",";
|
||||
jsonData += $"\"idx\":{idx},";
|
||||
jsonData += $"\"pidx\":{pidx}";
|
||||
jsonData += "}";
|
||||
}
|
||||
}
|
||||
jsonData += "]";
|
||||
|
||||
var resp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
jsonData,
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
|
||||
return resp;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorResp = new HttpResponseMessage()
|
||||
{
|
||||
Content = new StringContent(
|
||||
$"{{\"error\":\"{ex.Message}\"}}",
|
||||
System.Text.Encoding.UTF8,
|
||||
"application/json")
|
||||
};
|
||||
return errorResp;
|
||||
}
|
||||
}
|
||||
|
||||
private string EscapeJsonString(string input)
|
||||
{
|
||||
if (string.IsNullOrEmpty(input))
|
||||
return "";
|
||||
|
||||
// 제어 문자 제거 (0x00-0x1F 범위)
|
||||
var cleanInput = System.Text.RegularExpressions.Regex.Replace(input, @"[\x00-\x08\x0B\x0C\x0E-\x1F]", "");
|
||||
|
||||
return cleanInput
|
||||
.Replace("\\", "\\\\") // 백슬래시
|
||||
.Replace("\"", "\\\"") // 따옴표
|
||||
.Replace("\n", "\\n") // 개행
|
||||
.Replace("\r", "\\r") // 캐리지 리턴
|
||||
.Replace("\t", "\\t"); // 탭
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1052,11 +1052,13 @@
|
||||
${seqnoText}
|
||||
</span>
|
||||
</div>
|
||||
${expireText ? `<span class="text-xs ${expireClass}">${expireText}</span>` : ''}
|
||||
<div class="text-xs text-right">
|
||||
${expireText ? `<div class="${expireClass}">${expireText}</div>` : ''}
|
||||
${todo.request ? `<div class="text-white/50">요청: ${todo.request}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-white font-medium text-sm mb-1 line-clamp-1">${todo.title || '제목 없음'}</h3>
|
||||
<p class="text-white/70 text-xs line-clamp-2">${todo.remark || ''}</p>
|
||||
${todo.request ? `<p class="text-white/50 text-xs mt-1">요청자: ${todo.request}</p>` : ''}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
@@ -157,8 +157,9 @@
|
||||
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-white/80">총 근무시간</p>
|
||||
<p id="totalHours" class="text-2xl font-bold text-white">0h</p>
|
||||
<p class="text-sm font-medium text-white/80">오늘 근무시간</p>
|
||||
<p id="todayHours" class="text-2xl font-bold text-green-300">0h</p>
|
||||
<p id="todayProgress" class="text-sm text-white/60">(목표 8시간의 0%)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,29 +246,19 @@
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="status">
|
||||
상태 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="hrs">
|
||||
근무시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="projectName">
|
||||
프로젝트명 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">업무내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="requestpart">
|
||||
요청부서 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="package">
|
||||
패키지 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="type">
|
||||
타입 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="process">
|
||||
프로세스 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">업무내용</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="hrs">
|
||||
근무시간 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider cursor-pointer hover:bg-white/20 transition-colors" data-sort="ot">
|
||||
초과근무 <i data-feather="chevron-down" class="w-4 h-4 inline ml-1"></i>
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-white/80 uppercase tracking-wider">초과근무 시간</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="jobTableBody" class="divide-y divide-white/10">
|
||||
@@ -318,18 +309,32 @@
|
||||
</div>
|
||||
|
||||
<!-- 상세 모달 -->
|
||||
<div id="detailModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full hidden z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 md:w-3/4 lg:w-1/2 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900">업무 상세정보</h3>
|
||||
<button id="closeModal" class="text-gray-400 hover:text-gray-600">
|
||||
<i data-feather="x" class="w-6 h-6"></i>
|
||||
<div id="detailModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm hidden z-50">
|
||||
<div class="flex items-center justify-center min-h-screen p-4">
|
||||
<div class="glass-effect rounded-2xl w-full max-w-6xl animate-slide-up">
|
||||
<!-- 모달 헤더 -->
|
||||
<div class="px-6 py-4 border-b border-white/10 flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-white flex items-center">
|
||||
<svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
||||
</svg>
|
||||
업무일지 편집
|
||||
</h2>
|
||||
<button id="closeModal" class="text-white/70 hover:text-white transition-colors">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="modalContent" class="space-y-4">
|
||||
<!-- 모달 내용이 여기에 동적으로 로드됩니다 -->
|
||||
|
||||
<!-- 모달 내용 -->
|
||||
<div class="p-6">
|
||||
<div id="modalContent" class="space-y-4">
|
||||
<!-- 모달 내용이 여기에 동적으로 로드됩니다 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 모달 푸터는 동적으로 생성됨 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -486,24 +491,15 @@
|
||||
});
|
||||
|
||||
function initializeApp() {
|
||||
// 현재 날짜 표시
|
||||
// 조회기간 기본값 설정 (오늘부터 -2주)
|
||||
const now = new Date();
|
||||
document.getElementById('currentDate').textContent = now.toLocaleDateString('ko-KR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long'
|
||||
});
|
||||
|
||||
// 조회기간 기본값 설정 (이번 달)
|
||||
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
||||
|
||||
document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0];
|
||||
document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0];
|
||||
document.getElementById('startDate').value = twoWeeksAgo;
|
||||
document.getElementById('endDate').value = today;
|
||||
|
||||
// 이벤트 리스너 등록
|
||||
document.getElementById('refreshBtn').addEventListener('click', loadJobData);
|
||||
document.getElementById('startDate').addEventListener('change', loadJobData);
|
||||
document.getElementById('endDate').addEventListener('change', loadJobData);
|
||||
document.getElementById('statusFilter').addEventListener('change', filterData);
|
||||
@@ -557,15 +553,17 @@
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
|
||||
async function loadJobData() {
|
||||
showLoading(true);
|
||||
let responseData = null;
|
||||
try {
|
||||
// 조회기간 파라미터 가져오기
|
||||
const startDate = document.getElementById('startDate').value;
|
||||
const endDate = document.getElementById('endDate').value;
|
||||
|
||||
// API URL 구성
|
||||
let url = '/DashBoard/GetJobData';
|
||||
// API URL 구성 - 새로운 GetJobData API 사용
|
||||
let url = '/Jobreport/GetJobData';
|
||||
const params = new URLSearchParams();
|
||||
if (startDate) params.append('startDate', startDate);
|
||||
if (endDate) params.append('endDate', endDate);
|
||||
@@ -574,21 +572,43 @@
|
||||
url += '?' + params.toString();
|
||||
}
|
||||
|
||||
console.log('Fetching data from:', url);
|
||||
const response = await fetch(url);
|
||||
console.log('Response status:', response.status);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('데이터를 불러오는데 실패했습니다.');
|
||||
throw new Error(`HTTP ${response.status}: 데이터를 불러오는데 실패했습니다.`);
|
||||
}
|
||||
jobData = await response.json();
|
||||
|
||||
// JSON 응답 직접 파싱
|
||||
const responseText = await response.text();
|
||||
console.log('Raw response:', responseText);
|
||||
|
||||
responseData = JSON.parse(responseText);
|
||||
console.log('Parsed response:', responseData);
|
||||
|
||||
// 응답 데이터가 배열인지 확인
|
||||
if (Array.isArray(responseData)) {
|
||||
jobData = responseData;
|
||||
} else if (responseData.error) {
|
||||
throw new Error(responseData.error);
|
||||
} else {
|
||||
// 데이터가 배열이 아닌 경우 빈 배열로 초기화
|
||||
console.warn('Response is not an array, initializing empty array');
|
||||
jobData = [];
|
||||
}
|
||||
|
||||
filteredData = [...jobData];
|
||||
|
||||
updateStatistics();
|
||||
updateProjectFilter();
|
||||
sortData();
|
||||
renderTable();
|
||||
// 확장된 행들 닫기
|
||||
closeAllExpandedRows();
|
||||
} catch (error) {
|
||||
console.error('Error loading job data:', error);
|
||||
console.error('Response data:', responseData);
|
||||
jobData = []; // 에러 시 빈 배열로 초기화
|
||||
filteredData = [];
|
||||
showError('데이터를 불러오는데 실패했습니다: ' + error.message);
|
||||
} finally {
|
||||
showLoading(false);
|
||||
@@ -597,14 +617,53 @@
|
||||
|
||||
function updateStatistics() {
|
||||
const totalDays = new Set(jobData.map(item => item.pdate)).size;
|
||||
const totalHours = jobData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0);
|
||||
const totalOT = jobData.reduce((sum, item) => sum + (parseFloat(item.ot) || 0), 0);
|
||||
const activeProjects = new Set(jobData.filter(item => item.status === '진행중').map(item => item.projectName)).size;
|
||||
|
||||
// 오늘 날짜 계산
|
||||
const now = new Date();
|
||||
const today = now.toISOString().split('T')[0]; // YYYY-MM-DD 형식 (2025-07-31)
|
||||
|
||||
const todayData = jobData.filter(item => {
|
||||
if (!item.pdate) return false;
|
||||
|
||||
const itemDate = item.pdate.toString();
|
||||
|
||||
// 정확한 날짜 비교: 서버에서 오는 날짜 형식에 맞춰 비교
|
||||
if (itemDate.length >= 10) {
|
||||
// YYYY-MM-DD 형식인 경우
|
||||
return itemDate.substring(0, 10) === today;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
// 오늘 데이터가 있을 때만 시간을 계산, 없으면 명시적으로 0
|
||||
let todayHours = 0;
|
||||
if (todayData.length > 0) {
|
||||
todayHours = todayData.reduce((sum, item) => sum + (parseFloat(item.hrs) || 0), 0);
|
||||
}
|
||||
const todayProgress = (todayHours / 8) * 100; // 8시간 기준으로 퍼센트 계산
|
||||
|
||||
document.getElementById('totalDays').textContent = totalDays;
|
||||
document.getElementById('totalHours').textContent = totalHours.toFixed(1) + 'h';
|
||||
document.getElementById('totalOT').textContent = totalOT.toFixed(1) + 'h';
|
||||
document.getElementById('activeProjects').textContent = activeProjects;
|
||||
|
||||
// 오늘 근무시간 정보 업데이트
|
||||
const todayHoursElement = document.getElementById('todayHours');
|
||||
const todayProgressElement = document.getElementById('todayProgress');
|
||||
|
||||
if (todayHoursElement && todayProgressElement) {
|
||||
todayHoursElement.textContent = todayHours.toFixed(1) + 'h';
|
||||
todayProgressElement.textContent = `(목표 8시간의 ${todayProgress.toFixed(0)}%)`;
|
||||
|
||||
// 8시간 미만이면 빨간색, 8시간 이상이면 초록색으로 표시
|
||||
if (todayHours < 8) {
|
||||
todayHoursElement.className = 'text-2xl font-bold text-red-300';
|
||||
} else {
|
||||
todayHoursElement.className = 'text-2xl font-bold text-green-300';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateProjectFilter() {
|
||||
@@ -687,22 +746,61 @@
|
||||
hideEmptyState();
|
||||
tbody.innerHTML = '';
|
||||
|
||||
let lastDate = null;
|
||||
|
||||
pageData.forEach((item, index) => {
|
||||
const currentDate = item.pdate ? item.pdate.substring(0, 10) : null;
|
||||
|
||||
// 날짜가 변경되면 구분선 추가 (첫 번째 항목 제외)
|
||||
if (lastDate && currentDate && lastDate !== currentDate) {
|
||||
const separatorRow = document.createElement('tr');
|
||||
separatorRow.className = 'border-t-2 border-white/20';
|
||||
separatorRow.innerHTML = `
|
||||
<td colspan="7" class="px-6 py-2">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow border-t border-white/30"></div>
|
||||
<span class="px-4 text-xs text-white/60 bg-white/10 rounded-full">
|
||||
${formatDate(currentDate)}
|
||||
</span>
|
||||
<div class="flex-grow border-t border-white/30"></div>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(separatorRow);
|
||||
}
|
||||
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'hover:bg-white/10 cursor-pointer transition-colors';
|
||||
row.setAttribute('data-item-id', item.idx || index);
|
||||
row.addEventListener('click', () => toggleRowDetail(item, row));
|
||||
row.addEventListener('click', () => showDetailModal(item));
|
||||
|
||||
const statusColor = getStatusColor(item.status);
|
||||
const otTime = item.otStart && item.otEnd ?
|
||||
`${item.otStart} ~ ${item.otEnd}` : '-';
|
||||
|
||||
// 근무시간 표시 로직
|
||||
const hrs = parseFloat(item.hrs) || 0;
|
||||
const ot = parseFloat(item.ot) || 0;
|
||||
let workTimeDisplay = '';
|
||||
|
||||
if (hrs > 0) {
|
||||
workTimeDisplay = hrs.toFixed(1);
|
||||
if (ot > 0) {
|
||||
workTimeDisplay += '+' + ot.toFixed(1);
|
||||
}
|
||||
} else {
|
||||
workTimeDisplay = '-';
|
||||
}
|
||||
|
||||
// 프로젝트 연결 상태에 따른 버튼 표시
|
||||
const pidx = parseInt(item.pidx) || 0;
|
||||
const hasProject = pidx > 0;
|
||||
const projectButton = hasProject ?
|
||||
`<button onclick="showProjectDetail(${pidx}, event)" class="mr-2 p-1 rounded-full bg-blue-500/20 hover:bg-blue-500/40 text-blue-300 hover:text-blue-200 transition-colors" title="프로젝트 상세보기">
|
||||
<i data-feather="folder" class="w-3 h-3"></i>
|
||||
</button>` : '';
|
||||
|
||||
row.innerHTML = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
<div class="flex items-center">
|
||||
<i data-feather="chevron-right" class="w-4 h-4 mr-2 text-white/60 expand-icon"></i>
|
||||
${formatDate(item.pdate)}
|
||||
</div>
|
||||
${formatDate(item.pdate)}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${statusColor}">
|
||||
@@ -710,61 +808,29 @@
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.projectName || '-'}
|
||||
${workTimeDisplay}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
<div class="flex items-center">
|
||||
${projectButton}
|
||||
<span>${item.projectName || '-'}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-white">
|
||||
<div class="max-w-xs truncate" title="${item.description || ''}">
|
||||
${item.description || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.requestpart || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.package || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.type || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.process || '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-white">
|
||||
<div class="max-w-xs truncate cursor-pointer hover:text-white/80 hover:underline"
|
||||
title="${item.description || ''}"
|
||||
onclick="event.stopPropagation(); showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})">
|
||||
${item.description || '-'}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">
|
||||
${otTime}
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(row);
|
||||
|
||||
// 확장 행 추가 (숨겨진 상태)
|
||||
const expandRow = document.createElement('tr');
|
||||
expandRow.className = 'hidden expand-row';
|
||||
expandRow.setAttribute('data-parent-id', item.idx || index);
|
||||
expandRow.innerHTML = `
|
||||
<td colspan="11" class="px-6 py-4 bg-gray-50 border-t border-gray-200">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">업무내용</label>
|
||||
<div class="bg-white rounded-lg p-4 cursor-pointer hover:bg-gray-100 transition-colors border"
|
||||
onclick="showDetailModal(${JSON.stringify(item).replace(/"/g, '"')})">
|
||||
<p class="text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
|
||||
<div class="mt-2 text-xs text-gray-500 flex items-center">
|
||||
<i data-feather="maximize-2" class="w-4 h-4 mr-1"></i>
|
||||
클릭하여 전체 내용 보기
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
tbody.appendChild(expandRow);
|
||||
lastDate = currentDate;
|
||||
});
|
||||
|
||||
updatePagination();
|
||||
@@ -796,7 +862,7 @@
|
||||
document.getElementById('nextPage').disabled = currentPage >= maxPage;
|
||||
}
|
||||
|
||||
function showDetailModal(item) {
|
||||
async function showDetailModal(item) {
|
||||
// 문자열로 전달된 경우 JSON 파싱
|
||||
if (typeof item === 'string') {
|
||||
try {
|
||||
@@ -807,70 +873,221 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 전체 상세 정보를 서버에서 가져오기
|
||||
try {
|
||||
const response = await fetch(`/Jobreport/GetJobDetail?id=${item.idx}`);
|
||||
if (response.ok) {
|
||||
const fullItem = await response.json();
|
||||
// 전체 정보가 있으면 사용, 없으면 기존 item 사용
|
||||
item = fullItem.error ? item : fullItem;
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to load full details, using truncated data:', error);
|
||||
}
|
||||
|
||||
const modal = document.getElementById('detailModal');
|
||||
const content = document.getElementById('modalContent');
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">날짜</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p>
|
||||
<form id="editJobForm" class="space-y-6">
|
||||
<input type="hidden" id="editIdx" value="${item.idx || ''}">
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-8">
|
||||
<!-- 좌측: 기본 정보 (2열) -->
|
||||
<div class="lg:col-span-2">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">날짜 *</label>
|
||||
<input type="date" id="editPdate" value="${item.pdate || ''}"
|
||||
class="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" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">상태 *</label>
|
||||
<select id="editStatus" class="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" required>
|
||||
<option value="">선택하세요</option>
|
||||
<option value="진행 중" ${item.status === '진행 중' ? 'selected' : ''}>진행 중</option>
|
||||
<option value="완료" ${item.status === '완료' ? 'selected' : ''}>완료</option>
|
||||
<option value="대기" ${item.status === '대기' ? 'selected' : ''}>대기</option>
|
||||
<option value="보류" ${item.status === '보류' ? 'selected' : ''}>보류</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">요청부서</label>
|
||||
<input type="text" id="editRequestpart" value="${item.requestpart || ''}"
|
||||
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">타입</label>
|
||||
<select id="editType" class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
<option value="">선택하세요</option>
|
||||
<option value="개발" ${item.type === '개발' ? 'selected' : ''}>개발</option>
|
||||
<option value="유지보수" ${item.type === '유지보수' ? 'selected' : ''}>유지보수</option>
|
||||
<option value="분석" ${item.type === '분석' ? 'selected' : ''}>분석</option>
|
||||
<option value="테스트" ${item.type === '테스트' ? 'selected' : ''}>테스트</option>
|
||||
<option value="문서작업" ${item.type === '문서작업' ? 'selected' : ''}>문서작업</option>
|
||||
<option value="회의" ${item.type === '회의' ? 'selected' : ''}>회의</option>
|
||||
<option value="기타" ${item.type === '기타' ? 'selected' : ''}>기타</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">근무시간 (시간) *</label>
|
||||
<input type="number" id="editHrs" value="${item.hrs || ''}" step="any"
|
||||
class="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" required>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 (시간)</label>
|
||||
<input type="number" id="editOt" value="${item.ot || ''}" step="any"
|
||||
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 시작시간</label>
|
||||
<input type="time" id="editOtStart" value="${item.otStart || ''}"
|
||||
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">초과근무 종료시간</label>
|
||||
<input type="time" id="editOtEnd" value="${item.otEnd || ''}"
|
||||
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 우측: 프로젝트명과 업무내용 -->
|
||||
<div class="lg:col-span-3 space-y-4">
|
||||
<div>
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">프로젝트명 *</label>
|
||||
<input type="text" id="editProjectName" value="${item.projectName || ''}"
|
||||
class="w-full bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all" required>
|
||||
</div>
|
||||
<div class="flex-grow">
|
||||
<label class="block text-white/70 text-sm font-medium mb-2">업무내용 *</label>
|
||||
<textarea id="editDescription" rows="15" required
|
||||
class="w-full h-full min-h-[360px] bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg px-4 py-2 text-white placeholder-white/50 focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all resize-vertical"
|
||||
placeholder="상세한 업무 내용을 입력하세요...">${item.description || ''}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">상태</label>
|
||||
<p class="mt-1">
|
||||
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(item.status)}">
|
||||
${item.status || '-'}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">프로젝트명</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">요청부서</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">패키지</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">타입</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">프로세스</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">근무시간</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">초과근무</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.ot ? parseFloat(item.ot).toFixed(1) + 'h' : '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">초과근무 시간</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.otStart && item.otEnd ? `${item.otStart} ~ ${item.otEnd}` : '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm font-medium text-gray-700">업무내용</label>
|
||||
<p class="mt-1 text-sm text-gray-900 whitespace-pre-wrap">${item.description || '-'}</p>
|
||||
</form>
|
||||
|
||||
<!-- 모달 푸터 -->
|
||||
<div class="px-6 py-4 border-t border-white/10 flex justify-end space-x-3 bg-black/10 rounded-b-2xl">
|
||||
<button type="button" id="deleteJobBtn" class="bg-danger-500 hover:bg-danger-600 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
삭제
|
||||
</button>
|
||||
<button type="button" onclick="closeModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
|
||||
취소
|
||||
</button>
|
||||
<button type="submit" form="editJobForm" class="bg-primary-500 hover:bg-primary-600 text-white px-6 py-2 rounded-lg transition-colors">
|
||||
저장
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.remove('hidden');
|
||||
feather.replace();
|
||||
|
||||
// 폼 제출 이벤트 리스너 추가
|
||||
document.getElementById('editJobForm').addEventListener('submit', handleEditSubmit);
|
||||
document.getElementById('deleteJobBtn').addEventListener('click', handleDeleteJob);
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('detailModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function handleEditSubmit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
// URLSearchParams 사용 (application/x-www-form-urlencoded)
|
||||
const formData = new URLSearchParams();
|
||||
formData.append('idx', document.getElementById('editIdx').value);
|
||||
formData.append('pdate', document.getElementById('editPdate').value);
|
||||
formData.append('status', document.getElementById('editStatus').value);
|
||||
formData.append('projectName', document.getElementById('editProjectName').value);
|
||||
formData.append('requestpart', document.getElementById('editRequestpart').value);
|
||||
formData.append('type', document.getElementById('editType').value);
|
||||
formData.append('hrs', document.getElementById('editHrs').value);
|
||||
formData.append('ot', document.getElementById('editOt').value);
|
||||
formData.append('otStart', document.getElementById('editOtStart').value);
|
||||
formData.append('otEnd', document.getElementById('editOtEnd').value);
|
||||
formData.append('description', document.getElementById('editDescription').value);
|
||||
|
||||
try {
|
||||
const response = await fetch('/Jobreport/Edit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 업데이트에 실패했습니다.`);
|
||||
}
|
||||
|
||||
const result = await response.text();
|
||||
console.log('Edit result:', result);
|
||||
|
||||
// JSON 응답인지 확인
|
||||
try {
|
||||
const jsonResult = JSON.parse(result);
|
||||
if (!jsonResult.success) {
|
||||
throw new Error(jsonResult.message || '수정에 실패했습니다.');
|
||||
}
|
||||
// 성공시 alert 없이 바로 진행
|
||||
} catch (parseError) {
|
||||
// JSON이 아닌 경우도 성공으로 처리 (alert 없음)
|
||||
}
|
||||
|
||||
// 모달 닫기
|
||||
closeModal();
|
||||
|
||||
// 데이터 새로고침
|
||||
await loadJobData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error updating job:', error);
|
||||
alert('업무일지 수정 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteJob() {
|
||||
const idx = document.getElementById('editIdx').value;
|
||||
|
||||
if (!idx) {
|
||||
alert('삭제할 수 없는 항목입니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm('정말로 이 업무일지를 삭제하시겠습니까?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/Jobreport/Delete/${idx}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
// 성공시 alert 없이 바로 모달 닫기
|
||||
closeModal();
|
||||
await loadJobData();
|
||||
} else {
|
||||
throw new Error(result.message || '삭제에 실패했습니다.');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error deleting job:', error);
|
||||
alert('업무일지 삭제 중 오류가 발생했습니다: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showSelectedJobDetail(item) {
|
||||
const detailContainer = document.getElementById('selectedJobDetail');
|
||||
const contentContainer = document.getElementById('selectedJobContent');
|
||||
@@ -908,18 +1125,10 @@
|
||||
<label class="block text-sm font-medium text-gray-700">요청부서</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">패키지</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">타입</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">프로세스</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">근무시간</label>
|
||||
<p class="mt-1 text-sm text-gray-900">${item.hrs ? parseFloat(item.hrs).toFixed(1) + 'h' : '-'}</p>
|
||||
@@ -953,44 +1162,6 @@
|
||||
detailContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
}
|
||||
|
||||
function toggleRowDetail(item, row) {
|
||||
const itemId = item.idx || row.getAttribute('data-item-id');
|
||||
const expandRow = document.querySelector(`tr[data-parent-id="${itemId}"]`);
|
||||
const expandIcon = row.querySelector('.expand-icon');
|
||||
|
||||
if (expandRow.classList.contains('hidden')) {
|
||||
// 확장
|
||||
expandRow.classList.remove('hidden');
|
||||
expandIcon.setAttribute('data-feather', 'chevron-down');
|
||||
row.classList.add('bg-blue-50');
|
||||
} else {
|
||||
// 축소
|
||||
expandRow.classList.add('hidden');
|
||||
expandIcon.setAttribute('data-feather', 'chevron-right');
|
||||
row.classList.remove('bg-blue-50');
|
||||
}
|
||||
|
||||
// 아이콘 업데이트
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
function closeAllExpandedRows() {
|
||||
document.querySelectorAll('.expand-row').forEach(row => {
|
||||
row.classList.add('hidden');
|
||||
});
|
||||
|
||||
document.querySelectorAll('#jobTableBody tr').forEach(row => {
|
||||
if (!row.classList.contains('expand-row')) {
|
||||
row.classList.remove('bg-blue-50');
|
||||
const icon = row.querySelector('.expand-icon');
|
||||
if (icon) {
|
||||
icon.setAttribute('data-feather', 'chevron-right');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
feather.replace();
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const loadingState = document.getElementById('loadingState');
|
||||
@@ -1021,13 +1192,13 @@
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
// 조회기간을 이번 달로 초기화
|
||||
// 조회기간을 오늘부터 -2주로 초기화
|
||||
const now = new Date();
|
||||
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0);
|
||||
const today = now.toISOString().split('T')[0];
|
||||
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
|
||||
|
||||
document.getElementById('startDate').value = currentMonth.toISOString().split('T')[0];
|
||||
document.getElementById('endDate').value = lastDayOfMonth.toISOString().split('T')[0];
|
||||
document.getElementById('startDate').value = twoWeeksAgo;
|
||||
document.getElementById('endDate').value = today;
|
||||
|
||||
// 다른 필터들 초기화
|
||||
document.getElementById('statusFilter').value = '';
|
||||
@@ -1036,8 +1207,17 @@
|
||||
|
||||
// 서버에서 새로운 데이터 가져오기
|
||||
loadJobData();
|
||||
// 확장된 행들 닫기
|
||||
closeAllExpandedRows();
|
||||
}
|
||||
|
||||
function showProjectDetail(pidx, event) {
|
||||
// 이벤트 버블링 방지 (행 클릭 이벤트와 충돌 방지)
|
||||
event.stopPropagation();
|
||||
|
||||
// 임시 메시지 표시
|
||||
alert('프로젝트 보기 기능 준비중');
|
||||
|
||||
// TODO: 추후 프로젝트 상세 페이지로 이동
|
||||
// window.open(`/Project/Detail/${pidx}`, '_blank');
|
||||
}
|
||||
|
||||
function exportToExcel() {
|
||||
@@ -1052,7 +1232,7 @@
|
||||
const periodText = startDate && endDate ? `_${startDate}_${endDate}` : '';
|
||||
|
||||
// CSV 형식으로 데이터 변환
|
||||
const headers = ['날짜', '상태', '프로젝트명', '요청부서', '패키지', '타입', '프로세스', '업무내용', '근무시간', '초과근무', '초과근무시작', '초과근무종료'];
|
||||
const headers = ['날짜', '상태', '프로젝트명', '요청부서', '타입', '업무내용', '근무시간', '초과근무'];
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...filteredData.map(item => [
|
||||
@@ -1060,14 +1240,10 @@
|
||||
item.status || '',
|
||||
item.projectName || '',
|
||||
item.requestpart || '',
|
||||
item.package || '',
|
||||
item.type || '',
|
||||
item.process || '',
|
||||
`"${(item.description || '').replace(/"/g, '""')}"`,
|
||||
item.hrs || '',
|
||||
item.ot || '',
|
||||
item.otStart || '',
|
||||
item.otEnd || ''
|
||||
item.ot || ''
|
||||
].join(','))
|
||||
].join('\n');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user