프로젝트 시스템 통합 및 전반적인 개선사항

- 솔루션 설정 및 프로젝트 파일 업데이트
- BaseController 최적화 (HtmlAgilityPack 의존성 제거)
- CommonController 네비게이션 메뉴에 프로젝트 추가
- JobreportController 사용자 조회 기능 및 필터링 개선
- 모든 웹 화면 UI/UX 통합 및 일관성 개선
- 프로그램 시작 시 중복 실행 감지 개선
- 각종 폼 및 데이터셋 디자이너 업데이트

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ChiKyun Kim
2025-08-04 15:23:41 +09:00
parent a11780f725
commit 81f91f0897
15 changed files with 3766 additions and 3319 deletions

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web.Http;
using agi = HtmlAgilityPack;
using Project.Web.Model;
namespace Project.Web.Controllers
@@ -159,33 +158,7 @@ namespace Project.Web.Controllers
}
}
agi.HtmlDocument doc = new agi.HtmlDocument();
doc.LoadHtml(contents);
//파일참조 태그를 모두 가져옴
var tags_include = doc.QuerySelectorAll("include");
foreach (var item in tags_include)
{
var filename = item.InnerText;
var load_file = String.Concat(AppDomain.CurrentDomain.BaseDirectory, "View", filename.Replace("/", "\\"));
load_file = load_file.Replace("\\\\", "\\");
String fileContents;// = String.Empty;
Console.WriteLine("## " + item.OuterHtml);
if (System.IO.File.Exists(load_file))
{
fileContents = System.IO.File.ReadAllText(load_file, System.Text.Encoding.UTF8);
}
else
{
fileContents = string.Format("<div class=\"fg-red\">#include Error:nofile:{0}</div>",
filename); //파일이없다면 해당 부분은 오류 처리한다.
}
contents = contents.Replace(item.OuterHtml, fileContents);
}
//콘텐츠내의 file 을 찾아서 처리한다. ; 정규식의 처리속도가 느릴듯하여, 그냥 처리해본다
//시스템변수 replace
@@ -242,31 +215,7 @@ namespace Project.Web.Controllers
}
}
agi.HtmlDocument doc = new agi.HtmlDocument();
doc.LoadHtml(contents);
//파일참조 태그를 모두 가져옴
var tags_include = doc.QuerySelectorAll("include");
foreach (var item in tags_include)
{
var filename = item.InnerText;
var load_file = String.Concat(AppDomain.CurrentDomain.BaseDirectory, "View", filename.Replace("/", "\\"));
load_file = load_file.Replace("\\\\", "\\");
String fileContents;// = String.Empty;
Console.WriteLine("## " + item.OuterHtml);
if (System.IO.File.Exists(load_file))
{
fileContents = System.IO.File.ReadAllText(load_file, System.Text.Encoding.UTF8);
}
else
{
fileContents = string.Format("<div class=\"fg-red\">#include Error:nofile:{0}</div>",
filename); //파일이없다면 해당 부분은 오류 처리한다.
}
contents = contents.Replace(item.OuterHtml, fileContents);
}

View File

@@ -16,23 +16,16 @@ namespace Project.Web.Controllers
{
var sql = string.Empty;
if (string.IsNullOrEmpty(grp))
{
// grp가 없으면 모든 그룹의 데이터를 가져옴
sql = "select *" +
" from common" +
" where gcode = @gcode" +
" order by grp, code, svalue";
}
else
{
//코드그룹이 없다면 전체 목록을 조회할 수 있도록 99를 조회한다
if (string.IsNullOrEmpty(grp)) grp = "99";
// 특정 그룹의 데이터만 가져옴
sql = "select *" +
" from common" +
" where gcode = @gcode" +
" and grp = @grp" +
" order by code,svalue";
}
var cs = Properties.Settings.Default.gwcs;
var cn = new System.Data.SqlClient.SqlConnection(cs);
@@ -216,7 +209,7 @@ namespace Project.Web.Controllers
{
try
{
var sql = "select code, svalue from common WITH (nolock) " +
var sql = "select code, svalue, memo from common WITH (nolock) " +
"where gcode = @gcode and grp = '99' " +
"order by code";
@@ -381,6 +374,14 @@ namespace Project.Web.Controllers
icon = "M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4",
isVisible = true,
sortOrder = 5
},
new {
key = "project",
title = "프로젝트",
url = "/Project/",
icon = "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10",
isVisible = true,
sortOrder = 6
}
};

View File

@@ -1,10 +1,14 @@
using Microsoft.Owin;
using Project.Web.Controllers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Http;
using System.Data;
using System.Web.Http.Results;
using System.Data.SqlClient;
namespace Project.Web.Controllers
{
@@ -641,6 +645,90 @@ namespace Project.Web.Controllers
}
}
[HttpGet]
public HttpResponseMessage GetUsers()
{
try
{
string connectionString = Properties.Settings.Default.gwcs;
var users = new List<dynamic>();
using (var connection = new System.Data.SqlClient.SqlConnection(connectionString))
{
connection.Open();
string selectSql = @"
SELECT name, id, processs
FROM vGroupUser
WHERE gcode = @gcode AND useJobReport = 1 AND useUserState = 1
ORDER BY name";
using (var command = new System.Data.SqlClient.SqlCommand(selectSql, connection))
{
command.Parameters.AddWithValue("@gcode", FCOMMON.info.Login.gcode);
using (var reader = command.ExecuteReader())
{
while (reader.Read())
{
users.Add(new
{
name = reader["name"],
id = reader["id"],
process = reader["processs"]
});
}
}
}
}
// 디버깅 로그 추가
System.Diagnostics.Debug.WriteLine($"GetUsers: Found {users.Count} users for gcode {FCOMMON.info.Login.gcode}");
// JSON 형태로 변환
var jsonData = "[";
bool first = true;
foreach (var user in users)
{
if (!first) jsonData += ",";
first = false;
var name = EscapeJsonString(user.name?.ToString() ?? "");
var id = EscapeJsonString(user.id?.ToString() ?? "");
var process = EscapeJsonString(user.process?.ToString() ?? "");
jsonData += "{";
jsonData += $"\"name\":\"{name}\",";
jsonData += $"\"id\":\"{id}\",";
jsonData += $"\"process\":\"{process}\"";
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;
}
}
[HttpGet]
public HttpResponseMessage GetJobData()
{
@@ -649,9 +737,11 @@ namespace Project.Web.Controllers
var gets = Request.GetQueryNameValuePairs();
var startDateParam = gets.Where(t => t.Key == "startDate").FirstOrDefault();
var endDateParam = gets.Where(t => t.Key == "endDate").FirstOrDefault();
var userParam = gets.Where(t => t.Key == "user").FirstOrDefault();
var startDate = startDateParam.Key != null ? startDateParam.Value : null;
var endDate = endDateParam.Key != null ? endDateParam.Value : null;
var selectedUser = userParam.Key != null ? userParam.Value : null;
// 날짜 파라미터 처리
string sd, ed;
@@ -683,13 +773,17 @@ namespace Project.Web.Controllers
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";
WHERE gcode = @gcode AND pdate BETWEEN @startDate AND @endDate";
// 사용자 필터가 있으면 해당 사용자, 없으면 로그인한 사용자
selectSql += " AND uid = @uid";
selectSql += " 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("@uid", !string.IsNullOrEmpty(selectedUser) ? selectedUser : FCOMMON.info.Login.no);
command.Parameters.AddWithValue("@startDate", sd);
command.Parameters.AddWithValue("@endDate", ed);

View File

@@ -92,71 +92,95 @@
select option:checked {
background-color: #6366F1 !important;
}
/* 테이블 셀 텍스트 오버플로우 처리 */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 값(문자열) 열 최대 너비 제한 */
.svalue-cell {
max-width: 128px; /* w-32 = 128px */
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</head>
<body class="gradient-bg min-h-screen">
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<div class="container mx-auto px-4 py-8">
<!-- 개발중 경고 메시지 -->
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
<div class="flex items-center">
<svg class="w-5 h-5 text-orange-900 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z"></path>
</svg>
<div>
<p class="text-white font-bold text-base">🚧 개발중인 기능입니다</p>
<p class="text-orange-100 text-sm font-medium">일부 기능이 정상적으로 동작하지 않을 수 있습니다.</p>
<!-- 2열 구조 메인 컨테이너 -->
<div class="flex gap-6 h-[calc(100vh-200px)]">
<!-- 좌측: 코드그룹 리스트 -->
<div class="w-80">
<div class="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
<div class="p-4 border-b border-white/10">
<h3 class="text-lg 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="M19 11H5m14-7l-7 7-7-7M19 21l-7-7-7 7"></path>
</svg>
코드그룹 목록
</h3>
</div>
<div class="flex-1 overflow-y-auto custom-scrollbar p-2">
<div id="groupList" class="space-y-1">
<!-- 그룹 목록이 여기에 표시됩니다 -->
</div>
</div>
</div>
</div>
</div>
<!-- 검색 및 필-->
<div class="glass-effect rounded-2xl p-6 mb-6 card-hover animate-slide-up">
<div class="flex flex-wrap gap-4 items-end">
<div class="flex-1 min-w-0">
<label class="block text-sm font-medium text-white/70 mb-2">코드그룹</label>
<select id="grpFilter" class="w-full px-3 py-2 bg-white/20 backdrop-blur-sm border border-white/30 rounded-lg text-white focus:outline-none focus:ring-2 focus:ring-primary-400 transition-all">
<!-- 동적으로 로드됩니다 -->
</select>
<!-- 우측: 상세 데이-->
<div class="flex-1">
<div class="glass-effect rounded-2xl h-full card-hover animate-slide-up flex flex-col">
<!-- 상단 헤더 -->
<div class="p-4 border-b border-white/10 flex items-center justify-between">
<div>
<h3 class="text-lg 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="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
<span id="selectedGroupTitle">코드그룹을 선택하세요</span>
</h3>
<p class="text-white/70 text-sm mt-1"><span id="recordCount" class="text-white font-medium">0</span></p>
</div>
<button onclick="showAddModal()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg transition-all border border-white/30 flex items-center text-sm">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
추가
</button>
</div>
<!-- 데이터 테이블 -->
<div class="flex-1 overflow-x-auto overflow-y-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-white/10 sticky top-0">
<tr>
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
<th class="px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
<th class="w-32 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
<th class="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
<th class="w-20 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
<th class="w-24 px-4 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
</tr>
</thead>
<tbody id="dataTable" class="divide-y divide-white/10">
<tr>
<td colspan="6" class="px-4 py-8 text-center text-white/70">
<svg class="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
좌측에서 코드그룹을 선택하세요
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="flex gap-2">
<button onclick="loadData()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-2 rounded-lg transition-all border border-white/30 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg>
조회
</button>
<button onclick="showAddModal()" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-6 py-2 rounded-lg transition-all border border-white/30 flex items-center">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path>
</svg>
추가
</button>
</div>
</div>
</div>
<!-- 데이터 테이블 -->
<div class="glass-effect rounded-2xl overflow-hidden card-hover animate-slide-up">
<div class="overflow-x-auto max-h-[calc(100vh-300px)] overflow-y-auto custom-scrollbar">
<table class="w-full">
<thead class="bg-white/10 sticky top-0">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">코드</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">비고</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(문자열)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(숫자)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값(실수)</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">값2</th>
<th class="px-6 py-3 text-left text-xs font-medium text-white/70 uppercase tracking-wider">작업</th>
</tr>
</thead>
<tbody id="dataTable" class="divide-y divide-white/10">
<!-- 데이터가 여기에 표시됩니다 -->
</tbody>
</table>
</div>
<div class="px-6 py-3 border-t border-white/10 bg-white/5">
<p class="text-white/70 text-sm text-center"><span id="recordCount" class="text-white font-medium">0</span></p>
</div>
</div>
@@ -224,13 +248,21 @@
</form>
<!-- 모달 푸터 -->
<div class="px-6 py-4 border-t border-white/10 flex justify-end gap-2">
<button onclick="hideEditModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
취소
</button>
<button onclick="saveData()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
저장
<div class="px-6 py-4 border-t border-white/10 flex justify-between">
<button onclick="deleteCurrentItem()" class="bg-red-600 hover:bg-red-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
삭제
</button>
<div class="flex gap-2">
<button onclick="hideEditModal()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
취소
</button>
<button onclick="saveData()" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-lg transition-colors">
저장
</button>
</div>
</div>
</div>
</div>
@@ -252,7 +284,7 @@
<p class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</p>
</div>
</div>
<p class="text-gray-700 mb-6">선택한 공용코드를 삭제하시겠습니까?</p>
<p class="text-gray-700 mb-6">선택한 공용코드를 삭제하시겠습니까?<br><span class="text-sm text-gray-500">이 작업은 되돌릴 수 없습니다.</span></p>
<div class="flex justify-end gap-2">
<button onclick="hideDeleteModal()" class="bg-gray-300 hover:bg-gray-400 text-gray-700 px-4 py-2 rounded-lg transition-colors">
취소
@@ -331,22 +363,33 @@
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<h2 class="text-xl font-bold text-white">GroupWare</h2>
</div>
<div class="hidden md:flex items-center space-x-8">
${visibleItems.map(item => this.getMenuItemHTML(item)).join('')}
</div>
<div class="md:hidden">
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
<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="M4 6h16M4 12h16M4 18h16"></path>
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</button>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${visibleItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
${visibleItems.map(item => this.getMobileMenuItemHTML(item)).join('')}
</div>
</div>
`;
@@ -396,6 +439,7 @@
let currentData = [];
let deleteTargetIdx = null;
let groupData = [];
let selectedGroupCode = null;
// 페이지 로드시 초기 데이터 로드
document.addEventListener('DOMContentLoaded', function() {
@@ -410,11 +454,8 @@
.then(response => response.json())
.then(data => {
groupData = data || [];
renderGroupSelects();
// 기본적으로 "99-전체" 선택
document.getElementById('grpFilter').value = '99';
loadData();
renderGroupList();
renderEditGroupSelect();
hideLoading();
})
.catch(error => {
@@ -424,38 +465,64 @@
});
}
// 셀렉트 박스들에 그룹 옵션 렌더링
function renderGroupSelects() {
const grpFilter = document.getElementById('grpFilter');
// 좌측 그룹 리스트 렌더링
function renderGroupList() {
const groupList = document.getElementById('groupList');
if (groupData.length === 0) {
groupList.innerHTML = '<div class="text-white/70 text-center py-4">그룹 데이터가 없습니다.</div>';
return;
}
groupList.innerHTML = groupData.map(group => `
<div class="group-item cursor-pointer p-3 rounded-lg border border-white/20 hover:bg-white/10 transition-all ${selectedGroupCode === group.code ? 'bg-white/20' : ''}"
onclick="selectGroup('${group.code}', '${group.memo}')">
<div class="flex items-center">
<div class="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center mr-3">
<span class="text-white text-sm font-medium">${group.code}</span>
</div>
<div class="flex-1">
<div class="text-white font-medium">${group.memo}</div>
</div>
</div>
</div>
`).join('');
}
// 그룹 선택 처리
function selectGroup(code, name) {
selectedGroupCode = code;
// 선택된 그룹 하이라이트 업데이트
document.querySelectorAll('.group-item').forEach(item => {
item.classList.remove('bg-white/20');
});
event.target.closest('.group-item').classList.add('bg-white/20');
// 우측 제목 업데이트
document.getElementById('selectedGroupTitle').textContent = `${code} - ${name}`;
// 해당 그룹의 데이터 로드
loadDataByGroup(code);
}
// 편집 모달용 그룹 셀렉트 렌더링
function renderEditGroupSelect() {
const editGrp = document.getElementById('editGrp');
// 필터 셀렉트박스 - "99-전체" 옵션 추가
const filterOptions = [
'<option value="99" class="bg-gray-800 text-white">99-전체</option>'
].concat(
groupData.map(group =>
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.svalue}</option>`
)
);
grpFilter.innerHTML = filterOptions.join('');
// 편집 모달 셀렉트박스
editGrp.innerHTML = '<option value="" class="bg-gray-800 text-white">선택하세요</option>' +
groupData.map(group =>
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.svalue}</option>`
`<option value="${group.code}" class="bg-gray-800 text-white">${group.code}-${group.memo}</option>`
).join('');
}
// 데이터 로드
function loadData() {
const grp = document.getElementById('grpFilter').value;
// 특정 그룹의 데이터 로드
function loadDataByGroup(grp) {
showLoading();
let url = 'http://127.0.0.1:7979/Common/GetList';
if (grp && grp !== '99') {
if (grp) {
url += '?grp=' + encodeURIComponent(grp);
}
// grp가 '99'이면 모든 그룹의 데이터를 가져옴 (파라미터 없음)
fetch(url)
.then(response => response.json())
@@ -475,11 +542,18 @@
function renderTable() {
const tbody = document.getElementById('dataTable');
const recordCount = document.getElementById('recordCount');
const selectedGrp = document.getElementById('grpFilter').value;
const showAllGroups = selectedGrp === '99';
if (currentData.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" class="px-6 py-4 text-center text-white/70">데이터가 없습니다.</td></tr>';
tbody.innerHTML = `
<tr>
<td colspan="6" class="px-4 py-8 text-center text-white/70">
<svg class="w-12 h-12 mx-auto mb-2 text-white/30" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
${selectedGroupCode ? '데이터가 없습니다.' : '좌측에서 코드그룹을 선택하세요'}
</td>
</tr>
`;
recordCount.textContent = '0';
return;
}
@@ -487,35 +561,14 @@
recordCount.textContent = currentData.length;
tbody.innerHTML = currentData.map(item => {
// 전체 보기일 때는 코드에 그룹 정보도 표시
let codeDisplay = item.code || '-';
if (showAllGroups && item.grp) {
// 그룹 데이터에서 해당 그룹의 이름 찾기
const groupInfo = groupData.find(g => g.code === item.grp);
const groupName = groupInfo ? groupInfo.svalue : item.grp;
codeDisplay = `${item.grp}-${groupName}`;
}
return `
<tr class="hover:bg-white/5 transition-colors">
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${codeDisplay}</td>
<td class="px-6 py-4 text-sm text-white">${item.memo || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.svalue || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.ivalue || '0'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.fvalue || '0.0'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-white">${item.svalue2 || '-'}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button onclick="editItem(${item.idx})" class="text-blue-400 hover:text-blue-300 mr-2 transition-colors">
<svg class="w-4 h-4" 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>
</button>
<button onclick="deleteItem(${item.idx})" class="text-red-400 hover:text-red-300 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
</svg>
</button>
</td>
<tr class="hover:bg-white/5 transition-colors cursor-pointer" onclick="editItem(${item.idx})">
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.code || '-'}</td>
<td class="px-4 py-4 text-sm text-white">${item.memo || '-'}</td>
<td class="px-4 py-4 text-sm text-white svalue-cell" title="${item.svalue || '-'}">${item.svalue || '-'}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.ivalue || '0'}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.fvalue || '0.0'}</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-white">${item.svalue2 || '-'}</td>
</tr>
`;
}).join('');
@@ -523,13 +576,24 @@
// 추가 모달 표시
function showAddModal() {
if (!selectedGroupCode) {
showNotification('먼저 코드그룹을 선택하세요.', 'warning');
return;
}
document.getElementById('modalTitle').textContent = '공용코드 추가';
document.getElementById('editForm').reset();
document.getElementById('editIdx').value = '';
// 현재 선택된 코드그룹을 자동 입력
const selectedGrp = document.getElementById('grpFilter').value;
document.getElementById('editGrp').value = selectedGrp;
document.getElementById('editGrp').value = selectedGroupCode;
// 삭제 버튼 비활성화 (새로 추가하는 항목이므로)
const deleteBtn = document.querySelector('#editModal button[onclick="deleteCurrentItem()"]');
if (deleteBtn) {
deleteBtn.disabled = true;
deleteBtn.classList.add('opacity-50', 'cursor-not-allowed');
}
document.getElementById('editModal').classList.remove('hidden');
}
@@ -549,6 +613,13 @@
document.getElementById('editSvalue2').value = item.svalue2 || '';
document.getElementById('editMemo').value = item.memo || '';
// 삭제 버튼 활성화 (기존 항목 편집이므로)
const deleteBtn = document.querySelector('#editModal button[onclick="deleteCurrentItem()"]');
if (deleteBtn) {
deleteBtn.disabled = false;
deleteBtn.classList.remove('opacity-50', 'cursor-not-allowed');
}
document.getElementById('editModal').classList.remove('hidden');
}
@@ -588,7 +659,10 @@
if (data.Success) {
showNotification(data.Message, 'success');
hideDeleteModal();
loadData(); // 데이터 새로고침
// 현재 선택된 그룹의 데이터 새로고침
if (selectedGroupCode) {
loadDataByGroup(selectedGroupCode);
}
} else {
showNotification(data.Message, 'error');
}
@@ -600,6 +674,19 @@
});
}
// 편집 다이얼로그에서 현재 항목 삭제
function deleteCurrentItem() {
const editIdx = document.getElementById('editIdx').value;
if (!editIdx) {
showNotification('삭제할 항목이 없습니다.', 'warning');
return;
}
deleteTargetIdx = parseInt(editIdx);
hideEditModal();
document.getElementById('deleteModal').classList.remove('hidden');
}
// 데이터 저장
function saveData() {
const form = document.getElementById('editForm');
@@ -634,7 +721,10 @@
if (result.Success) {
showNotification(result.Message, 'success');
hideEditModal();
loadData(); // 데이터 새로고침
// 현재 선택된 그룹의 데이터 새로고침
if (selectedGroupCode) {
loadDataByGroup(selectedGroupCode);
}
} else {
showNotification(result.Message, 'error');
}

View File

@@ -121,7 +121,7 @@
</style>
</head>
<body class="gradient-bg min-h-screen">
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<div class="container mx-auto px-4 py-8">
<!-- 헤더 -->
<div class="text-center mb-8 animate-fade-in">
@@ -224,7 +224,7 @@
휴가/기타 현황
</h2>
</div>
<div class="overflow-x-auto max-h-96 custom-scrollbar">
<div class="overflow-x-auto max-h-[460px] custom-scrollbar">
<table class="w-full">
<thead class="bg-white/10 sticky top-0">
<tr>
@@ -266,7 +266,7 @@
</h2>
</div>
<div class="p-4">
<div id="urgentTodoList" class="space-y-3 max-h-80 overflow-y-auto custom-scrollbar">
<div id="urgentTodoList" class="space-y-3 max-h-[384px] overflow-y-auto custom-scrollbar">
<!-- 할일이 여기에 표시됩니다 -->
<div class="text-center text-white/50 py-8">
<svg class="w-8 h-8 mx-auto mb-2 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -720,7 +720,8 @@
{ key: 'common', title: '공용코드', url: '/Common', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', isVisible: true, sortOrder: 2 },
{ key: 'jobreport', title: '업무일지', url: '/Jobreport/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2', isVisible: true, sortOrder: 3 },
{ key: 'kuntae', title: '근태관리', url: '/Kuntae/', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', isVisible: true, sortOrder: 4 },
{ key: 'todo', title: '할일관리', url: '/Todo/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4', isVisible: true, sortOrder: 5 }
{ key: 'todo', title: '할일관리', url: '/Todo/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4', isVisible: true, sortOrder: 5 },
{ key: 'project', title: '프로젝트', url: '/Project/', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10', isVisible: true, sortOrder: 6 },
];
}
@@ -742,22 +743,33 @@
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<h2 class="text-xl font-bold text-white">GroupWare</h2>
</div>
<div class="hidden md:flex items-center space-x-8">
${visibleItems.map(item => this.getMenuItemHTML(item)).join('')}
</div>
<div class="md:hidden">
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
<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="M4 6h16M4 12h16M4 18h16"></path>
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z"></path>
</svg>
</button>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${visibleItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
${visibleItems.map(item => this.getMobileMenuItemHTML(item)).join('')}
</div>
</div>
`;

View File

@@ -119,9 +119,28 @@
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* 셀렉트 박스 옵션 스타일링 */
select option {
background-color: #1f2937 !important;
color: #f9fafb !important;
padding: 8px 12px;
}
select option:hover {
background-color: #374151 !important;
}
select option:checked {
background-color: #3b82f6 !important;
}
select option:focus {
background-color: #374151 !important;
}
</style>
</head>
<body class="gradient-bg min-h-screen">
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<!-- 네비게이션 메뉴 (동적으로 추가됨) -->
<div class="container mx-auto px-4 py-8">
@@ -189,46 +208,78 @@
<!-- 필터 및 검색 -->
<div class="glass-effect rounded-lg mb-6 animate-slide-up">
<div class="p-6">
<div class="flex flex-col md:flex-row md:items-center md:justify-between space-y-4 md:space-y-0">
<div class="flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-4">
<div>
<label class="block text-sm font-medium text-white/80 mb-1">조회기간</label>
<div class="flex space-x-2">
<input type="date" id="startDate" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent text-sm">
<span class="flex items-center text-white/60">~</span>
<input type="date" id="endDate" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent text-sm">
<div class="p-4">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- 좌측: 필터 컨트롤 -->
<div class="lg:col-span-2 space-y-3">
<div class="grid grid-cols-1 sm:grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-white/80 mb-1">조회기간</label>
<div class="flex space-x-1">
<input type="date" id="startDate" class="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<span class="flex items-center text-white/60 text-xs">~</span>
<input type="date" id="endDate" class="flex-1 bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
</div>
</div>
<div>
<label class="block text-xs font-medium text-white/80 mb-1">상태</label>
<select id="statusFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="">전체</option>
<option value="진행중">진행중</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-white/80 mb-1">타입</label>
<select id="typeFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="">전체</option>
<option value="개발">개발</option>
<option value="유지보수">유지보수</option>
<option value="분석">분석</option>
<option value="테스트">테스트</option>
<option value="문서작업">문서작업</option>
<option value="회의">회의</option>
<option value="기타">기타</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-1">상태</label>
<select id="statusFilter" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
<option value="">전체</option>
<option value="진행중">진행중</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
</select>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-white/80 mb-1">사용자</label>
<select id="userFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="">전체</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-white/80 mb-1">프로젝트</label>
<select id="projectFilter" class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
<option value="">전체</option>
</select>
</div>
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-1">프로젝트</label>
<select id="projectFilter" class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
<option value="">전체</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-white/80 mb-1">검색</label>
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="bg-white/20 border border-white/30 rounded-md px-3 py-2 text-white placeholder-white/60 focus:outline-none focus:ring-2 focus:ring-white/50 focus:border-transparent">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-white/80 mb-1">검색</label>
<input type="text" id="searchInput" placeholder="업무 내용 검색..." class="w-full bg-white/20 border border-white/30 rounded-md px-2 py-1 text-white placeholder-white/60 focus:outline-none focus:ring-1 focus:ring-white/50 focus:border-transparent text-xs">
</div>
</div>
</div>
<div class="flex space-x-2">
<button id="clearFilterBtn" class="glass-effect hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center transition-colors">
<i data-feather="x" class="w-4 h-4 mr-2"></i>
필터 초기화
<!-- 우측: 액션 버튼들 -->
<div class="flex flex-col space-y-2 justify-center">
<button id="addJobBtn" class="bg-primary-500 hover:bg-primary-600 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors" title="업무일지 추가">
<i data-feather="plus" class="w-4 h-4 mr-2"></i>
업무일지 추가
</button>
<button id="exportBtn" class="glass-effect hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center transition-colors">
<button id="exportBtn" class="bg-green-500 hover:bg-green-600 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors" title="엑셀 다운로드">
<i data-feather="download" class="w-4 h-4 mr-2"></i>
엑셀 다운로드
</button>
<button id="clearFilterBtn" class="bg-white/20 hover:bg-white/30 text-white px-4 py-2 rounded-md flex items-center justify-center transition-colors" title="필터 초기화">
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
필터 초기화
</button>
</div>
</div>
</div>
@@ -406,22 +457,33 @@
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<div class="flex items-center">
<h2 class="text-xl font-bold text-white">GroupWare</h2>
</div>
<div class="hidden md:flex items-center space-x-8">
${visibleItems.map(item => this.getMenuItemHTML(item)).join('')}
</div>
<div class="md:hidden">
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
<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="M4 6h16M4 12h16M4 18h16"></path>
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path>
</svg>
</button>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${visibleItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
${visibleItems.map(item => this.getMobileMenuItemHTML(item)).join('')}
</div>
</div>
`;
@@ -488,6 +550,19 @@
initNavigation('jobreport');
initializeApp();
loadJobData();
// 새로운 위치의 버튼들에 이벤트 리스너 추가
setTimeout(() => {
const addJobBtn = document.getElementById('addJobBtn');
const exportBtn = document.getElementById('exportBtn');
if (addJobBtn) {
addJobBtn.addEventListener('click', showAddJobModal);
}
if (exportBtn) {
exportBtn.addEventListener('click', exportToExcel);
}
}, 100);
});
function initializeApp() {
@@ -499,10 +574,16 @@
document.getElementById('startDate').value = twoWeeksAgo;
document.getElementById('endDate').value = today;
// 사용자 목록 로드
console.log('사용자 목록 로드 시작...');
loadUserList();
// 이벤트 리스너 등록
document.getElementById('startDate').addEventListener('change', loadJobData);
document.getElementById('endDate').addEventListener('change', loadJobData);
document.getElementById('statusFilter').addEventListener('change', filterData);
document.getElementById('typeFilter').addEventListener('change', filterData);
document.getElementById('userFilter').addEventListener('change', loadJobData);
document.getElementById('projectFilter').addEventListener('change', filterData);
document.getElementById('searchInput').addEventListener('input', filterData);
document.getElementById('clearFilterBtn').addEventListener('click', clearFilters);
@@ -524,7 +605,6 @@
renderTable();
}
});
document.getElementById('exportBtn').addEventListener('click', exportToExcel);
document.getElementById('closeModal').addEventListener('click', closeModal);
// 정렬 이벤트 리스너
@@ -561,12 +641,14 @@
// 조회기간 파라미터 가져오기
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const selectedUser = document.getElementById('userFilter').value;
// API URL 구성 - 새로운 GetJobData API 사용
let url = '/Jobreport/GetJobData';
const params = new URLSearchParams();
if (startDate) params.append('startDate', startDate);
if (endDate) params.append('endDate', endDate);
if (selectedUser) params.append('user', selectedUser);
if (params.toString()) {
url += '?' + params.toString();
@@ -684,20 +766,62 @@
});
}
function loadUserList() {
const userSelect = document.getElementById('userFilter');
// 기존 옵션 제거 (전체 옵션 제외)
while (userSelect.children.length > 1) {
userSelect.removeChild(userSelect.lastChild);
}
// 서버에서 사용자 목록 가져오기
fetch('/Jobreport/GetUsers')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP ${response.status}: 사용자 목록을 불러오는데 실패했습니다.`);
}
return response.json();
})
.then(data => {
console.log('사용자 목록 데이터:', data);
if (data && Array.isArray(data)) {
data.forEach(user => {
const option = document.createElement('option');
option.value = user.id;
option.textContent = `${user.name} [${user.id}] ${user.process}`;
userSelect.appendChild(option);
});
console.log('사용자 목록 로드 완료:', userSelect.children.length - 1, '명');
} else {
console.warn('사용자 목록 데이터가 배열이 아닙니다:', data);
}
})
.catch(error => {
console.error('사용자 목록 로드 중 오류:', error);
// 에러 시 기본 사용자 옵션 추가
const option = document.createElement('option');
option.value = FCOMMON?.info?.Login?.no || '';
option.textContent = '현재 사용자';
userSelect.appendChild(option);
});
}
function filterData() {
const statusFilter = document.getElementById('statusFilter').value;
const typeFilter = document.getElementById('typeFilter').value;
const projectFilter = document.getElementById('projectFilter').value;
const searchTerm = document.getElementById('searchInput').value.toLowerCase();
filteredData = jobData.filter(item => {
const statusMatch = !statusFilter || item.status === statusFilter;
const typeMatch = !typeFilter || item.type === typeFilter;
const projectMatch = !projectFilter || item.projectName === projectFilter;
const searchMatch = !searchTerm ||
(item.description && item.description.toLowerCase().includes(searchTerm)) ||
(item.projectName && item.projectName.toLowerCase().includes(searchTerm)) ||
(item.requestpart && item.requestpart.toLowerCase().includes(searchTerm));
return statusMatch && projectMatch && searchMatch;
return statusMatch && typeMatch && projectMatch && searchMatch;
});
currentPage = 1;
@@ -862,6 +986,112 @@
document.getElementById('nextPage').disabled = currentPage >= maxPage;
}
function showAddJobModal() {
const modal = document.getElementById('detailModal');
const content = document.getElementById('modalContent');
// 오늘 날짜를 기본값으로 설정
const today = new Date().toISOString().split('T')[0];
content.innerHTML = `
<form id="editJobForm" class="space-y-6">
<input type="hidden" id="editIdx" value="">
<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="${today}"
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="진행 중" selected>진행 중</option>
<option value="완료">완료</option>
<option value="대기">대기</option>
<option value="보류">보류</option>
</select>
</div>
<div>
<label class="block text-white/70 text-sm font-medium mb-2">요청부서</label>
<input type="text" id="editRequestpart" value=""
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="개발">개발</option>
<option value="유지보수">유지보수</option>
<option value="분석">분석</option>
<option value="테스트">테스트</option>
<option value="문서작업">문서작업</option>
<option value="회의">회의</option>
<option value="기타">기타</option>
</select>
</div>
<div>
<label class="block text-white/70 text-sm font-medium mb-2">근무시간 (시간) *</label>
<input type="number" id="editHrs" value="8" 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="" 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=""
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=""
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=""
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="상세한 업무 내용을 입력하세요..."></textarea>
</div>
</div>
</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" 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', handleAddSubmit);
}
async function showDetailModal(item) {
// 문자열로 전달된 경우 JSON 파싱
if (typeof item === 'string') {
@@ -970,16 +1200,18 @@
</form>
<!-- 모달 푸터 -->
<div class="px-6 py-4 border-t border-white/10 flex justify-end space-x-3 bg-black/10 rounded-b-2xl">
<div class="px-6 py-4 border-t border-white/10 flex justify-between items-center 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 class="flex space-x-3">
<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>
</div>
`;
@@ -995,6 +1227,61 @@
document.getElementById('detailModal').classList.add('hidden');
}
async function handleAddSubmit(event) {
event.preventDefault();
// URLSearchParams 사용 (application/x-www-form-urlencoded)
const formData = new URLSearchParams();
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/Add', {
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('Add 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 adding job:', error);
alert('업무일지 추가 중 오류가 발생했습니다: ' + error.message);
}
}
async function handleEditSubmit(event) {
event.preventDefault();
@@ -1202,6 +1489,8 @@
// 다른 필터들 초기화
document.getElementById('statusFilter').value = '';
document.getElementById('typeFilter').value = '';
document.getElementById('userFilter').value = '';
document.getElementById('projectFilter').value = '';
document.getElementById('searchInput').value = '';

View File

@@ -140,7 +140,7 @@
}
</style>
</head>
<body class="gradient-bg min-h-screen">
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<div class="container mx-auto px-4 py-8">
<!-- 개발중 경고 메시지 -->
<div class="bg-orange-500 rounded-lg p-4 mb-6 border-l-4 border-orange-700 animate-slide-up shadow-lg">
@@ -489,29 +489,33 @@
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<!-- 로고/타이틀 -->
<div class="flex items-center">
<h2 class="text-xl font-bold text-white">GroupWare</h2>
</div>
<!-- 메뉴 -->
<div class="hidden md:flex items-center space-x-8">
${visibleItems.map(item => this.getMenuItemHTML(item)).join('')}
</div>
<!-- 모바일 메뉴 버튼 -->
<div class="md:hidden">
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
<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="M4 6h16M4 12h16M4 18h16"></path>
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</button>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${visibleItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
<!-- 모바일 메뉴 -->
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
${visibleItems.map(item => this.getMobileMenuItemHTML(item)).join('')}
</div>
</div>
`;

View File

@@ -119,7 +119,7 @@
}
</style>
</head>
<body class="gradient-bg min-h-screen">
<body class="bg-gradient-to-br from-blue-900 via-purple-900 to-indigo-900 min-h-screen text-white">
<div class="container mx-auto px-4 py-8">
<!-- 할일 목록 -->
<div class="glass-effect rounded-2xl overflow-hidden animate-slide-up">
@@ -410,40 +410,45 @@
}
getNavigationHTML() {
const menuItems = [
{ key: 'dashboard', title: '대시보드', url: '/Dashboard/', icon: 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z' },
{ key: 'common', title: '공용코드', url: '/Common', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' },
{ key: 'jobreport', title: '업무일지', url: '/Jobreport/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2' },
{ key: 'kuntae', title: '근태관리', url: '/Kuntae/', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
{ key: 'todo', title: '할일관리', url: '/Todo/', icon: 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4' },
{ key: 'project', title: '프로젝트', url: '/Project/', icon: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' }
];
return `
<div class="container mx-auto px-4">
<div class="flex items-center justify-between h-16">
<!-- 로고/타이틀 -->
<div class="flex items-center">
<h2 class="text-xl font-bold text-white">GroupWare</h2>
</div>
<!-- 메뉴 -->
<div class="hidden md:flex items-center space-x-8">
${this.getMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
${this.getMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
${this.getMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
${this.getMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
${this.getMenuItemHTML('todo', '/Todo/', '할일관리', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4')}
</div>
<!-- 모바일 메뉴 버튼 -->
<div class="md:hidden">
<button id="mobile-menu-button" class="text-white/80 hover:text-white transition-colors p-2">
<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="M4 6h16M4 12h16M4 18h16"></path>
<div class="flex items-center space-x-8">
<div class="flex items-center space-x-2">
<svg class="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4"></path>
</svg>
</button>
<span class="text-xl font-bold text-white">GroupWare</span>
</div>
<nav class="hidden md:flex space-x-1">
${menuItems.map(item => `
<a href="${item.url}" class="px-3 py-2 rounded-md text-sm font-medium transition-colors ${
this.currentPage === item.key
? 'bg-white/20 text-white'
: 'text-white/60 hover:text-white hover:bg-white/10'
}">
<svg class="w-4 h-4 inline mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="${item.icon}"></path>
</svg>
${item.title}
</a>
`).join('')}
</nav>
</div>
<div class="flex items-center space-x-4">
<div class="text-sm text-white/60">
<span id="currentUser">사용자</span>
</div>
</div>
</div>
<!-- 모바일 메뉴 -->
<div id="mobile-menu" class="md:hidden hidden border-t border-white/10 pt-4 pb-4">
${this.getMobileMenuItemHTML('dashboard', '/Dashboard/', '대시보드', 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2H5a2 2 0 00-2-2z M8 5a2 2 0 012-2h4a2 2 0 012 2v2H8V5z')}
${this.getMobileMenuItemHTML('common', '/Common', '공용코드', 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z')}
${this.getMobileMenuItemHTML('jobreport', '/Jobreport/', '업무일지', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2')}
${this.getMobileMenuItemHTML('kuntae', '/Kuntae/', '근태관리', 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z')}
${this.getMobileMenuItemHTML('todo', '/Todo/', '할일관리', 'M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2M12 12l2 2 4-4')}
</div>
</div>
`;