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

- 솔루션 설정 및 프로젝트 파일 업데이트
- 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

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