업무일지 목록 및 상세화면 디자인 변경.

This commit is contained in:
ChiKyun Kim
2025-07-31 08:59:40 +09:00
parent 642c204a18
commit 5e0337a044
3 changed files with 912 additions and 228 deletions

View File

@@ -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();
}
}
//[HttpPost]
//public string Edit([FromBody] string value)
//{
// var vals = Request.GetQueryNameValuePairs();
// var req = Request.GetRequestContext();
// return string.Empty;
//}
return "{\"success\":true,\"message\":\"데이터가 성공적으로 저장되었습니다.\"}";
}
catch (Exception ex)
{
return $"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}";
}
}
[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"] ?? "";
var req = Request.GetRequestContext();
decimal hrs = 0;
decimal.TryParse(HttpContext.Current.Request.Form["hrs"], out hrs);
return string.Empty;
decimal ot = 0;
decimal.TryParse(HttpContext.Current.Request.Form["ot"], out ot);
int idxNum = 0;
int.TryParse(idx, out idxNum);
if (idxNum <= 0)
{
throw new Exception("유효하지 않은 업무일지 ID입니다.");
}
// 직접 SQL 업데이트 실행
string connectionString = Properties.Settings.Default.gwcs;
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
{
connection.Open();
string updateSql = @"
UPDATE JobReport
SET pdate = @pdate,
status = @status,
projectName = @projectName,
requestpart = @requestpart,
type = @type,
description = @description,
hrs = @hrs,
ot = @ot,
otStart = @otStart,
otEnd = @otEnd,
wuid = @wuid,
wdate = @wdate
WHERE idx = @idx AND gcode = @gcode";
using (var command = new System.Data.SqlClient.SqlCommand(updateSql, connection))
{
command.Parameters.AddWithValue("@idx", idxNum);
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
command.Parameters.AddWithValue("@pdate", pdate);
command.Parameters.AddWithValue("@status", status);
command.Parameters.AddWithValue("@projectName", projectName);
command.Parameters.AddWithValue("@requestpart", requestpart);
command.Parameters.AddWithValue("@type", type);
command.Parameters.AddWithValue("@description", description);
command.Parameters.AddWithValue("@hrs", hrs);
command.Parameters.AddWithValue("@ot", ot);
command.Parameters.AddWithValue("@otStart", string.IsNullOrEmpty(otStart) ? (object)DBNull.Value : otStart);
command.Parameters.AddWithValue("@otEnd", string.IsNullOrEmpty(otEnd) ? (object)DBNull.Value : otEnd);
command.Parameters.AddWithValue("@wuid", FCOMMON.info.Login.no);
command.Parameters.AddWithValue("@wdate", DateTime.Now);
int rowsAffected = command.ExecuteNonQuery();
if (rowsAffected == 0)
{
throw new Exception("업무일지를 찾을 수 없거나 수정 권한이 없습니다.");
}
}
}
var jsonData = "{\"success\":true,\"message\":\"데이터가 성공적으로 수정되었습니다.\"}";
var resp = new HttpResponseMessage()
{
Content = new StringContent(
jsonData,
System.Text.Encoding.UTF8,
"application/json")
};
return resp;
}
catch (Exception ex)
{
var errorResp = new HttpResponseMessage()
{
Content = new StringContent(
$"{{\"success\":false,\"message\":\"{EscapeJsonString(ex.Message)}\"}}",
System.Text.Encoding.UTF8,
"application/json")
};
return errorResp;
}
}
[HttpGet]
@@ -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"); // 탭
}
}
}

View File

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

View File

@@ -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,19 +309,33 @@
</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 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 today = now.toISOString().split('T')[0];
const twoWeeksAgo = new Date(now.getTime() - (14 * 24 * 60 * 60 * 1000)).toISOString().split('T')[0];
// 조회기간 기본값 설정 (이번 달)
const currentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
const lastDayOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 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>
</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, '&quot;')})">
${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, '&quot;')})">
<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 = `
<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-sm font-medium text-gray-700">날짜</label>
<p class="mt-1 text-sm text-gray-900">${formatDate(item.pdate)}</p>
<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-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>
<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-sm font-medium text-gray-700">프로젝트명</label>
<p class="mt-1 text-sm text-gray-900">${item.projectName || '-'}</p>
<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-sm font-medium text-gray-700">요청부서</label>
<p class="mt-1 text-sm text-gray-900">${item.requestpart || '-'}</p>
<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-sm font-medium text-gray-700">패키지</label>
<p class="mt-1 text-sm text-gray-900">${item.package || '-'}</p>
<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-sm font-medium text-gray-700">타입</label>
<p class="mt-1 text-sm text-gray-900">${item.type || '-'}</p>
<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-sm font-medium text-gray-700">프로세스</label>
<p class="mt-1 text-sm text-gray-900">${item.process || '-'}</p>
<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-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>
<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-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>
<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>
<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 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 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>
</div>
</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');